@stacksjs/cloud 0.58.48 → 0.58.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/index.js +144 -60
  2. package/package.json +34 -33
  3. package/src/cloud/ai.ts +94 -0
  4. package/src/cloud/aws-sdk-layer/nodejs/package-lock.json +386 -0
  5. package/src/cloud/aws-sdk-layer/nodejs/package.json +15 -0
  6. package/src/cloud/cache.ts +0 -0
  7. package/src/cloud/cdn.ts +374 -0
  8. package/src/cloud/cli.ts +45 -0
  9. package/src/cloud/compute.ts +202 -0
  10. package/src/cloud/dashboard.ts +85 -0
  11. package/src/cloud/database.ts +0 -0
  12. package/src/cloud/deployment.ts +41 -0
  13. package/src/cloud/dns.ts +34 -0
  14. package/src/cloud/docs.ts +51 -0
  15. package/src/cloud/email.ts +339 -0
  16. package/src/cloud/file-system.ts +35 -0
  17. package/src/cloud/index.ts +103 -0
  18. package/src/cloud/jump-box.ts +48 -0
  19. package/src/cloud/lambda/ask/index.js +35 -0
  20. package/src/cloud/lambda/cli-setup/index.js +67 -0
  21. package/src/cloud/lambda/summarize/index.js +35 -0
  22. package/src/cloud/network.ts +32 -0
  23. package/src/cloud/package/README.md +59 -0
  24. package/src/cloud/package/package.json +63 -0
  25. package/src/cloud/permissions.ts +33 -0
  26. package/src/cloud/queue.ts +0 -0
  27. package/src/cloud/redirects.ts +45 -0
  28. package/src/cloud/router-layer/nodejs/package.json +15 -0
  29. package/src/cloud/search-engine.ts +108 -0
  30. package/src/cloud/security.ts +259 -0
  31. package/src/cloud/storage.ts +168 -0
  32. package/src/edge/origin-request.ts +49 -0
  33. package/src/helpers.ts +597 -0
  34. package/src/index.ts +3 -0
  35. package/src/runtime/README.md +116 -0
  36. package/src/runtime/bootstrap +3 -0
  37. package/src/runtime/example/lambda.ts +36 -0
  38. package/src/runtime/runtime.ts +830 -0
  39. package/src/runtime/scripts/build-layer.ts +104 -0
  40. package/src/runtime/scripts/publish-layer.ts +110 -0
  41. package/src/runtime/server.ts +39 -0
  42. package/src/types.ts +28 -0
@@ -0,0 +1,374 @@
1
+ /* eslint-disable no-new */
2
+ import type { aws_certificatemanager as acm, aws_lambda as lambda, aws_s3 as s3, aws_wafv2 as wafv2 } from 'aws-cdk-lib'
3
+ import { Duration, Fn, CfnOutput as Output, aws_cloudfront as cloudfront, aws_cloudfront_origins as origins, aws_route53 as route53, aws_route53_targets as targets } from 'aws-cdk-lib'
4
+ import type { Construct } from 'constructs'
5
+ import { config } from '@stacksjs/config'
6
+ import { hasFiles } from '@stacksjs/storage'
7
+ import { path as p } from '@stacksjs/path'
8
+ import { env } from '@stacksjs/env'
9
+ import type { NestedCloudProps } from '../types'
10
+ import type { EnvKey } from '../../../../env'
11
+
12
+ export interface CdnStackProps extends NestedCloudProps {
13
+ certificate: acm.Certificate
14
+ logBucket: s3.Bucket
15
+ publicBucket: s3.Bucket
16
+ firewall: wafv2.CfnWebACL
17
+ originRequestFunction: lambda.Function
18
+ zone: route53.IHostedZone
19
+ webServer?: lambda.Function
20
+ webServerUrl?: lambda.FunctionUrl
21
+ cliSetupUrl: lambda.FunctionUrl
22
+ askAiUrl: lambda.FunctionUrl
23
+ summarizeAiUrl: lambda.FunctionUrl
24
+ }
25
+
26
+ export class CdnStack {
27
+ distribution: cloudfront.Distribution
28
+ originAccessIdentity: cloudfront.OriginAccessIdentity
29
+ cdnCachePolicy: cloudfront.CachePolicy
30
+ apiCachePolicy: cloudfront.CachePolicy | undefined
31
+ vanityUrl: string
32
+ props: CdnStackProps
33
+
34
+ constructor(scope: Construct, props: CdnStackProps) {
35
+ this.props = props
36
+
37
+ this.originAccessIdentity = new cloudfront.OriginAccessIdentity(scope, 'OAI')
38
+
39
+ this.cdnCachePolicy = new cloudfront.CachePolicy(scope, 'CdnCachePolicy', {
40
+ comment: 'Stacks CDN Cache Policy',
41
+ cachePolicyName: `${props.slug}-${props.appEnv}-cdn-cache-policy`,
42
+ minTtl: config.cloud.cdn?.minTtl ? Duration.seconds(config.cloud.cdn.minTtl) : undefined,
43
+ defaultTtl: config.cloud.cdn?.defaultTtl ? Duration.seconds(config.cloud.cdn.defaultTtl) : undefined,
44
+ maxTtl: config.cloud.cdn?.maxTtl ? Duration.seconds(config.cloud.cdn.maxTtl) : undefined,
45
+ cookieBehavior: this.getCookieBehavior(config.cloud.cdn?.cookieBehavior),
46
+ })
47
+
48
+ // the actual CDN distribution
49
+ this.distribution = new cloudfront.Distribution(scope, 'Cdn', {
50
+ domainNames: [props.domain],
51
+ defaultRootObject: 'index.html',
52
+ comment: `CDN for ${config.app.url}`,
53
+ certificate: props.certificate,
54
+ enableLogging: true,
55
+ logBucket: props.logBucket,
56
+ httpVersion: cloudfront.HttpVersion.HTTP2_AND_3,
57
+ priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
58
+ enabled: true,
59
+ minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
60
+ webAclId: props.firewall.attrArn,
61
+ enableIpv6: true,
62
+
63
+ defaultBehavior: {
64
+ origin: new origins.S3Origin(props.publicBucket, {
65
+ originAccessIdentity: this.originAccessIdentity,
66
+ }),
67
+ edgeLambdas: [
68
+ {
69
+ eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
70
+ functionVersion: props.originRequestFunction.currentVersion,
71
+ },
72
+ ],
73
+ compress: config.cloud.cdn?.compress,
74
+ allowedMethods: this.allowedMethods(),
75
+ cachedMethods: this.cachedMethods(),
76
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
77
+ cachePolicy: this.cdnCachePolicy,
78
+ },
79
+
80
+ additionalBehaviors: this.additionalBehaviors(scope, props),
81
+
82
+ // Add custom error responses
83
+ errorResponses: [
84
+ {
85
+ httpStatus: 403,
86
+ responsePagePath: '/index.html',
87
+ responseHttpStatus: 200,
88
+ ttl: Duration.seconds(0),
89
+ },
90
+ ],
91
+ })
92
+
93
+ new route53.ARecord(scope, 'AliasRecord', {
94
+ recordName: props.domain,
95
+ zone: props.zone,
96
+ target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(this.distribution)),
97
+ })
98
+
99
+ new Output(scope, 'DistributionId', {
100
+ value: this.distribution.distributionId,
101
+ })
102
+
103
+ new Output(scope, 'AppUrl', {
104
+ value: `https://${props.domain}`,
105
+ description: 'The URL of the deployed application',
106
+ })
107
+
108
+ this.vanityUrl = `https://${this.distribution.domainName}`
109
+ new Output(scope, 'AppVanityUrl', {
110
+ value: this.vanityUrl,
111
+ description: 'The vanity URL of the deployed application',
112
+ })
113
+
114
+ // this.originAccessIdentity = originAccessIdentity
115
+ // this.cdnCachePolicy = cdnCachePolicy
116
+ }
117
+
118
+ getCookieBehavior(behavior: string | undefined): cloudfront.CacheCookieBehavior | undefined {
119
+ switch (behavior) {
120
+ case 'all':
121
+ return cloudfront.CacheCookieBehavior.all()
122
+ case 'none':
123
+ return cloudfront.CacheCookieBehavior.none()
124
+ case 'allowList':
125
+ // If you have a list of cookies, replace `myCookie` with your cookie
126
+ return cloudfront.CacheCookieBehavior.allowList(...config.cloud.cdn?.allowList.cookies || [])
127
+ default:
128
+ return undefined
129
+ }
130
+ }
131
+
132
+ allowedMethods(): cloudfront.AllowedMethods {
133
+ switch (config.cloud.cdn?.allowedMethods) {
134
+ case 'ALL':
135
+ return cloudfront.AllowedMethods.ALLOW_ALL
136
+ case 'GET_HEAD':
137
+ return cloudfront.AllowedMethods.ALLOW_GET_HEAD
138
+ case 'GET_HEAD_OPTIONS':
139
+ return cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS
140
+ default:
141
+ return cloudfront.AllowedMethods.ALLOW_ALL
142
+ }
143
+ }
144
+
145
+ cachedMethods(): cloudfront.CachedMethods {
146
+ switch (config.cloud.cdn?.cachedMethods) {
147
+ case 'GET_HEAD':
148
+ return cloudfront.CachedMethods.CACHE_GET_HEAD
149
+ case 'GET_HEAD_OPTIONS':
150
+ return cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS
151
+ default:
152
+ return cloudfront.CachedMethods.CACHE_GET_HEAD
153
+ }
154
+ }
155
+
156
+ allowedMethodsFromString(methods?: 'ALL' | 'GET_HEAD' | 'GET_HEAD_OPTIONS'): cloudfront.AllowedMethods {
157
+ if (!methods)
158
+ return cloudfront.AllowedMethods.ALLOW_ALL
159
+
160
+ switch (methods) {
161
+ case 'ALL':
162
+ return cloudfront.AllowedMethods.ALLOW_ALL
163
+ case 'GET_HEAD':
164
+ return cloudfront.AllowedMethods.ALLOW_GET_HEAD
165
+ case 'GET_HEAD_OPTIONS':
166
+ return cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS
167
+ default:
168
+ return cloudfront.AllowedMethods.ALLOW_ALL
169
+ }
170
+ }
171
+
172
+ cachedMethodsFromString(methods?: 'GET_HEAD' | 'GET_HEAD_OPTIONS'): cloudfront.CachedMethods {
173
+ if (!methods)
174
+ return cloudfront.CachedMethods.CACHE_GET_HEAD
175
+
176
+ switch (methods) {
177
+ case 'GET_HEAD':
178
+ return cloudfront.CachedMethods.CACHE_GET_HEAD
179
+ case 'GET_HEAD_OPTIONS':
180
+ return cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS
181
+ default:
182
+ return cloudfront.CachedMethods.CACHE_GET_HEAD
183
+ }
184
+ }
185
+
186
+ shouldDeployApi() {
187
+ return config.cloud.api?.deploy
188
+ }
189
+
190
+ apiBehaviorOptions(scope: Construct, props: CdnStackProps): Record<string, cloudfront.BehaviorOptions> {
191
+ const hostname = Fn.select(2, Fn.split('/', props.webServerUrl!.url))
192
+ const origin = (path: '/api' | '/api/*' = '/api') => {
193
+ return new origins.HttpOrigin(hostname, {
194
+ originPath: path,
195
+ protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
196
+ })
197
+ }
198
+
199
+ return {
200
+ '/api': {
201
+ origin: origin(),
202
+ compress: true,
203
+ allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
204
+ cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
205
+ cachePolicy: this.setApiCachePolicy(scope),
206
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
207
+ },
208
+ '/api/*': {
209
+ origin: origin('/api/*'),
210
+ compress: true,
211
+ allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
212
+ cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
213
+ cachePolicy: this.apiCachePolicy,
214
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
215
+ },
216
+ }
217
+ }
218
+
219
+ docsBehaviorOptions(props: CdnStackProps): Record<string, cloudfront.BehaviorOptions> {
220
+ return {
221
+ '/docs': {
222
+ origin: new origins.S3Origin(props.publicBucket, {
223
+ originAccessIdentity: this.originAccessIdentity,
224
+ originPath: '/docs',
225
+ }),
226
+ compress: true,
227
+ allowedMethods: this.allowedMethodsFromString(config.cloud.cdn?.allowedMethods),
228
+ cachedMethods: this.cachedMethodsFromString(config.cloud.cdn?.cachedMethods),
229
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
230
+ cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
231
+ },
232
+ '/docs/*': {
233
+ origin: new origins.S3Origin(props.publicBucket, {
234
+ originAccessIdentity: this.originAccessIdentity,
235
+ originPath: '/docs',
236
+ }),
237
+ compress: true,
238
+ allowedMethods: this.allowedMethodsFromString(config.cloud.cdn?.allowedMethods),
239
+ cachedMethods: this.cachedMethodsFromString(config.cloud.cdn?.cachedMethods),
240
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
241
+ cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
242
+ },
243
+ }
244
+ }
245
+
246
+ aiBehaviorOptions(scope: Construct, props: CdnStackProps): Record<string, cloudfront.BehaviorOptions> {
247
+ const hostname = Fn.select(2, Fn.split('/', props.askAiUrl.url))
248
+ const summaryHostname = Fn.select(2, Fn.split('/', props.summarizeAiUrl.url))
249
+
250
+ const aiCachePolicy = new cloudfront.CachePolicy(scope, 'AiCachePolicy', {
251
+ comment: 'Stacks AI Cache Policy',
252
+ cachePolicyName: `${this.props.slug}-${this.props.appEnv}-ai-cache-policy`,
253
+ defaultTtl: Duration.seconds(0),
254
+ // minTtl: config.cloud.cdn?.minTtl ? Duration.seconds(config.cloud.cdn.minTtl) : undefined,
255
+ cookieBehavior: cloudfront.CacheCookieBehavior.none(),
256
+ headerBehavior: cloudfront.CacheHeaderBehavior.allowList('Accept', 'x-api-key', 'Authorization', 'Content-Type'),
257
+ queryStringBehavior: cloudfront.CacheQueryStringBehavior.all(),
258
+ })
259
+
260
+ return {
261
+ '/ai/ask': {
262
+ origin: new origins.HttpOrigin(hostname, {
263
+ originPath: '/ai',
264
+ protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
265
+ }),
266
+ compress: false,
267
+ allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
268
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
269
+ cachePolicy: aiCachePolicy,
270
+ },
271
+ '/ai/summary': {
272
+ origin: new origins.HttpOrigin(summaryHostname, {
273
+ originPath: '/ai',
274
+ protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
275
+ }),
276
+ compress: false,
277
+ allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
278
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
279
+ cachePolicy: aiCachePolicy,
280
+ },
281
+ }
282
+ }
283
+
284
+ cliSetupBehaviorOptions(scope: Construct, props: CdnStackProps): Record<string, cloudfront.BehaviorOptions> {
285
+ const hostname = Fn.select(2, Fn.split('/', props.cliSetupUrl.url))
286
+
287
+ return {
288
+ '/install': {
289
+ origin: new origins.HttpOrigin(hostname, {
290
+ // origin: new origins.HttpOrigin('tipevv3dfx35fb7ptyq7nrxtga0qkcgc.lambda-url.us-east-1.on.aws', {
291
+ originPath: '/cli-setup',
292
+ protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
293
+ }),
294
+ compress: false,
295
+ allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
296
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
297
+ cachePolicy: new cloudfront.CachePolicy(scope, 'CliSetupCachePolicy', {
298
+ comment: 'Stacks CLI Setup Cache Policy',
299
+ cachePolicyName: `${this.props.slug}-${this.props.appEnv}-cli-setup-cache-policy`,
300
+ defaultTtl: Duration.seconds(0),
301
+ // minTtl: config.cloud.cdn?.minTtl ? Duration.seconds(config.cloud.cdn.minTtl) : undefined,
302
+ cookieBehavior: cloudfront.CacheCookieBehavior.none(),
303
+ headerBehavior: cloudfront.CacheHeaderBehavior.none(),
304
+ queryStringBehavior: cloudfront.CacheQueryStringBehavior.none(),
305
+ }),
306
+ },
307
+ }
308
+ }
309
+
310
+ shouldDeployAiEndpoints() {
311
+ return config.cloud.ai
312
+ }
313
+
314
+ shouldDeployCliSetup() {
315
+ return config.cloud.cli
316
+ }
317
+
318
+ shouldDeployDocs() {
319
+ return hasFiles(p.projectPath('docs'))
320
+ }
321
+
322
+ additionalBehaviors(scope: Construct, props: CdnStackProps): Record<string, cloudfront.BehaviorOptions> {
323
+ let behaviorOptions: Record<string, cloudfront.BehaviorOptions> = {}
324
+
325
+ if (this.shouldDeployApi()) {
326
+ 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', '_']
327
+ keysToRemove.forEach(key => delete env[key as EnvKey])
328
+
329
+ // behaviorOptions = this.apiBehaviorOptions(scope, props)
330
+ }
331
+
332
+ // if docMode is used, we don't need to add a behavior for the docs
333
+ // because the docs will be the root of the site
334
+ if (this.shouldDeployDocs() && !config.app.docMode) {
335
+ behaviorOptions = {
336
+ ...this.docsBehaviorOptions(props),
337
+ ...behaviorOptions,
338
+ }
339
+ }
340
+
341
+ if (this.shouldDeployAiEndpoints()) {
342
+ behaviorOptions = {
343
+ ...this.aiBehaviorOptions(scope, props),
344
+ ...behaviorOptions,
345
+ }
346
+ }
347
+
348
+ if (this.shouldDeployCliSetup()) {
349
+ behaviorOptions = {
350
+ ...this.cliSetupBehaviorOptions(scope, props),
351
+ ...behaviorOptions,
352
+ }
353
+ }
354
+
355
+ return behaviorOptions
356
+ }
357
+
358
+ setApiCachePolicy(scope: Construct) {
359
+ if (this.apiCachePolicy)
360
+ return this.apiCachePolicy
361
+
362
+ this.apiCachePolicy = new cloudfront.CachePolicy(scope, 'ApiCachePolicy', {
363
+ comment: 'Stacks API Cache Policy',
364
+ cachePolicyName: `${this.props.slug}-${this.props.appEnv}-api-cache-policy`,
365
+ defaultTtl: Duration.seconds(0),
366
+ // minTtl: config.cloud.cdn?.minTtl ? Duration.seconds(config.cloud.cdn.minTtl) : undefined,
367
+ cookieBehavior: cloudfront.CacheCookieBehavior.none(),
368
+ headerBehavior: cloudfront.CacheHeaderBehavior.allowList('Accept', 'x-api-key', 'Authorization', 'Content-Type'),
369
+ queryStringBehavior: cloudfront.CacheQueryStringBehavior.none(),
370
+ })
371
+
372
+ return this.apiCachePolicy
373
+ }
374
+ }
@@ -0,0 +1,45 @@
1
+ /* eslint-disable no-new */
2
+ import { Duration, CfnOutput as Output, aws_lambda as lambda } from 'aws-cdk-lib'
3
+ import type { Construct } from 'constructs'
4
+ import type { NestedCloudProps } from '../types'
5
+
6
+ export interface CliStackProps extends NestedCloudProps {
7
+ }
8
+
9
+ export class CliStack {
10
+ cliSetupUrl: lambda.FunctionUrl
11
+
12
+ constructor(scope: Construct, props: CliStackProps) {
13
+ const cliSetupFunc = new lambda.Function(scope, 'CliSetupFunction', {
14
+ functionName: `${props.slug}-${props.appEnv}-cli-setup`,
15
+ description: 'Lambda function that triggers setup script for a Stacks project',
16
+ runtime: lambda.Runtime.NODEJS_20_X,
17
+ handler: 'index.handler',
18
+ code: lambda.Code.fromAsset('src/cloud/lambda/cli-setup'), // path relative to the cloud root package dir
19
+ timeout: Duration.seconds(30),
20
+ })
21
+
22
+ // create a Lambda.FunctionUrl to be used in the CloudFront OriginRequestPolicy
23
+ // this will be used to trigger the function
24
+ this.cliSetupUrl = new lambda.FunctionUrl(scope, 'CliSetupFunctionUrl', {
25
+ function: cliSetupFunc,
26
+ authType: lambda.FunctionUrlAuthType.NONE,
27
+ cors: {
28
+ allowedOrigins: ['*'],
29
+ },
30
+ })
31
+
32
+ new Output(scope, 'CliSetupVanityUrl', {
33
+ value: `${this.cliSetupUrl.url}cli-setup`,
34
+ })
35
+
36
+ new Output(scope, 'CliSetupUrl', {
37
+ value: `https://${props.domain}/install`,
38
+ description: 'URL to trigger the CLI setup function',
39
+ })
40
+
41
+ // once deployed, need to create logic in the cdn origin request to check if the request is for the cli
42
+ // if it is, then we need the to use the function url as the origin
43
+ // if it is not, then we need don't adjust the origin
44
+ }
45
+ }
@@ -0,0 +1,202 @@
1
+ /* eslint-disable no-new */
2
+ 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, RemovalPolicy, aws_ec2 as ec2, aws_ecs as ecs, aws_secretsmanager as secretsmanager } from 'aws-cdk-lib'
4
+ import type { Construct } from 'constructs'
5
+ import { path as p } from '@stacksjs/path'
6
+ import { env } from '@stacksjs/env'
7
+ import { LogGroup } from 'aws-cdk-lib/aws-logs'
8
+ import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'
9
+ import type { NestedCloudProps } from '../types'
10
+ import type { EnvKey } from '../../../../env'
11
+
12
+ export interface ComputeStackProps extends NestedCloudProps {
13
+ vpc: ec2.Vpc
14
+ fileSystem: efs.FileSystem
15
+ zone: route53.IHostedZone
16
+ certificate: acm.Certificate
17
+ }
18
+
19
+ export class ComputeStack {
20
+ lb: elbv2.ApplicationLoadBalancer
21
+ apiServer: lambda.Function
22
+ apiServerUrl: lambda.FunctionUrl
23
+
24
+ constructor(scope: Construct, props: ComputeStackProps) {
25
+ const vpc = props.vpc
26
+ const fileSystem = props.fileSystem
27
+
28
+ if (!fileSystem)
29
+ throw new Error('The file system is missing. Please make sure it was created properly.')
30
+
31
+ const cluster = new ecs.Cluster(scope, 'StacksCluster', {
32
+ clusterName: `${props.slug}-${props.appEnv}-web-server-cluster`,
33
+ vpc,
34
+ })
35
+
36
+ const taskDefinition = new ecs.FargateTaskDefinition(scope, 'TaskDefinition', {
37
+ family: `${props.appName}-${props.appEnv}-api`,
38
+ memoryLimitMiB: 512, // Match your Lambda memory size
39
+ cpu: 256, // Choose an appropriate value
40
+ runtimePlatform: {
41
+ cpuArchitecture: ecs.CpuArchitecture.ARM64,
42
+ },
43
+ })
44
+
45
+ const container = taskDefinition.addContainer('WebServerContainer', {
46
+ containerName: `${props.appName}-${props.appEnv}-api`,
47
+ image: ecs.ContainerImage.fromAsset(p.frameworkPath('server')),
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
+ },
119
+ })
120
+
121
+ const service = new ecs.FargateService(scope, 'StacksApiService', {
122
+ serviceName: `${props.appName}-${props.appEnv}-api-service`,
123
+ cluster,
124
+ taskDefinition,
125
+ desiredCount: 2,
126
+ assignPublicIp: true,
127
+ maxHealthyPercent: 200,
128
+ vpcSubnets: vpc.selectSubnets({
129
+ subnetType: ec2.SubnetType.PUBLIC,
130
+ onePerAz: true,
131
+ }),
132
+ minHealthyPercent: 75,
133
+ securityGroups: [serviceSecurityGroup],
134
+ })
135
+
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]),
142
+ })
143
+
144
+ props.fileSystem.connections.allowFromAnyIpv4(ec2.Port.tcp(2049)) // port 2049 (NFS) for EFS
145
+
146
+ const volumeName = `${props.slug}-${props.appEnv}-efs`
147
+ taskDefinition.addVolume({
148
+ name: volumeName,
149
+ efsVolumeConfiguration: {
150
+ fileSystemId: props.fileSystem.fileSystemId,
151
+ },
152
+ })
153
+
154
+ container.addMountPoints({
155
+ sourceVolume: volumeName,
156
+ containerPath: '/mnt/efs',
157
+ readOnly: false,
158
+ })
159
+
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
+
176
+ 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', '_']
177
+ keysToRemove.forEach(key => delete env[key as EnvKey])
178
+
179
+ const secrets = new secretsmanager.Secret(scope, 'StacksSecrets', {
180
+ secretName: `${props.slug}-${props.appEnv}-secrets`,
181
+ description: 'Secrets for the Stacks application',
182
+ generateSecretString: {
183
+ secretStringTemplate: JSON.stringify(env),
184
+ generateStringKey: Object.keys(env).join(',').length.toString(),
185
+ },
186
+ })
187
+
188
+ secrets.grantRead(service.taskDefinition.executionRole!)
189
+ container.addEnvironment('SECRETS_ARN', secrets.secretArn)
190
+
191
+ const apiPrefix = 'api'
192
+ new Output(scope, 'ApiUrl', {
193
+ value: `https://${props.domain}/${apiPrefix}`,
194
+ description: 'The URL of the deployed application',
195
+ })
196
+
197
+ new Output(scope, 'ApiVanityUrl', {
198
+ value: `http://${this.lb.loadBalancerDnsName}`,
199
+ description: 'The Vanity URL / DNS name of the load balancer',
200
+ })
201
+ }
202
+ }