@stacksjs/cloud 0.67.0 → 0.68.1
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/ai.d.ts +20 -0
- package/dist/cdn.d.ts +271 -0
- package/dist/cli.d.ts +12 -0
- package/dist/compute.d.ts +221 -0
- package/dist/database.d.ts +29 -0
- package/dist/deployment.d.ts +60 -0
- package/dist/dns.d.ts +34 -0
- package/dist/docs.d.ts +34 -0
- package/dist/email.d.ts +336 -0
- package/dist/file-system.d.ts +32 -0
- package/dist/helpers.d.ts +50 -50
- package/dist/index.d.ts +2 -2
- package/dist/index.js +19 -28
- package/dist/jump-box.d.ts +42 -0
- package/dist/network.d.ts +29 -0
- package/dist/origin-request.d.ts +5 -0
- package/dist/permissions.d.ts +25 -0
- package/dist/queue.d.ts +144 -0
- package/dist/redirects.d.ts +45 -0
- package/dist/security.d.ts +149 -0
- package/dist/storage.d.ts +189 -0
- package/dist/types.d.ts +26 -25
- package/package.json +18 -18
- package/dist/cloud/ai.d.ts +0 -6
- package/dist/cloud/cache.d.ts +0 -0
- package/dist/cloud/cdn.d.ts +0 -34
- package/dist/cloud/cli.d.ts +0 -6
- package/dist/cloud/compute.d.ts +0 -17
- package/dist/cloud/dashboard.d.ts +0 -0
- package/dist/cloud/database.d.ts +0 -11
- package/dist/cloud/deployment.d.ts +0 -17
- package/dist/cloud/dns.d.ts +0 -7
- package/dist/cloud/docs.d.ts +0 -8
- package/dist/cloud/email.d.ts +0 -10
- package/dist/cloud/file-system.d.ts +0 -12
- package/dist/cloud/index.d.ts +0 -42
- package/dist/cloud/jump-box.d.ts +0 -12
- package/dist/cloud/network.d.ts +0 -8
- package/dist/cloud/permissions.d.ts +0 -6
- package/dist/cloud/queue.d.ts +0 -22
- package/dist/cloud/redirects.d.ts +0 -8
- package/dist/cloud/search-engine.d.ts +0 -0
- package/dist/cloud/security.d.ts +0 -15
- package/dist/cloud/storage.d.ts +0 -21
- package/dist/edge/origin-request.d.ts +0 -1
package/dist/ai.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Construct } from 'constructs';
|
|
2
|
+
import type { NestedCloudProps } from '../types';
|
|
3
|
+
|
|
4
|
+
export declare interface AiStackProps extends NestedCloudProps {}
|
|
5
|
+
export declare class AiStack {
|
|
6
|
+
constructor(scope: Construct, props: AiStackProps) {
|
|
7
|
+
const bedrockAccessPolicy = new iam.PolicyStatement({
|
|
8
|
+
effect: iam.Effect.ALLOW,
|
|
9
|
+
actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'],
|
|
10
|
+
resources: config.ai.models?.map((model: string) => `arn:aws:bedrock:us-east-1::foundation-model/${model}`),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const bedrockAccessRole = new iam.Role(scope, 'BedrockAccessRole', {
|
|
14
|
+
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
|
|
15
|
+
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy')],
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
bedrockAccessRole.addToPolicy(bedrockAccessPolicy)
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/cdn.d.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import type { ApplicationLoadBalancer } from 'aws-cdk-lib/aws-elasticloadbalancingv2';
|
|
2
|
+
import type { Construct } from 'constructs';
|
|
3
|
+
import type { NestedCloudProps } from '../types';
|
|
4
|
+
|
|
5
|
+
export declare interface CdnStackProps extends NestedCloudProps {
|
|
6
|
+
certificate: acm.Certificate
|
|
7
|
+
logBucket: s3.Bucket
|
|
8
|
+
publicBucket: s3.Bucket
|
|
9
|
+
docsBucket?: s3.Bucket
|
|
10
|
+
firewall: wafv2.CfnWebACL
|
|
11
|
+
originRequestFunction: lambda.Function
|
|
12
|
+
zone: route53.IHostedZone
|
|
13
|
+
webServer?: lambda.Function
|
|
14
|
+
webServerUrl?: lambda.FunctionUrl
|
|
15
|
+
lb?: ApplicationLoadBalancer
|
|
16
|
+
}
|
|
17
|
+
export declare class CdnStack {
|
|
18
|
+
mainDistribution: cloudfront.Distribution
|
|
19
|
+
docsDistribution: cloudfront.Distribution | undefined
|
|
20
|
+
cdnCachePolicy: cloudfront.CachePolicy
|
|
21
|
+
mainVanityUrl!: string
|
|
22
|
+
docsVanityUrl: string | undefined
|
|
23
|
+
realtimeLogConfig!: cloudfront.RealtimeLogConfig
|
|
24
|
+
props: CdnStackProps
|
|
25
|
+
|
|
26
|
+
constructor(scope: Construct, props: CdnStackProps) {
|
|
27
|
+
this.props = props
|
|
28
|
+
|
|
29
|
+
this.cdnCachePolicy = new cloudfront.CachePolicy(scope, 'CdnCachePolicy', {
|
|
30
|
+
comment: 'Stacks CDN Cache Policy',
|
|
31
|
+
cachePolicyName: `${props.slug}-${props.appEnv}-cdn-cache-policy`,
|
|
32
|
+
minTtl: config.cloud.cdn?.minTtl ? Duration.seconds(config.cloud.cdn.minTtl) : undefined,
|
|
33
|
+
defaultTtl: config.cloud.cdn?.defaultTtl ? Duration.seconds(config.cloud.cdn.defaultTtl) : undefined,
|
|
34
|
+
maxTtl: config.cloud.cdn?.maxTtl ? Duration.seconds(config.cloud.cdn.maxTtl) : undefined,
|
|
35
|
+
cookieBehavior: this.getCookieBehavior(config.cloud.cdn?.cookieBehavior),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const originAccessControl = new cloudfront.S3OriginAccessControl(scope, 'WebOAC', {
|
|
39
|
+
originAccessControlName: `${props.slug}-${props.appEnv}-web-oac-${props.timestamp}`,
|
|
40
|
+
description: 'Access from CloudFront to the frontend bucket.',
|
|
41
|
+
signing: cloudfront.Signing.SIGV4_NO_OVERRIDE,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
if (config.app.docMode) {
|
|
45
|
+
this.mainDistribution = this.createDistribution(
|
|
46
|
+
scope,
|
|
47
|
+
props,
|
|
48
|
+
props.docsBucket as s3.Bucket,
|
|
49
|
+
this.createDocsOriginRequestFunction(scope),
|
|
50
|
+
props.domain,
|
|
51
|
+
'MainCdn',
|
|
52
|
+
originAccessControl,
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
this.mainDistribution = this.createDistribution(
|
|
57
|
+
scope,
|
|
58
|
+
props,
|
|
59
|
+
props.publicBucket,
|
|
60
|
+
props.originRequestFunction,
|
|
61
|
+
props.domain,
|
|
62
|
+
'MainCdn',
|
|
63
|
+
originAccessControl,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if (props.docsBucket) {
|
|
67
|
+
this.docsDistribution = this.createDistribution(
|
|
68
|
+
scope,
|
|
69
|
+
props,
|
|
70
|
+
props.docsBucket,
|
|
71
|
+
this.createDocsOriginRequestFunction(scope),
|
|
72
|
+
`docs.${props.domain}`,
|
|
73
|
+
'DocsCdn',
|
|
74
|
+
originAccessControl,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.createRoute53Records(scope, props)
|
|
80
|
+
|
|
81
|
+
this.createOutputs(scope, props)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
createDistribution(
|
|
85
|
+
scope: Construct,
|
|
86
|
+
props: CdnStackProps,
|
|
87
|
+
sourceBucket: s3.Bucket,
|
|
88
|
+
originRequestFunction: lambda.Function,
|
|
89
|
+
domainName: string,
|
|
90
|
+
id: string,
|
|
91
|
+
originAccessControl: cloudfront.S3OriginAccessControl,
|
|
92
|
+
): cloudfront.Distribution {
|
|
93
|
+
return new cloudfront.Distribution(scope, id, {
|
|
94
|
+
domainNames: [domainName],
|
|
95
|
+
defaultRootObject: 'index.html',
|
|
96
|
+
comment: `CDN for ${domainName}`,
|
|
97
|
+
certificate: props.certificate,
|
|
98
|
+
enableLogging: true,
|
|
99
|
+
logBucket: props.logBucket,
|
|
100
|
+
httpVersion: cloudfront.HttpVersion.HTTP2_AND_3,
|
|
101
|
+
priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
|
|
102
|
+
enabled: true,
|
|
103
|
+
minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
|
|
104
|
+
webAclId: props.firewall.attrArn,
|
|
105
|
+
enableIpv6: true,
|
|
106
|
+
defaultBehavior: {
|
|
107
|
+
origin: new origins.S3StaticWebsiteOrigin(sourceBucket, {
|
|
108
|
+
originPath: '/',
|
|
109
|
+
originAccessControlId: originAccessControl.originAccessControlId,
|
|
110
|
+
}),
|
|
111
|
+
edgeLambdas: [
|
|
112
|
+
{
|
|
113
|
+
eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
|
|
114
|
+
functionVersion: originRequestFunction.currentVersion,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
compress: config.cloud.cdn?.compress,
|
|
118
|
+
allowedMethods: this.allowedMethods(),
|
|
119
|
+
cachedMethods: this.cachedMethods(),
|
|
120
|
+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
121
|
+
cachePolicy: this.cdnCachePolicy,
|
|
122
|
+
},
|
|
123
|
+
errorResponses: [
|
|
124
|
+
{
|
|
125
|
+
httpStatus: 404,
|
|
126
|
+
responsePagePath: '/index.html',
|
|
127
|
+
responseHttpStatus: 200,
|
|
128
|
+
ttl: Duration.millis(0),
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
httpStatus: 403,
|
|
132
|
+
responsePagePath: '/index.html',
|
|
133
|
+
responseHttpStatus: 200,
|
|
134
|
+
ttl: Duration.millis(0),
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
createRoute53Records(scope: Construct, props: CdnStackProps): void {
|
|
141
|
+
new route53.ARecord(scope, 'MainAliasRecord', {
|
|
142
|
+
recordName: props.domain,
|
|
143
|
+
zone: props.zone,
|
|
144
|
+
target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(this.mainDistribution)),
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
if (this.docsDistribution) {
|
|
148
|
+
new route53.ARecord(scope, 'DocsAliasRecord', {
|
|
149
|
+
recordName: `docs.${props.domain}`,
|
|
150
|
+
zone: props.zone,
|
|
151
|
+
target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(this.docsDistribution)),
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
createOutputs(scope: Construct, props: CdnStackProps): void {
|
|
157
|
+
new Output(scope, 'MainDistributionId', {
|
|
158
|
+
value: this.mainDistribution.distributionId,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
new Output(scope, 'MainUrl', {
|
|
162
|
+
value: `https:
|
|
163
|
+
description: 'The URL of the deployed main application',
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
this.mainVanityUrl = `https:
|
|
167
|
+
new Output(scope, 'MainVanityUrl', {
|
|
168
|
+
value: this.mainVanityUrl,
|
|
169
|
+
description: 'The vanity URL of the deployed main application',
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
if (this.docsDistribution) {
|
|
173
|
+
new Output(scope, 'DocsDistributionId', {
|
|
174
|
+
value: this.docsDistribution.distributionId,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
this.docsVanityUrl = `https:
|
|
178
|
+
new Output(scope, 'DocsAppVanityUrl', {
|
|
179
|
+
value: this.docsVanityUrl,
|
|
180
|
+
description: 'The vanity URL of the deployed docs application',
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
createDocsOriginRequestFunction(scope: Construct): lambda.Function {
|
|
186
|
+
const docsOriginRequestFunction = new lambda.Function(scope, 'DocsOriginRequestFunction', {
|
|
187
|
+
functionName: `${this.props.slug}-${this.props.appEnv}-docs-origin-request-function-${this.props.timestamp}`,
|
|
188
|
+
description: 'Custom origin request function for the docs',
|
|
189
|
+
runtime: lambda.Runtime.NODEJS_20_X,
|
|
190
|
+
handler: 'index.handler',
|
|
191
|
+
code: lambda.Code.fromInline(`
|
|
192
|
+
const config = {
|
|
193
|
+
suffix: '.html',
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const regexSuffixless = /\\/[^/.]+$/
|
|
197
|
+
const regexTrailingSlash = /.+\\/$/
|
|
198
|
+
|
|
199
|
+
exports.handler = (event, context, callback) => {
|
|
200
|
+
const request = event.Records[0].cf.request;
|
|
201
|
+
const uri = request.uri;
|
|
202
|
+
|
|
203
|
+
if (uri === '/') {
|
|
204
|
+
request.uri = '/index.html';
|
|
205
|
+
callback(null, request);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (uri.match(regexSuffixless)) {
|
|
210
|
+
request.uri = uri + '.html';
|
|
211
|
+
callback(null, request);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (uri.match(regexTrailingSlash)) {
|
|
216
|
+
request.uri = uri.slice(0, -1) + '.html';
|
|
217
|
+
callback(null, request);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
callback(null, request);
|
|
222
|
+
};
|
|
223
|
+
`),
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
new lambda.CfnPermission(scope, 'DocsOriginRequestFunctionPermission', {
|
|
227
|
+
action: 'lambda:InvokeFunction',
|
|
228
|
+
principal: 'edgelambda.amazonaws.com',
|
|
229
|
+
functionName: docsOriginRequestFunction.functionName,
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
return docsOriginRequestFunction
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
getCookieBehavior(cookieBehavior: string | undefined): cloudfront.CacheCookieBehavior | undefined {
|
|
236
|
+
switch (cookieBehavior) {
|
|
237
|
+
case 'none':
|
|
238
|
+
return cloudfront.CacheCookieBehavior.none()
|
|
239
|
+
case 'all':
|
|
240
|
+
return cloudfront.CacheCookieBehavior.all()
|
|
241
|
+
case 'allowList':
|
|
242
|
+
return cloudfront.CacheCookieBehavior.allowList(...(config.cloud.cdn?.allowList.cookies || []))
|
|
243
|
+
default:
|
|
244
|
+
return undefined
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
allowedMethods(): cloudfront.AllowedMethods {
|
|
249
|
+
switch (config.cloud.cdn?.allowedMethods) {
|
|
250
|
+
case 'ALL':
|
|
251
|
+
return cloudfront.AllowedMethods.ALLOW_ALL
|
|
252
|
+
case 'GET_HEAD':
|
|
253
|
+
return cloudfront.AllowedMethods.ALLOW_GET_HEAD
|
|
254
|
+
case 'GET_HEAD_OPTIONS':
|
|
255
|
+
return cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS
|
|
256
|
+
default:
|
|
257
|
+
return cloudfront.AllowedMethods.ALLOW_ALL
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
cachedMethods(): cloudfront.CachedMethods {
|
|
262
|
+
switch (config.cloud.cdn?.cachedMethods) {
|
|
263
|
+
case 'GET_HEAD':
|
|
264
|
+
return cloudfront.CachedMethods.CACHE_GET_HEAD
|
|
265
|
+
case 'GET_HEAD_OPTIONS':
|
|
266
|
+
return cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS
|
|
267
|
+
default:
|
|
268
|
+
return cloudfront.CachedMethods.CACHE_GET_HEAD
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Construct } from 'constructs';
|
|
2
|
+
import type { NestedCloudProps } from '../types';
|
|
3
|
+
|
|
4
|
+
export declare interface CliStackProps extends NestedCloudProps {}
|
|
5
|
+
export declare class CliStack {
|
|
6
|
+
constructor(scope: Construct, props: CliStackProps) {
|
|
7
|
+
new Output(scope, 'CliSetupUrl', {
|
|
8
|
+
value: `https:
|
|
9
|
+
description: 'URL to trigger the CLI setup function',
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { Construct } from 'constructs';
|
|
2
|
+
import type { NestedCloudProps } from '../types';
|
|
3
|
+
|
|
4
|
+
export declare interface ComputeStackProps extends NestedCloudProps {
|
|
5
|
+
vpc: ec2.Vpc
|
|
6
|
+
fileSystem: efs.FileSystem
|
|
7
|
+
zone: route53.IHostedZone
|
|
8
|
+
certificate: acm.Certificate
|
|
9
|
+
}
|
|
10
|
+
export declare class ComputeStack {
|
|
11
|
+
lb: elbv2.ApplicationLoadBalancer
|
|
12
|
+
cluster: ecs.Cluster
|
|
13
|
+
taskDefinition: ecs.FargateTaskDefinition
|
|
14
|
+
|
|
15
|
+
constructor(scope: Construct, props: ComputeStackProps) {
|
|
16
|
+
const vpc = props.vpc
|
|
17
|
+
const fileSystem = props.fileSystem
|
|
18
|
+
|
|
19
|
+
if (!fileSystem)
|
|
20
|
+
throw new Error('The file system is missing. Please make sure it was created properly.')
|
|
21
|
+
|
|
22
|
+
this.cluster = new ecs.Cluster(scope, 'StacksCluster', {
|
|
23
|
+
clusterName: `${props.slug}-${props.appEnv}-web-server-cluster`,
|
|
24
|
+
vpc,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
this.taskDefinition = new ecs.FargateTaskDefinition(scope, 'TaskDefinition', {
|
|
28
|
+
family: `${props.appName}-${props.appEnv}-api`,
|
|
29
|
+
memoryLimitMiB: 512,
|
|
30
|
+
cpu: 256,
|
|
31
|
+
runtimePlatform: {
|
|
32
|
+
cpuArchitecture: ecs.CpuArchitecture.ARM64,
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
const container = this.taskDefinition.addContainer('WebServerContainer', {
|
|
38
|
+
containerName: `${props.appName}-${props.appEnv}-api`,
|
|
39
|
+
image: ecs.ContainerImage.fromAsset(p.frameworkPath('server')),
|
|
40
|
+
logging: new ecs.AwsLogDriver({
|
|
41
|
+
streamPrefix: `${props.appName}-${props.appEnv}-web-server-logs`,
|
|
42
|
+
logGroup: new LogGroup(scope, 'StacksApiLogs', {
|
|
43
|
+
logGroupName: '/aws/ecs/stacks-api',
|
|
44
|
+
removalPolicy: RemovalPolicy.DESTROY,
|
|
45
|
+
}),
|
|
46
|
+
}),
|
|
47
|
+
healthCheck: {
|
|
48
|
+
command: ['CMD-SHELL', 'curl -f http:
|
|
49
|
+
interval: Duration.seconds(10),
|
|
50
|
+
timeout: Duration.seconds(5),
|
|
51
|
+
retries: 3,
|
|
52
|
+
startPeriod: Duration.seconds(10),
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
container.addPortMappings({
|
|
57
|
+
containerPort: 3000,
|
|
58
|
+
hostPort: 3000,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const serviceSecurityGroup = new ec2.SecurityGroup(scope, 'ServiceSecurityGroup', {
|
|
62
|
+
securityGroupName: `${props.appName}-${props.appEnv}-api-service-sg`,
|
|
63
|
+
vpc,
|
|
64
|
+
description: 'Stacks Security Group for API Service',
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const publicLoadBalancerSG = new ec2.SecurityGroup(scope, 'PublicLoadBalancerSG', {
|
|
68
|
+
securityGroupName: `${props.appName}-${props.appEnv}-public-load-balancer-sg`,
|
|
69
|
+
vpc,
|
|
70
|
+
description: 'Access to the public facing load balancer',
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
serviceSecurityGroup.addIngressRule(publicLoadBalancerSG, ec2.Port.allTraffic(), 'Ingress from the public ALB')
|
|
74
|
+
|
|
75
|
+
this.lb = new elbv2.ApplicationLoadBalancer(scope, 'ApplicationLoadBalancer', {
|
|
76
|
+
http2Enabled: true,
|
|
77
|
+
loadBalancerName: `${props.appName}-${props.appEnv}-alb`,
|
|
78
|
+
vpc,
|
|
79
|
+
vpcSubnets: {
|
|
80
|
+
subnets: vpc.selectSubnets({
|
|
81
|
+
subnetType: ec2.SubnetType.PUBLIC,
|
|
82
|
+
onePerAz: true,
|
|
83
|
+
}).subnets,
|
|
84
|
+
},
|
|
85
|
+
internetFacing: true,
|
|
86
|
+
idleTimeout: Duration.seconds(30),
|
|
87
|
+
securityGroup: publicLoadBalancerSG,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
new route53.ARecord(scope, 'ApiDomainAliasRecord', {
|
|
91
|
+
zone: props.zone,
|
|
92
|
+
recordName: 'api',
|
|
93
|
+
target: route53.RecordTarget.fromAlias(new route53Targets.LoadBalancerTarget(this.lb)),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const serviceTargetGroup = new elbv2.ApplicationTargetGroup(scope, 'ServiceTargetGroup', {
|
|
97
|
+
targetGroupName: `${props.appName}-${props.appEnv}-api-tg`,
|
|
98
|
+
vpc,
|
|
99
|
+
targetType: elbv2.TargetType.IP,
|
|
100
|
+
protocol: elbv2.ApplicationProtocol.HTTP,
|
|
101
|
+
port: 3000,
|
|
102
|
+
healthCheck: {
|
|
103
|
+
interval: Duration.seconds(6),
|
|
104
|
+
path: '/health',
|
|
105
|
+
protocol: elbv2.Protocol.HTTP,
|
|
106
|
+
timeout: Duration.seconds(5),
|
|
107
|
+
healthyThresholdCount: 2,
|
|
108
|
+
unhealthyThresholdCount: 10,
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const service = new ecs.FargateService(scope, 'StacksApiService', {
|
|
113
|
+
serviceName: `${props.appName}-${props.appEnv}-api-service`,
|
|
114
|
+
cluster: this.cluster,
|
|
115
|
+
taskDefinition: this.taskDefinition,
|
|
116
|
+
desiredCount: 1,
|
|
117
|
+
assignPublicIp: true,
|
|
118
|
+
maxHealthyPercent: 200,
|
|
119
|
+
vpcSubnets: vpc.selectSubnets({
|
|
120
|
+
subnetType: ec2.SubnetType.PUBLIC,
|
|
121
|
+
onePerAz: true,
|
|
122
|
+
}),
|
|
123
|
+
minHealthyPercent: 75,
|
|
124
|
+
securityGroups: [serviceSecurityGroup],
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
service.attachToApplicationTargetGroup(serviceTargetGroup)
|
|
128
|
+
publicLoadBalancerSG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.allTraffic())
|
|
129
|
+
|
|
130
|
+
this.lb.addListener('HttpsListener', {
|
|
131
|
+
port: 443,
|
|
132
|
+
certificates: [props.certificate],
|
|
133
|
+
defaultAction: elbv2.ListenerAction.forward([serviceTargetGroup]),
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
this.lb.addListener('HttpListener', {
|
|
137
|
+
port: 80,
|
|
138
|
+
defaultAction: elbv2.ListenerAction.forward([serviceTargetGroup]),
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
props.fileSystem.connections.allowFromAnyIpv4(ec2.Port.tcp(2049))
|
|
142
|
+
|
|
143
|
+
const volumeName = `${props.slug}-${props.appEnv}-efs`
|
|
144
|
+
this.taskDefinition.addVolume({
|
|
145
|
+
name: volumeName,
|
|
146
|
+
efsVolumeConfiguration: {
|
|
147
|
+
fileSystemId: props.fileSystem.fileSystemId,
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
container.addMountPoints({
|
|
152
|
+
sourceVolume: volumeName,
|
|
153
|
+
containerPath: '/mnt/efs',
|
|
154
|
+
readOnly: false,
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const scaling = service.autoScaleTaskCount({ maxCapacity: 2 })
|
|
158
|
+
|
|
159
|
+
scaling.scaleOnCpuUtilization('CpuScaling', {
|
|
160
|
+
targetUtilizationPercent: 50,
|
|
161
|
+
scaleInCooldown: Duration.seconds(60),
|
|
162
|
+
scaleOutCooldown: Duration.seconds(60),
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
scaling.scaleOnMemoryUtilization('MemoryScaling', {
|
|
166
|
+
targetUtilizationPercent: 60,
|
|
167
|
+
scaleInCooldown: Duration.seconds(60),
|
|
168
|
+
scaleOutCooldown: Duration.seconds(60),
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
const keysToRemove = [
|
|
172
|
+
'_HANDLER',
|
|
173
|
+
'_X_AMZN_TRACE_ID',
|
|
174
|
+
'AWS_REGION',
|
|
175
|
+
'AWS_EXECUTION_ENV',
|
|
176
|
+
'AWS_LAMBDA_FUNCTION_NAME',
|
|
177
|
+
'AWS_LAMBDA_FUNCTION_MEMORY_SIZE',
|
|
178
|
+
'AWS_LAMBDA_FUNCTION_VERSION',
|
|
179
|
+
'AWS_LAMBDA_INITIALIZATION_TYPE',
|
|
180
|
+
'AWS_LAMBDA_LOG_GROUP_NAME',
|
|
181
|
+
'AWS_LAMBDA_LOG_STREAM_NAME',
|
|
182
|
+
'AWS_ACCESS_KEY',
|
|
183
|
+
'AWS_ACCESS_KEY_ID',
|
|
184
|
+
'AWS_SECRET_ACCESS_KEY',
|
|
185
|
+
'AWS_SESSION_TOKEN',
|
|
186
|
+
'AWS_LAMBDA_RUNTIME_API',
|
|
187
|
+
'LAMBDA_TASK_ROOT',
|
|
188
|
+
'LAMBDA_RUNTIME_DIR',
|
|
189
|
+
'_',
|
|
190
|
+
]
|
|
191
|
+
keysToRemove.forEach(key => delete env[key as EnvKey])
|
|
192
|
+
|
|
193
|
+
const secrets = new secretsmanager.Secret(scope, 'StacksSecrets', {
|
|
194
|
+
secretName: `${props.slug}-${props.appEnv}-secrets`,
|
|
195
|
+
description: 'Secrets for the Stacks application',
|
|
196
|
+
generateSecretString: {
|
|
197
|
+
secretStringTemplate: JSON.stringify(env),
|
|
198
|
+
generateStringKey: Object.keys(env).join(',').length.toString(),
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
if (service.taskDefinition.executionRole) {
|
|
203
|
+
secrets.grantRead(service.taskDefinition.executionRole)
|
|
204
|
+
container.addEnvironment('SECRETS_ARN', secrets.secretArn)
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
throw new Error('Service task execution role is undefined.')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const apiPrefix = 'api'
|
|
211
|
+
new Output(scope, 'ApiUrl', {
|
|
212
|
+
value: `https:
|
|
213
|
+
description: 'The URL of the deployed application',
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
new Output(scope, 'ApiVanityUrl', {
|
|
217
|
+
value: `http:
|
|
218
|
+
description: 'The Vanity URL / DNS name of the load balancer',
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Construct } from 'constructs';
|
|
2
|
+
import type { NestedCloudProps } from '../types';
|
|
3
|
+
|
|
4
|
+
export declare interface DatabaseStackProps extends NestedCloudProps {
|
|
5
|
+
vpc: ec2.Vpc
|
|
6
|
+
}
|
|
7
|
+
export declare class DatabaseStack {
|
|
8
|
+
database: dynamodb.Table
|
|
9
|
+
|
|
10
|
+
constructor(scope: Construct, props: DatabaseStackProps) {
|
|
11
|
+
this.database = new dynamodb.Table(scope, 'Database', {
|
|
12
|
+
tableName: `${props.slug}-${props.appEnv}-database`,
|
|
13
|
+
partitionKey: {
|
|
14
|
+
name: 'id',
|
|
15
|
+
type: dynamodb.AttributeType.STRING,
|
|
16
|
+
},
|
|
17
|
+
sortKey: {
|
|
18
|
+
name: 'sort',
|
|
19
|
+
type: dynamodb.AttributeType.STRING,
|
|
20
|
+
},
|
|
21
|
+
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
|
|
22
|
+
readCapacity: 5,
|
|
23
|
+
writeCapacity: 5,
|
|
24
|
+
pointInTimeRecovery: true,
|
|
25
|
+
removalPolicy: RemovalPolicy.DESTROY,
|
|
26
|
+
encryption: dynamodb.TableEncryption.AWS_MANAGED,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Construct } from 'constructs';
|
|
2
|
+
import type { NestedCloudProps } from '../types';
|
|
3
|
+
|
|
4
|
+
export declare interface DeploymentStackProps extends NestedCloudProps {
|
|
5
|
+
publicBucket: s3.Bucket
|
|
6
|
+
docsBucket?: s3.Bucket
|
|
7
|
+
privateBucket: s3.Bucket
|
|
8
|
+
mainDistribution: cloudfront.Distribution
|
|
9
|
+
docsDistribution?: cloudfront.Distribution
|
|
10
|
+
}
|
|
11
|
+
export declare class DeploymentStack {
|
|
12
|
+
privateSource: string
|
|
13
|
+
docsSource: string
|
|
14
|
+
publicSource: string
|
|
15
|
+
|
|
16
|
+
constructor(scope: Construct, props: DeploymentStackProps) {
|
|
17
|
+
this.privateSource = '../../private'
|
|
18
|
+
this.docsSource = '../docs/dist/'
|
|
19
|
+
this.publicSource = config.app.docMode === true ? this.docsSource : '../views/web/dist/'
|
|
20
|
+
const mainBucket = config.app.docMode === true ? props.docsBucket : props.publicBucket
|
|
21
|
+
|
|
22
|
+
new s3deploy.BucketDeployment(scope, 'Website', {
|
|
23
|
+
sources: [
|
|
24
|
+
s3deploy.Source.asset(this.publicSource, {
|
|
25
|
+
assetHash: websiteSourceHash(),
|
|
26
|
+
assetHashType: AssetHashType.CUSTOM,
|
|
27
|
+
}),
|
|
28
|
+
],
|
|
29
|
+
destinationBucket: mainBucket as s3.Bucket,
|
|
30
|
+
distribution: props.mainDistribution,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
if (this.shouldDeployDocs()) {
|
|
34
|
+
new s3deploy.BucketDeployment(scope, 'Docs', {
|
|
35
|
+
sources: [
|
|
36
|
+
s3deploy.Source.asset(this.docsSource, {
|
|
37
|
+
assetHash: docsSourceHash(),
|
|
38
|
+
assetHashType: AssetHashType.CUSTOM,
|
|
39
|
+
}),
|
|
40
|
+
],
|
|
41
|
+
destinationBucket: props.docsBucket as s3.Bucket,
|
|
42
|
+
distribution: props.docsDistribution,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (hasFiles(this.privateSource)) {
|
|
47
|
+
new s3deploy.BucketDeployment(scope, 'PrivateFiles', {
|
|
48
|
+
sources: [s3deploy.Source.asset(this.privateSource)],
|
|
49
|
+
destinationBucket: props.privateBucket,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.error(`The path ${this.privateSource} does not have any files`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
shouldDeployDocs(): boolean {
|
|
58
|
+
return hasFiles(p.projectPath('docs')) && !config.app.docMode
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/dns.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Construct } from 'constructs';
|
|
2
|
+
import type { NestedCloudProps } from '../types';
|
|
3
|
+
|
|
4
|
+
export declare class DnsStack {
|
|
5
|
+
zone: route53.IHostedZone
|
|
6
|
+
|
|
7
|
+
constructor(scope: Construct, props: NestedCloudProps) {
|
|
8
|
+
this.zone = route53.PublicHostedZone.fromLookup(scope, 'AppUrlHostedZone', {
|
|
9
|
+
domainName: props.domain,
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const wwwBucket = new s3.Bucket(scope, 'WwwBucket', {
|
|
13
|
+
bucketName: `www.${props.domain}`,
|
|
14
|
+
websiteRedirect: {
|
|
15
|
+
hostName: props.domain,
|
|
16
|
+
protocol: s3.RedirectProtocol.HTTPS,
|
|
17
|
+
},
|
|
18
|
+
removalPolicy: RemovalPolicy.DESTROY,
|
|
19
|
+
autoDeleteObjects: true,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
new route53.ARecord(scope, 'WwwAliasRecord', {
|
|
23
|
+
recordName: `www.${props.domain}`,
|
|
24
|
+
zone: this.zone,
|
|
25
|
+
target: route53.RecordTarget.fromAlias(new targets.BucketWebsiteTarget(wwwBucket)),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
new route53.ARecord(scope, 'StoreAliasRecord', {
|
|
29
|
+
recordName: `store.${props.domain}`,
|
|
30
|
+
zone: this.zone,
|
|
31
|
+
target: route53.RecordTarget.fromIpAddresses('137.66.37.136'),
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/docs.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CfnResource } from 'aws-cdk-lib';
|
|
2
|
+
import type { Construct } from 'constructs';
|
|
3
|
+
import type { NestedCloudProps } from '../types';
|
|
4
|
+
|
|
5
|
+
export declare interface DocsStackProps extends NestedCloudProps {
|
|
6
|
+
}
|
|
7
|
+
export declare class DocsStack {
|
|
8
|
+
originRequestFunction: lambda.Function
|
|
9
|
+
|
|
10
|
+
constructor(scope: Construct, props: DocsStackProps) {
|
|
11
|
+
const docsPrefix = 'docs'
|
|
12
|
+
|
|
13
|
+
this.originRequestFunction = new lambda.Function(scope, 'OriginRequestFunction', {
|
|
14
|
+
functionName: `${props.slug}-${props.appEnv}-origin-request-${props.timestamp}`,
|
|
15
|
+
description: 'The Stacks Origin Request function that prettifies URLs by removing the .html extension',
|
|
16
|
+
runtime: lambda.Runtime.NODEJS_20_X,
|
|
17
|
+
handler: 'dist/origin-request.handler',
|
|
18
|
+
code: lambda.Code.fromAsset(p.cloudPath('dist.zip'), {
|
|
19
|
+
assetHash: originRequestFunctionHash(),
|
|
20
|
+
assetHashType: AssetHashType.CUSTOM,
|
|
21
|
+
}),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const cfnOriginRequestFunction = this.originRequestFunction.node.defaultChild as CfnResource
|
|
25
|
+
cfnOriginRequestFunction.applyRemovalPolicy(RemovalPolicy.RETAIN)
|
|
26
|
+
|
|
27
|
+
if (!config.app.docMode && storage.hasFiles(p.projectPath('docs'))) {
|
|
28
|
+
new Output(scope, 'DocsUrl', {
|
|
29
|
+
value: `https:
|
|
30
|
+
description: 'The URL of the deployed documentation',
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|