@fishawack/lab-env 5.7.0-beta.2 → 5.7.0-beta.4
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/CHANGELOG.md +51 -0
- package/_Test/provision.js +1 -8
- package/_Test/prune.js +8 -5
- package/bitbucket-pipelines.yml +2 -2
- package/cli.js +1 -0
- package/commands/create/cmds/dekey.js +13 -9
- package/commands/create/cmds/deprovision.js +29 -0
- package/commands/create/cmds/key.js +60 -41
- package/commands/create/cmds/provision.js +425 -55
- package/commands/create/cmds/rekey.js +218 -0
- package/commands/create/libs/output-credentials.js +59 -0
- package/commands/create/libs/parallel-runner.js +204 -0
- package/commands/create/libs/resolve-operator.js +59 -0
- package/commands/create/libs/utilities.js +71 -8
- package/commands/create/libs/vars.js +70 -86
- package/commands/create/services/aws/acm.js +4 -2
- package/commands/create/services/aws/cloudfront.js +51 -45
- package/commands/create/services/aws/ec2.js +132 -53
- package/commands/create/services/aws/elasticache.js +159 -0
- package/commands/create/services/aws/elasticbeanstalk.js +141 -60
- package/commands/create/services/aws/iam.js +178 -98
- package/commands/create/services/aws/index.js +1202 -466
- package/commands/create/services/aws/opensearch.js +25 -17
- package/commands/create/services/aws/rds.js +22 -34
- package/commands/create/services/aws/s3.js +14 -27
- package/commands/create/services/aws/secretsmanager.js +8 -15
- package/commands/create/services/aws/ses.js +195 -0
- package/commands/create/templates/elasticbeanstalk/.ebextensions/laravel/cron.config +45 -0
- package/commands/create/templates/elasticbeanstalk/.ebextensions/laravel/queue-worker.config +29 -0
- package/commands/create/templates/elasticbeanstalk/.ebextensions/laravel/software.config +12 -0
- package/commands/create/templates/elasticbeanstalk/.ebextensions/misc/setvars.config +2 -16
- package/commands/create/templates/elasticbeanstalk/.platform/confighooks/postdeploy/rewrite-flush.sh +1 -1
- package/commands/create/templates/elasticbeanstalk/.platform/confighooks/postdeploy/setvars.sh +19 -0
- package/commands/create/templates/elasticbeanstalk/.platform/hooks/postdeploy/restart_queue.sh +4 -0
- package/commands/create/templates/elasticbeanstalk/.platform/hooks/prebuild/setvars.sh +19 -0
- package/commands/scan.js +1 -1
- package/package.json +5 -2
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const md5 = require("apache-md5");
|
|
2
|
-
const fs = require("fs-extra");
|
|
3
1
|
const generator = require("generate-password");
|
|
4
2
|
const { S3Client, ListBucketsCommand } = require("@aws-sdk/client-s3");
|
|
5
3
|
const {
|
|
@@ -14,8 +12,12 @@ const {
|
|
|
14
12
|
OpenSearchClient,
|
|
15
13
|
DescribeDomainCommand,
|
|
16
14
|
} = require("@aws-sdk/client-opensearch");
|
|
15
|
+
const {
|
|
16
|
+
ElastiCacheClient,
|
|
17
|
+
DescribeReplicationGroupsCommand,
|
|
18
|
+
} = require("@aws-sdk/client-elasticache");
|
|
17
19
|
|
|
18
|
-
const { nameSafe, Spinner } = require("../../libs/utilities.js");
|
|
20
|
+
const { nameSafe, Spinner, pollAll } = require("../../libs/utilities.js");
|
|
19
21
|
const { eb, secretGroups } = require("../../libs/vars.js");
|
|
20
22
|
|
|
21
23
|
module.exports.s3 = require("./s3.js");
|
|
@@ -25,8 +27,10 @@ module.exports.elasticbeanstalk = require("./elasticbeanstalk.js");
|
|
|
25
27
|
module.exports.ec2 = require("./ec2.js");
|
|
26
28
|
module.exports.rds = require("./rds.js");
|
|
27
29
|
module.exports.opensearch = require("./opensearch.js");
|
|
30
|
+
module.exports.elasticache = require("./elasticache.js");
|
|
28
31
|
module.exports.secretsmanager = require("./secretsmanager.js");
|
|
29
32
|
module.exports.acm = require("./acm.js");
|
|
33
|
+
module.exports.ses = require("./ses.js");
|
|
30
34
|
|
|
31
35
|
module.exports.slug = (repo, client, branch, service = "s3") =>
|
|
32
36
|
nameSafe(`${branch}-${repo}-${client}`, service);
|
|
@@ -35,14 +39,27 @@ module.exports.fullstackPreflight = async (
|
|
|
35
39
|
name,
|
|
36
40
|
repo,
|
|
37
41
|
branch,
|
|
38
|
-
{
|
|
42
|
+
{
|
|
43
|
+
storage = false,
|
|
44
|
+
database = false,
|
|
45
|
+
opensearch = false,
|
|
46
|
+
cache = false,
|
|
47
|
+
} = {},
|
|
39
48
|
) => {
|
|
40
49
|
const existing = {
|
|
41
50
|
environment: false,
|
|
42
51
|
storage: false,
|
|
43
52
|
database: false,
|
|
44
53
|
opensearch: false,
|
|
45
|
-
|
|
54
|
+
cache: false,
|
|
55
|
+
secrets: {
|
|
56
|
+
app: null,
|
|
57
|
+
storage: null,
|
|
58
|
+
database: null,
|
|
59
|
+
opensearch: null,
|
|
60
|
+
cache: null,
|
|
61
|
+
mail: null,
|
|
62
|
+
},
|
|
46
63
|
};
|
|
47
64
|
|
|
48
65
|
const spinner = new Spinner("Running pre-flight checks", true);
|
|
@@ -132,6 +149,33 @@ module.exports.fullstackPreflight = async (
|
|
|
132
149
|
}
|
|
133
150
|
}
|
|
134
151
|
|
|
152
|
+
// Check ElastiCache
|
|
153
|
+
if (cache) {
|
|
154
|
+
const cacheSlug = module.exports.slug(
|
|
155
|
+
repo,
|
|
156
|
+
process.env.AWS_PROFILE,
|
|
157
|
+
branch,
|
|
158
|
+
"cache",
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
spinner.ora.text = "Checking for existing ElastiCache cluster";
|
|
162
|
+
try {
|
|
163
|
+
const { ReplicationGroups } = await new ElastiCacheClient(
|
|
164
|
+
{},
|
|
165
|
+
).send(
|
|
166
|
+
new DescribeReplicationGroupsCommand({
|
|
167
|
+
ReplicationGroupId: cacheSlug,
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
const group = ReplicationGroups[0];
|
|
171
|
+
if (group && group.Status !== "deleting") {
|
|
172
|
+
existing.cache = true;
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
/* does not exist */
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
135
179
|
// Check for existing secrets
|
|
136
180
|
for (const group of secretGroups) {
|
|
137
181
|
spinner.ora.text = `Checking for existing ${group} secret`;
|
|
@@ -298,75 +342,39 @@ module.exports.staticTerminate = async (name, repo, branch, id) => {
|
|
|
298
342
|
}
|
|
299
343
|
};
|
|
300
344
|
|
|
301
|
-
|
|
302
|
-
name,
|
|
303
|
-
tags = [],
|
|
304
|
-
credentials = [],
|
|
305
|
-
repo,
|
|
306
|
-
branch,
|
|
307
|
-
framework,
|
|
308
|
-
availability,
|
|
309
|
-
database = false,
|
|
310
|
-
storage = false,
|
|
311
|
-
opensearch = false,
|
|
312
|
-
existingSecrets = {
|
|
313
|
-
app: null,
|
|
314
|
-
storage: null,
|
|
315
|
-
database: null,
|
|
316
|
-
opensearch: null,
|
|
317
|
-
},
|
|
318
|
-
snapshot = null,
|
|
319
|
-
elbScheme = "public",
|
|
320
|
-
existingEnvironment = false,
|
|
321
|
-
existingResources = {
|
|
322
|
-
storage: false,
|
|
323
|
-
database: false,
|
|
324
|
-
opensearch: false,
|
|
325
|
-
},
|
|
326
|
-
appName,
|
|
327
|
-
appDomain,
|
|
328
|
-
certArn,
|
|
329
|
-
) => {
|
|
330
|
-
const { platform, language } = eb.configurations[framework];
|
|
331
|
-
const isLaravel = framework === "laravel";
|
|
332
|
-
|
|
333
|
-
// Always compute s3Slug if storage is requested or pre-exists
|
|
334
|
-
// (needed for instance profile S3 access even when bucket creation is skipped)
|
|
335
|
-
const s3Slug =
|
|
336
|
-
storage || existingSecrets.storage
|
|
337
|
-
? module.exports.slug(repo, process.env.AWS_PROFILE, branch, "s3")
|
|
338
|
-
: undefined;
|
|
339
|
-
|
|
340
|
-
// Always compute osSlug if opensearch is requested or pre-exists
|
|
341
|
-
const osSlug =
|
|
342
|
-
opensearch || existingSecrets.opensearch
|
|
343
|
-
? module.exports.slug(repo, process.env.AWS_PROFILE, branch, "os")
|
|
344
|
-
: undefined;
|
|
345
|
+
// Private provisioner functions for parallel execution
|
|
345
346
|
|
|
347
|
+
const provisionStorage = async (
|
|
348
|
+
aws,
|
|
349
|
+
s3Slug,
|
|
350
|
+
tags,
|
|
351
|
+
isLaravel,
|
|
352
|
+
existingResources,
|
|
353
|
+
) => {
|
|
346
354
|
let s3Data = {};
|
|
347
355
|
|
|
348
|
-
if (
|
|
356
|
+
if (!existingResources.storage) {
|
|
349
357
|
if (isLaravel) {
|
|
350
|
-
await
|
|
358
|
+
await aws.s3.createS3Bucket(s3Slug, tags, {
|
|
351
359
|
BlockPublicAcls: true,
|
|
352
360
|
IgnorePublicAcls: true,
|
|
353
361
|
BlockPublicPolicy: false,
|
|
354
362
|
RestrictPublicBuckets: false,
|
|
355
363
|
});
|
|
356
364
|
|
|
357
|
-
await
|
|
365
|
+
await aws.s3.setPublicReadPolicy(s3Slug, "public/*");
|
|
358
366
|
} else {
|
|
359
|
-
await
|
|
360
|
-
await
|
|
367
|
+
await aws.iam.createIAMUser(s3Slug, tags);
|
|
368
|
+
await aws.iam.attachIAMPolicy(
|
|
361
369
|
s3Slug,
|
|
362
370
|
"arn:aws:iam::aws:policy/AmazonS3FullAccess",
|
|
363
371
|
);
|
|
364
372
|
const {
|
|
365
373
|
AccessKey: { AccessKeyId },
|
|
366
374
|
AccessKey: { SecretAccessKey },
|
|
367
|
-
} = await
|
|
375
|
+
} = await aws.iam.createAccessKey(s3Slug);
|
|
368
376
|
|
|
369
|
-
await
|
|
377
|
+
await aws.s3.createS3Bucket(
|
|
370
378
|
s3Slug,
|
|
371
379
|
tags,
|
|
372
380
|
false,
|
|
@@ -377,58 +385,46 @@ module.exports.fullstack = async (
|
|
|
377
385
|
}
|
|
378
386
|
}
|
|
379
387
|
|
|
380
|
-
if (
|
|
388
|
+
if (!s3Data.s3Slug) {
|
|
381
389
|
s3Data = { s3Slug };
|
|
382
390
|
}
|
|
383
391
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (osSlug) {
|
|
388
|
-
opensearchDomainArn = `arn:aws:es:${process.env.AWS_REGION}:*:domain/${osSlug}`;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (isLaravel) {
|
|
392
|
-
instanceProfileName =
|
|
393
|
-
await module.exports.iam.ensureEnvironmentInstanceProfile(
|
|
394
|
-
name,
|
|
395
|
-
s3Slug,
|
|
396
|
-
opensearchDomainArn,
|
|
397
|
-
);
|
|
398
|
-
} else {
|
|
399
|
-
await module.exports.iam.ensureEBInstanceProfileExists();
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
await module.exports.iam.ensureEBManagedUpdateProfileExists();
|
|
403
|
-
const vpcData = await module.exports.ec2.ensurePrivateSubnets();
|
|
404
|
-
|
|
405
|
-
// Create decoupled RDS instance if database is required
|
|
406
|
-
let rdsData = {};
|
|
392
|
+
return s3Data;
|
|
393
|
+
};
|
|
407
394
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
395
|
+
const provisionRDS = async (
|
|
396
|
+
aws,
|
|
397
|
+
name,
|
|
398
|
+
repo,
|
|
399
|
+
branch,
|
|
400
|
+
tags,
|
|
401
|
+
vpcData,
|
|
402
|
+
existingResources,
|
|
403
|
+
existingSecrets,
|
|
404
|
+
snapshot,
|
|
405
|
+
) => {
|
|
406
|
+
const rdsSlug = aws.slug(repo, process.env.AWS_PROFILE, branch, "rds");
|
|
407
|
+
const rdsClient = new RDSClient({});
|
|
415
408
|
|
|
409
|
+
if (!existingResources.database) {
|
|
416
410
|
const dbPassword = generator.generate({
|
|
417
411
|
length: 10,
|
|
418
412
|
numbers: true,
|
|
419
413
|
});
|
|
420
414
|
|
|
421
|
-
await
|
|
415
|
+
await aws.rds.ensureSSLParameterGroup();
|
|
422
416
|
|
|
423
|
-
const rdsSecurityGroupId =
|
|
424
|
-
|
|
417
|
+
const rdsSecurityGroupId = await aws.ec2.createRDSSecurityGroup(
|
|
418
|
+
rdsSlug,
|
|
419
|
+
tags,
|
|
420
|
+
);
|
|
425
421
|
|
|
426
|
-
const dbSubnetGroupName = await
|
|
422
|
+
const dbSubnetGroupName = await aws.rds.ensureDBSubnetGroup(
|
|
427
423
|
vpcData.privateSubnetIds,
|
|
428
424
|
);
|
|
429
425
|
|
|
430
426
|
if (snapshot) {
|
|
431
|
-
await
|
|
427
|
+
await aws.rds.restoreRDSInstanceFromSnapshot(
|
|
432
428
|
rdsSlug,
|
|
433
429
|
snapshot,
|
|
434
430
|
rdsSecurityGroupId,
|
|
@@ -436,11 +432,10 @@ module.exports.fullstack = async (
|
|
|
436
432
|
dbSubnetGroupName,
|
|
437
433
|
);
|
|
438
434
|
|
|
439
|
-
await
|
|
440
|
-
|
|
441
|
-
await module.exports.rds.modifyRDSPassword(rdsSlug, dbPassword);
|
|
435
|
+
await aws.rds.waitForRDSInstance(rdsSlug);
|
|
436
|
+
await aws.rds.modifyRDSPassword(rdsSlug, dbPassword);
|
|
442
437
|
} else {
|
|
443
|
-
await
|
|
438
|
+
await aws.rds.createRDSInstance(
|
|
444
439
|
rdsSlug,
|
|
445
440
|
tags,
|
|
446
441
|
dbPassword,
|
|
@@ -449,194 +444,574 @@ module.exports.fullstack = async (
|
|
|
449
444
|
);
|
|
450
445
|
}
|
|
451
446
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
447
|
+
return {
|
|
448
|
+
rdsData: { RDS_PASSWORD: dbPassword },
|
|
449
|
+
rdsSlug,
|
|
450
|
+
poll: {
|
|
451
|
+
name: "RDS",
|
|
452
|
+
cb: async () =>
|
|
453
|
+
(
|
|
454
|
+
await rdsClient.send(
|
|
455
|
+
new DescribeDBInstancesCommand({
|
|
456
|
+
DBInstanceIdentifier: rdsSlug,
|
|
457
|
+
}),
|
|
458
|
+
)
|
|
459
|
+
).DBInstances[0],
|
|
460
|
+
shouldContinue: ({ DBInstanceStatus }) =>
|
|
461
|
+
DBInstanceStatus !== "available",
|
|
462
|
+
},
|
|
459
463
|
};
|
|
460
|
-
} else if (
|
|
461
|
-
database &&
|
|
462
|
-
existingResources.database &&
|
|
463
|
-
!existingSecrets.database
|
|
464
|
-
) {
|
|
465
|
-
// Recovery: RDS exists but secret was lost — reset password and populate data
|
|
466
|
-
const rdsSlug = module.exports.slug(
|
|
467
|
-
repo,
|
|
468
|
-
process.env.AWS_PROFILE,
|
|
469
|
-
branch,
|
|
470
|
-
"rds",
|
|
471
|
-
);
|
|
472
|
-
|
|
464
|
+
} else if (existingResources.database && !existingSecrets.database) {
|
|
473
465
|
const dbPassword = generator.generate({
|
|
474
466
|
length: 10,
|
|
475
467
|
numbers: true,
|
|
476
468
|
});
|
|
477
469
|
|
|
478
|
-
await
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
470
|
+
await aws.rds.modifyRDSPassword(rdsSlug, dbPassword);
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
rdsData: { RDS_PASSWORD: dbPassword },
|
|
474
|
+
rdsSlug,
|
|
475
|
+
poll: {
|
|
476
|
+
name: "RDS",
|
|
477
|
+
cb: async () =>
|
|
478
|
+
(
|
|
479
|
+
await rdsClient.send(
|
|
480
|
+
new DescribeDBInstancesCommand({
|
|
481
|
+
DBInstanceIdentifier: rdsSlug,
|
|
482
|
+
}),
|
|
483
|
+
)
|
|
484
|
+
).DBInstances[0],
|
|
485
|
+
shouldContinue: ({ DBInstanceStatus }) =>
|
|
486
|
+
DBInstanceStatus !== "available",
|
|
487
|
+
},
|
|
486
488
|
};
|
|
487
|
-
} else
|
|
488
|
-
|
|
489
|
-
existingResources.database &&
|
|
490
|
-
existingSecrets.database
|
|
491
|
-
) {
|
|
492
|
-
// Existing RDS + secret — read secret values for template interpolation
|
|
493
|
-
const secretValue = await module.exports.secretsmanager.getSecretValue(
|
|
489
|
+
} else {
|
|
490
|
+
const secretValue = await aws.secretsmanager.getSecretValue(
|
|
494
491
|
`${name}-database`,
|
|
495
492
|
);
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
493
|
+
return {
|
|
494
|
+
rdsData: {
|
|
495
|
+
RDS_HOSTNAME: secretValue.RDS_HOSTNAME,
|
|
496
|
+
RDS_PORT: secretValue.RDS_PORT,
|
|
497
|
+
RDS_PASSWORD: secretValue.RDS_PASSWORD,
|
|
498
|
+
},
|
|
499
|
+
rdsSlug,
|
|
500
500
|
};
|
|
501
501
|
}
|
|
502
|
+
};
|
|
502
503
|
|
|
503
|
-
|
|
504
|
-
|
|
504
|
+
const provisionOpenSearch = async (
|
|
505
|
+
aws,
|
|
506
|
+
osSlug,
|
|
507
|
+
tags,
|
|
508
|
+
vpcData,
|
|
509
|
+
instanceProfileName,
|
|
510
|
+
existingResources,
|
|
511
|
+
) => {
|
|
512
|
+
const osClient = new OpenSearchClient({});
|
|
505
513
|
|
|
506
|
-
if (
|
|
507
|
-
await
|
|
514
|
+
if (!existingResources.opensearch) {
|
|
515
|
+
await aws.iam.ensureOpenSearchServiceRole();
|
|
508
516
|
|
|
509
517
|
const roleArn = instanceProfileName
|
|
510
|
-
? (await
|
|
518
|
+
? (await aws.iam.getRole(instanceProfileName)).Role.Arn
|
|
511
519
|
: undefined;
|
|
512
520
|
|
|
513
|
-
const osSecurityGroupId =
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
);
|
|
521
|
+
const osSecurityGroupId = await aws.ec2.createOpenSearchSecurityGroup(
|
|
522
|
+
osSlug,
|
|
523
|
+
tags,
|
|
524
|
+
);
|
|
518
525
|
|
|
519
|
-
await
|
|
526
|
+
await aws.opensearch.createOpenSearchDomain(
|
|
520
527
|
osSlug,
|
|
521
528
|
osSecurityGroupId,
|
|
522
529
|
vpcData.privateSubnetIds,
|
|
523
530
|
roleArn,
|
|
531
|
+
tags,
|
|
524
532
|
);
|
|
525
533
|
|
|
526
|
-
|
|
527
|
-
|
|
534
|
+
return {
|
|
535
|
+
osData: {},
|
|
536
|
+
poll: {
|
|
537
|
+
name: "OpenSearch",
|
|
538
|
+
cb: async () => {
|
|
539
|
+
const { DomainStatus } = await osClient.send(
|
|
540
|
+
new DescribeDomainCommand({ DomainName: osSlug }),
|
|
541
|
+
);
|
|
542
|
+
return DomainStatus;
|
|
543
|
+
},
|
|
544
|
+
shouldContinue: (status) =>
|
|
545
|
+
status.Processing ||
|
|
546
|
+
(!status.Endpoint && !status.Endpoints),
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
} else {
|
|
550
|
+
const osDomain = await aws.opensearch.describeOpenSearchDomain(osSlug);
|
|
528
551
|
|
|
529
552
|
const osEndpoint =
|
|
530
553
|
osDomain.Endpoint || (osDomain.Endpoints && osDomain.Endpoints.vpc);
|
|
531
554
|
|
|
532
|
-
osData
|
|
533
|
-
|
|
555
|
+
return { osData: { SEARCH_HOST: `https://${osEndpoint}` } };
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const provisionCache = async (
|
|
560
|
+
aws,
|
|
561
|
+
name,
|
|
562
|
+
cacheSlug,
|
|
563
|
+
tags,
|
|
564
|
+
vpcData,
|
|
565
|
+
existingResources,
|
|
566
|
+
existingSecrets,
|
|
567
|
+
) => {
|
|
568
|
+
const cacheClient = new ElastiCacheClient({});
|
|
569
|
+
|
|
570
|
+
if (!existingResources.cache) {
|
|
571
|
+
const cachePassword = generator.generate({
|
|
572
|
+
length: 16,
|
|
573
|
+
numbers: true,
|
|
574
|
+
symbols: false,
|
|
575
|
+
strict: true,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const cacheSubnetGroupName =
|
|
579
|
+
await aws.elasticache.ensureCacheSubnetGroup(
|
|
580
|
+
vpcData.privateSubnetIds,
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
const cacheSecurityGroupId = await aws.ec2.createCacheSecurityGroup(
|
|
584
|
+
cacheSlug,
|
|
585
|
+
tags,
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
await aws.elasticache.createCacheCluster(
|
|
589
|
+
cacheSlug,
|
|
590
|
+
cacheSecurityGroupId,
|
|
591
|
+
cacheSubnetGroupName,
|
|
592
|
+
cachePassword,
|
|
593
|
+
tags,
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
return {
|
|
597
|
+
cacheData: { REDIS_PASSWORD: cachePassword },
|
|
598
|
+
poll: {
|
|
599
|
+
name: "ElastiCache",
|
|
600
|
+
cb: async () => {
|
|
601
|
+
const { ReplicationGroups } = await cacheClient.send(
|
|
602
|
+
new DescribeReplicationGroupsCommand({
|
|
603
|
+
ReplicationGroupId: cacheSlug,
|
|
604
|
+
}),
|
|
605
|
+
);
|
|
606
|
+
return ReplicationGroups[0];
|
|
607
|
+
},
|
|
608
|
+
shouldContinue: (group) => group.Status !== "available",
|
|
609
|
+
},
|
|
534
610
|
};
|
|
535
|
-
} else if (
|
|
536
|
-
|
|
537
|
-
const osDomain =
|
|
538
|
-
await module.exports.opensearch.describeOpenSearchDomain(osSlug);
|
|
611
|
+
} else if (existingResources.cache && !existingSecrets.cache) {
|
|
612
|
+
const cluster = await aws.elasticache.describeCacheCluster(cacheSlug);
|
|
539
613
|
|
|
540
|
-
const
|
|
541
|
-
osDomain.Endpoint || (osDomain.Endpoints && osDomain.Endpoints.vpc);
|
|
614
|
+
const cacheEndpoint = cluster.NodeGroups[0].PrimaryEndpoint;
|
|
542
615
|
|
|
543
|
-
|
|
544
|
-
|
|
616
|
+
return {
|
|
617
|
+
cacheData: {
|
|
618
|
+
REDIS_HOST: cacheEndpoint.Address,
|
|
619
|
+
REDIS_PORT: String(cacheEndpoint.Port),
|
|
620
|
+
REDIS_PASSWORD: "",
|
|
621
|
+
},
|
|
622
|
+
};
|
|
623
|
+
} else {
|
|
624
|
+
const secretValue = await aws.secretsmanager.getSecretValue(
|
|
625
|
+
`${name}-cache`,
|
|
626
|
+
);
|
|
627
|
+
return {
|
|
628
|
+
cacheData: {
|
|
629
|
+
REDIS_HOST: secretValue.REDIS_HOST,
|
|
630
|
+
REDIS_PORT: secretValue.REDIS_PORT,
|
|
631
|
+
REDIS_PASSWORD: secretValue.REDIS_PASSWORD,
|
|
632
|
+
},
|
|
545
633
|
};
|
|
546
634
|
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const provisionMail = async (aws, name, tags, mailData) => {
|
|
638
|
+
const {
|
|
639
|
+
IAMClient,
|
|
640
|
+
CreateUserCommand,
|
|
641
|
+
GetUserCommand,
|
|
642
|
+
CreateAccessKeyCommand,
|
|
643
|
+
ListAccessKeysCommand,
|
|
644
|
+
} = require("@aws-sdk/client-iam");
|
|
645
|
+
const { fromIni } = require("@aws-sdk/credential-providers");
|
|
646
|
+
|
|
647
|
+
const sesProfile = "fishawack";
|
|
648
|
+
const sesRegion = "us-east-1";
|
|
649
|
+
const groupName = "AWSSESSendingGroupDoNotRename";
|
|
650
|
+
|
|
651
|
+
const iamClient = new IAMClient({
|
|
652
|
+
region: sesRegion,
|
|
653
|
+
credentials: fromIni({ profile: sesProfile }),
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
const userName = `${name}-ses`;
|
|
547
657
|
|
|
658
|
+
// Create user in the fishawack account
|
|
548
659
|
try {
|
|
549
|
-
await
|
|
550
|
-
|
|
660
|
+
await Spinner.prototype.simple(
|
|
661
|
+
`Creating SES IAM user ${userName}`,
|
|
662
|
+
() => {
|
|
663
|
+
return iamClient.send(
|
|
664
|
+
new CreateUserCommand({
|
|
665
|
+
UserName: userName,
|
|
666
|
+
Tags: [
|
|
667
|
+
{ Key: "client", Value: process.env.AWS_PROFILE },
|
|
668
|
+
].concat(
|
|
669
|
+
tags.map((t) => ({
|
|
670
|
+
Key: t.Key,
|
|
671
|
+
Value: String(t.Value),
|
|
672
|
+
})),
|
|
673
|
+
),
|
|
674
|
+
}),
|
|
675
|
+
);
|
|
676
|
+
},
|
|
551
677
|
);
|
|
552
678
|
} catch {
|
|
553
|
-
|
|
679
|
+
await Spinner.prototype.simple(
|
|
680
|
+
`Retrieving existing SES IAM user ${userName}`,
|
|
681
|
+
() => {
|
|
682
|
+
return iamClient.send(
|
|
683
|
+
new GetUserCommand({ UserName: userName }),
|
|
684
|
+
);
|
|
685
|
+
},
|
|
686
|
+
);
|
|
554
687
|
}
|
|
555
688
|
|
|
556
|
-
|
|
689
|
+
// Add user to SES sending group
|
|
690
|
+
await aws.iam.addUserToGroup(userName, groupName, iamClient);
|
|
557
691
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
/* empty */
|
|
563
|
-
}
|
|
692
|
+
// Create access key
|
|
693
|
+
const existingKeys = await iamClient.send(
|
|
694
|
+
new ListAccessKeysCommand({ UserName: userName }),
|
|
695
|
+
);
|
|
564
696
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
697
|
+
if (existingKeys.AccessKeyMetadata.length) {
|
|
698
|
+
// Keys already exist — we can't retrieve the secret again so return what we have in secrets
|
|
699
|
+
return {
|
|
700
|
+
MAIL_USERNAME: existingKeys.AccessKeyMetadata[0].AccessKeyId,
|
|
701
|
+
MAIL_PASSWORD: "",
|
|
702
|
+
MAIL_FROM_ADDRESS: mailData.fromAddress,
|
|
703
|
+
MAIL_FROM_NAME: mailData.fromName,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
568
706
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
{
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
language,
|
|
576
|
-
database,
|
|
577
|
-
storage,
|
|
578
|
-
opensearch,
|
|
579
|
-
},
|
|
580
|
-
{
|
|
581
|
-
...s3Data,
|
|
582
|
-
...rdsData,
|
|
583
|
-
...osData,
|
|
584
|
-
APP_NAME: appName,
|
|
585
|
-
APP_DOMAIN: appDomain,
|
|
707
|
+
const keyRes = await Spinner.prototype.simple(
|
|
708
|
+
`Creating access key for SES user`,
|
|
709
|
+
() => {
|
|
710
|
+
return iamClient.send(
|
|
711
|
+
new CreateAccessKeyCommand({ UserName: userName }),
|
|
712
|
+
);
|
|
586
713
|
},
|
|
587
714
|
);
|
|
588
715
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
716
|
+
// Derive SMTP credentials
|
|
717
|
+
const smtpPassword = aws.ses.deriveSmtpPassword(
|
|
718
|
+
keyRes.AccessKey.SecretAccessKey,
|
|
719
|
+
sesRegion,
|
|
720
|
+
);
|
|
594
721
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
(k) => !(k in currentValue),
|
|
603
|
-
);
|
|
722
|
+
return {
|
|
723
|
+
MAIL_USERNAME: keyRes.AccessKey.AccessKeyId,
|
|
724
|
+
MAIL_PASSWORD: smtpPassword,
|
|
725
|
+
MAIL_FROM_ADDRESS: mailData.fromAddress,
|
|
726
|
+
MAIL_FROM_NAME: mailData.fromName,
|
|
727
|
+
};
|
|
728
|
+
};
|
|
604
729
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
tags,
|
|
621
|
-
);
|
|
622
|
-
secretArns[`${group}SecretArn`] = res.ARN;
|
|
730
|
+
const authorizeIngress = async (aws, name, slug, service, authorizer) => {
|
|
731
|
+
try {
|
|
732
|
+
const sg = await aws.ec2.findSecurityGroupByName(`${slug}-${service}`);
|
|
733
|
+
const ebSg = await aws.ec2.findSecurityGroupByTag([
|
|
734
|
+
{
|
|
735
|
+
Name: "tag:elasticbeanstalk:environment-name",
|
|
736
|
+
Values: [name],
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
Name: "tag:aws:cloudformation:logical-id",
|
|
740
|
+
Values: ["AWSEBSecurityGroup"],
|
|
741
|
+
},
|
|
742
|
+
]);
|
|
743
|
+
if (sg && ebSg) {
|
|
744
|
+
await authorizer(sg.GroupId, ebSg.GroupId);
|
|
623
745
|
}
|
|
746
|
+
} catch {
|
|
747
|
+
/* empty */
|
|
624
748
|
}
|
|
749
|
+
};
|
|
625
750
|
|
|
626
|
-
|
|
627
|
-
|
|
751
|
+
module.exports.fullstack = async (
|
|
752
|
+
name,
|
|
753
|
+
tags = [],
|
|
754
|
+
repo,
|
|
755
|
+
branch,
|
|
756
|
+
framework,
|
|
757
|
+
database = false,
|
|
758
|
+
storage = false,
|
|
759
|
+
opensearch = false,
|
|
760
|
+
cache = false,
|
|
761
|
+
mail = false,
|
|
762
|
+
existingSecrets = {
|
|
763
|
+
app: null,
|
|
764
|
+
storage: null,
|
|
765
|
+
database: null,
|
|
766
|
+
opensearch: null,
|
|
767
|
+
cache: null,
|
|
768
|
+
mail: null,
|
|
769
|
+
},
|
|
770
|
+
snapshot = null,
|
|
771
|
+
elbScheme = "public",
|
|
772
|
+
existingEnvironment = false,
|
|
773
|
+
existingResources = {
|
|
774
|
+
storage: false,
|
|
775
|
+
database: false,
|
|
776
|
+
opensearch: false,
|
|
777
|
+
cache: false,
|
|
778
|
+
},
|
|
779
|
+
appName,
|
|
780
|
+
appDomain,
|
|
781
|
+
certArn,
|
|
782
|
+
instanceType = "t3.micro",
|
|
783
|
+
mailData = {},
|
|
784
|
+
) => {
|
|
785
|
+
const { platform, language } = eb.configurations[framework];
|
|
786
|
+
const isLaravel = framework === "laravel";
|
|
787
|
+
const aws = module.exports;
|
|
628
788
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
789
|
+
// Compute slugs
|
|
790
|
+
const s3Slug =
|
|
791
|
+
storage || existingSecrets.storage
|
|
792
|
+
? aws.slug(repo, process.env.AWS_PROFILE, branch, "s3")
|
|
793
|
+
: undefined;
|
|
632
794
|
|
|
633
|
-
|
|
795
|
+
const osSlug =
|
|
796
|
+
opensearch || existingSecrets.opensearch
|
|
797
|
+
? aws.slug(repo, process.env.AWS_PROFILE, branch, "os")
|
|
798
|
+
: undefined;
|
|
634
799
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
800
|
+
const cacheSlug =
|
|
801
|
+
cache || existingSecrets.cache
|
|
802
|
+
? aws.slug(repo, process.env.AWS_PROFILE, branch, "cache")
|
|
803
|
+
: undefined;
|
|
804
|
+
|
|
805
|
+
const opensearchDomainArn = osSlug
|
|
806
|
+
? `arn:aws:es:${process.env.AWS_REGION}:*:domain/${osSlug}`
|
|
807
|
+
: undefined;
|
|
808
|
+
|
|
809
|
+
// Phase 1: IAM prerequisites (needed before OpenSearch and EB env)
|
|
810
|
+
let instanceProfileName;
|
|
811
|
+
|
|
812
|
+
if (isLaravel) {
|
|
813
|
+
instanceProfileName = await aws.iam.ensureEnvironmentInstanceProfile(
|
|
814
|
+
name,
|
|
815
|
+
s3Slug,
|
|
816
|
+
opensearchDomainArn,
|
|
817
|
+
tags,
|
|
818
|
+
);
|
|
819
|
+
} else {
|
|
820
|
+
await aws.iam.ensureEBInstanceProfileExists();
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Phase 2: VPC + EB app in parallel
|
|
824
|
+
const [vpcData] = await Promise.all([
|
|
825
|
+
aws.ec2.ensurePrivateSubnets(),
|
|
826
|
+
aws.iam.ensureEBManagedUpdateProfileExists(),
|
|
827
|
+
]);
|
|
828
|
+
|
|
829
|
+
try {
|
|
830
|
+
await aws.elasticbeanstalk.createElasticBeanstalkApplication(
|
|
831
|
+
repo,
|
|
832
|
+
tags,
|
|
833
|
+
);
|
|
834
|
+
} catch {
|
|
835
|
+
/* empty */
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Phase 3: Provision services in parallel
|
|
839
|
+
const servicePromises = [];
|
|
840
|
+
|
|
841
|
+
if (storage) {
|
|
842
|
+
servicePromises.push(
|
|
843
|
+
provisionStorage(aws, s3Slug, tags, isLaravel, existingResources),
|
|
844
|
+
);
|
|
845
|
+
} else {
|
|
846
|
+
servicePromises.push(Promise.resolve(s3Slug ? { s3Slug } : {}));
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (database) {
|
|
850
|
+
servicePromises.push(
|
|
851
|
+
provisionRDS(
|
|
852
|
+
aws,
|
|
853
|
+
name,
|
|
854
|
+
repo,
|
|
855
|
+
branch,
|
|
856
|
+
tags,
|
|
857
|
+
vpcData,
|
|
858
|
+
existingResources,
|
|
859
|
+
existingSecrets,
|
|
860
|
+
snapshot,
|
|
861
|
+
),
|
|
862
|
+
);
|
|
863
|
+
} else {
|
|
864
|
+
servicePromises.push(Promise.resolve({ rdsData: {}, rdsSlug: null }));
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (opensearch) {
|
|
868
|
+
servicePromises.push(
|
|
869
|
+
provisionOpenSearch(
|
|
870
|
+
aws,
|
|
871
|
+
osSlug,
|
|
872
|
+
tags,
|
|
873
|
+
vpcData,
|
|
874
|
+
instanceProfileName,
|
|
875
|
+
existingResources,
|
|
876
|
+
),
|
|
877
|
+
);
|
|
878
|
+
} else {
|
|
879
|
+
servicePromises.push(Promise.resolve({ osData: {} }));
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (cache) {
|
|
883
|
+
servicePromises.push(
|
|
884
|
+
provisionCache(
|
|
885
|
+
aws,
|
|
886
|
+
name,
|
|
887
|
+
cacheSlug,
|
|
888
|
+
tags,
|
|
889
|
+
vpcData,
|
|
890
|
+
existingResources,
|
|
891
|
+
existingSecrets,
|
|
892
|
+
),
|
|
893
|
+
);
|
|
894
|
+
} else {
|
|
895
|
+
servicePromises.push(Promise.resolve({ cacheData: {} }));
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const [s3Data, rdsResult, osResult, cacheResult] =
|
|
899
|
+
await Promise.all(servicePromises);
|
|
900
|
+
|
|
901
|
+
// Phase 3b: Wait for all pending services with single spinner
|
|
902
|
+
const pendingPolls = [rdsResult, osResult, cacheResult]
|
|
903
|
+
.filter((r) => r && r.poll)
|
|
904
|
+
.map((r) => r.poll);
|
|
905
|
+
|
|
906
|
+
const pollResults = pendingPolls.length ? await pollAll(pendingPolls) : [];
|
|
907
|
+
|
|
908
|
+
// Extract endpoint data from poll results
|
|
909
|
+
let rdsData = rdsResult.rdsData || {};
|
|
910
|
+
|
|
911
|
+
if (rdsResult.poll) {
|
|
912
|
+
const rdsInstance = pollResults[pendingPolls.indexOf(rdsResult.poll)];
|
|
913
|
+
rdsData.RDS_HOSTNAME = rdsInstance.Endpoint.Address;
|
|
914
|
+
rdsData.RDS_PORT = String(rdsInstance.Endpoint.Port);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
let osData = osResult.osData || {};
|
|
918
|
+
|
|
919
|
+
if (osResult.poll) {
|
|
920
|
+
const osDomain = pollResults[pendingPolls.indexOf(osResult.poll)];
|
|
921
|
+
const osEndpoint =
|
|
922
|
+
osDomain.Endpoint || (osDomain.Endpoints && osDomain.Endpoints.vpc);
|
|
923
|
+
osData.SEARCH_HOST = `https://${osEndpoint}`;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
let cacheData = cacheResult.cacheData || {};
|
|
927
|
+
|
|
928
|
+
if (cacheResult.poll) {
|
|
929
|
+
const cluster = pollResults[pendingPolls.indexOf(cacheResult.poll)];
|
|
930
|
+
const cacheEndpoint = cluster.NodeGroups[0].PrimaryEndpoint;
|
|
931
|
+
cacheData.REDIS_HOST = cacheEndpoint.Address;
|
|
932
|
+
cacheData.REDIS_PORT = String(cacheEndpoint.Port);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Phase 4: Build secrets and EB environment (sequential)
|
|
936
|
+
const CNAMEPrefix = `fw-auto-${name.split("-")[name.split("-").length - 1]}`;
|
|
937
|
+
process.env.DOMAIN_LINK = `${CNAMEPrefix}.${process.env.AWS_REGION}.elasticbeanstalk.com`;
|
|
938
|
+
|
|
939
|
+
// Provision mail credentials if needed
|
|
940
|
+
let mailCreds = {};
|
|
941
|
+
if (mail && !existingSecrets.mail) {
|
|
942
|
+
mailCreds = await provisionMail(aws, name, tags, mailData);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const payloads = eb.buildSecretPayloads(
|
|
946
|
+
{
|
|
947
|
+
framework,
|
|
948
|
+
platform,
|
|
949
|
+
language,
|
|
950
|
+
database,
|
|
951
|
+
storage,
|
|
952
|
+
opensearch,
|
|
953
|
+
cache,
|
|
954
|
+
mail: mail && !existingSecrets.mail,
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
...s3Data,
|
|
958
|
+
...rdsData,
|
|
959
|
+
...osData,
|
|
960
|
+
...cacheData,
|
|
961
|
+
...mailCreds,
|
|
962
|
+
APP_NAME: appName,
|
|
963
|
+
APP_DOMAIN: appDomain,
|
|
964
|
+
SESSION_DOMAIN: appDomain || undefined,
|
|
965
|
+
},
|
|
966
|
+
);
|
|
967
|
+
|
|
968
|
+
const secretArns = {};
|
|
969
|
+
|
|
970
|
+
for (const group of secretGroups) {
|
|
971
|
+
if (existingSecrets[group]) {
|
|
972
|
+
secretArns[`${group}SecretArn`] = existingSecrets[group];
|
|
973
|
+
|
|
974
|
+
if (Object.keys(payloads[group]).length) {
|
|
975
|
+
const currentValue = await aws.secretsmanager.getSecretValue(
|
|
638
976
|
`${name}-${group}`,
|
|
639
977
|
);
|
|
978
|
+
const newKeys = Object.keys(payloads[group]).filter(
|
|
979
|
+
(k) => !(k in currentValue),
|
|
980
|
+
);
|
|
981
|
+
|
|
982
|
+
if (newKeys.length) {
|
|
983
|
+
const merged = { ...currentValue };
|
|
984
|
+
for (const k of newKeys) {
|
|
985
|
+
merged[k] = payloads[group][k];
|
|
986
|
+
}
|
|
987
|
+
await aws.secretsmanager.updateSecretValue(
|
|
988
|
+
`${name}-${group}`,
|
|
989
|
+
merged,
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
} else if (Object.keys(payloads[group]).length) {
|
|
994
|
+
const res = await aws.secretsmanager.createSecret(
|
|
995
|
+
`${name}-${group}`,
|
|
996
|
+
payloads[group],
|
|
997
|
+
tags,
|
|
998
|
+
);
|
|
999
|
+
secretArns[`${group}SecretArn`] = res.ARN;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const secretSettings = [];
|
|
1004
|
+
|
|
1005
|
+
for (const group of secretGroups) {
|
|
1006
|
+
const arn = secretArns[`${group}SecretArn`];
|
|
1007
|
+
if (!arn) continue;
|
|
1008
|
+
|
|
1009
|
+
let keys;
|
|
1010
|
+
|
|
1011
|
+
if (existingSecrets[group]) {
|
|
1012
|
+
const secretValue = await aws.secretsmanager.getSecretValue(
|
|
1013
|
+
`${name}-${group}`,
|
|
1014
|
+
);
|
|
640
1015
|
keys = Object.keys(secretValue);
|
|
641
1016
|
} else {
|
|
642
1017
|
keys = Object.keys(payloads[group]);
|
|
@@ -652,14 +1027,14 @@ module.exports.fullstack = async (
|
|
|
652
1027
|
}
|
|
653
1028
|
}
|
|
654
1029
|
|
|
1030
|
+
// Phase 5: Create or update EB environment
|
|
655
1031
|
let environment;
|
|
656
1032
|
|
|
657
1033
|
if (existingEnvironment) {
|
|
658
|
-
// Re-provision: sync secrets to existing EB environment
|
|
659
1034
|
const SECRETS_NAMESPACE =
|
|
660
1035
|
"aws:elasticbeanstalk:application:environmentsecrets";
|
|
661
1036
|
const currentSettings =
|
|
662
|
-
await
|
|
1037
|
+
await aws.elasticbeanstalk.describeConfigurationSettings(
|
|
663
1038
|
repo,
|
|
664
1039
|
name,
|
|
665
1040
|
);
|
|
@@ -689,7 +1064,7 @@ module.exports.fullstack = async (
|
|
|
689
1064
|
const optionSettings = [...toAdd, ...toUpdate];
|
|
690
1065
|
|
|
691
1066
|
if (optionSettings.length || toRemove.length) {
|
|
692
|
-
await
|
|
1067
|
+
await aws.elasticbeanstalk.updateEnvironmentSettings(
|
|
693
1068
|
name,
|
|
694
1069
|
optionSettings,
|
|
695
1070
|
toRemove,
|
|
@@ -702,7 +1077,6 @@ module.exports.fullstack = async (
|
|
|
702
1077
|
"environments",
|
|
703
1078
|
{
|
|
704
1079
|
framework,
|
|
705
|
-
availability,
|
|
706
1080
|
platform,
|
|
707
1081
|
language,
|
|
708
1082
|
database,
|
|
@@ -713,17 +1087,19 @@ module.exports.fullstack = async (
|
|
|
713
1087
|
publicSubnetIds: vpcData.publicSubnetIds.join(","),
|
|
714
1088
|
elbScheme,
|
|
715
1089
|
instanceProfileName,
|
|
1090
|
+
instanceType,
|
|
716
1091
|
},
|
|
717
1092
|
{
|
|
718
1093
|
...s3Data,
|
|
719
1094
|
...rdsData,
|
|
720
1095
|
...osData,
|
|
1096
|
+
...cacheData,
|
|
721
1097
|
APP_NAME: appName,
|
|
722
1098
|
APP_DOMAIN: appDomain,
|
|
1099
|
+
SESSION_DOMAIN: appDomain || undefined,
|
|
723
1100
|
},
|
|
724
1101
|
);
|
|
725
1102
|
|
|
726
|
-
// HTTPS listener via ACM cert
|
|
727
1103
|
if (certArn) {
|
|
728
1104
|
OptionSettings.push(
|
|
729
1105
|
{
|
|
@@ -756,18 +1132,10 @@ module.exports.fullstack = async (
|
|
|
756
1132
|
|
|
757
1133
|
OptionSettings.push(...secretSettings);
|
|
758
1134
|
|
|
759
|
-
if (hasKeyPair) {
|
|
760
|
-
OptionSettings.push({
|
|
761
|
-
OptionName: "EC2KeyName",
|
|
762
|
-
Value: "id_rsa_prev",
|
|
763
|
-
Namespace: "aws:autoscaling:launchconfiguration",
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
|
|
767
1135
|
environment =
|
|
768
|
-
await
|
|
1136
|
+
await aws.elasticbeanstalk.createElasticBeanstalkEnvironment(
|
|
769
1137
|
name,
|
|
770
|
-
{ framework,
|
|
1138
|
+
{ framework, platform, language },
|
|
771
1139
|
repo,
|
|
772
1140
|
OptionSettings,
|
|
773
1141
|
CNAMEPrefix,
|
|
@@ -775,309 +1143,677 @@ module.exports.fullstack = async (
|
|
|
775
1143
|
);
|
|
776
1144
|
}
|
|
777
1145
|
|
|
778
|
-
//
|
|
779
|
-
const rdsSlugForIngress =
|
|
1146
|
+
// Phase 6: SG ingress rules in parallel
|
|
1147
|
+
const rdsSlugForIngress = aws.slug(
|
|
780
1148
|
repo,
|
|
781
1149
|
process.env.AWS_PROFILE,
|
|
782
1150
|
branch,
|
|
783
1151
|
"rds",
|
|
784
1152
|
);
|
|
785
1153
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1154
|
+
const ingressPromises = [
|
|
1155
|
+
authorizeIngress(
|
|
1156
|
+
aws,
|
|
1157
|
+
name,
|
|
1158
|
+
rdsSlugForIngress,
|
|
1159
|
+
"rds",
|
|
1160
|
+
aws.ec2.authorizeRDSIngress,
|
|
1161
|
+
),
|
|
1162
|
+
];
|
|
1163
|
+
|
|
1164
|
+
if (osSlug) {
|
|
1165
|
+
ingressPromises.push(
|
|
1166
|
+
authorizeIngress(
|
|
1167
|
+
aws,
|
|
1168
|
+
name,
|
|
1169
|
+
osSlug,
|
|
1170
|
+
"os",
|
|
1171
|
+
aws.ec2.authorizeOpenSearchIngress,
|
|
1172
|
+
),
|
|
789
1173
|
);
|
|
790
|
-
const ebSg = await module.exports.ec2.findSecurityGroupByTag([
|
|
791
|
-
{
|
|
792
|
-
Name: "tag:elasticbeanstalk:environment-name",
|
|
793
|
-
Values: [name],
|
|
794
|
-
},
|
|
795
|
-
{
|
|
796
|
-
Name: "tag:aws:cloudformation:logical-id",
|
|
797
|
-
Values: ["AWSEBSecurityGroup"],
|
|
798
|
-
},
|
|
799
|
-
]);
|
|
800
|
-
if (rdsSg && ebSg) {
|
|
801
|
-
await module.exports.ec2.authorizeRDSIngress(
|
|
802
|
-
rdsSg.GroupId,
|
|
803
|
-
ebSg.GroupId,
|
|
804
|
-
);
|
|
805
|
-
}
|
|
806
|
-
} catch {
|
|
807
|
-
/* empty */
|
|
808
1174
|
}
|
|
809
1175
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
},
|
|
821
|
-
{
|
|
822
|
-
Name: "tag:aws:cloudformation:logical-id",
|
|
823
|
-
Values: ["AWSEBSecurityGroup"],
|
|
824
|
-
},
|
|
825
|
-
]);
|
|
826
|
-
if (osSg && ebSgForOs) {
|
|
827
|
-
await module.exports.ec2.authorizeOpenSearchIngress(
|
|
828
|
-
osSg.GroupId,
|
|
829
|
-
ebSgForOs.GroupId,
|
|
830
|
-
);
|
|
831
|
-
}
|
|
832
|
-
} catch {
|
|
833
|
-
/* empty */
|
|
834
|
-
}
|
|
1176
|
+
if (cacheSlug) {
|
|
1177
|
+
ingressPromises.push(
|
|
1178
|
+
authorizeIngress(
|
|
1179
|
+
aws,
|
|
1180
|
+
name,
|
|
1181
|
+
cacheSlug,
|
|
1182
|
+
"cache",
|
|
1183
|
+
aws.ec2.authorizeCacheIngress,
|
|
1184
|
+
),
|
|
1185
|
+
);
|
|
835
1186
|
}
|
|
836
1187
|
|
|
1188
|
+
await Promise.all(ingressPromises);
|
|
1189
|
+
|
|
1190
|
+
// Phase 7: Copy configs and return
|
|
837
1191
|
const configurations = eb.merge("configurations", {
|
|
838
1192
|
framework,
|
|
839
|
-
availability,
|
|
840
1193
|
platform,
|
|
841
1194
|
language,
|
|
842
1195
|
});
|
|
843
1196
|
|
|
844
|
-
|
|
845
|
-
configurations.push(
|
|
846
|
-
`.platform/${platform}/conf.d/elasticbeanstalk/${platform === "httpd" ? `z-${framework}-` : ""}htpasswd.conf`,
|
|
847
|
-
);
|
|
848
|
-
fs.outputFileSync(
|
|
849
|
-
`.platform/${platform}/conf.d/elasticbeanstalk/.htpasswd-${branch}`,
|
|
850
|
-
credentials
|
|
851
|
-
.map((d) => `${d.username}:${md5(d.password)}`)
|
|
852
|
-
.join("\n"),
|
|
853
|
-
);
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
await module.exports.elasticbeanstalk.copyElasticBeanstalkConfigs(
|
|
1197
|
+
await aws.elasticbeanstalk.copyElasticBeanstalkConfigs(
|
|
857
1198
|
configurations,
|
|
1199
|
+
branch,
|
|
858
1200
|
);
|
|
859
1201
|
|
|
860
|
-
if (credentials.length) {
|
|
861
|
-
configurations.push(
|
|
862
|
-
`.platform/${platform}/conf.d/elasticbeanstalk/.htpasswd-${branch}`,
|
|
863
|
-
);
|
|
864
|
-
}
|
|
865
|
-
|
|
866
1202
|
let config = {
|
|
867
1203
|
url: `https://${environment.CNAME}`,
|
|
868
1204
|
environment: environment.EnvironmentName,
|
|
869
|
-
paths:
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
return obj;
|
|
877
|
-
}),
|
|
1205
|
+
paths: [`_IaC/${branch}/*`],
|
|
1206
|
+
copy: [
|
|
1207
|
+
{
|
|
1208
|
+
src: `_IaC/${branch}/.elasticbeanstalk/config.yml`,
|
|
1209
|
+
dest: `.elasticbeanstalk/config.yml`,
|
|
1210
|
+
},
|
|
1211
|
+
],
|
|
878
1212
|
};
|
|
879
1213
|
|
|
880
1214
|
return config;
|
|
881
1215
|
};
|
|
882
1216
|
|
|
883
|
-
module.exports.
|
|
1217
|
+
module.exports.fullstackClone = async (
|
|
884
1218
|
name,
|
|
1219
|
+
tags,
|
|
885
1220
|
repo,
|
|
886
1221
|
branch,
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
deleteAppSecret = false,
|
|
892
|
-
} = {},
|
|
1222
|
+
applicationName,
|
|
1223
|
+
sourceEnvironmentName,
|
|
1224
|
+
sourceVersionLabel,
|
|
1225
|
+
sourceBranch,
|
|
893
1226
|
) => {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1227
|
+
const aws = module.exports;
|
|
1228
|
+
|
|
1229
|
+
// Phase 1: Read source configuration
|
|
1230
|
+
const sourceSettings =
|
|
1231
|
+
await aws.elasticbeanstalk.describeConfigurationSettings(
|
|
1232
|
+
applicationName,
|
|
1233
|
+
sourceEnvironmentName,
|
|
900
1234
|
);
|
|
901
1235
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1236
|
+
// Phase 2: Filter settings — keep relevant namespaces, exclude immutable/auto-managed
|
|
1237
|
+
const excludedOptions = new Set([
|
|
1238
|
+
"aws:elasticbeanstalk:environment::EnvironmentName",
|
|
1239
|
+
]);
|
|
1240
|
+
|
|
1241
|
+
const includedNamespaces = new Set([
|
|
1242
|
+
"aws:elasticbeanstalk:application:environment",
|
|
1243
|
+
"aws:elasticbeanstalk:application:environmentsecrets",
|
|
1244
|
+
"aws:autoscaling:launchconfiguration",
|
|
1245
|
+
"aws:autoscaling:asg",
|
|
1246
|
+
"aws:ec2:vpc",
|
|
1247
|
+
"aws:elbv2:listener:443",
|
|
1248
|
+
"aws:elbv2:listener:default",
|
|
1249
|
+
"aws:elasticbeanstalk:environment",
|
|
1250
|
+
"aws:elasticbeanstalk:environment:process:default",
|
|
1251
|
+
"aws:elasticbeanstalk:healthreporting:system",
|
|
1252
|
+
"aws:elasticbeanstalk:managedactions",
|
|
1253
|
+
"aws:elasticbeanstalk:managedactions:platformupdate",
|
|
1254
|
+
"aws:elasticbeanstalk:cloudwatch:logs",
|
|
1255
|
+
"aws:elasticbeanstalk:cloudwatch:logs:health",
|
|
1256
|
+
]);
|
|
1257
|
+
|
|
1258
|
+
const OptionSettings = sourceSettings
|
|
1259
|
+
.filter((s) => {
|
|
1260
|
+
if (!includedNamespaces.has(s.Namespace)) return false;
|
|
1261
|
+
const key = `${s.Namespace}::${s.OptionName}`;
|
|
1262
|
+
if (excludedOptions.has(key)) return false;
|
|
1263
|
+
// Exclude empty values
|
|
1264
|
+
if (s.Value === undefined || s.Value === null || s.Value === "")
|
|
1265
|
+
return false;
|
|
1266
|
+
return true;
|
|
1267
|
+
})
|
|
1268
|
+
.map((s) => ({
|
|
1269
|
+
Namespace: s.Namespace,
|
|
1270
|
+
OptionName: s.OptionName,
|
|
1271
|
+
Value: s.Value,
|
|
1272
|
+
}));
|
|
1273
|
+
|
|
1274
|
+
// Detect which services exist from source secrets
|
|
1275
|
+
const sourceSecrets = sourceSettings.filter(
|
|
1276
|
+
(s) =>
|
|
1277
|
+
s.Namespace ===
|
|
1278
|
+
"aws:elasticbeanstalk:application:environmentsecrets",
|
|
1279
|
+
);
|
|
907
1280
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
/* empty */
|
|
912
|
-
}
|
|
1281
|
+
const hasOpenSearch = sourceSecrets.some(
|
|
1282
|
+
(s) => s.OptionName === "SEARCH_HOST",
|
|
1283
|
+
);
|
|
913
1284
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1285
|
+
// Set SECRET_MIRROR_ENV so setvars.sh reads secrets from the source env
|
|
1286
|
+
OptionSettings.push({
|
|
1287
|
+
Namespace: "aws:elasticbeanstalk:application:environment",
|
|
1288
|
+
OptionName: "SECRET_MIRROR_ENV",
|
|
1289
|
+
Value: sourceEnvironmentName,
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
// Override HealthCheckGracePeriod for initial deploy window
|
|
1293
|
+
const healthGraceIdx = OptionSettings.findIndex(
|
|
1294
|
+
(s) =>
|
|
1295
|
+
s.Namespace ===
|
|
1296
|
+
"aws:elasticbeanstalk:environment:process:default" &&
|
|
1297
|
+
s.OptionName === "HealthCheckGracePeriod",
|
|
1298
|
+
);
|
|
1299
|
+
if (healthGraceIdx !== -1) {
|
|
1300
|
+
OptionSettings[healthGraceIdx].Value = "600";
|
|
919
1301
|
}
|
|
920
1302
|
|
|
921
|
-
//
|
|
922
|
-
const
|
|
1303
|
+
// Phase 4: Create the environment
|
|
1304
|
+
const CNAMEPrefix = `fw-auto-${name.split("-")[name.split("-").length - 1]}`;
|
|
1305
|
+
|
|
1306
|
+
const environment =
|
|
1307
|
+
await aws.elasticbeanstalk.createElasticBeanstalkEnvironment(
|
|
1308
|
+
name,
|
|
1309
|
+
{ language: detectLanguageFromSettings(sourceSettings) },
|
|
1310
|
+
applicationName,
|
|
1311
|
+
OptionSettings,
|
|
1312
|
+
CNAMEPrefix,
|
|
1313
|
+
tags,
|
|
1314
|
+
sourceVersionLabel,
|
|
1315
|
+
);
|
|
1316
|
+
|
|
1317
|
+
// Phase 5: Authorize SG ingress on source service security groups
|
|
1318
|
+
const rdsSlug = aws.slug(
|
|
923
1319
|
repo,
|
|
924
1320
|
process.env.AWS_PROFILE,
|
|
925
|
-
|
|
1321
|
+
sourceBranch,
|
|
926
1322
|
"rds",
|
|
927
1323
|
);
|
|
1324
|
+
const cacheSlug = aws.slug(
|
|
1325
|
+
repo,
|
|
1326
|
+
process.env.AWS_PROFILE,
|
|
1327
|
+
sourceBranch,
|
|
1328
|
+
"cache",
|
|
1329
|
+
);
|
|
928
1330
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1331
|
+
const ingressPromises = [
|
|
1332
|
+
authorizeIngress(
|
|
1333
|
+
aws,
|
|
1334
|
+
name,
|
|
1335
|
+
rdsSlug,
|
|
1336
|
+
"rds",
|
|
1337
|
+
aws.ec2.authorizeRDSIngress,
|
|
1338
|
+
),
|
|
1339
|
+
];
|
|
1340
|
+
|
|
1341
|
+
if (hasOpenSearch) {
|
|
1342
|
+
const sourceOsSlug = aws.slug(
|
|
1343
|
+
repo,
|
|
1344
|
+
process.env.AWS_PROFILE,
|
|
1345
|
+
sourceBranch,
|
|
1346
|
+
"os",
|
|
1347
|
+
);
|
|
1348
|
+
ingressPromises.push(
|
|
1349
|
+
authorizeIngress(
|
|
1350
|
+
aws,
|
|
1351
|
+
name,
|
|
1352
|
+
sourceOsSlug,
|
|
1353
|
+
"os",
|
|
1354
|
+
aws.ec2.authorizeOpenSearchIngress,
|
|
1355
|
+
),
|
|
932
1356
|
);
|
|
933
|
-
const ebSg = await module.exports.ec2.findSecurityGroupByTag([
|
|
934
|
-
{
|
|
935
|
-
Name: "tag:elasticbeanstalk:environment-name",
|
|
936
|
-
Values: [name],
|
|
937
|
-
},
|
|
938
|
-
{
|
|
939
|
-
Name: "tag:aws:cloudformation:logical-id",
|
|
940
|
-
Values: ["AWSEBSecurityGroup"],
|
|
941
|
-
},
|
|
942
|
-
]);
|
|
943
|
-
if (rdsSg && ebSg) {
|
|
944
|
-
await module.exports.ec2.revokeRDSIngress(
|
|
945
|
-
rdsSg.GroupId,
|
|
946
|
-
ebSg.GroupId,
|
|
947
|
-
);
|
|
948
|
-
}
|
|
949
|
-
} catch {
|
|
950
|
-
/* empty */
|
|
951
1357
|
}
|
|
952
1358
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1359
|
+
if (sourceSecrets.some((s) => s.OptionName === "REDIS_HOST")) {
|
|
1360
|
+
ingressPromises.push(
|
|
1361
|
+
authorizeIngress(
|
|
1362
|
+
aws,
|
|
1363
|
+
name,
|
|
1364
|
+
cacheSlug,
|
|
1365
|
+
"cache",
|
|
1366
|
+
aws.ec2.authorizeCacheIngress,
|
|
1367
|
+
),
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
await Promise.all(ingressPromises);
|
|
1372
|
+
|
|
1373
|
+
// Phase 6: Copy configs
|
|
1374
|
+
const framework = detectFrameworkFromSettings(sourceSettings);
|
|
1375
|
+
const { platform, language } = eb.configurations[framework];
|
|
1376
|
+
const configurations = eb.merge("configurations", {
|
|
1377
|
+
framework,
|
|
1378
|
+
platform,
|
|
1379
|
+
language,
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
await aws.elasticbeanstalk.copyElasticBeanstalkConfigs(
|
|
1383
|
+
configurations,
|
|
957
1384
|
branch,
|
|
958
|
-
"os",
|
|
959
1385
|
);
|
|
960
1386
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1387
|
+
return {
|
|
1388
|
+
url: `https://${environment.CNAME}`,
|
|
1389
|
+
environment: environment.EnvironmentName,
|
|
1390
|
+
paths: [`_IaC/${branch}/*`],
|
|
1391
|
+
copy: [
|
|
966
1392
|
{
|
|
967
|
-
|
|
968
|
-
|
|
1393
|
+
src: `_IaC/${branch}/.elasticbeanstalk/config.yml`,
|
|
1394
|
+
dest: `.elasticbeanstalk/config.yml`,
|
|
969
1395
|
},
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
},
|
|
974
|
-
]);
|
|
975
|
-
if (osSg && ebSgForOs) {
|
|
976
|
-
await module.exports.ec2.revokeOpenSearchIngress(
|
|
977
|
-
osSg.GroupId,
|
|
978
|
-
ebSgForOs.GroupId,
|
|
979
|
-
);
|
|
980
|
-
}
|
|
981
|
-
} catch {
|
|
982
|
-
/* empty */
|
|
983
|
-
}
|
|
1396
|
+
],
|
|
1397
|
+
};
|
|
1398
|
+
};
|
|
984
1399
|
|
|
985
|
-
|
|
986
|
-
|
|
1400
|
+
const detectLanguageFromSettings = (settings) => {
|
|
1401
|
+
const envVars = settings.filter(
|
|
1402
|
+
(s) =>
|
|
1403
|
+
s.Namespace === "aws:elasticbeanstalk:application:environment" ||
|
|
1404
|
+
s.Namespace ===
|
|
1405
|
+
"aws:elasticbeanstalk:application:environmentsecrets",
|
|
1406
|
+
);
|
|
1407
|
+
const keys = envVars.map((s) => s.OptionName);
|
|
1408
|
+
if (keys.includes("DJANGO_SETTINGS_MODULE")) return "python";
|
|
1409
|
+
if (keys.includes("NODE_ENV")) return "node";
|
|
1410
|
+
return "php";
|
|
1411
|
+
};
|
|
1412
|
+
|
|
1413
|
+
const detectFrameworkFromSettings = (settings) => {
|
|
1414
|
+
const envVars = settings.filter(
|
|
1415
|
+
(s) =>
|
|
1416
|
+
s.Namespace === "aws:elasticbeanstalk:application:environment" ||
|
|
1417
|
+
s.Namespace ===
|
|
1418
|
+
"aws:elasticbeanstalk:application:environmentsecrets",
|
|
1419
|
+
);
|
|
1420
|
+
const keys = envVars.map((s) => s.OptionName);
|
|
1421
|
+
if (keys.includes("DJANGO_SETTINGS_MODULE")) return "python";
|
|
1422
|
+
if (keys.includes("ADONIS_ACE_CWD")) return "adonis";
|
|
1423
|
+
if (keys.includes("APP_KEY")) return "laravel";
|
|
1424
|
+
return "laravel";
|
|
1425
|
+
};
|
|
1426
|
+
|
|
1427
|
+
module.exports.fullstackCloneTerminate = async (
|
|
1428
|
+
name,
|
|
1429
|
+
repo,
|
|
1430
|
+
branch,
|
|
1431
|
+
sourceBranch,
|
|
1432
|
+
) => {
|
|
1433
|
+
const aws = module.exports;
|
|
1434
|
+
|
|
1435
|
+
const rdsSlug = aws.slug(
|
|
1436
|
+
repo,
|
|
1437
|
+
process.env.AWS_PROFILE,
|
|
1438
|
+
sourceBranch,
|
|
1439
|
+
"rds",
|
|
1440
|
+
);
|
|
1441
|
+
const osSlug = aws.slug(repo, process.env.AWS_PROFILE, sourceBranch, "os");
|
|
1442
|
+
const cacheSlug = aws.slug(
|
|
1443
|
+
repo,
|
|
1444
|
+
process.env.AWS_PROFILE,
|
|
1445
|
+
sourceBranch,
|
|
1446
|
+
"cache",
|
|
1447
|
+
);
|
|
1448
|
+
|
|
1449
|
+
// Phase 1: Revoke SG ingress rules
|
|
1450
|
+
await Promise.all([
|
|
1451
|
+
authorizeIngress(aws, name, rdsSlug, "rds", aws.ec2.revokeRDSIngress),
|
|
1452
|
+
authorizeIngress(
|
|
1453
|
+
aws,
|
|
987
1454
|
name,
|
|
988
|
-
|
|
1455
|
+
osSlug,
|
|
1456
|
+
"os",
|
|
1457
|
+
aws.ec2.revokeOpenSearchIngress,
|
|
1458
|
+
),
|
|
1459
|
+
authorizeIngress(
|
|
1460
|
+
aws,
|
|
1461
|
+
name,
|
|
1462
|
+
cacheSlug,
|
|
1463
|
+
"cache",
|
|
1464
|
+
aws.ec2.revokeCacheIngress,
|
|
1465
|
+
),
|
|
1466
|
+
]);
|
|
1467
|
+
|
|
1468
|
+
// Phase 2: Terminate EB environment
|
|
1469
|
+
try {
|
|
1470
|
+
await aws.elasticbeanstalk.removeElasticBeanstalkEnvironment(name);
|
|
989
1471
|
} catch {
|
|
990
1472
|
/* empty */
|
|
991
1473
|
}
|
|
992
1474
|
|
|
993
|
-
try
|
|
994
|
-
|
|
995
|
-
|
|
1475
|
+
// Phase 3: Terminate EB environment + try removing app if empty
|
|
1476
|
+
await Promise.all([
|
|
1477
|
+
(async () => {
|
|
1478
|
+
try {
|
|
1479
|
+
const envs = await aws.elasticbeanstalk.listEnvironments(repo);
|
|
1480
|
+
if (!envs.length) {
|
|
1481
|
+
await aws.elasticbeanstalk.removeElasticBeanstalkApplication(
|
|
1482
|
+
repo,
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
} catch {
|
|
1486
|
+
/* empty */
|
|
1487
|
+
}
|
|
1488
|
+
})(),
|
|
1489
|
+
]);
|
|
1490
|
+
};
|
|
1491
|
+
|
|
1492
|
+
module.exports.fullstackTerminate = async (
|
|
1493
|
+
name,
|
|
1494
|
+
repo,
|
|
1495
|
+
branch,
|
|
1496
|
+
{
|
|
1497
|
+
deleteStorage = false,
|
|
1498
|
+
deleteDatabase = false,
|
|
1499
|
+
deleteOpenSearch = false,
|
|
1500
|
+
deleteCache = false,
|
|
1501
|
+
deleteMail = false,
|
|
1502
|
+
deleteAppSecret = false,
|
|
1503
|
+
} = {},
|
|
1504
|
+
) => {
|
|
1505
|
+
const aws = module.exports;
|
|
1506
|
+
|
|
1507
|
+
const rdsSlug = aws.slug(repo, process.env.AWS_PROFILE, branch, "rds");
|
|
1508
|
+
const osSlug = aws.slug(repo, process.env.AWS_PROFILE, branch, "os");
|
|
1509
|
+
const cacheSlug = aws.slug(repo, process.env.AWS_PROFILE, branch, "cache");
|
|
1510
|
+
|
|
1511
|
+
// Phase 1: Storage cleanup + revoke all SG ingress rules in parallel
|
|
1512
|
+
const phase1 = [];
|
|
1513
|
+
|
|
1514
|
+
if (deleteStorage) {
|
|
1515
|
+
const s3Slug = aws.slug(repo, process.env.AWS_PROFILE, branch, "s3");
|
|
1516
|
+
|
|
1517
|
+
phase1.push(
|
|
1518
|
+
(async () => {
|
|
1519
|
+
try {
|
|
1520
|
+
await aws.iam.removeIAMUser(s3Slug);
|
|
1521
|
+
} catch {
|
|
1522
|
+
/* empty */
|
|
1523
|
+
}
|
|
1524
|
+
try {
|
|
1525
|
+
await aws.s3.emptyS3Bucket(s3Slug);
|
|
1526
|
+
} catch {
|
|
1527
|
+
/* empty */
|
|
1528
|
+
}
|
|
1529
|
+
try {
|
|
1530
|
+
await aws.s3.removeS3Bucket(s3Slug);
|
|
1531
|
+
} catch {
|
|
1532
|
+
/* empty */
|
|
1533
|
+
}
|
|
1534
|
+
})(),
|
|
996
1535
|
);
|
|
997
|
-
} catch {
|
|
998
|
-
/* empty */
|
|
999
1536
|
}
|
|
1000
1537
|
|
|
1538
|
+
phase1.push(
|
|
1539
|
+
authorizeIngress(aws, name, rdsSlug, "rds", aws.ec2.revokeRDSIngress),
|
|
1540
|
+
authorizeIngress(
|
|
1541
|
+
aws,
|
|
1542
|
+
name,
|
|
1543
|
+
osSlug,
|
|
1544
|
+
"os",
|
|
1545
|
+
aws.ec2.revokeOpenSearchIngress,
|
|
1546
|
+
),
|
|
1547
|
+
authorizeIngress(
|
|
1548
|
+
aws,
|
|
1549
|
+
name,
|
|
1550
|
+
cacheSlug,
|
|
1551
|
+
"cache",
|
|
1552
|
+
aws.ec2.revokeCacheIngress,
|
|
1553
|
+
),
|
|
1554
|
+
);
|
|
1555
|
+
|
|
1556
|
+
await Promise.all(phase1);
|
|
1557
|
+
|
|
1558
|
+
// Phase 2: Terminate EB environment (must complete before SG deletion)
|
|
1001
1559
|
try {
|
|
1002
|
-
await
|
|
1560
|
+
await aws.elasticbeanstalk.removeElasticBeanstalkEnvironment(name);
|
|
1003
1561
|
} catch {
|
|
1004
1562
|
/* empty */
|
|
1005
1563
|
}
|
|
1006
1564
|
|
|
1565
|
+
// Phase 3: EB app + instance profile + initiate service deletions in parallel
|
|
1566
|
+
const phase3 = [
|
|
1567
|
+
(async () => {
|
|
1568
|
+
try {
|
|
1569
|
+
await aws.elasticbeanstalk.removeElasticBeanstalkApplication(
|
|
1570
|
+
repo,
|
|
1571
|
+
);
|
|
1572
|
+
} catch {
|
|
1573
|
+
/* empty */
|
|
1574
|
+
}
|
|
1575
|
+
})(),
|
|
1576
|
+
(async () => {
|
|
1577
|
+
try {
|
|
1578
|
+
await aws.iam.removeEnvironmentInstanceProfile(name);
|
|
1579
|
+
} catch {
|
|
1580
|
+
/* empty */
|
|
1581
|
+
}
|
|
1582
|
+
})(),
|
|
1583
|
+
];
|
|
1584
|
+
|
|
1007
1585
|
if (deleteDatabase) {
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
await module.exports.rds.waitForRDSDeletion(rdsSlug);
|
|
1011
|
-
} catch {
|
|
1012
|
-
/* empty */
|
|
1013
|
-
}
|
|
1586
|
+
phase3.push(aws.rds.deleteRDSInstance(rdsSlug).catch(() => {}));
|
|
1587
|
+
}
|
|
1014
1588
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1589
|
+
if (deleteOpenSearch) {
|
|
1590
|
+
phase3.push(
|
|
1591
|
+
aws.opensearch.deleteOpenSearchDomain(osSlug).catch(() => {}),
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
if (deleteCache) {
|
|
1596
|
+
phase3.push(
|
|
1597
|
+
aws.elasticache.deleteCacheCluster(cacheSlug).catch(() => {}),
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
await Promise.all(phase3);
|
|
1602
|
+
|
|
1603
|
+
// Phase 4: Wait for service deletions with round-robin polling
|
|
1604
|
+
const deletionPolls = [];
|
|
1605
|
+
|
|
1606
|
+
if (deleteDatabase) {
|
|
1607
|
+
const rdsClient = new RDSClient({});
|
|
1608
|
+
|
|
1609
|
+
deletionPolls.push({
|
|
1610
|
+
name: "RDS",
|
|
1611
|
+
cb: async () => {
|
|
1612
|
+
try {
|
|
1613
|
+
const res = await rdsClient.send(
|
|
1614
|
+
new DescribeDBInstancesCommand({
|
|
1615
|
+
DBInstanceIdentifier: rdsSlug,
|
|
1616
|
+
}),
|
|
1617
|
+
);
|
|
1618
|
+
return res.DBInstances[0];
|
|
1619
|
+
} catch (e) {
|
|
1620
|
+
if (e.name === "DBInstanceNotFoundFault") {
|
|
1621
|
+
return { DBInstanceStatus: "deleted" };
|
|
1622
|
+
}
|
|
1623
|
+
throw e;
|
|
1624
|
+
}
|
|
1625
|
+
},
|
|
1626
|
+
shouldContinue: ({ DBInstanceStatus }) =>
|
|
1627
|
+
DBInstanceStatus !== "deleted",
|
|
1628
|
+
});
|
|
1025
1629
|
}
|
|
1026
1630
|
|
|
1027
1631
|
if (deleteOpenSearch) {
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1632
|
+
const osClient = new OpenSearchClient({});
|
|
1633
|
+
|
|
1634
|
+
deletionPolls.push({
|
|
1635
|
+
name: "OpenSearch",
|
|
1636
|
+
cb: async () => {
|
|
1637
|
+
try {
|
|
1638
|
+
const { DomainStatus } = await osClient.send(
|
|
1639
|
+
new DescribeDomainCommand({ DomainName: osSlug }),
|
|
1640
|
+
);
|
|
1641
|
+
return DomainStatus;
|
|
1642
|
+
} catch (e) {
|
|
1643
|
+
if (e.name === "ResourceNotFoundException") {
|
|
1644
|
+
return { deleted: true };
|
|
1645
|
+
}
|
|
1646
|
+
throw e;
|
|
1647
|
+
}
|
|
1648
|
+
},
|
|
1649
|
+
shouldContinue: (status) => !status.deleted,
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1034
1652
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1653
|
+
if (deleteCache) {
|
|
1654
|
+
const cacheClient = new ElastiCacheClient({});
|
|
1655
|
+
|
|
1656
|
+
deletionPolls.push({
|
|
1657
|
+
name: "ElastiCache",
|
|
1658
|
+
cb: async () => {
|
|
1659
|
+
try {
|
|
1660
|
+
const { ReplicationGroups } = await cacheClient.send(
|
|
1661
|
+
new DescribeReplicationGroupsCommand({
|
|
1662
|
+
ReplicationGroupId: cacheSlug,
|
|
1663
|
+
}),
|
|
1664
|
+
);
|
|
1665
|
+
return ReplicationGroups[0];
|
|
1666
|
+
} catch (e) {
|
|
1667
|
+
if (e.name === "ReplicationGroupNotFoundFault") {
|
|
1668
|
+
return { deleted: true };
|
|
1669
|
+
}
|
|
1670
|
+
throw e;
|
|
1671
|
+
}
|
|
1672
|
+
},
|
|
1673
|
+
shouldContinue: (group) => !group.deleted,
|
|
1674
|
+
});
|
|
1045
1675
|
}
|
|
1046
1676
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
try {
|
|
1050
|
-
await module.exports.secretsmanager.deleteSecret(`${name}-storage`);
|
|
1051
|
-
} catch {
|
|
1052
|
-
/* empty */
|
|
1053
|
-
}
|
|
1677
|
+
if (deletionPolls.length) {
|
|
1678
|
+
await pollAll(deletionPolls, "Waiting for services to terminate");
|
|
1054
1679
|
}
|
|
1055
1680
|
|
|
1681
|
+
// Phase 5: Delete security groups after services are gone
|
|
1682
|
+
const sgDeletions = [];
|
|
1683
|
+
|
|
1056
1684
|
if (deleteDatabase) {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1685
|
+
sgDeletions.push(
|
|
1686
|
+
(async () => {
|
|
1687
|
+
try {
|
|
1688
|
+
const sg = await aws.ec2.findSecurityGroupByName(
|
|
1689
|
+
`${rdsSlug}-rds`,
|
|
1690
|
+
);
|
|
1691
|
+
if (sg) {
|
|
1692
|
+
await aws.ec2.deleteSecurityGroup(sg.GroupId);
|
|
1693
|
+
}
|
|
1694
|
+
} catch {
|
|
1695
|
+
/* empty */
|
|
1696
|
+
}
|
|
1697
|
+
})(),
|
|
1698
|
+
);
|
|
1064
1699
|
}
|
|
1065
1700
|
|
|
1066
1701
|
if (deleteOpenSearch) {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1702
|
+
sgDeletions.push(
|
|
1703
|
+
(async () => {
|
|
1704
|
+
try {
|
|
1705
|
+
const sg = await aws.ec2.findSecurityGroupByName(
|
|
1706
|
+
`${osSlug}-os`,
|
|
1707
|
+
);
|
|
1708
|
+
if (sg) {
|
|
1709
|
+
await aws.ec2.deleteSecurityGroup(sg.GroupId);
|
|
1710
|
+
}
|
|
1711
|
+
} catch {
|
|
1712
|
+
/* empty */
|
|
1713
|
+
}
|
|
1714
|
+
})(),
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
if (deleteCache) {
|
|
1719
|
+
sgDeletions.push(
|
|
1720
|
+
(async () => {
|
|
1721
|
+
try {
|
|
1722
|
+
const sg = await aws.ec2.findSecurityGroupByName(
|
|
1723
|
+
`${cacheSlug}-cache`,
|
|
1724
|
+
);
|
|
1725
|
+
if (sg) {
|
|
1726
|
+
await aws.ec2.deleteSecurityGroup(sg.GroupId);
|
|
1727
|
+
}
|
|
1728
|
+
} catch {
|
|
1729
|
+
/* empty */
|
|
1730
|
+
}
|
|
1731
|
+
})(),
|
|
1732
|
+
);
|
|
1074
1733
|
}
|
|
1075
1734
|
|
|
1735
|
+
if (sgDeletions.length) {
|
|
1736
|
+
await Promise.all(sgDeletions);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// Phase 6: Delete secrets in parallel
|
|
1740
|
+
const secretDeletions = [];
|
|
1741
|
+
|
|
1742
|
+
if (deleteStorage) {
|
|
1743
|
+
secretDeletions.push(
|
|
1744
|
+
aws.secretsmanager.deleteSecret(`${name}-storage`).catch(() => {}),
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
if (deleteDatabase) {
|
|
1748
|
+
secretDeletions.push(
|
|
1749
|
+
aws.secretsmanager.deleteSecret(`${name}-database`).catch(() => {}),
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
if (deleteOpenSearch) {
|
|
1753
|
+
secretDeletions.push(
|
|
1754
|
+
aws.secretsmanager
|
|
1755
|
+
.deleteSecret(`${name}-opensearch`)
|
|
1756
|
+
.catch(() => {}),
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
if (deleteCache) {
|
|
1760
|
+
secretDeletions.push(
|
|
1761
|
+
aws.secretsmanager.deleteSecret(`${name}-cache`).catch(() => {}),
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1076
1764
|
if (deleteAppSecret) {
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
/* empty */
|
|
1081
|
-
}
|
|
1765
|
+
secretDeletions.push(
|
|
1766
|
+
aws.secretsmanager.deleteSecret(`${name}-app`).catch(() => {}),
|
|
1767
|
+
);
|
|
1082
1768
|
}
|
|
1769
|
+
if (deleteMail) {
|
|
1770
|
+
secretDeletions.push(
|
|
1771
|
+
aws.secretsmanager.deleteSecret(`${name}-mail`).catch(() => {}),
|
|
1772
|
+
);
|
|
1773
|
+
secretDeletions.push(
|
|
1774
|
+
(async () => {
|
|
1775
|
+
const {
|
|
1776
|
+
IAMClient,
|
|
1777
|
+
DeleteAccessKeyCommand,
|
|
1778
|
+
ListAccessKeysCommand,
|
|
1779
|
+
RemoveUserFromGroupCommand,
|
|
1780
|
+
DeleteUserCommand,
|
|
1781
|
+
} = require("@aws-sdk/client-iam");
|
|
1782
|
+
const { fromIni } = require("@aws-sdk/credential-providers");
|
|
1783
|
+
|
|
1784
|
+
const iamClient = new IAMClient({
|
|
1785
|
+
region: "us-east-1",
|
|
1786
|
+
credentials: fromIni({ profile: "fishawack" }),
|
|
1787
|
+
});
|
|
1788
|
+
const userName = `${name}-ses`;
|
|
1789
|
+
|
|
1790
|
+
try {
|
|
1791
|
+
const keys = await iamClient.send(
|
|
1792
|
+
new ListAccessKeysCommand({ UserName: userName }),
|
|
1793
|
+
);
|
|
1794
|
+
for (const key of keys.AccessKeyMetadata || []) {
|
|
1795
|
+
await iamClient.send(
|
|
1796
|
+
new DeleteAccessKeyCommand({
|
|
1797
|
+
UserName: userName,
|
|
1798
|
+
AccessKeyId: key.AccessKeyId,
|
|
1799
|
+
}),
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
await iamClient.send(
|
|
1803
|
+
new RemoveUserFromGroupCommand({
|
|
1804
|
+
UserName: userName,
|
|
1805
|
+
GroupName: "AWSSESSendingGroupDoNotRename",
|
|
1806
|
+
}),
|
|
1807
|
+
);
|
|
1808
|
+
await iamClient.send(
|
|
1809
|
+
new DeleteUserCommand({ UserName: userName }),
|
|
1810
|
+
);
|
|
1811
|
+
} catch {
|
|
1812
|
+
/* user may not exist */
|
|
1813
|
+
}
|
|
1814
|
+
})(),
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
await Promise.all(secretDeletions);
|
|
1083
1819
|
};
|