@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.
- package/dist/index.js +144 -60
- package/package.json +34 -33
- package/src/cloud/ai.ts +94 -0
- package/src/cloud/aws-sdk-layer/nodejs/package-lock.json +386 -0
- package/src/cloud/aws-sdk-layer/nodejs/package.json +15 -0
- package/src/cloud/cache.ts +0 -0
- package/src/cloud/cdn.ts +374 -0
- package/src/cloud/cli.ts +45 -0
- package/src/cloud/compute.ts +202 -0
- package/src/cloud/dashboard.ts +85 -0
- package/src/cloud/database.ts +0 -0
- package/src/cloud/deployment.ts +41 -0
- package/src/cloud/dns.ts +34 -0
- package/src/cloud/docs.ts +51 -0
- package/src/cloud/email.ts +339 -0
- package/src/cloud/file-system.ts +35 -0
- package/src/cloud/index.ts +103 -0
- package/src/cloud/jump-box.ts +48 -0
- package/src/cloud/lambda/ask/index.js +35 -0
- package/src/cloud/lambda/cli-setup/index.js +67 -0
- package/src/cloud/lambda/summarize/index.js +35 -0
- package/src/cloud/network.ts +32 -0
- package/src/cloud/package/README.md +59 -0
- package/src/cloud/package/package.json +63 -0
- package/src/cloud/permissions.ts +33 -0
- package/src/cloud/queue.ts +0 -0
- package/src/cloud/redirects.ts +45 -0
- package/src/cloud/router-layer/nodejs/package.json +15 -0
- package/src/cloud/search-engine.ts +108 -0
- package/src/cloud/security.ts +259 -0
- package/src/cloud/storage.ts +168 -0
- package/src/edge/origin-request.ts +49 -0
- package/src/helpers.ts +597 -0
- package/src/index.ts +3 -0
- package/src/runtime/README.md +116 -0
- package/src/runtime/bootstrap +3 -0
- package/src/runtime/example/lambda.ts +36 -0
- package/src/runtime/runtime.ts +830 -0
- package/src/runtime/scripts/build-layer.ts +104 -0
- package/src/runtime/scripts/publish-layer.ts +110 -0
- package/src/runtime/server.ts +39 -0
- package/src/types.ts +28 -0
package/src/cloud/cdn.ts
ADDED
|
@@ -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
|
+
}
|
package/src/cloud/cli.ts
ADDED
|
@@ -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
|
+
}
|