1
1
/* eslint-disable no-new */
2
- import type { aws_certificatemanager as acm , aws_efs as efs } from 'aws-cdk-lib'
3
- import { Duration , CfnOutput as Output , Stack , aws_dynamodb as dynamodb , aws_ec2 as ec2 , aws_ecr as ecr , aws_ecs as ecs , aws_elasticloadbalancingv2 as elbv2 , aws_iam as iam , aws_logs as logs , aws_route53 as route53 , aws_route53_targets as route53Targets } from 'aws-cdk-lib'
2
+ import type { aws_certificatemanager as acm , aws_ec2 as ec2 , aws_efs as efs , aws_route53 as route53 } from 'aws-cdk-lib'
3
+ import { Duration , CfnOutput as Output , aws_ecr_assets as ecr_assets , aws_lambda as lambda , aws_logs as logs , aws_secretsmanager as secretsmanager } from 'aws-cdk-lib'
4
4
import type { Construct } from 'constructs'
5
+ import { path as p } from '@stacksjs/path'
6
+ import { env } from '@stacksjs/env'
5
7
import type { NestedCloudProps } from '../types'
8
+ import type { EnvKey } from '~/storage/framework/stacks/env'
6
9
7
10
export interface ComputeStackProps extends NestedCloudProps {
8
11
vpc : ec2 . Vpc
@@ -12,207 +15,65 @@ export interface ComputeStackProps extends NestedCloudProps {
12
15
}
13
16
14
17
export class ComputeStack {
15
- lb : elbv2 . ApplicationLoadBalancer
16
-
17
18
constructor ( scope : Construct , props : ComputeStackProps ) {
18
19
const vpc = props . vpc
19
20
const fileSystem = props . fileSystem
20
21
21
22
if ( ! fileSystem )
22
23
throw new Error ( 'The file system is missing. Please make sure it was created properly.' )
23
24
24
- const ecsCluster = new ecs . Cluster ( scope , 'DefaultEcsCluster' , {
25
- clusterName : `${ props . appName } -${ props . appEnv } -ecs-cluster` ,
26
- containerInsights : true ,
27
- vpc,
28
- } )
29
-
30
- fileSystem . addToResourcePolicy (
31
- new iam . PolicyStatement ( {
32
- actions : [ 'elasticfilesystem:ClientMount' ] ,
33
- principals : [ new iam . AnyPrincipal ( ) ] ,
34
- conditions : {
35
- Bool : {
36
- 'elasticfilesystem:AccessedViaMountTarget' : 'true' ,
37
- } ,
38
- } ,
39
- } ) ,
40
- )
41
-
42
- const cacheTable = new dynamodb . Table ( scope , 'CacheTable' , {
43
- partitionKey : { name : 'counter' , type : dynamodb . AttributeType . STRING } ,
44
- billingMode : dynamodb . BillingMode . PAY_PER_REQUEST ,
45
- } )
46
-
47
- const taskRole = new iam . Role ( scope , 'TaskRole' , {
48
- assumedBy : new iam . ServicePrincipal ( 'ecs-tasks.amazonaws.com' ) ,
49
- inlinePolicies : {
50
- AccessToHitCounterTable : new iam . PolicyDocument ( {
51
- statements : [
52
- new iam . PolicyStatement ( {
53
- actions : [ 'dynamodb:Get*' , 'dynamodb:UpdateItem' ] ,
54
- resources : [ cacheTable . tableArn ] ,
55
- conditions : {
56
- ArnLike : {
57
- 'aws:SourceArn' : `arn:aws:ecs:${ Stack . of ( scope ) . region } :${ Stack . of ( scope ) . account } :*` ,
58
- } ,
59
- StringEquals : {
60
- 'aws:SourceAccount' : Stack . of ( scope ) . account ,
61
- } ,
62
- } ,
63
- } ) ,
64
- ] ,
65
- } ) ,
66
- } ,
67
- } )
68
-
69
- const taskDefinition = new ecs . FargateTaskDefinition ( scope , 'FargateTaskDefinition' , {
70
- memoryLimitMiB : 512 , // TODO: make configurable in cloud.compute
71
- cpu : 256 , // TODO: make configurable in cloud.compute
72
- volumes : [
73
- {
74
- name : 'stacks-efs' ,
75
- efsVolumeConfiguration : {
76
- fileSystemId : fileSystem . fileSystemId ,
77
- } ,
78
- } ,
79
- ] ,
80
- taskRole,
81
- executionRole : new iam . Role ( scope , 'ExecutionRole' , {
82
- assumedBy : new iam . ServicePrincipal ( 'ecs-tasks.amazonaws.com' ) ,
83
- } ) ,
84
- } )
85
-
86
- const repositoryName = 'stacks-bun-hitcounter' // replace with your repository name
87
- const repository = ecr . Repository . fromRepositoryName ( scope , 'ServerRepo' , repositoryName )
88
- const containerDef = taskDefinition . addContainer ( 'WebContainer' , {
89
- containerName : `${ props . appName } -${ props . appEnv } -web-container` ,
90
- // image: ecs.ContainerImage.fromRegistry('public.ecr.aws/docker/library/nginx:latest'),
91
- image : ecs . ContainerImage . fromEcrRepository ( repository ) ,
92
- logging : new ecs . AwsLogDriver ( {
93
- streamPrefix : `${ props . appName } -${ props . appEnv } -web` ,
94
- logGroup : new logs . LogGroup ( scope , 'LogGroup' ) ,
95
- } ) ,
96
- // gpuCount: 0,
97
- } )
98
-
99
- containerDef . addMountPoints (
100
- {
101
- sourceVolume : 'stacks-efs' ,
102
- containerPath : '/mnt/efs' ,
103
- readOnly : false ,
104
- } ,
105
- )
106
-
107
- containerDef . addPortMappings ( {
108
- containerPort : 3000 ,
109
- hostPort : 3000 ,
110
- } )
111
-
112
- const serviceSecurityGroup = new ec2 . SecurityGroup ( scope , 'ServiceSecurityGroup' , {
113
- securityGroupName : `${ props . appName } -${ props . appEnv } -service-sg` ,
114
- vpc,
115
- description : 'Security group for service' ,
116
- } )
117
-
118
- const publicLoadBalancerSG = new ec2 . SecurityGroup ( scope , 'PublicLoadBalancerSG' , {
119
- securityGroupName : `${ props . appName } -${ props . appEnv } -public-load-balancer-sg` ,
120
- vpc,
121
- description : 'Access to the public facing load balancer' ,
122
- } )
123
-
124
- // Assuming serviceSecurityGroup and publicLoadBalancerSG are already defined
125
- serviceSecurityGroup . addIngressRule ( publicLoadBalancerSG , ec2 . Port . allTraffic ( ) , 'Ingress from the public ALB' )
126
-
127
- this . lb = new elbv2 . ApplicationLoadBalancer ( scope , 'ApplicationLoadBalancer' , {
128
- loadBalancerName : `${ props . appName } -${ props . appEnv } -alb` ,
129
- vpc,
130
- vpcSubnets : {
131
- subnets : vpc . selectSubnets ( {
132
- subnetType : ec2 . SubnetType . PUBLIC ,
133
- onePerAz : true ,
134
- } ) . subnets ,
135
- } ,
136
- internetFacing : true ,
137
- idleTimeout : Duration . seconds ( 30 ) ,
138
- securityGroup : publicLoadBalancerSG ,
139
- } )
25
+ // const dockerImageAsset = new ecr_assets.DockerImageAsset(scope, 'ServerBuildImage', {
26
+ // directory: p.cloudPath('src/server'),
27
+ // })
140
28
141
- const serviceTargetGroup = new elbv2 . ApplicationTargetGroup ( scope , 'ServiceTargetGroup' , {
142
- // targetGroupName: `${props.appName}-${props.appEnv}-service-tg`,
29
+ const webServer = new lambda . Function ( scope , 'WebServer' , {
30
+ // code: lambda.Code.fromDockerBuild(p.cloudPath('src/server')),
31
+ description : 'The web server for the Stacks application' ,
32
+ code : lambda . Code . fromAssetImage ( p . cloudPath ( 'src/server' ) ) ,
33
+ handler : lambda . Handler . FROM_IMAGE ,
34
+ runtime : lambda . Runtime . FROM_IMAGE ,
143
35
vpc,
144
- targetType : elbv2 . TargetType . IP ,
145
- protocol : elbv2 . ApplicationProtocol . HTTP ,
146
- port : 3000 ,
147
- healthCheck : {
148
- interval : Duration . seconds ( 6 ) ,
149
- path : '/' ,
150
- protocol : elbv2 . Protocol . HTTP ,
151
- timeout : Duration . seconds ( 5 ) ,
152
- healthyThresholdCount : 2 ,
153
- unhealthyThresholdCount : 10 ,
36
+ memorySize : 512 , // replace with your actual memory size
37
+ timeout : Duration . minutes ( 5 ) , // replace with your actual timeout
38
+ logRetention : logs . RetentionDays . ONE_WEEK ,
39
+ architecture : lambda . Architecture . ARM_64 ,
40
+ // filesystem: lambda.FileSystem.fromEfsAccessPoint(this.storage.accessPoint!, '/mnt/efs'),
41
+ } )
42
+
43
+ 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' , '_' ]
44
+ keysToRemove . forEach ( key => delete env [ key as EnvKey ] )
45
+
46
+ const secrets = new secretsmanager . Secret ( scope , 'StacksSecrets' , {
47
+ secretName : `${ props . appName } -${ props . appEnv } -secrets` ,
48
+ description : 'Secrets for the Stacks application' ,
49
+ generateSecretString : {
50
+ secretStringTemplate : JSON . stringify ( env ) ,
51
+ generateStringKey : Object . keys ( env ) . join ( ',' ) . length . toString ( ) ,
154
52
} ,
155
53
} )
156
54
157
- this . lb . addListener ( 'HttpListener' , {
158
- port : 80 ,
159
- defaultAction : elbv2 . ListenerAction . forward ( [ serviceTargetGroup ] ) ,
160
- } )
161
-
162
- const service = new ecs . FargateService ( scope , 'WebService' , {
163
- serviceName : `${ props . appName } -${ props . appEnv } -web-service` ,
164
- cluster : ecsCluster ,
165
- taskDefinition,
166
- desiredCount : 2 ,
167
- assignPublicIp : true ,
168
- maxHealthyPercent : 200 ,
169
- vpcSubnets : vpc . selectSubnets ( {
170
- subnetType : ec2 . SubnetType . PUBLIC ,
171
- onePerAz : true ,
172
- } ) ,
173
- minHealthyPercent : 75 ,
174
- securityGroups : [ serviceSecurityGroup ] ,
175
- } )
176
-
177
- service . attachToApplicationTargetGroup ( serviceTargetGroup )
178
-
179
- publicLoadBalancerSG . addIngressRule ( ec2 . Peer . anyIpv4 ( ) , ec2 . Port . allTraffic ( ) )
180
-
181
- new route53 . ARecord ( scope , 'AliasApiRecord' , {
182
- zone : props . zone ,
183
- recordName : 'api' ,
184
- target : route53 . RecordTarget . fromAlias ( new route53Targets . LoadBalancerTarget ( this . lb ) ) ,
185
- } )
55
+ secrets . grantRead ( webServer )
56
+ webServer . addEnvironment ( 'SECRETS_ARN' , secrets . secretArn )
186
57
187
- // Setup AutoScaling policy
188
- // TODO: make this configurable in cloud.compute
189
- const scaling = service . autoScaleTaskCount ( { maxCapacity : 2 } )
190
- scaling . scaleOnCpuUtilization ( 'CpuScaling' , {
191
- targetUtilizationPercent : 50 ,
192
- scaleInCooldown : Duration . seconds ( 60 ) ,
193
- scaleOutCooldown : Duration . seconds ( 60 ) ,
194
- } )
195
- scaling . scaleOnMemoryUtilization ( 'MemoryScaling' , {
196
- targetUtilizationPercent : 60 ,
197
- scaleInCooldown : Duration . seconds ( 60 ) ,
198
- scaleOutCooldown : Duration . seconds ( 60 ) ,
58
+ const api = new lambda . FunctionUrl ( scope , 'StacksServerUrl' , {
59
+ function : webServer ,
60
+ authType : lambda . FunctionUrlAuthType . NONE , // becomes a public API
61
+ cors : {
62
+ allowedOrigins : [ '*' ] ,
63
+ } ,
199
64
} )
200
65
201
- // this.compute.fargate.targetGroup.setAttribute('deregistration_delay.timeout_seconds', '0')
202
-
203
- // Allow access to EFS from Fargate ECS
204
- fileSystem . grantRootAccess ( service . taskDefinition . taskRole . grantPrincipal )
205
- fileSystem . connections . allowDefaultPortFrom ( service . connections )
206
-
66
+ const apiVanityUrl = api . url
207
67
const apiPrefix = 'api'
68
+
208
69
new Output ( scope , 'ApiUrl' , {
209
70
value : `https://${ props . domain } /${ apiPrefix } ` ,
210
71
description : 'The URL of the deployed application' ,
211
72
} )
212
73
213
- new Output ( scope , 'LoadBalancerDNSName ' , {
214
- value : `http:// ${ this . lb . loadBalancerDnsName } ` ,
215
- description : 'The DNS name of the load balancer ' ,
74
+ new Output ( scope , 'ApiVanityUrl ' , {
75
+ value : apiVanityUrl ,
76
+ description : 'The Vanity URL of the deployed application ' ,
216
77
} )
217
78
}
218
79
}
0 commit comments