Skip to content

Commit 2c3df3a

Browse files
committed
chore: wip
chore: wip chore: wip
1 parent cdf2615 commit 2c3df3a

File tree

5 files changed

+161
-30
lines changed

5 files changed

+161
-30
lines changed

config/cloud.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export default {
3333
},
3434

3535
api: {
36-
deploy: false,
36+
deploy: true,
3737
prefix: env.API_PREFIX || 'api',
3838
description: 'Stacks API',
3939
memorySize: 512,

storage/framework/core/cloud/src/cloud/compute.ts

Lines changed: 117 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
/* eslint-disable no-new */
22
import type { aws_certificatemanager as acm, aws_efs as efs, aws_lambda as lambda, aws_route53 as route53 } from 'aws-cdk-lib'
3-
import { Duration, CfnOutput as Output, aws_ec2 as ec2, aws_ecs as ecs, aws_ecs_patterns as ecs_patterns, aws_secretsmanager as secretsmanager } from 'aws-cdk-lib'
3+
import { Duration, CfnOutput as Output, RemovalPolicy, aws_ec2 as ec2, aws_ecs as ecs, aws_secretsmanager as secretsmanager } from 'aws-cdk-lib'
44
import type { Construct } from 'constructs'
55
import { path as p } from '@stacksjs/path'
66
import { env } from '@stacksjs/env'
7+
import { LogGroup } from 'aws-cdk-lib/aws-logs'
8+
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'
79
import type { NestedCloudProps } from '../types'
810
import type { EnvKey } from '../../../../env'
911

@@ -15,6 +17,7 @@ export interface ComputeStackProps extends NestedCloudProps {
1517
}
1618

1719
export class ComputeStack {
20+
lb: elbv2.ApplicationLoadBalancer
1821
apiServer: lambda.Function
1922
apiServerUrl: lambda.FunctionUrl
2023

@@ -30,35 +33,115 @@ export class ComputeStack {
3033
vpc,
3134
})
3235

33-
const taskDefinition = new ecs.FargateTaskDefinition(scope, 'TaskDef', {
36+
const taskDefinition = new ecs.FargateTaskDefinition(scope, 'TaskDefinition', {
37+
family: `${props.appName}-${props.appEnv}-api`,
3438
memoryLimitMiB: 512, // Match your Lambda memory size
3539
cpu: 256, // Choose an appropriate value
40+
runtimePlatform: {
41+
cpuArchitecture: ecs.CpuArchitecture.ARM64,
42+
},
3643
})
3744

3845
const container = taskDefinition.addContainer('WebServerContainer', {
46+
containerName: `${props.appName}-${props.appEnv}-api`,
3947
image: ecs.ContainerImage.fromAsset(p.frameworkPath('server')),
40-
portMappings: [{ containerPort: 80 }],
48+
logging: new ecs.AwsLogDriver({
49+
streamPrefix: `${props.appName}-${props.appEnv}-web`,
50+
logGroup: new LogGroup(scope, 'StacksApiLogs', {
51+
logGroupName: '/ecs/stacks-api',
52+
removalPolicy: RemovalPolicy.DESTROY, // Automatically remove logs on stack deletion
53+
}),
54+
}),
55+
healthCheck: {
56+
command: ['CMD-SHELL', 'curl -f http://localhost:3000/health || exit 1'],
57+
interval: Duration.seconds(10),
58+
timeout: Duration.seconds(5),
59+
retries: 3,
60+
startPeriod: Duration.seconds(10),
61+
},
62+
})
63+
64+
container.addPortMappings({
65+
containerPort: 3000,
66+
hostPort: 3000,
67+
})
68+
69+
const serviceSecurityGroup = new ec2.SecurityGroup(scope, 'ServiceSecurityGroup', {
70+
securityGroupName: `${props.appName}-${props.appEnv}-api-service-sg`,
71+
vpc,
72+
description: 'Stacks Security Group for API Service',
73+
})
74+
75+
const publicLoadBalancerSG = new ec2.SecurityGroup(scope, 'PublicLoadBalancerSG', {
76+
securityGroupName: `${props.appName}-${props.appEnv}-public-load-balancer-sg`,
77+
vpc,
78+
description: 'Access to the public facing load balancer',
79+
})
80+
81+
// Assuming serviceSecurityGroup and publicLoadBalancerSG are already defined
82+
serviceSecurityGroup.addIngressRule(
83+
publicLoadBalancerSG,
84+
ec2.Port.allTraffic(),
85+
'Ingress from the public ALB',
86+
)
87+
88+
// Assuming serviceSecurityGroup and publicLoadBalancerSG are already defined
89+
serviceSecurityGroup.addIngressRule(publicLoadBalancerSG, ec2.Port.allTraffic(), 'Ingress from the public ALB')
90+
91+
this.lb = new elbv2.ApplicationLoadBalancer(scope, 'ApplicationLoadBalancer', {
92+
loadBalancerName: `${props.appName}-${props.appEnv}-alb`,
93+
vpc,
94+
vpcSubnets: {
95+
subnets: vpc.selectSubnets({
96+
subnetType: ec2.SubnetType.PUBLIC,
97+
onePerAz: true,
98+
}).subnets,
99+
},
100+
internetFacing: true,
101+
idleTimeout: Duration.seconds(30),
102+
securityGroup: publicLoadBalancerSG,
103+
})
104+
105+
const serviceTargetGroup = new elbv2.ApplicationTargetGroup(scope, 'ServiceTargetGroup', {
106+
// targetGroupName: `${props.appName}-${props.appEnv}-service-tg`,
107+
vpc,
108+
targetType: elbv2.TargetType.IP,
109+
protocol: elbv2.ApplicationProtocol.HTTP,
110+
port: 3000,
111+
healthCheck: {
112+
interval: Duration.seconds(6),
113+
path: '/',
114+
protocol: elbv2.Protocol.HTTP,
115+
timeout: Duration.seconds(5),
116+
healthyThresholdCount: 2,
117+
unhealthyThresholdCount: 10,
118+
},
41119
})
42120

43-
const fargateService = new ecs_patterns.ApplicationLoadBalancedFargateService(scope, 'FargateService', {
44-
serviceName: `${props.slug}-${props.appEnv}-fargate-service`,
121+
const service = new ecs.FargateService(scope, 'StacksApiService', {
122+
serviceName: `${props.appName}-${props.appEnv}-api-service`,
45123
cluster,
46124
taskDefinition,
47-
desiredCount: 1, // Start with 1 task instance
48-
// Other configurations like public load balancer, domain name, etc.
49-
publicLoadBalancer: true,
50-
taskSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PUBLIC, onePerAz: true }),
125+
desiredCount: 2,
51126
assignPublicIp: true,
52-
listenerPort: 80,
127+
maxHealthyPercent: 200,
128+
vpcSubnets: vpc.selectSubnets({
129+
subnetType: ec2.SubnetType.PUBLIC,
130+
onePerAz: true,
131+
}),
132+
minHealthyPercent: 75,
133+
securityGroups: [serviceSecurityGroup],
53134
})
54135

55-
fargateService.targetGroup.configureHealthCheck({
56-
path: '/health',
57-
interval: Duration.seconds(60),
136+
service.attachToApplicationTargetGroup(serviceTargetGroup)
137+
publicLoadBalancerSG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.allTraffic())
138+
139+
this.lb.addListener('HttpListener', {
140+
port: 80,
141+
defaultAction: elbv2.ListenerAction.forward([serviceTargetGroup]),
58142
})
59143

60-
// ec2.Peer.securityGroupId('YOUR_SECURITY_GROUP_ID'
61-
props.fileSystem.connections.allowFromAnyIpv4(ec2.Port.tcp(2049))
144+
props.fileSystem.connections.allowFromAnyIpv4(ec2.Port.tcp(2049)) // port 2049 (NFS) for EFS
62145

63146
const volumeName = `${props.slug}-${props.appEnv}-efs`
64147
taskDefinition.addVolume({
@@ -74,6 +157,22 @@ export class ComputeStack {
74157
readOnly: false,
75158
})
76159

160+
// Setup AutoScaling policy
161+
// TODO: make this configurable in cloud.compute
162+
const scaling = service.autoScaleTaskCount({ maxCapacity: 2 })
163+
164+
scaling.scaleOnCpuUtilization('CpuScaling', {
165+
targetUtilizationPercent: 50,
166+
scaleInCooldown: Duration.seconds(60),
167+
scaleOutCooldown: Duration.seconds(60),
168+
})
169+
170+
scaling.scaleOnMemoryUtilization('MemoryScaling', {
171+
targetUtilizationPercent: 60,
172+
scaleInCooldown: Duration.seconds(60),
173+
scaleOutCooldown: Duration.seconds(60),
174+
})
175+
77176
const keysToRemove = ['_HANDLER', '_X_AMZN_TRACE_ID', 'AWS_REGION', 'AWS_EXECUTION_ENV', 'AWS_LAMBDA_FUNCTION_NAME', 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE', 'AWS_LAMBDA_FUNCTION_VERSION', 'AWS_LAMBDA_INITIALIZATION_TYPE', 'AWS_LAMBDA_LOG_GROUP_NAME', 'AWS_LAMBDA_LOG_STREAM_NAME', 'AWS_ACCESS_KEY', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN', 'AWS_LAMBDA_RUNTIME_API', 'LAMBDA_TASK_ROOT', 'LAMBDA_RUNTIME_DIR', '_']
78177
keysToRemove.forEach(key => delete env[key as EnvKey])
79178

@@ -86,7 +185,7 @@ export class ComputeStack {
86185
},
87186
})
88187

89-
secrets.grantRead(fargateService.taskDefinition.executionRole!)
188+
secrets.grantRead(service.taskDefinition.executionRole!)
90189
container.addEnvironment('SECRETS_ARN', secrets.secretArn)
91190

92191
const apiPrefix = 'api'
@@ -96,8 +195,8 @@ export class ComputeStack {
96195
})
97196

98197
new Output(scope, 'ApiVanityUrl', {
99-
value: '',
100-
description: 'The Vanity URL of the deployed application',
198+
value: `http://${this.lb.loadBalancerDnsName}`,
199+
description: 'The Vanity URL / DNS name of the load balancer',
101200
})
102201
}
103202
}

storage/framework/core/config/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
],
2727
"exports": {
2828
".": {
29-
"bun": "./src/index.ts",
29+
"bun": "./dist/index.js",
3030
"import": "./dist/index.js"
3131
},
3232
"./*": {

storage/framework/server/Dockerfile

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,42 @@
1-
FROM oven/bun
2-
WORKDIR /srv
1+
# use the official Bun image
2+
# see all versions at https://hub.docker.com/r/oven/bun/tags
3+
FROM oven/bun:1 as base
4+
WORKDIR /usr/src/app
35

4-
# Add the package manifest and install packages
5-
ADD package.json .
6-
RUN bun install
6+
# install dependencies into temp directory
7+
# this will cache them and speed up future builds
8+
FROM base AS install
9+
RUN mkdir -p /temp/dev
10+
# COPY package.json bun.lockb /temp/dev/
11+
COPY package.json /temp/dev/
12+
RUN cd /temp/dev && bun install
13+
# RUN cd /temp/dev && bun install --frozen-lockfile
714

8-
# Add the application code
9-
ADD index.ts .
15+
# install with --production (exclude devDependencies)
16+
RUN mkdir -p /temp/prod
17+
# COPY package.json bun.lockb /temp/prod/
18+
COPY package.json /temp/prod/
19+
RUN cd /temp/prod && bun install
20+
# RUN cd /temp/prod && bun install --production --frozen-lockfile
1021

11-
# Specify the command to run when launching the container
12-
CMD bun index.ts
22+
# copy node_modules from temp directory
23+
# then copy all (non-ignored) project files into the image
24+
FROM base AS prerelease
25+
COPY --from=install /temp/dev/node_modules node_modules
26+
COPY . .
27+
28+
# [optional] tests & build
29+
# ENV NODE_ENV=production
30+
# RUN bun test
31+
# RUN bun run build
32+
33+
# copy production dependencies and source code into final image
34+
FROM base AS release
35+
COPY --from=install /temp/prod/node_modules node_modules
36+
COPY --from=prerelease /usr/src/app/index.ts .
37+
COPY --from=prerelease /usr/src/app/package.json .
38+
39+
# run the app
40+
USER bun
41+
EXPOSE 3000/tcp
42+
ENTRYPOINT [ "bun", "run", "index.ts" ]

storage/framework/server/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import type { Server, ServerWebSocket } from 'bun'
22
import { serverResponse } from '@stacksjs/router'
33

44
const server = Bun.serve({
5-
port: 80,
5+
port: 3000,
6+
67
async fetch(request: Request, server: Server): Promise<Response | undefined> {
78
// eslint-disable-next-line no-console
89
console.log('Request', {
@@ -20,6 +21,7 @@ const server = Bun.serve({
2021

2122
return serverResponse(request)
2223
},
24+
2325
websocket: {
2426
async open(ws: ServerWebSocket): Promise<void> {
2527
// eslint-disable-next-line no-console

0 commit comments

Comments
 (0)