@stacksjs/cloud 0.59.11 → 0.61.0
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 +843 -754
- package/package.json +30 -30
- package/src/cloud/ai.ts +5 -12
- package/src/cloud/aws-sdk-layer/nodejs/package-lock.json +7 -6
- package/src/cloud/aws-sdk-layer/nodejs/package.json +2 -2
- package/src/cloud/cdn.ts +41 -20
- package/src/cloud/cli.ts +1 -3
- package/src/cloud/compute.ts +39 -17
- package/src/cloud/dashboard.ts +0 -1
- package/src/cloud/database.ts +34 -0
- package/src/cloud/deployment.ts +8 -7
- package/src/cloud/dns.ts +0 -1
- package/src/cloud/docs.ts +3 -4
- package/src/cloud/email.ts +57 -40
- package/src/cloud/index.ts +9 -9
- package/src/cloud/jump-box.ts +0 -1
- package/src/cloud/lambda/ask/{index.js → index.cjs} +16 -15
- package/src/cloud/lambda/summarize/index.cjs +36 -0
- package/src/cloud/permissions.ts +3 -3
- package/src/cloud/queue.ts +73 -53
- package/src/cloud/redirects.ts +11 -4
- package/src/cloud/router-layer/nodejs/package.json +2 -2
- package/src/cloud/security.ts +19 -11
- package/src/edge/origin-request.ts +2 -1
- package/src/helpers.ts +147 -132
- package/dist/cloud/ai.d.ts +0 -10
- package/dist/cloud/cdn.d.ts +0 -44
- package/dist/cloud/cli.d.ts +0 -9
- package/dist/cloud/compute.d.ts +0 -17
- package/dist/cloud/deployment.d.ts +0 -14
- package/dist/cloud/dns.d.ts +0 -7
- package/dist/cloud/docs.d.ts +0 -9
- package/dist/cloud/email.d.ts +0 -10
- package/dist/cloud/file-system.d.ts +0 -12
- package/dist/cloud/index.d.ts +0 -41
- package/dist/cloud/jump-box.d.ts +0 -12
- package/dist/cloud/network.d.ts +0 -9
- package/dist/cloud/permissions.d.ts +0 -7
- package/dist/cloud/queue.d.ts +0 -24
- package/dist/cloud/redirects.d.ts +0 -9
- package/dist/cloud/security.d.ts +0 -14
- package/dist/cloud/storage.d.ts +0 -18
- package/dist/edge/origin-request.d.ts +0 -1
- package/dist/helpers.d.ts +0 -67
- package/dist/index.d.ts +0 -2
- package/dist/types.d.ts +0 -26
- package/src/cloud/lambda/summarize/index.js +0 -35
- /package/src/cloud/lambda/cli-setup/{index.js → index.cjs} +0 -0
package/dist/index.js
CHANGED
|
@@ -5,13 +5,13 @@ import {CloudWatchLogsClient, DeleteLogGroupCommand, DescribeLogGroupsCommand} f
|
|
|
5
5
|
import {EC2, _InstanceType as InstanceType} from "@aws-sdk/client-ec2";
|
|
6
6
|
import {DescribeFileSystemsCommand, EFSClient} from "@aws-sdk/client-efs";
|
|
7
7
|
import {IAM} from "@aws-sdk/client-iam";
|
|
8
|
-
import {SSM} from "@aws-sdk/client-ssm";
|
|
9
8
|
import {Lambda} from "@aws-sdk/client-lambda";
|
|
10
9
|
import {ContactType, Route53Domains} from "@aws-sdk/client-route-53-domains";
|
|
11
10
|
import {ListBucketsCommand, S3} from "@aws-sdk/client-s3";
|
|
11
|
+
import {SSM} from "@aws-sdk/client-ssm";
|
|
12
|
+
import {runCommand} from "@stacksjs/cli";
|
|
12
13
|
import {config as config2} from "@stacksjs/config";
|
|
13
14
|
import {err, handleError, ok} from "@stacksjs/error-handling";
|
|
14
|
-
import {runCommand} from "@stacksjs/cli";
|
|
15
15
|
import {log} from "@stacksjs/logging";
|
|
16
16
|
import {path as p} from "@stacksjs/path";
|
|
17
17
|
import {slug} from "@stacksjs/strings";
|
|
@@ -97,7 +97,7 @@ async function getJumpBoxInstanceId(name) {
|
|
|
97
97
|
}
|
|
98
98
|
]
|
|
99
99
|
});
|
|
100
|
-
if (data.Reservations
|
|
100
|
+
if (data.Reservations?.[0].Instances?.[0])
|
|
101
101
|
return data.Reservations[0].Instances[0].InstanceId;
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
@@ -133,14 +133,20 @@ async function deleteIamUsers() {
|
|
|
133
133
|
const userName = user.UserName || "";
|
|
134
134
|
log.info(`Deleting IAM user: ${userName}`);
|
|
135
135
|
const policies = await iam.listAttachedUserPolicies({ UserName: userName });
|
|
136
|
-
await Promise.all(policies.AttachedPolicies?.map((policy) => iam.detachUserPolicy({
|
|
136
|
+
await Promise.all(policies.AttachedPolicies?.map((policy) => iam.detachUserPolicy({
|
|
137
|
+
UserName: userName,
|
|
138
|
+
PolicyArn: policy.PolicyArn || ""
|
|
139
|
+
})) || []);
|
|
137
140
|
const accessKeys = await iam.listAccessKeys({ UserName: userName });
|
|
138
|
-
await Promise.all(accessKeys.AccessKeyMetadata?.map((key) => iam.deleteAccessKey({
|
|
141
|
+
await Promise.all(accessKeys.AccessKeyMetadata?.map((key) => iam.deleteAccessKey({
|
|
142
|
+
UserName: userName,
|
|
143
|
+
AccessKeyId: key.AccessKeyId || ""
|
|
144
|
+
})) || []);
|
|
139
145
|
return iam.deleteUser({ UserName: userName });
|
|
140
146
|
});
|
|
141
147
|
await Promise.all(promises).catch((error) => {
|
|
142
|
-
console.error(
|
|
143
|
-
return err(handleError("Error deleting Stacks IAM users"
|
|
148
|
+
console.error(error);
|
|
149
|
+
return err(handleError("Error deleting Stacks IAM users"));
|
|
144
150
|
});
|
|
145
151
|
return ok(`Stacks IAM users deleted for team ${teamName}`);
|
|
146
152
|
}
|
|
@@ -165,18 +171,30 @@ async function deleteStacksBuckets() {
|
|
|
165
171
|
try {
|
|
166
172
|
const versions = await s3.listObjectVersions({ Bucket: bucketName });
|
|
167
173
|
if (versions.Versions) {
|
|
168
|
-
await Promise.all(versions.Versions.map((version) => s3.deleteObject({
|
|
174
|
+
await Promise.all(versions.Versions.map((version) => s3.deleteObject({
|
|
175
|
+
Bucket: bucketName,
|
|
176
|
+
Key: version.Key || "",
|
|
177
|
+
VersionId: version.VersionId
|
|
178
|
+
}))).catch((error) => handleError(error));
|
|
169
179
|
log.info(`Finished deleting versions from bucket ${bucketName}`);
|
|
170
180
|
}
|
|
171
181
|
log.info(`Deleting bucket ${bucketName} delete markers...`);
|
|
172
182
|
if (versions.DeleteMarkers) {
|
|
173
|
-
await Promise.all(versions.DeleteMarkers.map((marker) => s3.deleteObject({
|
|
183
|
+
await Promise.all(versions.DeleteMarkers.map((marker) => s3.deleteObject({
|
|
184
|
+
Bucket: bucketName,
|
|
185
|
+
Key: marker.Key || "",
|
|
186
|
+
VersionId: marker.VersionId
|
|
187
|
+
}))).catch((error) => handleError(error));
|
|
174
188
|
log.info(`Finished deleting delete markers from bucket ${bucketName}`);
|
|
175
189
|
}
|
|
176
190
|
const uploads = await s3.listMultipartUploads({ Bucket: bucketName });
|
|
177
191
|
if (uploads.Uploads) {
|
|
178
192
|
log.info("Aborting bucket multipart uploads...");
|
|
179
|
-
await Promise.all(uploads.Uploads.map((upload) => s3.abortMultipartUpload({
|
|
193
|
+
await Promise.all(uploads.Uploads.map((upload) => s3.abortMultipartUpload({
|
|
194
|
+
Bucket: bucketName,
|
|
195
|
+
Key: upload.Key || "",
|
|
196
|
+
UploadId: upload.UploadId
|
|
197
|
+
}))).catch((error) => handleError(error));
|
|
180
198
|
log.info(`Finished aborting multipart uploads from bucket ${bucketName}`);
|
|
181
199
|
}
|
|
182
200
|
await s3.deleteBucket({ Bucket: bucketName }).catch((error) => handleError(error));
|
|
@@ -186,11 +204,13 @@ async function deleteStacksBuckets() {
|
|
|
186
204
|
}
|
|
187
205
|
});
|
|
188
206
|
await Promise.all(promises).catch((error) => {
|
|
189
|
-
|
|
207
|
+
console.error(error);
|
|
208
|
+
return err(handleError("Error deleting stacks buckets"));
|
|
190
209
|
});
|
|
191
210
|
return ok("Stacks buckets deleted");
|
|
192
211
|
} catch (error) {
|
|
193
|
-
|
|
212
|
+
console.error(error);
|
|
213
|
+
return err(handleError("Error deleting stacks buckets"));
|
|
194
214
|
}
|
|
195
215
|
}
|
|
196
216
|
async function deleteStacksFunctions() {
|
|
@@ -205,7 +225,8 @@ async function deleteStacksFunctions() {
|
|
|
205
225
|
log.info("Function is replicated, skipping...");
|
|
206
226
|
return ok("CloudFront is still deleting the some functions. Try again later.");
|
|
207
227
|
}
|
|
208
|
-
|
|
228
|
+
console.error(error);
|
|
229
|
+
return err(handleError("Error deleting stacks functions"));
|
|
209
230
|
});
|
|
210
231
|
return ok("Stacks functions deleted");
|
|
211
232
|
}
|
|
@@ -221,7 +242,8 @@ async function deleteLogGroups() {
|
|
|
221
242
|
}
|
|
222
243
|
return ok("Log groups deleted");
|
|
223
244
|
} catch (error) {
|
|
224
|
-
|
|
245
|
+
console.error(error);
|
|
246
|
+
return err(handleError("Error deleting log groups"));
|
|
225
247
|
}
|
|
226
248
|
}
|
|
227
249
|
async function deleteParameterStore() {
|
|
@@ -234,7 +256,8 @@ async function deleteParameterStore() {
|
|
|
234
256
|
return ok("No stacks parameters found");
|
|
235
257
|
const promises = stacksParameters.map((param) => ssm.deleteParameter({ Name: param.Name || "" }));
|
|
236
258
|
await Promise.all(promises).catch((error) => {
|
|
237
|
-
|
|
259
|
+
console.error(error);
|
|
260
|
+
return err(handleError("Error deleting parameter store"));
|
|
238
261
|
});
|
|
239
262
|
return ok("Parameter store deleted");
|
|
240
263
|
}
|
|
@@ -242,7 +265,8 @@ async function deleteCdkRemnants() {
|
|
|
242
265
|
try {
|
|
243
266
|
return ok(await runCommand(`bunx rimraf ${p.cloudPath("cdk.out/")} ${p.cloudPath("cdk.context.json")}`));
|
|
244
267
|
} catch (error) {
|
|
245
|
-
|
|
268
|
+
console.error(error);
|
|
269
|
+
return err(handleError("Error deleting CDK remnants"));
|
|
246
270
|
}
|
|
247
271
|
}
|
|
248
272
|
async function hasBeenDeployed() {
|
|
@@ -251,7 +275,8 @@ async function hasBeenDeployed() {
|
|
|
251
275
|
const response = await s3.send(new ListBucketsCommand({}));
|
|
252
276
|
return ok(response.Buckets?.some((bucket) => bucket.Name?.includes(config2.app.name?.toLocaleLowerCase() || "stacks")) || false);
|
|
253
277
|
} catch (error) {
|
|
254
|
-
|
|
278
|
+
console.error(error);
|
|
279
|
+
return err(handleError("Error checking if the app has been deployed"));
|
|
255
280
|
}
|
|
256
281
|
}
|
|
257
282
|
async function getJumpBoxInstanceProfileName() {
|
|
@@ -274,11 +299,9 @@ async function addJumpBox(stackName) {
|
|
|
274
299
|
if (!r.value)
|
|
275
300
|
return err("Security group not found when adding jump-box");
|
|
276
301
|
const result = await getSecurityGroupId(r.value);
|
|
277
|
-
let sgId;
|
|
278
302
|
if (result.isErr())
|
|
279
303
|
return err(result.error);
|
|
280
|
-
|
|
281
|
-
sgId = result.value;
|
|
304
|
+
const sgId = result.value;
|
|
282
305
|
if (!sgId)
|
|
283
306
|
return err("Security group not found when adding jump-box");
|
|
284
307
|
const client = new EFSClient({ region: "us-east-1" });
|
|
@@ -329,7 +352,7 @@ git clone https://github.com/stacksjs/stacks.git /mnt/efs
|
|
|
329
352
|
Name: jumpBoxInstanceProfileName
|
|
330
353
|
}
|
|
331
354
|
});
|
|
332
|
-
return instance.Instances
|
|
355
|
+
return instance.Instances?.[0] ? ok(`Jump-box created with id ${instance.Instances[0].InstanceId}`) : err("Jump-box creation failed");
|
|
333
356
|
}
|
|
334
357
|
async function getJumpBoxSecurityGroupName() {
|
|
335
358
|
const jumpBoxId = await getJumpBoxInstanceId();
|
|
@@ -337,10 +360,10 @@ async function getJumpBoxSecurityGroupName() {
|
|
|
337
360
|
return err("Jump-box not found");
|
|
338
361
|
const ec2 = new EC2({ region: "us-east-1" });
|
|
339
362
|
const data = await ec2.describeInstances({ InstanceIds: [jumpBoxId] });
|
|
340
|
-
if (data.Reservations
|
|
363
|
+
if (data.Reservations?.[0].Instances?.[0]) {
|
|
341
364
|
const instance = data.Reservations[0].Instances[0];
|
|
342
365
|
const securityGroups = instance.SecurityGroups;
|
|
343
|
-
if (securityGroups
|
|
366
|
+
if (securityGroups?.[0])
|
|
344
367
|
return ok(securityGroups[0].GroupName);
|
|
345
368
|
}
|
|
346
369
|
return err("Security group not found");
|
|
@@ -348,10 +371,10 @@ async function getJumpBoxSecurityGroupName() {
|
|
|
348
371
|
async function getSecurityGroupFromInstanceId(instanceId) {
|
|
349
372
|
const ec2 = new EC2({ region: "us-east-1" });
|
|
350
373
|
const data = await ec2.describeInstances({ InstanceIds: [instanceId] });
|
|
351
|
-
if (data.Reservations
|
|
374
|
+
if (data.Reservations?.[0].Instances?.[0]) {
|
|
352
375
|
const instance = data.Reservations[0].Instances[0];
|
|
353
376
|
const securityGroups = instance.SecurityGroups;
|
|
354
|
-
if (securityGroups
|
|
377
|
+
if (securityGroups?.[0])
|
|
355
378
|
return securityGroups[0].GroupId;
|
|
356
379
|
}
|
|
357
380
|
return;
|
|
@@ -359,13 +382,17 @@ async function getSecurityGroupFromInstanceId(instanceId) {
|
|
|
359
382
|
async function isFirstDeployment() {
|
|
360
383
|
const stackName = cloudName;
|
|
361
384
|
const cloudFormation = new CloudFormation;
|
|
362
|
-
const data = await cloudFormation.listStacks({
|
|
385
|
+
const data = await cloudFormation.listStacks({
|
|
386
|
+
StackStatusFilter: ["CREATE_COMPLETE", "UPDATE_COMPLETE"]
|
|
387
|
+
});
|
|
363
388
|
const isStacksCloudPresent = data.StackSummaries?.some((stack) => stack.StackName === stackName);
|
|
364
389
|
return !isStacksCloudPresent;
|
|
365
390
|
}
|
|
366
391
|
async function isFailedState() {
|
|
367
392
|
const cloudFormation = new CloudFormation;
|
|
368
|
-
const data = await cloudFormation.listStacks({
|
|
393
|
+
const data = await cloudFormation.listStacks({
|
|
394
|
+
StackStatusFilter: ["CREATE_FAILED", "UPDATE_FAILED", "ROLLBACK_COMPLETE", "UPDATE_ROLLBACK_COMPLETE"]
|
|
395
|
+
});
|
|
369
396
|
const isStacksCloudPresent = data.StackSummaries?.some((stack) => stack.StackName === cloudName);
|
|
370
397
|
return !isStacksCloudPresent;
|
|
371
398
|
}
|
|
@@ -392,12 +419,12 @@ async function getOrCreateTimestamp() {
|
|
|
392
419
|
var appEnv = config2.app.env === "local" ? "dev" : config2.app.env;
|
|
393
420
|
var cloudName = `stacks-cloud-${appEnv}`;
|
|
394
421
|
// src/cloud/index.ts
|
|
395
|
-
import {Stack as Stack2} from "aws-cdk-lib";
|
|
396
422
|
import {config as config20} from "@stacksjs/config";
|
|
423
|
+
import {Stack as Stack2} from "aws-cdk-lib";
|
|
397
424
|
|
|
398
425
|
// src/cloud/ai.ts
|
|
399
|
-
import {Duration, CfnOutput as Output, aws_iam as iam, aws_lambda as lambda} from "aws-cdk-lib";
|
|
400
426
|
import {config as config4} from "@stacksjs/config";
|
|
427
|
+
import {Duration, CfnOutput as Output, aws_iam as iam, aws_lambda as lambda} from "aws-cdk-lib";
|
|
401
428
|
|
|
402
429
|
class AiStack {
|
|
403
430
|
askAiUrl;
|
|
@@ -410,17 +437,12 @@ class AiStack {
|
|
|
410
437
|
});
|
|
411
438
|
const bedrockAccessPolicy = new iam.PolicyStatement({
|
|
412
439
|
effect: iam.Effect.ALLOW,
|
|
413
|
-
actions: [
|
|
414
|
-
"bedrock:InvokeModel",
|
|
415
|
-
"bedrock:InvokeModelWithResponseStream"
|
|
416
|
-
],
|
|
440
|
+
actions: ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
|
|
417
441
|
resources: config4.ai.models?.map((model) => `arn:aws:bedrock:us-east-1::foundation-model/${model}`)
|
|
418
442
|
});
|
|
419
443
|
const bedrockAccessRole = new iam.Role(scope, "BedrockAccessRole", {
|
|
420
444
|
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
|
|
421
|
-
managedPolicies: [
|
|
422
|
-
iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
|
|
423
|
-
]
|
|
445
|
+
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")]
|
|
424
446
|
});
|
|
425
447
|
bedrockAccessRole.addToPolicy(bedrockAccessPolicy);
|
|
426
448
|
const askAi = new lambda.Function(scope, "AskAiFunction", {
|
|
@@ -473,11 +495,19 @@ class AiStack {
|
|
|
473
495
|
}
|
|
474
496
|
|
|
475
497
|
// src/cloud/cdn.ts
|
|
476
|
-
import {Duration as Duration2, Fn, CfnOutput as Output2, aws_cloudfront as cloudfront, aws_cloudfront_origins as origins, aws_route53 as route53, aws_route53_targets as targets} from "aws-cdk-lib";
|
|
477
498
|
import {config as config6} from "@stacksjs/config";
|
|
478
|
-
import {hasFiles} from "@stacksjs/storage";
|
|
479
|
-
import {path as p2} from "@stacksjs/path";
|
|
480
499
|
import {env as env2} from "@stacksjs/env";
|
|
500
|
+
import {path as p2} from "@stacksjs/path";
|
|
501
|
+
import {hasFiles} from "@stacksjs/storage";
|
|
502
|
+
import {
|
|
503
|
+
Duration as Duration2,
|
|
504
|
+
Fn,
|
|
505
|
+
CfnOutput as Output2,
|
|
506
|
+
aws_cloudfront as cloudfront,
|
|
507
|
+
aws_cloudfront_origins as origins,
|
|
508
|
+
aws_route53 as route53,
|
|
509
|
+
aws_route53_targets as targets
|
|
510
|
+
} from "aws-cdk-lib";
|
|
481
511
|
import * as kinesis from "aws-cdk-lib/aws-kinesis";
|
|
482
512
|
|
|
483
513
|
class CdnStack {
|
|
@@ -506,9 +536,7 @@ class CdnStack {
|
|
|
506
536
|
encryption: kinesis.StreamEncryption.UNENCRYPTED
|
|
507
537
|
});
|
|
508
538
|
this.realtimeLogConfig = new cloudfront.RealtimeLogConfig(scope, "StacksRealTimeLogConfig", {
|
|
509
|
-
endPoints: [
|
|
510
|
-
cloudfront.Endpoint.fromKinesisStream(logStream)
|
|
511
|
-
],
|
|
539
|
+
endPoints: [cloudfront.Endpoint.fromKinesisStream(logStream)],
|
|
512
540
|
fields: [
|
|
513
541
|
"timestamp",
|
|
514
542
|
"c-ip",
|
|
@@ -640,7 +668,7 @@ class CdnStack {
|
|
|
640
668
|
}
|
|
641
669
|
}
|
|
642
670
|
shouldDeployApi() {
|
|
643
|
-
return config6.
|
|
671
|
+
return config6.api?.deploy;
|
|
644
672
|
}
|
|
645
673
|
apiBehaviorOptions(scope, props) {
|
|
646
674
|
const hostname = `api.${props.domain}`;
|
|
@@ -770,7 +798,26 @@ class CdnStack {
|
|
|
770
798
|
additionalBehaviors(scope, props) {
|
|
771
799
|
let behaviorOptions = {};
|
|
772
800
|
if (this.shouldDeployApi()) {
|
|
773
|
-
const keysToRemove = [
|
|
801
|
+
const keysToRemove = [
|
|
802
|
+
"_HANDLER",
|
|
803
|
+
"_X_AMZN_TRACE_ID",
|
|
804
|
+
"AWS_REGION",
|
|
805
|
+
"AWS_EXECUTION_ENV",
|
|
806
|
+
"AWS_LAMBDA_FUNCTION_NAME",
|
|
807
|
+
"AWS_LAMBDA_FUNCTION_MEMORY_SIZE",
|
|
808
|
+
"AWS_LAMBDA_FUNCTION_VERSION",
|
|
809
|
+
"AWS_LAMBDA_INITIALIZATION_TYPE",
|
|
810
|
+
"AWS_LAMBDA_LOG_GROUP_NAME",
|
|
811
|
+
"AWS_LAMBDA_LOG_STREAM_NAME",
|
|
812
|
+
"AWS_ACCESS_KEY",
|
|
813
|
+
"AWS_ACCESS_KEY_ID",
|
|
814
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
815
|
+
"AWS_SESSION_TOKEN",
|
|
816
|
+
"AWS_LAMBDA_RUNTIME_API",
|
|
817
|
+
"LAMBDA_TASK_ROOT",
|
|
818
|
+
"LAMBDA_RUNTIME_DIR",
|
|
819
|
+
"_"
|
|
820
|
+
];
|
|
774
821
|
keysToRemove.forEach((key) => delete env2[key]);
|
|
775
822
|
behaviorOptions = this.apiBehaviorOptions(scope, props);
|
|
776
823
|
}
|
|
@@ -840,13 +887,240 @@ class CliStack {
|
|
|
840
887
|
}
|
|
841
888
|
}
|
|
842
889
|
|
|
890
|
+
// src/cloud/compute.ts
|
|
891
|
+
import {env as env4} from "@stacksjs/env";
|
|
892
|
+
import {path as p3} from "@stacksjs/path";
|
|
893
|
+
import {
|
|
894
|
+
Duration as Duration4,
|
|
895
|
+
CfnOutput as Output4,
|
|
896
|
+
RemovalPolicy,
|
|
897
|
+
aws_ec2 as ec2,
|
|
898
|
+
aws_ecs as ecs,
|
|
899
|
+
aws_route53 as route532,
|
|
900
|
+
aws_route53_targets as route53Targets,
|
|
901
|
+
aws_secretsmanager as secretsmanager
|
|
902
|
+
} from "aws-cdk-lib";
|
|
903
|
+
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
|
|
904
|
+
import {LogGroup} from "aws-cdk-lib/aws-logs";
|
|
905
|
+
|
|
906
|
+
class ComputeStack {
|
|
907
|
+
lb;
|
|
908
|
+
cluster;
|
|
909
|
+
taskDefinition;
|
|
910
|
+
constructor(scope, props) {
|
|
911
|
+
const vpc = props.vpc;
|
|
912
|
+
const fileSystem = props.fileSystem;
|
|
913
|
+
if (!fileSystem)
|
|
914
|
+
throw new Error("The file system is missing. Please make sure it was created properly.");
|
|
915
|
+
this.cluster = new ecs.Cluster(scope, "StacksCluster", {
|
|
916
|
+
clusterName: `${props.slug}-${props.appEnv}-web-server-cluster`,
|
|
917
|
+
vpc
|
|
918
|
+
});
|
|
919
|
+
this.taskDefinition = new ecs.FargateTaskDefinition(scope, "TaskDefinition", {
|
|
920
|
+
family: `${props.appName}-${props.appEnv}-api`,
|
|
921
|
+
memoryLimitMiB: 512,
|
|
922
|
+
cpu: 256,
|
|
923
|
+
runtimePlatform: {
|
|
924
|
+
cpuArchitecture: ecs.CpuArchitecture.ARM64
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
const container = this.taskDefinition.addContainer("WebServerContainer", {
|
|
928
|
+
containerName: `${props.appName}-${props.appEnv}-api`,
|
|
929
|
+
image: ecs.ContainerImage.fromAsset(p3.frameworkPath("server")),
|
|
930
|
+
logging: new ecs.AwsLogDriver({
|
|
931
|
+
streamPrefix: `${props.appName}-${props.appEnv}-web-server-logs`,
|
|
932
|
+
logGroup: new LogGroup(scope, "StacksApiLogs", {
|
|
933
|
+
logGroupName: "/aws/ecs/stacks-api",
|
|
934
|
+
removalPolicy: RemovalPolicy.DESTROY
|
|
935
|
+
})
|
|
936
|
+
}),
|
|
937
|
+
healthCheck: {
|
|
938
|
+
command: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"],
|
|
939
|
+
interval: Duration4.seconds(10),
|
|
940
|
+
timeout: Duration4.seconds(5),
|
|
941
|
+
retries: 3,
|
|
942
|
+
startPeriod: Duration4.seconds(10)
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
container.addPortMappings({
|
|
946
|
+
containerPort: 3000,
|
|
947
|
+
hostPort: 3000
|
|
948
|
+
});
|
|
949
|
+
const serviceSecurityGroup = new ec2.SecurityGroup(scope, "ServiceSecurityGroup", {
|
|
950
|
+
securityGroupName: `${props.appName}-${props.appEnv}-api-service-sg`,
|
|
951
|
+
vpc,
|
|
952
|
+
description: "Stacks Security Group for API Service"
|
|
953
|
+
});
|
|
954
|
+
const publicLoadBalancerSG = new ec2.SecurityGroup(scope, "PublicLoadBalancerSG", {
|
|
955
|
+
securityGroupName: `${props.appName}-${props.appEnv}-public-load-balancer-sg`,
|
|
956
|
+
vpc,
|
|
957
|
+
description: "Access to the public facing load balancer"
|
|
958
|
+
});
|
|
959
|
+
serviceSecurityGroup.addIngressRule(publicLoadBalancerSG, ec2.Port.allTraffic(), "Ingress from the public ALB");
|
|
960
|
+
this.lb = new elbv2.ApplicationLoadBalancer(scope, "ApplicationLoadBalancer", {
|
|
961
|
+
http2Enabled: true,
|
|
962
|
+
loadBalancerName: `${props.appName}-${props.appEnv}-alb`,
|
|
963
|
+
vpc,
|
|
964
|
+
vpcSubnets: {
|
|
965
|
+
subnets: vpc.selectSubnets({
|
|
966
|
+
subnetType: ec2.SubnetType.PUBLIC,
|
|
967
|
+
onePerAz: true
|
|
968
|
+
}).subnets
|
|
969
|
+
},
|
|
970
|
+
internetFacing: true,
|
|
971
|
+
idleTimeout: Duration4.seconds(30),
|
|
972
|
+
securityGroup: publicLoadBalancerSG
|
|
973
|
+
});
|
|
974
|
+
new route532.ARecord(scope, "ApiDomainAliasRecord", {
|
|
975
|
+
zone: props.zone,
|
|
976
|
+
recordName: "api",
|
|
977
|
+
target: route532.RecordTarget.fromAlias(new route53Targets.LoadBalancerTarget(this.lb))
|
|
978
|
+
});
|
|
979
|
+
const serviceTargetGroup = new elbv2.ApplicationTargetGroup(scope, "ServiceTargetGroup", {
|
|
980
|
+
targetGroupName: `${props.appName}-${props.appEnv}-api-tg`,
|
|
981
|
+
vpc,
|
|
982
|
+
targetType: elbv2.TargetType.IP,
|
|
983
|
+
protocol: elbv2.ApplicationProtocol.HTTP,
|
|
984
|
+
port: 3000,
|
|
985
|
+
healthCheck: {
|
|
986
|
+
interval: Duration4.seconds(6),
|
|
987
|
+
path: "/api/health",
|
|
988
|
+
protocol: elbv2.Protocol.HTTP,
|
|
989
|
+
timeout: Duration4.seconds(5),
|
|
990
|
+
healthyThresholdCount: 2,
|
|
991
|
+
unhealthyThresholdCount: 10
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
const service = new ecs.FargateService(scope, "StacksApiService", {
|
|
995
|
+
serviceName: `${props.appName}-${props.appEnv}-api-service`,
|
|
996
|
+
cluster: this.cluster,
|
|
997
|
+
taskDefinition: this.taskDefinition,
|
|
998
|
+
desiredCount: 1,
|
|
999
|
+
assignPublicIp: true,
|
|
1000
|
+
maxHealthyPercent: 200,
|
|
1001
|
+
vpcSubnets: vpc.selectSubnets({
|
|
1002
|
+
subnetType: ec2.SubnetType.PUBLIC,
|
|
1003
|
+
onePerAz: true
|
|
1004
|
+
}),
|
|
1005
|
+
minHealthyPercent: 75,
|
|
1006
|
+
securityGroups: [serviceSecurityGroup]
|
|
1007
|
+
});
|
|
1008
|
+
service.attachToApplicationTargetGroup(serviceTargetGroup);
|
|
1009
|
+
publicLoadBalancerSG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.allTraffic());
|
|
1010
|
+
this.lb.addListener("HttpsListener", {
|
|
1011
|
+
port: 443,
|
|
1012
|
+
certificates: [props.certificate],
|
|
1013
|
+
defaultAction: elbv2.ListenerAction.forward([serviceTargetGroup])
|
|
1014
|
+
});
|
|
1015
|
+
this.lb.addListener("HttpListener", {
|
|
1016
|
+
port: 80,
|
|
1017
|
+
defaultAction: elbv2.ListenerAction.forward([serviceTargetGroup])
|
|
1018
|
+
});
|
|
1019
|
+
props.fileSystem.connections.allowFromAnyIpv4(ec2.Port.tcp(2049));
|
|
1020
|
+
const volumeName = `${props.slug}-${props.appEnv}-efs`;
|
|
1021
|
+
this.taskDefinition.addVolume({
|
|
1022
|
+
name: volumeName,
|
|
1023
|
+
efsVolumeConfiguration: {
|
|
1024
|
+
fileSystemId: props.fileSystem.fileSystemId
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
container.addMountPoints({
|
|
1028
|
+
sourceVolume: volumeName,
|
|
1029
|
+
containerPath: "/mnt/efs",
|
|
1030
|
+
readOnly: false
|
|
1031
|
+
});
|
|
1032
|
+
const scaling = service.autoScaleTaskCount({ maxCapacity: 2 });
|
|
1033
|
+
scaling.scaleOnCpuUtilization("CpuScaling", {
|
|
1034
|
+
targetUtilizationPercent: 50,
|
|
1035
|
+
scaleInCooldown: Duration4.seconds(60),
|
|
1036
|
+
scaleOutCooldown: Duration4.seconds(60)
|
|
1037
|
+
});
|
|
1038
|
+
scaling.scaleOnMemoryUtilization("MemoryScaling", {
|
|
1039
|
+
targetUtilizationPercent: 60,
|
|
1040
|
+
scaleInCooldown: Duration4.seconds(60),
|
|
1041
|
+
scaleOutCooldown: Duration4.seconds(60)
|
|
1042
|
+
});
|
|
1043
|
+
const keysToRemove = [
|
|
1044
|
+
"_HANDLER",
|
|
1045
|
+
"_X_AMZN_TRACE_ID",
|
|
1046
|
+
"AWS_REGION",
|
|
1047
|
+
"AWS_EXECUTION_ENV",
|
|
1048
|
+
"AWS_LAMBDA_FUNCTION_NAME",
|
|
1049
|
+
"AWS_LAMBDA_FUNCTION_MEMORY_SIZE",
|
|
1050
|
+
"AWS_LAMBDA_FUNCTION_VERSION",
|
|
1051
|
+
"AWS_LAMBDA_INITIALIZATION_TYPE",
|
|
1052
|
+
"AWS_LAMBDA_LOG_GROUP_NAME",
|
|
1053
|
+
"AWS_LAMBDA_LOG_STREAM_NAME",
|
|
1054
|
+
"AWS_ACCESS_KEY",
|
|
1055
|
+
"AWS_ACCESS_KEY_ID",
|
|
1056
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
1057
|
+
"AWS_SESSION_TOKEN",
|
|
1058
|
+
"AWS_LAMBDA_RUNTIME_API",
|
|
1059
|
+
"LAMBDA_TASK_ROOT",
|
|
1060
|
+
"LAMBDA_RUNTIME_DIR",
|
|
1061
|
+
"_"
|
|
1062
|
+
];
|
|
1063
|
+
keysToRemove.forEach((key) => delete env4[key]);
|
|
1064
|
+
const secrets = new secretsmanager.Secret(scope, "StacksSecrets", {
|
|
1065
|
+
secretName: `${props.slug}-${props.appEnv}-secrets`,
|
|
1066
|
+
description: "Secrets for the Stacks application",
|
|
1067
|
+
generateSecretString: {
|
|
1068
|
+
secretStringTemplate: JSON.stringify(env4),
|
|
1069
|
+
generateStringKey: Object.keys(env4).join(",").length.toString()
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
secrets.grantRead(service.taskDefinition.executionRole);
|
|
1073
|
+
container.addEnvironment("SECRETS_ARN", secrets.secretArn);
|
|
1074
|
+
const apiPrefix = "api";
|
|
1075
|
+
new Output4(scope, "ApiUrl", {
|
|
1076
|
+
value: `https://${props.domain}/${apiPrefix}`,
|
|
1077
|
+
description: "The URL of the deployed application"
|
|
1078
|
+
});
|
|
1079
|
+
new Output4(scope, "ApiVanityUrl", {
|
|
1080
|
+
value: `http://${this.lb.loadBalancerDnsName}`,
|
|
1081
|
+
description: "The Vanity URL / DNS name of the load balancer"
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/cloud/deployment.ts
|
|
1087
|
+
import {config as config8} from "@stacksjs/config";
|
|
1088
|
+
import {websiteSourceHash} from "@stacksjs/utils";
|
|
1089
|
+
import {AssetHashType, aws_s3_deployment as s3deploy} from "aws-cdk-lib";
|
|
1090
|
+
|
|
1091
|
+
class DeploymentStack {
|
|
1092
|
+
privateSource;
|
|
1093
|
+
docsSource;
|
|
1094
|
+
websiteSource;
|
|
1095
|
+
constructor(scope, props) {
|
|
1096
|
+
this.privateSource = "../../private";
|
|
1097
|
+
this.docsSource = "../docs/dist/";
|
|
1098
|
+
this.websiteSource = config8.app.docMode === true ? this.docsSource : "../views/dist/";
|
|
1099
|
+
new s3deploy.BucketDeployment(scope, "Website", {
|
|
1100
|
+
sources: [
|
|
1101
|
+
s3deploy.Source.asset(this.websiteSource, {
|
|
1102
|
+
assetHash: websiteSourceHash(),
|
|
1103
|
+
assetHashType: AssetHashType.CUSTOM
|
|
1104
|
+
})
|
|
1105
|
+
],
|
|
1106
|
+
destinationBucket: props.publicBucket,
|
|
1107
|
+
distribution: props.cdn,
|
|
1108
|
+
distributionPaths: ["/*"]
|
|
1109
|
+
});
|
|
1110
|
+
new s3deploy.BucketDeployment(scope, "PrivateFiles", {
|
|
1111
|
+
sources: [s3deploy.Source.asset(this.privateSource)],
|
|
1112
|
+
destinationBucket: props.privateBucket
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
843
1117
|
// src/cloud/dns.ts
|
|
844
|
-
import {RemovalPolicy, aws_route53 as
|
|
1118
|
+
import {RemovalPolicy as RemovalPolicy2, aws_route53 as route533, aws_s3 as s3, aws_route53_targets as targets2} from "aws-cdk-lib";
|
|
845
1119
|
|
|
846
1120
|
class DnsStack {
|
|
847
1121
|
zone;
|
|
848
1122
|
constructor(scope, props) {
|
|
849
|
-
this.zone =
|
|
1123
|
+
this.zone = route533.PublicHostedZone.fromLookup(scope, "AppUrlHostedZone", {
|
|
850
1124
|
domainName: props.domain
|
|
851
1125
|
});
|
|
852
1126
|
const wwwBucket = new s3.Bucket(scope, "WwwBucket", {
|
|
@@ -855,47 +1129,47 @@ class DnsStack {
|
|
|
855
1129
|
hostName: props.domain,
|
|
856
1130
|
protocol: s3.RedirectProtocol.HTTPS
|
|
857
1131
|
},
|
|
858
|
-
removalPolicy:
|
|
1132
|
+
removalPolicy: RemovalPolicy2.DESTROY,
|
|
859
1133
|
autoDeleteObjects: true
|
|
860
1134
|
});
|
|
861
|
-
new
|
|
1135
|
+
new route533.ARecord(scope, "WwwAliasRecord", {
|
|
862
1136
|
recordName: `www.${props.domain}`,
|
|
863
1137
|
zone: this.zone,
|
|
864
|
-
target:
|
|
1138
|
+
target: route533.RecordTarget.fromAlias(new targets2.BucketWebsiteTarget(wwwBucket))
|
|
865
1139
|
});
|
|
866
|
-
new
|
|
1140
|
+
new route533.ARecord(scope, "StoreAliasRecord", {
|
|
867
1141
|
recordName: `store.${props.domain}`,
|
|
868
1142
|
zone: this.zone,
|
|
869
|
-
target:
|
|
1143
|
+
target: route533.RecordTarget.fromIpAddresses("137.66.37.136")
|
|
870
1144
|
});
|
|
871
1145
|
}
|
|
872
1146
|
}
|
|
873
1147
|
|
|
874
1148
|
// src/cloud/docs.ts
|
|
875
|
-
import {
|
|
876
|
-
import {
|
|
877
|
-
import {path as p3} from "@stacksjs/path";
|
|
1149
|
+
import {config as config10} from "@stacksjs/config";
|
|
1150
|
+
import {path as p4} from "@stacksjs/path";
|
|
878
1151
|
import {storage as storage3} from "@stacksjs/storage";
|
|
879
1152
|
import {originRequestFunctionHash} from "@stacksjs/utils";
|
|
1153
|
+
import {AssetHashType as AssetHashType2, CfnOutput as Output5, RemovalPolicy as RemovalPolicy3, aws_lambda as lambda3} from "aws-cdk-lib";
|
|
880
1154
|
|
|
881
1155
|
class DocsStack {
|
|
882
1156
|
originRequestFunction;
|
|
883
1157
|
constructor(scope, props) {
|
|
884
|
-
const docsPrefix =
|
|
1158
|
+
const docsPrefix = config10.app.docMode ? "" : config10.docs.base;
|
|
885
1159
|
this.originRequestFunction = new lambda3.Function(scope, "OriginRequestFunction", {
|
|
886
1160
|
functionName: `${props.slug}-${props.appEnv}-origin-request-${props.timestamp}`,
|
|
887
1161
|
description: "The Stacks Origin Request function that prettifies URLs",
|
|
888
1162
|
runtime: lambda3.Runtime.NODEJS_18_X,
|
|
889
1163
|
handler: "dist/origin-request.handler",
|
|
890
|
-
code: lambda3.Code.fromAsset(
|
|
1164
|
+
code: lambda3.Code.fromAsset(p4.frameworkCloudPath("dist.zip"), {
|
|
891
1165
|
assetHash: originRequestFunctionHash(),
|
|
892
|
-
assetHashType:
|
|
1166
|
+
assetHashType: AssetHashType2.CUSTOM
|
|
893
1167
|
})
|
|
894
1168
|
});
|
|
895
1169
|
const cfnOriginRequestFunction = this.originRequestFunction.node.defaultChild;
|
|
896
|
-
cfnOriginRequestFunction.applyRemovalPolicy(
|
|
897
|
-
if (!
|
|
898
|
-
new
|
|
1170
|
+
cfnOriginRequestFunction.applyRemovalPolicy(RemovalPolicy3.RETAIN);
|
|
1171
|
+
if (!config10.app.docMode && storage3.hasFiles(p4.projectPath("docs"))) {
|
|
1172
|
+
new Output5(scope, "DocsUrl", {
|
|
899
1173
|
value: `https://${props.domain}/${docsPrefix}`,
|
|
900
1174
|
description: "The URL of the deployed documentation"
|
|
901
1175
|
});
|
|
@@ -903,479 +1177,76 @@ class DocsStack {
|
|
|
903
1177
|
}
|
|
904
1178
|
}
|
|
905
1179
|
|
|
906
|
-
// src/cloud/
|
|
907
|
-
import {
|
|
1180
|
+
// src/cloud/email.ts
|
|
1181
|
+
import {config as config12} from "@stacksjs/config";
|
|
1182
|
+
import {
|
|
1183
|
+
Duration as Duration5,
|
|
1184
|
+
RemovalPolicy as RemovalPolicy4,
|
|
1185
|
+
Stack,
|
|
1186
|
+
Tags,
|
|
1187
|
+
aws_iam as iam2,
|
|
1188
|
+
aws_lambda as lambda4,
|
|
1189
|
+
aws_route53 as route534,
|
|
1190
|
+
aws_s3 as s32,
|
|
1191
|
+
aws_s3_notifications as s3n,
|
|
1192
|
+
aws_ses as ses
|
|
1193
|
+
} from "aws-cdk-lib";
|
|
908
1194
|
|
|
909
|
-
class
|
|
910
|
-
|
|
911
|
-
privateBucket;
|
|
912
|
-
logBucket;
|
|
913
|
-
bucketPrefix;
|
|
914
|
-
vault;
|
|
915
|
-
backupPlan;
|
|
916
|
-
backupRole;
|
|
1195
|
+
class EmailStack {
|
|
1196
|
+
emailBucket;
|
|
917
1197
|
constructor(scope, props) {
|
|
918
|
-
|
|
919
|
-
this.
|
|
920
|
-
bucketName: `${
|
|
921
|
-
versioned: true,
|
|
922
|
-
autoDeleteObjects: true,
|
|
923
|
-
removalPolicy: RemovalPolicy3.DESTROY,
|
|
924
|
-
encryption: s32.BucketEncryption.S3_MANAGED
|
|
925
|
-
});
|
|
926
|
-
Tags.of(this.publicBucket).add("daily-backup", "true");
|
|
927
|
-
this.privateBucket = new s32.Bucket(scope, "PrivateBucket", {
|
|
928
|
-
bucketName: `${this.bucketPrefix}-private-${props.timestamp}`,
|
|
1198
|
+
const bucketPrefix = `${props.slug}-${props.appEnv}`;
|
|
1199
|
+
this.emailBucket = new s32.Bucket(scope, "EmailBucket", {
|
|
1200
|
+
bucketName: `${bucketPrefix}-email-${props.timestamp}`,
|
|
929
1201
|
versioned: true,
|
|
930
|
-
removalPolicy:
|
|
1202
|
+
removalPolicy: RemovalPolicy4.DESTROY,
|
|
931
1203
|
autoDeleteObjects: true,
|
|
932
1204
|
encryption: s32.BucketEncryption.S3_MANAGED,
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1205
|
+
lifecycleRules: [
|
|
1206
|
+
{
|
|
1207
|
+
id: "24h",
|
|
1208
|
+
enabled: true,
|
|
1209
|
+
expiration: Duration5.days(1),
|
|
1210
|
+
noncurrentVersionExpiration: Duration5.days(1),
|
|
1211
|
+
prefix: "today/"
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
id: "Intelligent transition for Inbox",
|
|
1215
|
+
enabled: true,
|
|
1216
|
+
prefix: "inbox/",
|
|
1217
|
+
transitions: [
|
|
1218
|
+
{
|
|
1219
|
+
storageClass: s32.StorageClass.INTELLIGENT_TIERING,
|
|
1220
|
+
transitionAfter: Duration5.days(0)
|
|
1221
|
+
}
|
|
1222
|
+
]
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
id: "Intelligent transition for Sent",
|
|
1226
|
+
enabled: true,
|
|
1227
|
+
prefix: "sent/",
|
|
1228
|
+
transitions: [
|
|
1229
|
+
{
|
|
1230
|
+
storageClass: s32.StorageClass.INTELLIGENT_TIERING,
|
|
1231
|
+
transitionAfter: Duration5.days(0)
|
|
1232
|
+
}
|
|
1233
|
+
]
|
|
1234
|
+
}
|
|
1235
|
+
]
|
|
953
1236
|
});
|
|
954
|
-
Tags.of(this.
|
|
955
|
-
|
|
956
|
-
this.vault = new backup.BackupVault(scope, "BackupVault", {
|
|
957
|
-
backupVaultName: `${props.slug}-${props.appEnv}-daily-backup-vault`,
|
|
958
|
-
encryptionKey: props.kmsKey,
|
|
959
|
-
removalPolicy: RemovalPolicy3.DESTROY
|
|
960
|
-
});
|
|
961
|
-
this.backupPlan = backup.BackupPlan.daily35DayRetention(scope, "BackupPlan", this.vault);
|
|
962
|
-
this.backupPlan.addSelection("Selection", {
|
|
963
|
-
role: this.backupRole,
|
|
964
|
-
resources: [backup.BackupResource.fromTag("daily-backup", "true")]
|
|
965
|
-
});
|
|
966
|
-
}
|
|
967
|
-
createBackupRole(scope) {
|
|
968
|
-
const backupRole = new iam2.Role(scope, "BackupRole", {
|
|
969
|
-
assumedBy: new iam2.ServicePrincipal("backup.amazonaws.com")
|
|
970
|
-
});
|
|
971
|
-
backupRole.addToPolicy(new iam2.PolicyStatement({
|
|
972
|
-
actions: [
|
|
973
|
-
"s3:GetInventoryConfiguration",
|
|
974
|
-
"s3:PutInventoryConfiguration",
|
|
975
|
-
"s3:ListBucketVersions",
|
|
976
|
-
"s3:ListBucket",
|
|
977
|
-
"s3:GetBucketVersioning",
|
|
978
|
-
"s3:GetBucketNotification",
|
|
979
|
-
"s3:PutBucketNotification",
|
|
980
|
-
"s3:GetBucketLocation",
|
|
981
|
-
"s3:GetBucketTagging"
|
|
982
|
-
],
|
|
983
|
-
resources: ["arn:aws:s3:::*"],
|
|
984
|
-
sid: "S3BucketBackupPermissions"
|
|
985
|
-
}));
|
|
986
|
-
backupRole.addToPolicy(new iam2.PolicyStatement({
|
|
987
|
-
actions: [
|
|
988
|
-
"s3:GetObjectAcl",
|
|
989
|
-
"s3:GetObject",
|
|
990
|
-
"s3:GetObjectVersionTagging",
|
|
991
|
-
"s3:GetObjectVersionAcl",
|
|
992
|
-
"s3:GetObjectTagging",
|
|
993
|
-
"s3:GetObjectVersion"
|
|
994
|
-
],
|
|
995
|
-
resources: ["arn:aws:s3:::*/*"],
|
|
996
|
-
sid: "S3ObjectBackupPermissions"
|
|
997
|
-
}));
|
|
998
|
-
backupRole.addToPolicy(new iam2.PolicyStatement({
|
|
999
|
-
actions: ["s3:ListAllMyBuckets"],
|
|
1000
|
-
resources: ["*"],
|
|
1001
|
-
sid: "S3GlobalPermissions"
|
|
1002
|
-
}));
|
|
1003
|
-
backupRole.addToPolicy(new iam2.PolicyStatement({
|
|
1004
|
-
actions: ["kms:Decrypt", "kms:DescribeKey"],
|
|
1005
|
-
resources: ["*"],
|
|
1006
|
-
sid: "KMSBackupPermissions",
|
|
1007
|
-
conditions: {
|
|
1008
|
-
StringLike: {
|
|
1009
|
-
"kms:ViaService": "s3.*.amazonaws.com"
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
}));
|
|
1013
|
-
backupRole.addToPolicy(new iam2.PolicyStatement({
|
|
1014
|
-
actions: [
|
|
1015
|
-
"events:DescribeRule",
|
|
1016
|
-
"events:EnableRule",
|
|
1017
|
-
"events:PutRule",
|
|
1018
|
-
"events:DeleteRule",
|
|
1019
|
-
"events:PutTargets",
|
|
1020
|
-
"events:RemoveTargets",
|
|
1021
|
-
"events:ListTargetsByRule",
|
|
1022
|
-
"events:DisableRule"
|
|
1023
|
-
],
|
|
1024
|
-
resources: ["arn:aws:events:*:*:rule/AwsBackupManagedRule*"],
|
|
1025
|
-
sid: "EventsPermissions"
|
|
1026
|
-
}));
|
|
1027
|
-
backupRole.addToPolicy(new iam2.PolicyStatement({
|
|
1028
|
-
actions: ["cloudwatch:GetMetricData", "events:ListRules"],
|
|
1029
|
-
resources: ["*"],
|
|
1030
|
-
sid: "EventsMetricsGlobalPermissions"
|
|
1031
|
-
}));
|
|
1032
|
-
return backupRole;
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
// src/cloud/security.ts
|
|
1037
|
-
import {config as config10} from "@stacksjs/config";
|
|
1038
|
-
import {Duration as Duration4, RemovalPolicy as RemovalPolicy4, Tags as Tags2, aws_certificatemanager as acm, aws_kms as kms, aws_wafv2 as wafv2} from "aws-cdk-lib";
|
|
1039
|
-
|
|
1040
|
-
class SecurityStack {
|
|
1041
|
-
firewall;
|
|
1042
|
-
kmsKey;
|
|
1043
|
-
certificate;
|
|
1044
|
-
constructor(scope, props) {
|
|
1045
|
-
const firewallOptions = config10.cloud.firewall;
|
|
1046
|
-
if (!firewallOptions)
|
|
1047
|
-
throw new Error("No firewall options found in config");
|
|
1048
|
-
const options = {
|
|
1049
|
-
defaultAction: { allow: {} },
|
|
1050
|
-
scope: "CLOUDFRONT",
|
|
1051
|
-
visibilityConfig: {
|
|
1052
|
-
sampledRequestsEnabled: true,
|
|
1053
|
-
cloudWatchMetricsEnabled: true,
|
|
1054
|
-
metricName: "firewallMetric"
|
|
1055
|
-
},
|
|
1056
|
-
rules: this.getFirewallRules()
|
|
1057
|
-
};
|
|
1058
|
-
this.firewall = new wafv2.CfnWebACL(scope, "StacksWebFirewall", options);
|
|
1059
|
-
Tags2.of(this.firewall).add("Name", "waf-cloudfront", { priority: 300 });
|
|
1060
|
-
Tags2.of(this.firewall).add("Purpose", "CloudFront", { priority: 300 });
|
|
1061
|
-
Tags2.of(this.firewall).add("CreatedBy", "CloudFormation", { priority: 300 });
|
|
1062
|
-
this.kmsKey = new kms.Key(scope, "EncryptionKey", {
|
|
1063
|
-
alias: "stacks-encryption-key",
|
|
1064
|
-
description: "KMS key for Stacks Cloud",
|
|
1065
|
-
enableKeyRotation: true,
|
|
1066
|
-
removalPolicy: RemovalPolicy4.DESTROY,
|
|
1067
|
-
pendingWindow: Duration4.days(30)
|
|
1068
|
-
});
|
|
1069
|
-
this.certificate = new acm.Certificate(scope, "Certificate", {
|
|
1070
|
-
domainName: props.domain,
|
|
1071
|
-
validation: acm.CertificateValidation.fromDns(props.zone),
|
|
1072
|
-
subjectAlternativeNames: [`www.${props.domain}`, `api.${props.domain}`]
|
|
1073
|
-
});
|
|
1074
|
-
}
|
|
1075
|
-
getFirewallRules() {
|
|
1076
|
-
const rules = [];
|
|
1077
|
-
const priorities = [];
|
|
1078
|
-
if (config10.security.firewall?.countryCodes?.length) {
|
|
1079
|
-
priorities.push(1);
|
|
1080
|
-
rules.push({
|
|
1081
|
-
name: "CountryRule",
|
|
1082
|
-
priority: priorities.length,
|
|
1083
|
-
statement: {
|
|
1084
|
-
geoMatchStatement: {
|
|
1085
|
-
countryCodes: config10.security.firewall.countryCodes
|
|
1086
|
-
}
|
|
1087
|
-
},
|
|
1088
|
-
action: {
|
|
1089
|
-
block: {}
|
|
1090
|
-
},
|
|
1091
|
-
visibilityConfig: {
|
|
1092
|
-
sampledRequestsEnabled: true,
|
|
1093
|
-
cloudWatchMetricsEnabled: true,
|
|
1094
|
-
metricName: "CountryRule"
|
|
1095
|
-
}
|
|
1096
|
-
});
|
|
1097
|
-
}
|
|
1098
|
-
if (config10.security.firewall?.ipAddresses?.length) {
|
|
1099
|
-
const ipSet = new wafv2.CfnIPSet(this, "IpSet", {
|
|
1100
|
-
name: "IpSet",
|
|
1101
|
-
description: "IP Set",
|
|
1102
|
-
scope: "CLOUDFRONT",
|
|
1103
|
-
addresses: config10.security.firewall.ipAddresses,
|
|
1104
|
-
ipAddressVersion: "IPV4"
|
|
1105
|
-
});
|
|
1106
|
-
priorities.push(1);
|
|
1107
|
-
rules.push({
|
|
1108
|
-
name: "IpAddressRule",
|
|
1109
|
-
priority: priorities.length,
|
|
1110
|
-
statement: {
|
|
1111
|
-
ipSetReferenceStatement: {
|
|
1112
|
-
arn: ipSet.attrArn
|
|
1113
|
-
}
|
|
1114
|
-
},
|
|
1115
|
-
action: {
|
|
1116
|
-
block: {}
|
|
1117
|
-
},
|
|
1118
|
-
visibilityConfig: {
|
|
1119
|
-
sampledRequestsEnabled: true,
|
|
1120
|
-
cloudWatchMetricsEnabled: true,
|
|
1121
|
-
metricName: "IpAddressRule"
|
|
1122
|
-
}
|
|
1123
|
-
});
|
|
1124
|
-
}
|
|
1125
|
-
if (config10.security.firewall?.httpHeaders?.length) {
|
|
1126
|
-
config10.security.firewall.httpHeaders.forEach((header, index) => {
|
|
1127
|
-
priorities.push(1);
|
|
1128
|
-
rules.push({
|
|
1129
|
-
name: `HttpHeaderRule${index}`,
|
|
1130
|
-
priority: priorities.length,
|
|
1131
|
-
statement: {
|
|
1132
|
-
byteMatchStatement: {
|
|
1133
|
-
fieldToMatch: {
|
|
1134
|
-
singleHeader: {
|
|
1135
|
-
name: header
|
|
1136
|
-
}
|
|
1137
|
-
},
|
|
1138
|
-
positionalConstraint: "EXACTLY",
|
|
1139
|
-
searchString: "true",
|
|
1140
|
-
textTransformations: [
|
|
1141
|
-
{
|
|
1142
|
-
priority: index,
|
|
1143
|
-
type: "NONE"
|
|
1144
|
-
}
|
|
1145
|
-
]
|
|
1146
|
-
}
|
|
1147
|
-
},
|
|
1148
|
-
action: {
|
|
1149
|
-
block: {}
|
|
1150
|
-
},
|
|
1151
|
-
visibilityConfig: {
|
|
1152
|
-
sampledRequestsEnabled: true,
|
|
1153
|
-
cloudWatchMetricsEnabled: true,
|
|
1154
|
-
metricName: `HttpHeaderRule${index}`
|
|
1155
|
-
}
|
|
1156
|
-
});
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1159
|
-
return rules;
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
// src/cloud/deployment.ts
|
|
1164
|
-
import {AssetHashType as AssetHashType2, aws_s3_deployment as s3deploy} from "aws-cdk-lib";
|
|
1165
|
-
import {config as config12} from "@stacksjs/config";
|
|
1166
|
-
import {websiteSourceHash} from "@stacksjs/utils";
|
|
1167
|
-
|
|
1168
|
-
class DeploymentStack {
|
|
1169
|
-
privateSource;
|
|
1170
|
-
docsSource;
|
|
1171
|
-
websiteSource;
|
|
1172
|
-
constructor(scope, props) {
|
|
1173
|
-
this.privateSource = "../../private";
|
|
1174
|
-
this.docsSource = "../docs/dist/";
|
|
1175
|
-
this.websiteSource = config12.app.docMode === true ? this.docsSource : "../views/dist/";
|
|
1176
|
-
new s3deploy.BucketDeployment(scope, "Website", {
|
|
1177
|
-
sources: [s3deploy.Source.asset(this.websiteSource, {
|
|
1178
|
-
assetHash: websiteSourceHash(),
|
|
1179
|
-
assetHashType: AssetHashType2.CUSTOM
|
|
1180
|
-
})],
|
|
1181
|
-
destinationBucket: props.publicBucket,
|
|
1182
|
-
distribution: props.cdn,
|
|
1183
|
-
distributionPaths: ["/*"]
|
|
1184
|
-
});
|
|
1185
|
-
new s3deploy.BucketDeployment(scope, "PrivateFiles", {
|
|
1186
|
-
sources: [s3deploy.Source.asset(this.privateSource)],
|
|
1187
|
-
destinationBucket: props.privateBucket
|
|
1188
|
-
});
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
// src/cloud/jump-box.ts
|
|
1193
|
-
import {CfnOutput as Output5, aws_ec2 as ec2, aws_iam as iam3} from "aws-cdk-lib";
|
|
1194
|
-
|
|
1195
|
-
class JumpBoxStack {
|
|
1196
|
-
jumpBox;
|
|
1197
|
-
constructor(scope, props) {
|
|
1198
|
-
const role = new iam3.Role(scope, "JumpBoxInstanceRole", {
|
|
1199
|
-
assumedBy: new iam3.ServicePrincipal("ec2.amazonaws.com"),
|
|
1200
|
-
managedPolicies: [
|
|
1201
|
-
iam3.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"),
|
|
1202
|
-
iam3.ManagedPolicy.fromAwsManagedPolicyName("CloudWatchAgentServerPolicy")
|
|
1203
|
-
]
|
|
1204
|
-
});
|
|
1205
|
-
this.jumpBox = new ec2.Instance(scope, "JumpBox", {
|
|
1206
|
-
vpc: props.vpc,
|
|
1207
|
-
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
|
|
1208
|
-
machineImage: new ec2.AmazonLinuxImage,
|
|
1209
|
-
role,
|
|
1210
|
-
userData: ec2.UserData.custom(`
|
|
1211
|
-
#!/bin/bash
|
|
1212
|
-
yum update -y
|
|
1213
|
-
yum install -y amazon-efs-utils
|
|
1214
|
-
yum install -y git
|
|
1215
|
-
yum install -y https://s3.us-east-1.amazonaws.com/amazon-ssm-us-east-1/latest/linux_amd64/amazon-ssm-agent.rpm
|
|
1216
|
-
mkdir /mnt/efs
|
|
1217
|
-
mount -t efs ${props.fileSystem.fileSystemId}:/ /mnt/efs
|
|
1218
|
-
git clone https://github.com/stacksjs/stacks.git /mnt/efs
|
|
1219
|
-
`)
|
|
1220
|
-
});
|
|
1221
|
-
new Output5(scope, "JumpBoxInstanceId", {
|
|
1222
|
-
value: this.jumpBox.instanceId,
|
|
1223
|
-
description: "The ID of the EC2 instance that can be used to SSH into the Stacks Cloud."
|
|
1224
|
-
});
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
// src/cloud/file-system.ts
|
|
1229
|
-
import {RemovalPolicy as RemovalPolicy5, aws_efs as efs} from "aws-cdk-lib";
|
|
1230
|
-
|
|
1231
|
-
class FileSystemStack {
|
|
1232
|
-
fileSystem;
|
|
1233
|
-
accessPoint;
|
|
1234
|
-
constructor(scope, props) {
|
|
1235
|
-
this.fileSystem = new efs.FileSystem(scope, "FileSystem", {
|
|
1236
|
-
fileSystemName: `${props.slug}-${props.appEnv}-efs`,
|
|
1237
|
-
vpc: props.vpc,
|
|
1238
|
-
removalPolicy: RemovalPolicy5.DESTROY,
|
|
1239
|
-
lifecyclePolicy: efs.LifecyclePolicy.AFTER_7_DAYS,
|
|
1240
|
-
performanceMode: efs.PerformanceMode.GENERAL_PURPOSE,
|
|
1241
|
-
throughputMode: efs.ThroughputMode.BURSTING,
|
|
1242
|
-
enableAutomaticBackups: true,
|
|
1243
|
-
encrypted: true
|
|
1244
|
-
});
|
|
1245
|
-
this.accessPoint = new efs.AccessPoint(scope, "FileSystemAccessPoint", {
|
|
1246
|
-
fileSystem: this.fileSystem,
|
|
1247
|
-
path: "/",
|
|
1248
|
-
posixUser: {
|
|
1249
|
-
uid: "1000",
|
|
1250
|
-
gid: "1000"
|
|
1251
|
-
}
|
|
1252
|
-
});
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
// src/cloud/network.ts
|
|
1257
|
-
import {aws_ec2 as ec22} from "aws-cdk-lib";
|
|
1258
|
-
|
|
1259
|
-
class NetworkStack {
|
|
1260
|
-
vpc;
|
|
1261
|
-
constructor(scope, props) {
|
|
1262
|
-
this.vpc = new ec22.Vpc(scope, "Network", {
|
|
1263
|
-
vpcName: `${props.slug}-${props.appEnv}-vpc`,
|
|
1264
|
-
ipAddresses: ec22.IpAddresses.cidr("10.0.0.0/16"),
|
|
1265
|
-
maxAzs: 3,
|
|
1266
|
-
natGateways: 0,
|
|
1267
|
-
subnetConfiguration: [
|
|
1268
|
-
{
|
|
1269
|
-
name: "public-subnet-1",
|
|
1270
|
-
subnetType: ec22.SubnetType.PUBLIC,
|
|
1271
|
-
cidrMask: 24
|
|
1272
|
-
},
|
|
1273
|
-
{
|
|
1274
|
-
name: "private-subnet-1",
|
|
1275
|
-
subnetType: ec22.SubnetType.PRIVATE_ISOLATED,
|
|
1276
|
-
cidrMask: 28
|
|
1277
|
-
}
|
|
1278
|
-
]
|
|
1279
|
-
});
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
// src/cloud/redirects.ts
|
|
1284
|
-
import {config as config14} from "@stacksjs/config";
|
|
1285
|
-
import {RemovalPolicy as RemovalPolicy6, aws_route53 as route533, aws_s3 as s33} from "aws-cdk-lib";
|
|
1286
|
-
|
|
1287
|
-
class RedirectsStack {
|
|
1288
|
-
redirectZones = [];
|
|
1289
|
-
constructor(scope, props) {
|
|
1290
|
-
config14.dns.redirects?.forEach((redirect) => {
|
|
1291
|
-
const slug2 = redirect.split(".").map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1292
|
-
const hostedZone = route533.HostedZone.fromLookup(scope, "HostedZone", { domainName: redirect });
|
|
1293
|
-
const redirectBucket = new s33.Bucket(scope, `RedirectBucket${slug2}`, {
|
|
1294
|
-
bucketName: `${redirect}-redirect`,
|
|
1295
|
-
websiteRedirect: {
|
|
1296
|
-
hostName: props.domain,
|
|
1297
|
-
protocol: s33.RedirectProtocol.HTTPS
|
|
1298
|
-
},
|
|
1299
|
-
removalPolicy: RemovalPolicy6.DESTROY,
|
|
1300
|
-
autoDeleteObjects: true
|
|
1301
|
-
});
|
|
1302
|
-
new route533.CnameRecord(scope, `RedirectRecord${slug2}`, {
|
|
1303
|
-
zone: hostedZone,
|
|
1304
|
-
recordName: "redirect",
|
|
1305
|
-
domainName: redirectBucket.bucketWebsiteDomainName
|
|
1306
|
-
});
|
|
1307
|
-
});
|
|
1308
|
-
config14.dns.redirects?.forEach((redirect) => {
|
|
1309
|
-
const slug2 = redirect.split(".").map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1310
|
-
const hostedZone = route533.HostedZone.fromLookup(scope, `RedirectHostedZone${slug2}`, { domainName: redirect });
|
|
1311
|
-
this.redirectZones.push(hostedZone);
|
|
1312
|
-
});
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
// src/cloud/email.ts
|
|
1317
|
-
import {Duration as Duration5, RemovalPolicy as RemovalPolicy7, Stack, Tags as Tags3, aws_iam as iam4, aws_lambda as lambda4, aws_route53 as route534, aws_s3 as s34, aws_s3_notifications as s3n, aws_ses as ses} from "aws-cdk-lib";
|
|
1318
|
-
import {config as config16} from "@stacksjs/config";
|
|
1319
|
-
|
|
1320
|
-
class EmailStack {
|
|
1321
|
-
emailBucket;
|
|
1322
|
-
constructor(scope, props) {
|
|
1323
|
-
const bucketPrefix = `${props.slug}-${props.appEnv}`;
|
|
1324
|
-
this.emailBucket = new s34.Bucket(scope, "EmailBucket", {
|
|
1325
|
-
bucketName: `${bucketPrefix}-email-${props.timestamp}`,
|
|
1326
|
-
versioned: true,
|
|
1327
|
-
removalPolicy: RemovalPolicy7.DESTROY,
|
|
1328
|
-
autoDeleteObjects: true,
|
|
1329
|
-
encryption: s34.BucketEncryption.S3_MANAGED,
|
|
1330
|
-
lifecycleRules: [
|
|
1331
|
-
{
|
|
1332
|
-
id: "24h",
|
|
1333
|
-
enabled: true,
|
|
1334
|
-
expiration: Duration5.days(1),
|
|
1335
|
-
noncurrentVersionExpiration: Duration5.days(1),
|
|
1336
|
-
prefix: "today/"
|
|
1337
|
-
},
|
|
1338
|
-
{
|
|
1339
|
-
id: "Intelligent transition for Inbox",
|
|
1340
|
-
enabled: true,
|
|
1341
|
-
prefix: "inbox/",
|
|
1342
|
-
transitions: [
|
|
1343
|
-
{
|
|
1344
|
-
storageClass: s34.StorageClass.INTELLIGENT_TIERING,
|
|
1345
|
-
transitionAfter: Duration5.days(0)
|
|
1346
|
-
}
|
|
1347
|
-
]
|
|
1348
|
-
},
|
|
1349
|
-
{
|
|
1350
|
-
id: "Intelligent transition for Sent",
|
|
1351
|
-
enabled: true,
|
|
1352
|
-
prefix: "sent/",
|
|
1353
|
-
transitions: [
|
|
1354
|
-
{
|
|
1355
|
-
storageClass: s34.StorageClass.INTELLIGENT_TIERING,
|
|
1356
|
-
transitionAfter: Duration5.days(0)
|
|
1357
|
-
}
|
|
1358
|
-
]
|
|
1359
|
-
}
|
|
1360
|
-
]
|
|
1361
|
-
});
|
|
1362
|
-
Tags3.of(this.emailBucket).add("daily-backup", "true");
|
|
1363
|
-
const sesPrincipal = new iam4.ServicePrincipal("ses.amazonaws.com");
|
|
1237
|
+
Tags.of(this.emailBucket).add("daily-backup", "true");
|
|
1238
|
+
const sesPrincipal = new iam2.ServicePrincipal("ses.amazonaws.com");
|
|
1364
1239
|
const ruleSetName = `${props.slug}-${props.appEnv}-email-receipt-rule-set`;
|
|
1365
1240
|
const receiptRuleName = `${props.slug}-${props.appEnv}-email-receipt-rule`;
|
|
1366
1241
|
const ruleSet = new ses.CfnReceiptRuleSet(scope, "SESReceiptRuleSet", {
|
|
1367
1242
|
ruleSetName
|
|
1368
1243
|
});
|
|
1369
|
-
this.emailBucket.addToResourcePolicy(new
|
|
1244
|
+
this.emailBucket.addToResourcePolicy(new iam2.PolicyStatement({
|
|
1370
1245
|
sid: "AllowSESPuts",
|
|
1371
|
-
effect:
|
|
1246
|
+
effect: iam2.Effect.ALLOW,
|
|
1372
1247
|
principals: [sesPrincipal],
|
|
1373
|
-
actions: [
|
|
1374
|
-
|
|
1375
|
-
],
|
|
1376
|
-
resources: [
|
|
1377
|
-
`${this.emailBucket.bucketArn}/*`
|
|
1378
|
-
],
|
|
1248
|
+
actions: ["s3:PutObject"],
|
|
1249
|
+
resources: [`${this.emailBucket.bucketArn}/*`],
|
|
1379
1250
|
conditions: {
|
|
1380
1251
|
StringEquals: {
|
|
1381
1252
|
"aws:SourceAccount": Stack.of(scope).account
|
|
@@ -1398,22 +1269,22 @@ class EmailStack {
|
|
|
1398
1269
|
}
|
|
1399
1270
|
}
|
|
1400
1271
|
],
|
|
1401
|
-
recipients:
|
|
1402
|
-
scanEnabled:
|
|
1272
|
+
recipients: config12.email.mailboxes || [],
|
|
1273
|
+
scanEnabled: config12.email.server?.scan || true,
|
|
1403
1274
|
tlsPolicy: "Require"
|
|
1404
1275
|
}
|
|
1405
1276
|
});
|
|
1406
1277
|
receiptRule.node.addDependency(this.emailBucket);
|
|
1407
|
-
const iamGroup = new
|
|
1278
|
+
const iamGroup = new iam2.Group(scope, "IAMGroup", {
|
|
1408
1279
|
groupName: `${props.slug}-${props.appEnv}-email-management-s3-group`
|
|
1409
1280
|
});
|
|
1410
|
-
const listBucketsPolicyStatement = new
|
|
1411
|
-
effect:
|
|
1281
|
+
const listBucketsPolicyStatement = new iam2.PolicyStatement({
|
|
1282
|
+
effect: iam2.Effect.ALLOW,
|
|
1412
1283
|
actions: ["s3:ListAllMyBuckets"],
|
|
1413
1284
|
resources: ["*"]
|
|
1414
1285
|
});
|
|
1415
|
-
const policyStatement = new
|
|
1416
|
-
effect:
|
|
1286
|
+
const policyStatement = new iam2.PolicyStatement({
|
|
1287
|
+
effect: iam2.Effect.ALLOW,
|
|
1417
1288
|
actions: [
|
|
1418
1289
|
"s3:ListBucket",
|
|
1419
1290
|
"s3:GetObject",
|
|
@@ -1424,12 +1295,9 @@ class EmailStack {
|
|
|
1424
1295
|
"s3:PutObjectAcl",
|
|
1425
1296
|
"s3:PutObjectVersionAcl"
|
|
1426
1297
|
],
|
|
1427
|
-
resources: [
|
|
1428
|
-
this.emailBucket.bucketArn,
|
|
1429
|
-
`${this.emailBucket.bucketArn}/*`
|
|
1430
|
-
]
|
|
1298
|
+
resources: [this.emailBucket.bucketArn, `${this.emailBucket.bucketArn}/*`]
|
|
1431
1299
|
});
|
|
1432
|
-
const policy = new
|
|
1300
|
+
const policy = new iam2.Policy(scope, "EmailAccessPolicy", {
|
|
1433
1301
|
policyName: `${props.slug}-${props.appEnv}-email-management-s3-policy`,
|
|
1434
1302
|
statements: [policyStatement, listBucketsPolicyStatement]
|
|
1435
1303
|
});
|
|
@@ -1474,10 +1342,12 @@ class EmailStack {
|
|
|
1474
1342
|
new route534.MxRecord(scope, "MxRecord", {
|
|
1475
1343
|
zone: props.zone,
|
|
1476
1344
|
recordName: "mail",
|
|
1477
|
-
values: [
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1345
|
+
values: [
|
|
1346
|
+
{
|
|
1347
|
+
priority: 10,
|
|
1348
|
+
hostName: "feedback-smtp.us-east-1.amazonses.com"
|
|
1349
|
+
}
|
|
1350
|
+
]
|
|
1481
1351
|
});
|
|
1482
1352
|
new route534.TxtRecord(scope, "TxtSpfRecord", {
|
|
1483
1353
|
zone: props.zone,
|
|
@@ -1489,12 +1359,10 @@ class EmailStack {
|
|
|
1489
1359
|
recordName: "_dmarc",
|
|
1490
1360
|
values: [`v=DMARC1;p=quarantine;pct=25;rua=mailto:dmarcreports@${props.domain}`]
|
|
1491
1361
|
});
|
|
1492
|
-
const lambdaEmailOutboundRole = new
|
|
1362
|
+
const lambdaEmailOutboundRole = new iam2.Role(scope, "LambdaEmailOutboundRole", {
|
|
1493
1363
|
roleName: `${props.slug}-${props.appEnv}-email-outbound`,
|
|
1494
|
-
assumedBy: new
|
|
1495
|
-
managedPolicies: [
|
|
1496
|
-
iam4.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
|
|
1497
|
-
]
|
|
1364
|
+
assumedBy: new iam2.ServicePrincipal("lambda.amazonaws.com"),
|
|
1365
|
+
managedPolicies: [iam2.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")]
|
|
1498
1366
|
});
|
|
1499
1367
|
const lambdaEmailOutbound = new lambda4.Function(scope, "LambdaEmailOutbound", {
|
|
1500
1368
|
functionName: `${props.slug}-${props.appEnv}-email-outbound`,
|
|
@@ -1510,18 +1378,16 @@ class EmailStack {
|
|
|
1510
1378
|
role: lambdaEmailOutboundRole
|
|
1511
1379
|
});
|
|
1512
1380
|
lambdaEmailOutboundRole.addToPolicy(policyStatement);
|
|
1513
|
-
const sesPolicyStatement = new
|
|
1514
|
-
effect:
|
|
1381
|
+
const sesPolicyStatement = new iam2.PolicyStatement({
|
|
1382
|
+
effect: iam2.Effect.ALLOW,
|
|
1515
1383
|
actions: ["ses:SendRawEmail"],
|
|
1516
1384
|
resources: ["*"]
|
|
1517
1385
|
});
|
|
1518
1386
|
lambdaEmailOutboundRole.addToPolicy(sesPolicyStatement);
|
|
1519
|
-
const lambdaEmailInboundRole = new
|
|
1387
|
+
const lambdaEmailInboundRole = new iam2.Role(scope, "LambdaEmailInboundRole", {
|
|
1520
1388
|
roleName: `${props.slug}-${props.appEnv}-email-inbound`,
|
|
1521
|
-
assumedBy: new
|
|
1522
|
-
managedPolicies: [
|
|
1523
|
-
iam4.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
|
|
1524
|
-
]
|
|
1389
|
+
assumedBy: new iam2.ServicePrincipal("lambda.amazonaws.com"),
|
|
1390
|
+
managedPolicies: [iam2.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")]
|
|
1525
1391
|
});
|
|
1526
1392
|
const lambdaEmailInbound = new lambda4.Function(scope, "LambdaEmailInbound", {
|
|
1527
1393
|
functionName: `${props.slug}-${props.appEnv}-email-inbound`,
|
|
@@ -1541,27 +1407,22 @@ class EmailStack {
|
|
|
1541
1407
|
functionName: lambdaEmailInbound.functionName,
|
|
1542
1408
|
principal: "s3.amazonaws.com"
|
|
1543
1409
|
});
|
|
1544
|
-
const inboundS3PolicyStatement = new
|
|
1545
|
-
effect:
|
|
1410
|
+
const inboundS3PolicyStatement = new iam2.PolicyStatement({
|
|
1411
|
+
effect: iam2.Effect.ALLOW,
|
|
1546
1412
|
actions: ["s3:*"],
|
|
1547
|
-
resources: [
|
|
1548
|
-
this.emailBucket.bucketArn,
|
|
1549
|
-
`${this.emailBucket.bucketArn}/*`
|
|
1550
|
-
]
|
|
1413
|
+
resources: [this.emailBucket.bucketArn, `${this.emailBucket.bucketArn}/*`]
|
|
1551
1414
|
});
|
|
1552
1415
|
lambdaEmailInboundRole.addToPolicy(inboundS3PolicyStatement);
|
|
1553
|
-
const sesInboundPolicyStatement = new
|
|
1554
|
-
effect:
|
|
1416
|
+
const sesInboundPolicyStatement = new iam2.PolicyStatement({
|
|
1417
|
+
effect: iam2.Effect.ALLOW,
|
|
1555
1418
|
actions: ["ses:ListIdentities"],
|
|
1556
1419
|
resources: ["*"]
|
|
1557
1420
|
});
|
|
1558
1421
|
lambdaEmailInboundRole.addToPolicy(sesInboundPolicyStatement);
|
|
1559
|
-
const lambdaEmailConverterRole = new
|
|
1422
|
+
const lambdaEmailConverterRole = new iam2.Role(scope, "LambdaEmailConverterRole", {
|
|
1560
1423
|
roleName: `${props.slug}-${props.appEnv}-email-converter`,
|
|
1561
|
-
assumedBy: new
|
|
1562
|
-
managedPolicies: [
|
|
1563
|
-
iam4.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
|
|
1564
|
-
]
|
|
1424
|
+
assumedBy: new iam2.ServicePrincipal("lambda.amazonaws.com"),
|
|
1425
|
+
managedPolicies: [iam2.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")]
|
|
1565
1426
|
});
|
|
1566
1427
|
const lambdaEmailConverter = new lambda4.Function(scope, "LambdaEmailConverter", {
|
|
1567
1428
|
functionName: `${props.slug}-${props.appEnv}-email-converter`,
|
|
@@ -1576,13 +1437,10 @@ class EmailStack {
|
|
|
1576
1437
|
BUCKET: this.emailBucket.bucketName
|
|
1577
1438
|
}
|
|
1578
1439
|
});
|
|
1579
|
-
const converterS3PolicyStatement = new
|
|
1580
|
-
effect:
|
|
1440
|
+
const converterS3PolicyStatement = new iam2.PolicyStatement({
|
|
1441
|
+
effect: iam2.Effect.ALLOW,
|
|
1581
1442
|
actions: ["s3:*"],
|
|
1582
|
-
resources: [
|
|
1583
|
-
this.emailBucket.bucketArn,
|
|
1584
|
-
`${this.emailBucket.bucketArn}/*`
|
|
1585
|
-
]
|
|
1443
|
+
resources: [this.emailBucket.bucketArn, `${this.emailBucket.bucketArn}/*`]
|
|
1586
1444
|
});
|
|
1587
1445
|
new lambda4.CfnPermission(scope, "S3ConverterPermission", {
|
|
1588
1446
|
action: "lambda:InvokeFunction",
|
|
@@ -1590,211 +1448,134 @@ class EmailStack {
|
|
|
1590
1448
|
principal: "s3.amazonaws.com"
|
|
1591
1449
|
});
|
|
1592
1450
|
lambdaEmailConverterRole.addToPolicy(converterS3PolicyStatement);
|
|
1593
|
-
this.emailBucket.addEventNotification(
|
|
1594
|
-
this.emailBucket.addEventNotification(
|
|
1595
|
-
this.emailBucket.addEventNotification(
|
|
1596
|
-
this.emailBucket.addEventNotification(
|
|
1597
|
-
this.emailBucket.addEventNotification(
|
|
1451
|
+
this.emailBucket.addEventNotification(s32.EventType.OBJECT_CREATED_PUT, new s3n.LambdaDestination(lambdaEmailInbound), { prefix: "tmp/email_in/" });
|
|
1452
|
+
this.emailBucket.addEventNotification(s32.EventType.OBJECT_CREATED_PUT, new s3n.LambdaDestination(lambdaEmailOutbound), { prefix: "tmp/email_out/json/" });
|
|
1453
|
+
this.emailBucket.addEventNotification(s32.EventType.OBJECT_CREATED_COPY, new s3n.LambdaDestination(lambdaEmailConverter), { prefix: "sent/" });
|
|
1454
|
+
this.emailBucket.addEventNotification(s32.EventType.OBJECT_CREATED_COPY, new s3n.LambdaDestination(lambdaEmailConverter), { prefix: "inbox/" });
|
|
1455
|
+
this.emailBucket.addEventNotification(s32.EventType.OBJECT_CREATED_COPY, new s3n.LambdaDestination(lambdaEmailConverter), { prefix: "today/" });
|
|
1598
1456
|
}
|
|
1599
1457
|
}
|
|
1600
1458
|
|
|
1601
|
-
// src/cloud/
|
|
1602
|
-
import {
|
|
1603
|
-
import {config as config18} from "@stacksjs/config";
|
|
1604
|
-
import {string} from "@stacksjs/strings";
|
|
1605
|
-
import {env as env4} from "@stacksjs/env";
|
|
1459
|
+
// src/cloud/file-system.ts
|
|
1460
|
+
import {RemovalPolicy as RemovalPolicy5, aws_efs as efs} from "aws-cdk-lib";
|
|
1606
1461
|
|
|
1607
|
-
class
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1462
|
+
class FileSystemStack {
|
|
1463
|
+
fileSystem;
|
|
1464
|
+
accessPoint;
|
|
1465
|
+
constructor(scope, props) {
|
|
1466
|
+
this.fileSystem = new efs.FileSystem(scope, "FileSystem", {
|
|
1467
|
+
fileSystemName: `${props.slug}-${props.appEnv}-efs`,
|
|
1468
|
+
vpc: props.vpc,
|
|
1469
|
+
removalPolicy: RemovalPolicy5.DESTROY,
|
|
1470
|
+
lifecyclePolicy: efs.LifecyclePolicy.AFTER_7_DAYS,
|
|
1471
|
+
performanceMode: efs.PerformanceMode.GENERAL_PURPOSE,
|
|
1472
|
+
throughputMode: efs.ThroughputMode.BURSTING,
|
|
1473
|
+
enableAutomaticBackups: true,
|
|
1474
|
+
encrypted: true
|
|
1475
|
+
});
|
|
1476
|
+
this.accessPoint = new efs.AccessPoint(scope, "FileSystemAccessPoint", {
|
|
1477
|
+
fileSystem: this.fileSystem,
|
|
1478
|
+
path: "/",
|
|
1479
|
+
posixUser: {
|
|
1480
|
+
uid: "1000",
|
|
1481
|
+
gid: "1000"
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1622
1484
|
}
|
|
1623
1485
|
}
|
|
1624
1486
|
|
|
1625
|
-
// src/cloud/
|
|
1626
|
-
import {
|
|
1627
|
-
import {path as p4} from "@stacksjs/path";
|
|
1628
|
-
import {env as env6} from "@stacksjs/env";
|
|
1629
|
-
import {LogGroup} from "aws-cdk-lib/aws-logs";
|
|
1630
|
-
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
|
|
1487
|
+
// src/cloud/jump-box.ts
|
|
1488
|
+
import {CfnOutput as Output6, aws_ec2 as ec22, aws_iam as iam3} from "aws-cdk-lib";
|
|
1631
1489
|
|
|
1632
|
-
class
|
|
1633
|
-
|
|
1634
|
-
cluster;
|
|
1635
|
-
taskDefinition;
|
|
1490
|
+
class JumpBoxStack {
|
|
1491
|
+
jumpBox;
|
|
1636
1492
|
constructor(scope, props) {
|
|
1637
|
-
const
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
vpc
|
|
1644
|
-
});
|
|
1645
|
-
this.taskDefinition = new ecs.FargateTaskDefinition(scope, "TaskDefinition", {
|
|
1646
|
-
family: `${props.appName}-${props.appEnv}-api`,
|
|
1647
|
-
memoryLimitMiB: 512,
|
|
1648
|
-
cpu: 256,
|
|
1649
|
-
runtimePlatform: {
|
|
1650
|
-
cpuArchitecture: ecs.CpuArchitecture.ARM64
|
|
1651
|
-
}
|
|
1652
|
-
});
|
|
1653
|
-
const container = this.taskDefinition.addContainer("WebServerContainer", {
|
|
1654
|
-
containerName: `${props.appName}-${props.appEnv}-api`,
|
|
1655
|
-
image: ecs.ContainerImage.fromAsset(p4.frameworkPath("server")),
|
|
1656
|
-
logging: new ecs.AwsLogDriver({
|
|
1657
|
-
streamPrefix: `${props.appName}-${props.appEnv}-web-server-logs`,
|
|
1658
|
-
logGroup: new LogGroup(scope, "StacksApiLogs", {
|
|
1659
|
-
logGroupName: "/aws/ecs/stacks-api",
|
|
1660
|
-
removalPolicy: RemovalPolicy8.DESTROY
|
|
1661
|
-
})
|
|
1662
|
-
}),
|
|
1663
|
-
healthCheck: {
|
|
1664
|
-
command: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"],
|
|
1665
|
-
interval: Duration6.seconds(10),
|
|
1666
|
-
timeout: Duration6.seconds(5),
|
|
1667
|
-
retries: 3,
|
|
1668
|
-
startPeriod: Duration6.seconds(10)
|
|
1669
|
-
}
|
|
1670
|
-
});
|
|
1671
|
-
container.addPortMappings({
|
|
1672
|
-
containerPort: 3000,
|
|
1673
|
-
hostPort: 3000
|
|
1493
|
+
const role = new iam3.Role(scope, "JumpBoxInstanceRole", {
|
|
1494
|
+
assumedBy: new iam3.ServicePrincipal("ec2.amazonaws.com"),
|
|
1495
|
+
managedPolicies: [
|
|
1496
|
+
iam3.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"),
|
|
1497
|
+
iam3.ManagedPolicy.fromAwsManagedPolicyName("CloudWatchAgentServerPolicy")
|
|
1498
|
+
]
|
|
1674
1499
|
});
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1500
|
+
this.jumpBox = new ec22.Instance(scope, "JumpBox", {
|
|
1501
|
+
vpc: props.vpc,
|
|
1502
|
+
instanceType: ec22.InstanceType.of(ec22.InstanceClass.T2, ec22.InstanceSize.MICRO),
|
|
1503
|
+
machineImage: new ec22.AmazonLinuxImage,
|
|
1504
|
+
role,
|
|
1505
|
+
userData: ec22.UserData.custom(`
|
|
1506
|
+
#!/bin/bash
|
|
1507
|
+
yum update -y
|
|
1508
|
+
yum install -y amazon-efs-utils
|
|
1509
|
+
yum install -y git
|
|
1510
|
+
yum install -y https://s3.us-east-1.amazonaws.com/amazon-ssm-us-east-1/latest/linux_amd64/amazon-ssm-agent.rpm
|
|
1511
|
+
mkdir /mnt/efs
|
|
1512
|
+
mount -t efs ${props.fileSystem.fileSystemId}:/ /mnt/efs
|
|
1513
|
+
git clone https://github.com/stacksjs/stacks.git /mnt/efs
|
|
1514
|
+
`)
|
|
1679
1515
|
});
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
description: "Access to the public facing load balancer"
|
|
1516
|
+
new Output6(scope, "JumpBoxInstanceId", {
|
|
1517
|
+
value: this.jumpBox.instanceId,
|
|
1518
|
+
description: "The ID of the EC2 instance that can be used to SSH into the Stacks Cloud."
|
|
1684
1519
|
});
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// src/cloud/network.ts
|
|
1524
|
+
import {aws_ec2 as ec23} from "aws-cdk-lib";
|
|
1525
|
+
|
|
1526
|
+
class NetworkStack {
|
|
1527
|
+
vpc;
|
|
1528
|
+
constructor(scope, props) {
|
|
1529
|
+
this.vpc = new ec23.Vpc(scope, "Network", {
|
|
1530
|
+
vpcName: `${props.slug}-${props.appEnv}-vpc`,
|
|
1531
|
+
ipAddresses: ec23.IpAddresses.cidr("10.0.0.0/16"),
|
|
1532
|
+
maxAzs: 3,
|
|
1533
|
+
natGateways: 0,
|
|
1534
|
+
subnetConfiguration: [
|
|
1535
|
+
{
|
|
1536
|
+
name: "public-subnet-1",
|
|
1692
1537
|
subnetType: ec23.SubnetType.PUBLIC,
|
|
1693
|
-
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
zone: props.zone,
|
|
1702
|
-
recordName: "api",
|
|
1703
|
-
target: route535.RecordTarget.fromAlias(new route53Targets.LoadBalancerTarget(this.lb))
|
|
1704
|
-
});
|
|
1705
|
-
const serviceTargetGroup = new elbv2.ApplicationTargetGroup(scope, "ServiceTargetGroup", {
|
|
1706
|
-
targetGroupName: `${props.appName}-${props.appEnv}-api-tg`,
|
|
1707
|
-
vpc,
|
|
1708
|
-
targetType: elbv2.TargetType.IP,
|
|
1709
|
-
protocol: elbv2.ApplicationProtocol.HTTP,
|
|
1710
|
-
port: 3000,
|
|
1711
|
-
healthCheck: {
|
|
1712
|
-
interval: Duration6.seconds(6),
|
|
1713
|
-
path: "/api/health",
|
|
1714
|
-
protocol: elbv2.Protocol.HTTP,
|
|
1715
|
-
timeout: Duration6.seconds(5),
|
|
1716
|
-
healthyThresholdCount: 2,
|
|
1717
|
-
unhealthyThresholdCount: 10
|
|
1718
|
-
}
|
|
1719
|
-
});
|
|
1720
|
-
const service = new ecs.FargateService(scope, "StacksApiService", {
|
|
1721
|
-
serviceName: `${props.appName}-${props.appEnv}-api-service`,
|
|
1722
|
-
cluster: this.cluster,
|
|
1723
|
-
taskDefinition: this.taskDefinition,
|
|
1724
|
-
desiredCount: 1,
|
|
1725
|
-
assignPublicIp: true,
|
|
1726
|
-
maxHealthyPercent: 200,
|
|
1727
|
-
vpcSubnets: vpc.selectSubnets({
|
|
1728
|
-
subnetType: ec23.SubnetType.PUBLIC,
|
|
1729
|
-
onePerAz: true
|
|
1730
|
-
}),
|
|
1731
|
-
minHealthyPercent: 75,
|
|
1732
|
-
securityGroups: [serviceSecurityGroup]
|
|
1733
|
-
});
|
|
1734
|
-
service.attachToApplicationTargetGroup(serviceTargetGroup);
|
|
1735
|
-
publicLoadBalancerSG.addIngressRule(ec23.Peer.anyIpv4(), ec23.Port.allTraffic());
|
|
1736
|
-
this.lb.addListener("HttpsListener", {
|
|
1737
|
-
port: 443,
|
|
1738
|
-
certificates: [props.certificate],
|
|
1739
|
-
defaultAction: elbv2.ListenerAction.forward([serviceTargetGroup])
|
|
1740
|
-
});
|
|
1741
|
-
this.lb.addListener("HttpListener", {
|
|
1742
|
-
port: 80,
|
|
1743
|
-
defaultAction: elbv2.ListenerAction.forward([serviceTargetGroup])
|
|
1744
|
-
});
|
|
1745
|
-
props.fileSystem.connections.allowFromAnyIpv4(ec23.Port.tcp(2049));
|
|
1746
|
-
const volumeName = `${props.slug}-${props.appEnv}-efs`;
|
|
1747
|
-
this.taskDefinition.addVolume({
|
|
1748
|
-
name: volumeName,
|
|
1749
|
-
efsVolumeConfiguration: {
|
|
1750
|
-
fileSystemId: props.fileSystem.fileSystemId
|
|
1751
|
-
}
|
|
1752
|
-
});
|
|
1753
|
-
container.addMountPoints({
|
|
1754
|
-
sourceVolume: volumeName,
|
|
1755
|
-
containerPath: "/mnt/efs",
|
|
1756
|
-
readOnly: false
|
|
1757
|
-
});
|
|
1758
|
-
const scaling = service.autoScaleTaskCount({ maxCapacity: 2 });
|
|
1759
|
-
scaling.scaleOnCpuUtilization("CpuScaling", {
|
|
1760
|
-
targetUtilizationPercent: 50,
|
|
1761
|
-
scaleInCooldown: Duration6.seconds(60),
|
|
1762
|
-
scaleOutCooldown: Duration6.seconds(60)
|
|
1763
|
-
});
|
|
1764
|
-
scaling.scaleOnMemoryUtilization("MemoryScaling", {
|
|
1765
|
-
targetUtilizationPercent: 60,
|
|
1766
|
-
scaleInCooldown: Duration6.seconds(60),
|
|
1767
|
-
scaleOutCooldown: Duration6.seconds(60)
|
|
1768
|
-
});
|
|
1769
|
-
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", "_"];
|
|
1770
|
-
keysToRemove.forEach((key) => delete env6[key]);
|
|
1771
|
-
const secrets = new secretsmanager.Secret(scope, "StacksSecrets", {
|
|
1772
|
-
secretName: `${props.slug}-${props.appEnv}-secrets`,
|
|
1773
|
-
description: "Secrets for the Stacks application",
|
|
1774
|
-
generateSecretString: {
|
|
1775
|
-
secretStringTemplate: JSON.stringify(env6),
|
|
1776
|
-
generateStringKey: Object.keys(env6).join(",").length.toString()
|
|
1777
|
-
}
|
|
1778
|
-
});
|
|
1779
|
-
secrets.grantRead(service.taskDefinition.executionRole);
|
|
1780
|
-
container.addEnvironment("SECRETS_ARN", secrets.secretArn);
|
|
1781
|
-
const apiPrefix = "api";
|
|
1782
|
-
new Output6(scope, "ApiUrl", {
|
|
1783
|
-
value: `https://${props.domain}/${apiPrefix}`,
|
|
1784
|
-
description: "The URL of the deployed application"
|
|
1785
|
-
});
|
|
1786
|
-
new Output6(scope, "ApiVanityUrl", {
|
|
1787
|
-
value: `http://${this.lb.loadBalancerDnsName}`,
|
|
1788
|
-
description: "The Vanity URL / DNS name of the load balancer"
|
|
1538
|
+
cidrMask: 24
|
|
1539
|
+
},
|
|
1540
|
+
{
|
|
1541
|
+
name: "private-subnet-1",
|
|
1542
|
+
subnetType: ec23.SubnetType.PRIVATE_ISOLATED,
|
|
1543
|
+
cidrMask: 28
|
|
1544
|
+
}
|
|
1545
|
+
]
|
|
1789
1546
|
});
|
|
1790
1547
|
}
|
|
1791
1548
|
}
|
|
1792
1549
|
|
|
1550
|
+
// src/cloud/permissions.ts
|
|
1551
|
+
import {config as config14} from "@stacksjs/config";
|
|
1552
|
+
import {env as env6} from "@stacksjs/env";
|
|
1553
|
+
import {string} from "@stacksjs/strings";
|
|
1554
|
+
import {SecretValue, aws_iam as iam4} from "aws-cdk-lib";
|
|
1555
|
+
|
|
1556
|
+
class PermissionsStack {
|
|
1557
|
+
constructor(scope) {
|
|
1558
|
+
const teamName = config14.team.name;
|
|
1559
|
+
const users = config14.team.members;
|
|
1560
|
+
const password = env6.AWS_DEFAULT_PASSWORD || string.random();
|
|
1561
|
+
for (const name in users) {
|
|
1562
|
+
const id = `User${string.pascalCase(teamName)}${string.pascalCase(name)}`;
|
|
1563
|
+
const userName = string.slug(`${teamName}-${name}`);
|
|
1564
|
+
const user = new iam4.User(scope, id, {
|
|
1565
|
+
userName,
|
|
1566
|
+
password: SecretValue.unsafePlainText(password),
|
|
1567
|
+
passwordResetRequired: true
|
|
1568
|
+
});
|
|
1569
|
+
user.addManagedPolicy(iam4.ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess"));
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1793
1574
|
// src/cloud/queue.ts
|
|
1794
|
-
import {aws_ec2 as ec24} from "aws-cdk-lib";
|
|
1795
|
-
import {pascalCase, slug as slug2} from "@stacksjs/strings";
|
|
1796
|
-
import {fs} from "@stacksjs/storage";
|
|
1797
1575
|
import {path as path6} from "@stacksjs/path";
|
|
1576
|
+
import {fs} from "@stacksjs/storage";
|
|
1577
|
+
import {pascalCase, slug as slug2} from "@stacksjs/strings";
|
|
1578
|
+
import {aws_ec2 as ec24} from "aws-cdk-lib";
|
|
1798
1579
|
import {Rule, Schedule} from "aws-cdk-lib/aws-events";
|
|
1799
1580
|
import {EcsTask} from "aws-cdk-lib/aws-events-targets";
|
|
1800
1581
|
|
|
@@ -1808,8 +1589,10 @@ class QueueStack {
|
|
|
1808
1589
|
async init() {
|
|
1809
1590
|
const jobsDir = path6.jobsPath();
|
|
1810
1591
|
const actionsDir = path6.appPath("Actions");
|
|
1592
|
+
const ormActionDir = path6.projectStoragePath("framework/orm/Actions");
|
|
1811
1593
|
const jobFiles = await fs.readdir(jobsDir);
|
|
1812
1594
|
const actionFiles = await fs.readdir(actionsDir);
|
|
1595
|
+
const ormActionFiles = await fs.readdir(ormActionDir);
|
|
1813
1596
|
const jobs = [];
|
|
1814
1597
|
for (const file of jobFiles) {
|
|
1815
1598
|
if (!file.endsWith(".ts"))
|
|
@@ -1819,10 +1602,19 @@ class QueueStack {
|
|
|
1819
1602
|
this.createQueueRule(job, file);
|
|
1820
1603
|
jobs.push(job);
|
|
1821
1604
|
}
|
|
1605
|
+
for (const ormFile of ormActionFiles) {
|
|
1606
|
+
if (!ormFile.endsWith(".ts"))
|
|
1607
|
+
continue;
|
|
1608
|
+
const ormActionPath = path6.projectStoragePath(`framework/orm/Actions/${ormFile}`);
|
|
1609
|
+
const ormAction = await this.loadModule(ormActionPath);
|
|
1610
|
+
this.createQueueRule(ormAction, ormFile);
|
|
1611
|
+
jobs.push(ormAction);
|
|
1612
|
+
}
|
|
1822
1613
|
for (const file of actionFiles) {
|
|
1823
1614
|
if (!file.endsWith(".ts"))
|
|
1824
1615
|
continue;
|
|
1825
1616
|
const actionPath = path6.appPath(`Actions/${file}`);
|
|
1617
|
+
console.log(actionPath);
|
|
1826
1618
|
const action = await this.loadModule(actionPath);
|
|
1827
1619
|
this.createQueueRule(action, file);
|
|
1828
1620
|
jobs.push(action);
|
|
@@ -1894,6 +1686,303 @@ class QueueStack {
|
|
|
1894
1686
|
}
|
|
1895
1687
|
}
|
|
1896
1688
|
|
|
1689
|
+
// src/cloud/redirects.ts
|
|
1690
|
+
import {config as config16} from "@stacksjs/config";
|
|
1691
|
+
import {RemovalPolicy as RemovalPolicy6, aws_route53 as route535, aws_s3 as s33} from "aws-cdk-lib";
|
|
1692
|
+
|
|
1693
|
+
class RedirectsStack {
|
|
1694
|
+
redirectZones = [];
|
|
1695
|
+
constructor(scope, props) {
|
|
1696
|
+
config16.dns.redirects?.forEach((redirect) => {
|
|
1697
|
+
const slug3 = redirect.split(".").map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1698
|
+
const hostedZone = route535.HostedZone.fromLookup(scope, "HostedZone", {
|
|
1699
|
+
domainName: redirect
|
|
1700
|
+
});
|
|
1701
|
+
const redirectBucket = new s33.Bucket(scope, `RedirectBucket${slug3}`, {
|
|
1702
|
+
bucketName: `${redirect}-redirect`,
|
|
1703
|
+
websiteRedirect: {
|
|
1704
|
+
hostName: props.domain,
|
|
1705
|
+
protocol: s33.RedirectProtocol.HTTPS
|
|
1706
|
+
},
|
|
1707
|
+
removalPolicy: RemovalPolicy6.DESTROY,
|
|
1708
|
+
autoDeleteObjects: true
|
|
1709
|
+
});
|
|
1710
|
+
new route535.CnameRecord(scope, `RedirectRecord${slug3}`, {
|
|
1711
|
+
zone: hostedZone,
|
|
1712
|
+
recordName: "redirect",
|
|
1713
|
+
domainName: redirectBucket.bucketWebsiteDomainName
|
|
1714
|
+
});
|
|
1715
|
+
});
|
|
1716
|
+
config16.dns.redirects?.forEach((redirect) => {
|
|
1717
|
+
const slug3 = redirect.split(".").map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1718
|
+
const hostedZone = route535.HostedZone.fromLookup(scope, `RedirectHostedZone${slug3}`, { domainName: redirect });
|
|
1719
|
+
this.redirectZones.push(hostedZone);
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// src/cloud/security.ts
|
|
1725
|
+
import {config as config18} from "@stacksjs/config";
|
|
1726
|
+
import {
|
|
1727
|
+
Duration as Duration6,
|
|
1728
|
+
RemovalPolicy as RemovalPolicy7,
|
|
1729
|
+
Tags as Tags2,
|
|
1730
|
+
aws_certificatemanager as acm,
|
|
1731
|
+
aws_kms as kms,
|
|
1732
|
+
aws_wafv2 as wafv2
|
|
1733
|
+
} from "aws-cdk-lib";
|
|
1734
|
+
|
|
1735
|
+
class SecurityStack {
|
|
1736
|
+
firewall;
|
|
1737
|
+
kmsKey;
|
|
1738
|
+
certificate;
|
|
1739
|
+
constructor(scope, props) {
|
|
1740
|
+
const firewallOptions = config18.cloud.firewall;
|
|
1741
|
+
if (!firewallOptions)
|
|
1742
|
+
throw new Error("No firewall options found in config");
|
|
1743
|
+
const options = {
|
|
1744
|
+
defaultAction: { allow: {} },
|
|
1745
|
+
scope: "CLOUDFRONT",
|
|
1746
|
+
visibilityConfig: {
|
|
1747
|
+
sampledRequestsEnabled: true,
|
|
1748
|
+
cloudWatchMetricsEnabled: true,
|
|
1749
|
+
metricName: "firewallMetric"
|
|
1750
|
+
},
|
|
1751
|
+
rules: this.getFirewallRules(scope)
|
|
1752
|
+
};
|
|
1753
|
+
this.firewall = new wafv2.CfnWebACL(scope, "StacksWebFirewall", options);
|
|
1754
|
+
Tags2.of(this.firewall).add("Name", "waf-cloudfront", { priority: 300 });
|
|
1755
|
+
Tags2.of(this.firewall).add("Purpose", "CloudFront", { priority: 300 });
|
|
1756
|
+
Tags2.of(this.firewall).add("CreatedBy", "CloudFormation", {
|
|
1757
|
+
priority: 300
|
|
1758
|
+
});
|
|
1759
|
+
this.kmsKey = new kms.Key(scope, "EncryptionKey", {
|
|
1760
|
+
alias: "stacks-encryption-key",
|
|
1761
|
+
description: "KMS key for Stacks Cloud",
|
|
1762
|
+
enableKeyRotation: true,
|
|
1763
|
+
removalPolicy: RemovalPolicy7.DESTROY,
|
|
1764
|
+
pendingWindow: Duration6.days(30)
|
|
1765
|
+
});
|
|
1766
|
+
this.certificate = new acm.Certificate(scope, "Certificate", {
|
|
1767
|
+
domainName: props.domain,
|
|
1768
|
+
validation: acm.CertificateValidation.fromDns(props.zone),
|
|
1769
|
+
subjectAlternativeNames: [`www.${props.domain}`, `api.${props.domain}`]
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
getFirewallRules(scope) {
|
|
1773
|
+
const rules = [];
|
|
1774
|
+
const priorities = [];
|
|
1775
|
+
if (config18.security.firewall?.countryCodes?.length) {
|
|
1776
|
+
priorities.push(1);
|
|
1777
|
+
rules.push({
|
|
1778
|
+
name: "CountryRule",
|
|
1779
|
+
priority: priorities.length,
|
|
1780
|
+
statement: {},
|
|
1781
|
+
action: {
|
|
1782
|
+
block: {}
|
|
1783
|
+
},
|
|
1784
|
+
visibilityConfig: {
|
|
1785
|
+
sampledRequestsEnabled: true,
|
|
1786
|
+
cloudWatchMetricsEnabled: true,
|
|
1787
|
+
metricName: "CountryRule"
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
if (config18.security.firewall?.ipAddresses?.length) {
|
|
1792
|
+
const ipSet = new wafv2.CfnIPSet(scope, "IpSet", {
|
|
1793
|
+
name: "IpSet",
|
|
1794
|
+
description: "IP Set",
|
|
1795
|
+
scope: "CLOUDFRONT",
|
|
1796
|
+
addresses: config18.security.firewall.ipAddresses,
|
|
1797
|
+
ipAddressVersion: "IPV4"
|
|
1798
|
+
});
|
|
1799
|
+
priorities.push(1);
|
|
1800
|
+
rules.push({
|
|
1801
|
+
name: "IpAddressRule",
|
|
1802
|
+
priority: priorities.length,
|
|
1803
|
+
statement: {
|
|
1804
|
+
ipSetReferenceStatement: {
|
|
1805
|
+
arn: ipSet.attrArn
|
|
1806
|
+
}
|
|
1807
|
+
},
|
|
1808
|
+
action: {
|
|
1809
|
+
block: {}
|
|
1810
|
+
},
|
|
1811
|
+
visibilityConfig: {
|
|
1812
|
+
sampledRequestsEnabled: true,
|
|
1813
|
+
cloudWatchMetricsEnabled: true,
|
|
1814
|
+
metricName: "IpAddressRule"
|
|
1815
|
+
}
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
if (config18.security.firewall?.httpHeaders?.length) {
|
|
1819
|
+
config18.security.firewall.httpHeaders.forEach((header, index) => {
|
|
1820
|
+
priorities.push(1);
|
|
1821
|
+
rules.push({
|
|
1822
|
+
name: `HttpHeaderRule${index}`,
|
|
1823
|
+
priority: priorities.length,
|
|
1824
|
+
statement: {
|
|
1825
|
+
byteMatchStatement: {
|
|
1826
|
+
fieldToMatch: {
|
|
1827
|
+
singleHeader: {
|
|
1828
|
+
name: header
|
|
1829
|
+
}
|
|
1830
|
+
},
|
|
1831
|
+
positionalConstraint: "EXACTLY",
|
|
1832
|
+
searchString: "true",
|
|
1833
|
+
textTransformations: [
|
|
1834
|
+
{
|
|
1835
|
+
priority: index,
|
|
1836
|
+
type: "NONE"
|
|
1837
|
+
}
|
|
1838
|
+
]
|
|
1839
|
+
}
|
|
1840
|
+
},
|
|
1841
|
+
action: {
|
|
1842
|
+
block: {}
|
|
1843
|
+
},
|
|
1844
|
+
visibilityConfig: {
|
|
1845
|
+
sampledRequestsEnabled: true,
|
|
1846
|
+
cloudWatchMetricsEnabled: true,
|
|
1847
|
+
metricName: `HttpHeaderRule${index}`
|
|
1848
|
+
}
|
|
1849
|
+
});
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
return rules;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// src/cloud/storage.ts
|
|
1857
|
+
import {RemovalPolicy as RemovalPolicy8, Tags as Tags3, aws_backup as backup, aws_iam as iam5, aws_s3 as s34} from "aws-cdk-lib";
|
|
1858
|
+
|
|
1859
|
+
class StorageStack {
|
|
1860
|
+
publicBucket;
|
|
1861
|
+
privateBucket;
|
|
1862
|
+
logBucket;
|
|
1863
|
+
bucketPrefix;
|
|
1864
|
+
vault;
|
|
1865
|
+
backupPlan;
|
|
1866
|
+
backupRole;
|
|
1867
|
+
constructor(scope, props) {
|
|
1868
|
+
this.bucketPrefix = `${props.slug}-${props.appEnv}`;
|
|
1869
|
+
this.publicBucket = new s34.Bucket(scope, "PublicBucket", {
|
|
1870
|
+
bucketName: `${this.bucketPrefix}-public-${props.timestamp}`,
|
|
1871
|
+
versioned: true,
|
|
1872
|
+
autoDeleteObjects: true,
|
|
1873
|
+
removalPolicy: RemovalPolicy8.DESTROY,
|
|
1874
|
+
encryption: s34.BucketEncryption.S3_MANAGED
|
|
1875
|
+
});
|
|
1876
|
+
Tags3.of(this.publicBucket).add("daily-backup", "true");
|
|
1877
|
+
this.privateBucket = new s34.Bucket(scope, "PrivateBucket", {
|
|
1878
|
+
bucketName: `${this.bucketPrefix}-private-${props.timestamp}`,
|
|
1879
|
+
versioned: true,
|
|
1880
|
+
removalPolicy: RemovalPolicy8.DESTROY,
|
|
1881
|
+
autoDeleteObjects: true,
|
|
1882
|
+
encryption: s34.BucketEncryption.S3_MANAGED,
|
|
1883
|
+
enforceSSL: true,
|
|
1884
|
+
publicReadAccess: false,
|
|
1885
|
+
blockPublicAccess: {
|
|
1886
|
+
blockPublicAcls: true,
|
|
1887
|
+
blockPublicPolicy: true,
|
|
1888
|
+
ignorePublicAcls: true,
|
|
1889
|
+
restrictPublicBuckets: true
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1892
|
+
Tags3.of(this.privateBucket).add("daily-backup", "true");
|
|
1893
|
+
this.logBucket = new s34.Bucket(scope, "LogsBucket", {
|
|
1894
|
+
bucketName: `${this.bucketPrefix}-logs-${props.timestamp}`,
|
|
1895
|
+
removalPolicy: RemovalPolicy8.RETAIN,
|
|
1896
|
+
blockPublicAccess: new s34.BlockPublicAccess({
|
|
1897
|
+
blockPublicAcls: false,
|
|
1898
|
+
ignorePublicAcls: true,
|
|
1899
|
+
blockPublicPolicy: true,
|
|
1900
|
+
restrictPublicBuckets: true
|
|
1901
|
+
}),
|
|
1902
|
+
objectOwnership: s34.ObjectOwnership.BUCKET_OWNER_PREFERRED
|
|
1903
|
+
});
|
|
1904
|
+
Tags3.of(this.logBucket).add("daily-backup", "true");
|
|
1905
|
+
this.backupRole = this.createBackupRole(scope);
|
|
1906
|
+
this.vault = new backup.BackupVault(scope, "BackupVault", {
|
|
1907
|
+
backupVaultName: `${props.slug}-${props.appEnv}-daily-backup-vault`,
|
|
1908
|
+
encryptionKey: props.kmsKey,
|
|
1909
|
+
removalPolicy: RemovalPolicy8.DESTROY
|
|
1910
|
+
});
|
|
1911
|
+
this.backupPlan = backup.BackupPlan.daily35DayRetention(scope, "BackupPlan", this.vault);
|
|
1912
|
+
this.backupPlan.addSelection("Selection", {
|
|
1913
|
+
role: this.backupRole,
|
|
1914
|
+
resources: [backup.BackupResource.fromTag("daily-backup", "true")]
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
createBackupRole(scope) {
|
|
1918
|
+
const backupRole = new iam5.Role(scope, "BackupRole", {
|
|
1919
|
+
assumedBy: new iam5.ServicePrincipal("backup.amazonaws.com")
|
|
1920
|
+
});
|
|
1921
|
+
backupRole.addToPolicy(new iam5.PolicyStatement({
|
|
1922
|
+
actions: [
|
|
1923
|
+
"s3:GetInventoryConfiguration",
|
|
1924
|
+
"s3:PutInventoryConfiguration",
|
|
1925
|
+
"s3:ListBucketVersions",
|
|
1926
|
+
"s3:ListBucket",
|
|
1927
|
+
"s3:GetBucketVersioning",
|
|
1928
|
+
"s3:GetBucketNotification",
|
|
1929
|
+
"s3:PutBucketNotification",
|
|
1930
|
+
"s3:GetBucketLocation",
|
|
1931
|
+
"s3:GetBucketTagging"
|
|
1932
|
+
],
|
|
1933
|
+
resources: ["arn:aws:s3:::*"],
|
|
1934
|
+
sid: "S3BucketBackupPermissions"
|
|
1935
|
+
}));
|
|
1936
|
+
backupRole.addToPolicy(new iam5.PolicyStatement({
|
|
1937
|
+
actions: [
|
|
1938
|
+
"s3:GetObjectAcl",
|
|
1939
|
+
"s3:GetObject",
|
|
1940
|
+
"s3:GetObjectVersionTagging",
|
|
1941
|
+
"s3:GetObjectVersionAcl",
|
|
1942
|
+
"s3:GetObjectTagging",
|
|
1943
|
+
"s3:GetObjectVersion"
|
|
1944
|
+
],
|
|
1945
|
+
resources: ["arn:aws:s3:::*/*"],
|
|
1946
|
+
sid: "S3ObjectBackupPermissions"
|
|
1947
|
+
}));
|
|
1948
|
+
backupRole.addToPolicy(new iam5.PolicyStatement({
|
|
1949
|
+
actions: ["s3:ListAllMyBuckets"],
|
|
1950
|
+
resources: ["*"],
|
|
1951
|
+
sid: "S3GlobalPermissions"
|
|
1952
|
+
}));
|
|
1953
|
+
backupRole.addToPolicy(new iam5.PolicyStatement({
|
|
1954
|
+
actions: ["kms:Decrypt", "kms:DescribeKey"],
|
|
1955
|
+
resources: ["*"],
|
|
1956
|
+
sid: "KMSBackupPermissions",
|
|
1957
|
+
conditions: {
|
|
1958
|
+
StringLike: {
|
|
1959
|
+
"kms:ViaService": "s3.*.amazonaws.com"
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
}));
|
|
1963
|
+
backupRole.addToPolicy(new iam5.PolicyStatement({
|
|
1964
|
+
actions: [
|
|
1965
|
+
"events:DescribeRule",
|
|
1966
|
+
"events:EnableRule",
|
|
1967
|
+
"events:PutRule",
|
|
1968
|
+
"events:DeleteRule",
|
|
1969
|
+
"events:PutTargets",
|
|
1970
|
+
"events:RemoveTargets",
|
|
1971
|
+
"events:ListTargetsByRule",
|
|
1972
|
+
"events:DisableRule"
|
|
1973
|
+
],
|
|
1974
|
+
resources: ["arn:aws:events:*:*:rule/AwsBackupManagedRule*"],
|
|
1975
|
+
sid: "EventsPermissions"
|
|
1976
|
+
}));
|
|
1977
|
+
backupRole.addToPolicy(new iam5.PolicyStatement({
|
|
1978
|
+
actions: ["cloudwatch:GetMetricData", "events:ListRules"],
|
|
1979
|
+
resources: ["*"],
|
|
1980
|
+
sid: "EventsMetricsGlobalPermissions"
|
|
1981
|
+
}));
|
|
1982
|
+
return backupRole;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1897
1986
|
// src/cloud/index.ts
|
|
1898
1987
|
class Cloud extends Stack2 {
|
|
1899
1988
|
dns;
|
|
@@ -1948,7 +2037,7 @@ class Cloud extends Stack2 {
|
|
|
1948
2037
|
this.cli = new CliStack(this, props);
|
|
1949
2038
|
}
|
|
1950
2039
|
async init() {
|
|
1951
|
-
if (config20.
|
|
2040
|
+
if (config20.api?.deploy) {
|
|
1952
2041
|
const props = this.props;
|
|
1953
2042
|
this.api = new ComputeStack(this, {
|
|
1954
2043
|
...props,
|