@fishawack/lab-env 5.6.1 → 5.7.0-beta.2
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 +35 -0
- package/_Test/provision.js +3 -1
- package/cli.js +1 -0
- package/commands/create/cmds/deprovision.js +37 -6
- package/commands/create/cmds/provision.js +250 -1
- package/commands/create/cmds/sync-secrets.js +198 -0
- package/commands/create/libs/utilities.js +2 -0
- package/commands/create/libs/vars.js +218 -303
- package/commands/create/services/aws/acm.js +59 -0
- package/commands/create/services/aws/ec2.js +699 -2
- package/commands/create/services/aws/elasticbeanstalk.js +70 -0
- package/commands/create/services/aws/iam.js +268 -0
- package/commands/create/services/aws/index.js +804 -52
- package/commands/create/services/aws/opensearch.js +147 -0
- package/commands/create/services/aws/rds.js +284 -0
- package/commands/create/services/aws/s3.js +41 -7
- package/commands/create/services/aws/secretsmanager.js +89 -0
- package/commands/create/templates/elasticbeanstalk/.ebextensions/laravel/rds-ssl.config +5 -0
- package/commands/create/templates/elasticbeanstalk/.ebextensions/misc/setvars.config +16 -1
- package/package.json +5 -1
|
@@ -1,18 +1,159 @@
|
|
|
1
1
|
const md5 = require("apache-md5");
|
|
2
2
|
const fs = require("fs-extra");
|
|
3
|
+
const generator = require("generate-password");
|
|
4
|
+
const { S3Client, ListBucketsCommand } = require("@aws-sdk/client-s3");
|
|
5
|
+
const {
|
|
6
|
+
RDSClient,
|
|
7
|
+
DescribeDBInstancesCommand,
|
|
8
|
+
} = require("@aws-sdk/client-rds");
|
|
9
|
+
const {
|
|
10
|
+
SecretsManagerClient,
|
|
11
|
+
DescribeSecretCommand,
|
|
12
|
+
} = require("@aws-sdk/client-secrets-manager");
|
|
13
|
+
const {
|
|
14
|
+
OpenSearchClient,
|
|
15
|
+
DescribeDomainCommand,
|
|
16
|
+
} = require("@aws-sdk/client-opensearch");
|
|
3
17
|
|
|
4
|
-
const { nameSafe } = require("../../libs/utilities.js");
|
|
5
|
-
const { eb } = require("../../libs/vars.js");
|
|
18
|
+
const { nameSafe, Spinner } = require("../../libs/utilities.js");
|
|
19
|
+
const { eb, secretGroups } = require("../../libs/vars.js");
|
|
6
20
|
|
|
7
21
|
module.exports.s3 = require("./s3.js");
|
|
8
22
|
module.exports.cloudfront = require("./cloudfront.js");
|
|
9
23
|
module.exports.iam = require("./iam.js");
|
|
10
24
|
module.exports.elasticbeanstalk = require("./elasticbeanstalk.js");
|
|
11
25
|
module.exports.ec2 = require("./ec2.js");
|
|
26
|
+
module.exports.rds = require("./rds.js");
|
|
27
|
+
module.exports.opensearch = require("./opensearch.js");
|
|
28
|
+
module.exports.secretsmanager = require("./secretsmanager.js");
|
|
29
|
+
module.exports.acm = require("./acm.js");
|
|
12
30
|
|
|
13
31
|
module.exports.slug = (repo, client, branch, service = "s3") =>
|
|
14
32
|
nameSafe(`${branch}-${repo}-${client}`, service);
|
|
15
33
|
|
|
34
|
+
module.exports.fullstackPreflight = async (
|
|
35
|
+
name,
|
|
36
|
+
repo,
|
|
37
|
+
branch,
|
|
38
|
+
{ storage = false, database = false, opensearch = false } = {},
|
|
39
|
+
) => {
|
|
40
|
+
const existing = {
|
|
41
|
+
environment: false,
|
|
42
|
+
storage: false,
|
|
43
|
+
database: false,
|
|
44
|
+
opensearch: false,
|
|
45
|
+
secrets: { app: null, storage: null, database: null, opensearch: null },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const spinner = new Spinner("Running pre-flight checks", true);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
// Check EB environment
|
|
52
|
+
spinner.ora.text = "Checking for existing EB environment";
|
|
53
|
+
try {
|
|
54
|
+
const { Environments } =
|
|
55
|
+
await module.exports.elasticbeanstalk.describeEnvironment(name);
|
|
56
|
+
const active = Environments.filter(
|
|
57
|
+
(e) => e.Status !== "Terminated",
|
|
58
|
+
);
|
|
59
|
+
if (active.length) {
|
|
60
|
+
existing.environment = active[0];
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
/* does not exist */
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check S3
|
|
67
|
+
if (storage) {
|
|
68
|
+
const s3Slug = module.exports.slug(
|
|
69
|
+
repo,
|
|
70
|
+
process.env.AWS_PROFILE,
|
|
71
|
+
branch,
|
|
72
|
+
"s3",
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
spinner.ora.text = "Checking for existing S3 bucket";
|
|
76
|
+
try {
|
|
77
|
+
const { Buckets } = await new S3Client({}).send(
|
|
78
|
+
new ListBucketsCommand({}),
|
|
79
|
+
);
|
|
80
|
+
if (Buckets.some((b) => b.Name === s3Slug)) {
|
|
81
|
+
existing.storage = true;
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
/* empty */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check RDS
|
|
89
|
+
if (database) {
|
|
90
|
+
const rdsSlug = module.exports.slug(
|
|
91
|
+
repo,
|
|
92
|
+
process.env.AWS_PROFILE,
|
|
93
|
+
branch,
|
|
94
|
+
"rds",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
spinner.ora.text = "Checking for existing RDS instance";
|
|
98
|
+
try {
|
|
99
|
+
const res = await new RDSClient({}).send(
|
|
100
|
+
new DescribeDBInstancesCommand({
|
|
101
|
+
DBInstanceIdentifier: rdsSlug,
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
const instance = res.DBInstances[0];
|
|
105
|
+
if (instance && instance.DBInstanceStatus !== "deleting") {
|
|
106
|
+
existing.database = true;
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
/* does not exist */
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check OpenSearch
|
|
114
|
+
if (opensearch) {
|
|
115
|
+
const osSlug = module.exports.slug(
|
|
116
|
+
repo,
|
|
117
|
+
process.env.AWS_PROFILE,
|
|
118
|
+
branch,
|
|
119
|
+
"os",
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
spinner.ora.text = "Checking for existing OpenSearch domain";
|
|
123
|
+
try {
|
|
124
|
+
const { DomainStatus } = await new OpenSearchClient({}).send(
|
|
125
|
+
new DescribeDomainCommand({ DomainName: osSlug }),
|
|
126
|
+
);
|
|
127
|
+
if (DomainStatus && !DomainStatus.Deleted) {
|
|
128
|
+
existing.opensearch = true;
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
/* does not exist */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check for existing secrets
|
|
136
|
+
for (const group of secretGroups) {
|
|
137
|
+
spinner.ora.text = `Checking for existing ${group} secret`;
|
|
138
|
+
try {
|
|
139
|
+
const secret = await new SecretsManagerClient({}).send(
|
|
140
|
+
new DescribeSecretCommand({ SecretId: `${name}-${group}` }),
|
|
141
|
+
);
|
|
142
|
+
existing.secrets[group] = secret.ARN;
|
|
143
|
+
} catch {
|
|
144
|
+
/* does not exist */
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
spinner.ora.succeed("Pre-flight checks complete");
|
|
149
|
+
} catch (e) {
|
|
150
|
+
spinner.ora.fail("Pre-flight checks failed");
|
|
151
|
+
throw e;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { existing };
|
|
155
|
+
};
|
|
156
|
+
|
|
16
157
|
module.exports.clients = [
|
|
17
158
|
"fishawack",
|
|
18
159
|
"abbvie",
|
|
@@ -166,35 +307,243 @@ module.exports.fullstack = async (
|
|
|
166
307
|
framework,
|
|
167
308
|
availability,
|
|
168
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,
|
|
169
329
|
) => {
|
|
170
330
|
const { platform, language } = eb.configurations[framework];
|
|
331
|
+
const isLaravel = framework === "laravel";
|
|
171
332
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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;
|
|
178
339
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
+
|
|
346
|
+
let s3Data = {};
|
|
347
|
+
|
|
348
|
+
if (storage && !existingResources.storage) {
|
|
349
|
+
if (isLaravel) {
|
|
350
|
+
await module.exports.s3.createS3Bucket(s3Slug, tags, {
|
|
351
|
+
BlockPublicAcls: true,
|
|
352
|
+
IgnorePublicAcls: true,
|
|
353
|
+
BlockPublicPolicy: false,
|
|
354
|
+
RestrictPublicBuckets: false,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
await module.exports.s3.setPublicReadPolicy(s3Slug, "public/*");
|
|
358
|
+
} else {
|
|
359
|
+
await module.exports.iam.createIAMUser(s3Slug, tags);
|
|
360
|
+
await module.exports.iam.attachIAMPolicy(
|
|
361
|
+
s3Slug,
|
|
362
|
+
"arn:aws:iam::aws:policy/AmazonS3FullAccess",
|
|
363
|
+
);
|
|
364
|
+
const {
|
|
365
|
+
AccessKey: { AccessKeyId },
|
|
366
|
+
AccessKey: { SecretAccessKey },
|
|
367
|
+
} = await module.exports.iam.createAccessKey(s3Slug);
|
|
368
|
+
|
|
369
|
+
await module.exports.s3.createS3Bucket(
|
|
370
|
+
s3Slug,
|
|
371
|
+
tags,
|
|
372
|
+
false,
|
|
373
|
+
"BucketOwnerPreferred",
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
s3Data = { s3Slug, AccessKeyId, SecretAccessKey };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (storage && !s3Data.s3Slug) {
|
|
381
|
+
s3Data = { s3Slug };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let instanceProfileName;
|
|
385
|
+
let opensearchDomainArn;
|
|
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
|
+
}
|
|
195
401
|
|
|
196
|
-
await module.exports.iam.ensureEBInstanceProfileExists();
|
|
197
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 = {};
|
|
407
|
+
|
|
408
|
+
if (database && !existingResources.database) {
|
|
409
|
+
const rdsSlug = module.exports.slug(
|
|
410
|
+
repo,
|
|
411
|
+
process.env.AWS_PROFILE,
|
|
412
|
+
branch,
|
|
413
|
+
"rds",
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
const dbPassword = generator.generate({
|
|
417
|
+
length: 10,
|
|
418
|
+
numbers: true,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
await module.exports.rds.ensureSSLParameterGroup();
|
|
422
|
+
|
|
423
|
+
const rdsSecurityGroupId =
|
|
424
|
+
await module.exports.ec2.createRDSSecurityGroup(rdsSlug, tags);
|
|
425
|
+
|
|
426
|
+
const dbSubnetGroupName = await module.exports.rds.ensureDBSubnetGroup(
|
|
427
|
+
vpcData.privateSubnetIds,
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
if (snapshot) {
|
|
431
|
+
await module.exports.rds.restoreRDSInstanceFromSnapshot(
|
|
432
|
+
rdsSlug,
|
|
433
|
+
snapshot,
|
|
434
|
+
rdsSecurityGroupId,
|
|
435
|
+
tags,
|
|
436
|
+
dbSubnetGroupName,
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
await module.exports.rds.waitForRDSInstance(rdsSlug);
|
|
440
|
+
|
|
441
|
+
await module.exports.rds.modifyRDSPassword(rdsSlug, dbPassword);
|
|
442
|
+
} else {
|
|
443
|
+
await module.exports.rds.createRDSInstance(
|
|
444
|
+
rdsSlug,
|
|
445
|
+
tags,
|
|
446
|
+
dbPassword,
|
|
447
|
+
rdsSecurityGroupId,
|
|
448
|
+
dbSubnetGroupName,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const rdsInstance =
|
|
453
|
+
await module.exports.rds.waitForRDSInstance(rdsSlug);
|
|
454
|
+
|
|
455
|
+
rdsData = {
|
|
456
|
+
RDS_HOSTNAME: rdsInstance.Endpoint.Address,
|
|
457
|
+
RDS_PORT: String(rdsInstance.Endpoint.Port),
|
|
458
|
+
RDS_PASSWORD: dbPassword,
|
|
459
|
+
};
|
|
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
|
+
|
|
473
|
+
const dbPassword = generator.generate({
|
|
474
|
+
length: 10,
|
|
475
|
+
numbers: true,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
await module.exports.rds.modifyRDSPassword(rdsSlug, dbPassword);
|
|
479
|
+
const rdsInstance =
|
|
480
|
+
await module.exports.rds.waitForRDSInstance(rdsSlug);
|
|
481
|
+
|
|
482
|
+
rdsData = {
|
|
483
|
+
RDS_HOSTNAME: rdsInstance.Endpoint.Address,
|
|
484
|
+
RDS_PORT: String(rdsInstance.Endpoint.Port),
|
|
485
|
+
RDS_PASSWORD: dbPassword,
|
|
486
|
+
};
|
|
487
|
+
} else if (
|
|
488
|
+
database &&
|
|
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(
|
|
494
|
+
`${name}-database`,
|
|
495
|
+
);
|
|
496
|
+
rdsData = {
|
|
497
|
+
RDS_HOSTNAME: secretValue.RDS_HOSTNAME,
|
|
498
|
+
RDS_PORT: secretValue.RDS_PORT,
|
|
499
|
+
RDS_PASSWORD: secretValue.RDS_PASSWORD,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Create decoupled OpenSearch domain if requested
|
|
504
|
+
let osData = {};
|
|
505
|
+
|
|
506
|
+
if (opensearch && !existingResources.opensearch) {
|
|
507
|
+
await module.exports.iam.ensureOpenSearchServiceRole();
|
|
508
|
+
|
|
509
|
+
const roleArn = instanceProfileName
|
|
510
|
+
? (await module.exports.iam.getRole(instanceProfileName)).Role.Arn
|
|
511
|
+
: undefined;
|
|
512
|
+
|
|
513
|
+
const osSecurityGroupId =
|
|
514
|
+
await module.exports.ec2.createOpenSearchSecurityGroup(
|
|
515
|
+
osSlug,
|
|
516
|
+
tags,
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
await module.exports.opensearch.createOpenSearchDomain(
|
|
520
|
+
osSlug,
|
|
521
|
+
osSecurityGroupId,
|
|
522
|
+
vpcData.privateSubnetIds,
|
|
523
|
+
roleArn,
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
const osDomain =
|
|
527
|
+
await module.exports.opensearch.waitForOpenSearchDomain(osSlug);
|
|
528
|
+
|
|
529
|
+
const osEndpoint =
|
|
530
|
+
osDomain.Endpoint || (osDomain.Endpoints && osDomain.Endpoints.vpc);
|
|
531
|
+
|
|
532
|
+
osData = {
|
|
533
|
+
SEARCH_HOST: `https://${osEndpoint}`,
|
|
534
|
+
};
|
|
535
|
+
} else if (opensearch && existingResources.opensearch) {
|
|
536
|
+
// Existing domain — describe to populate osData for secret payloads
|
|
537
|
+
const osDomain =
|
|
538
|
+
await module.exports.opensearch.describeOpenSearchDomain(osSlug);
|
|
539
|
+
|
|
540
|
+
const osEndpoint =
|
|
541
|
+
osDomain.Endpoint || (osDomain.Endpoints && osDomain.Endpoints.vpc);
|
|
542
|
+
|
|
543
|
+
osData = {
|
|
544
|
+
SEARCH_HOST: `https://${osEndpoint}`,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
198
547
|
|
|
199
548
|
try {
|
|
200
549
|
await module.exports.elasticbeanstalk.createElasticBeanstalkApplication(
|
|
@@ -217,29 +566,273 @@ module.exports.fullstack = async (
|
|
|
217
566
|
const CNAMEPrefix = `fw-auto-${name.split("-")[name.split("-").length - 1]}`;
|
|
218
567
|
process.env.DOMAIN_LINK = `${CNAMEPrefix}.${process.env.AWS_REGION}.elasticbeanstalk.com`;
|
|
219
568
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
{
|
|
223
|
-
|
|
569
|
+
// Build secret payloads and create/reuse secrets
|
|
570
|
+
const payloads = eb.buildSecretPayloads(
|
|
571
|
+
{
|
|
572
|
+
framework,
|
|
573
|
+
availability,
|
|
574
|
+
platform,
|
|
575
|
+
language,
|
|
576
|
+
database,
|
|
577
|
+
storage,
|
|
578
|
+
opensearch,
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
...s3Data,
|
|
582
|
+
...rdsData,
|
|
583
|
+
...osData,
|
|
584
|
+
APP_NAME: appName,
|
|
585
|
+
APP_DOMAIN: appDomain,
|
|
586
|
+
},
|
|
224
587
|
);
|
|
225
588
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
589
|
+
const secretArns = {};
|
|
590
|
+
|
|
591
|
+
for (const group of secretGroups) {
|
|
592
|
+
if (existingSecrets[group]) {
|
|
593
|
+
secretArns[`${group}SecretArn`] = existingSecrets[group];
|
|
594
|
+
|
|
595
|
+
// Merge new keys into existing secret if payload has additions
|
|
596
|
+
if (Object.keys(payloads[group]).length) {
|
|
597
|
+
const currentValue =
|
|
598
|
+
await module.exports.secretsmanager.getSecretValue(
|
|
599
|
+
`${name}-${group}`,
|
|
600
|
+
);
|
|
601
|
+
const newKeys = Object.keys(payloads[group]).filter(
|
|
602
|
+
(k) => !(k in currentValue),
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
if (newKeys.length) {
|
|
606
|
+
const merged = { ...currentValue };
|
|
607
|
+
for (const k of newKeys) {
|
|
608
|
+
merged[k] = payloads[group][k];
|
|
609
|
+
}
|
|
610
|
+
await module.exports.secretsmanager.updateSecretValue(
|
|
611
|
+
`${name}-${group}`,
|
|
612
|
+
merged,
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
} else if (Object.keys(payloads[group]).length) {
|
|
617
|
+
const res = await module.exports.secretsmanager.createSecret(
|
|
618
|
+
`${name}-${group}`,
|
|
619
|
+
payloads[group],
|
|
620
|
+
tags,
|
|
621
|
+
);
|
|
622
|
+
secretArns[`${group}SecretArn`] = res.ARN;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Auto-generate environmentsecrets entries from secret keys + ARNs
|
|
627
|
+
const secretSettings = [];
|
|
628
|
+
|
|
629
|
+
for (const group of secretGroups) {
|
|
630
|
+
const arn = secretArns[`${group}SecretArn`];
|
|
631
|
+
if (!arn) continue;
|
|
632
|
+
|
|
633
|
+
let keys;
|
|
634
|
+
|
|
635
|
+
if (existingSecrets[group]) {
|
|
636
|
+
const secretValue =
|
|
637
|
+
await module.exports.secretsmanager.getSecretValue(
|
|
638
|
+
`${name}-${group}`,
|
|
639
|
+
);
|
|
640
|
+
keys = Object.keys(secretValue);
|
|
641
|
+
} else {
|
|
642
|
+
keys = Object.keys(payloads[group]);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
for (const key of keys) {
|
|
646
|
+
secretSettings.push({
|
|
647
|
+
OptionName: key,
|
|
648
|
+
Value: `${arn}:${key}`,
|
|
649
|
+
Namespace:
|
|
650
|
+
"aws:elasticbeanstalk:application:environmentsecrets",
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
let environment;
|
|
656
|
+
|
|
657
|
+
if (existingEnvironment) {
|
|
658
|
+
// Re-provision: sync secrets to existing EB environment
|
|
659
|
+
const SECRETS_NAMESPACE =
|
|
660
|
+
"aws:elasticbeanstalk:application:environmentsecrets";
|
|
661
|
+
const currentSettings =
|
|
662
|
+
await module.exports.elasticbeanstalk.describeConfigurationSettings(
|
|
663
|
+
repo,
|
|
664
|
+
name,
|
|
665
|
+
);
|
|
666
|
+
const currentSecrets = currentSettings.filter(
|
|
667
|
+
(s) => s.Namespace === SECRETS_NAMESPACE,
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
const currentKeys = new Set(currentSecrets.map((s) => s.OptionName));
|
|
671
|
+
const expectedKeys = new Set(secretSettings.map((s) => s.OptionName));
|
|
672
|
+
|
|
673
|
+
const toAdd = secretSettings.filter(
|
|
674
|
+
(s) => !currentKeys.has(s.OptionName),
|
|
675
|
+
);
|
|
676
|
+
const toUpdate = secretSettings.filter((s) => {
|
|
677
|
+
const current = currentSecrets.find(
|
|
678
|
+
(c) => c.OptionName === s.OptionName,
|
|
679
|
+
);
|
|
680
|
+
return current && current.Value !== s.Value;
|
|
231
681
|
});
|
|
682
|
+
const toRemove = currentSecrets
|
|
683
|
+
.filter((s) => !expectedKeys.has(s.OptionName))
|
|
684
|
+
.map((s) => ({
|
|
685
|
+
OptionName: s.OptionName,
|
|
686
|
+
Namespace: SECRETS_NAMESPACE,
|
|
687
|
+
}));
|
|
688
|
+
|
|
689
|
+
const optionSettings = [...toAdd, ...toUpdate];
|
|
690
|
+
|
|
691
|
+
if (optionSettings.length || toRemove.length) {
|
|
692
|
+
await module.exports.elasticbeanstalk.updateEnvironmentSettings(
|
|
693
|
+
name,
|
|
694
|
+
optionSettings,
|
|
695
|
+
toRemove,
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
environment = existingEnvironment;
|
|
700
|
+
} else {
|
|
701
|
+
const OptionSettings = eb.merge(
|
|
702
|
+
"environments",
|
|
703
|
+
{
|
|
704
|
+
framework,
|
|
705
|
+
availability,
|
|
706
|
+
platform,
|
|
707
|
+
language,
|
|
708
|
+
database,
|
|
709
|
+
storage,
|
|
710
|
+
opensearch,
|
|
711
|
+
vpcId: vpcData.vpcId,
|
|
712
|
+
privateSubnetIds: vpcData.privateSubnetIds.join(","),
|
|
713
|
+
publicSubnetIds: vpcData.publicSubnetIds.join(","),
|
|
714
|
+
elbScheme,
|
|
715
|
+
instanceProfileName,
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
...s3Data,
|
|
719
|
+
...rdsData,
|
|
720
|
+
...osData,
|
|
721
|
+
APP_NAME: appName,
|
|
722
|
+
APP_DOMAIN: appDomain,
|
|
723
|
+
},
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
// HTTPS listener via ACM cert
|
|
727
|
+
if (certArn) {
|
|
728
|
+
OptionSettings.push(
|
|
729
|
+
{
|
|
730
|
+
OptionName: "ListenerEnabled",
|
|
731
|
+
Value: "true",
|
|
732
|
+
Namespace: "aws:elbv2:listener:443",
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
OptionName: "Protocol",
|
|
736
|
+
Value: "HTTPS",
|
|
737
|
+
Namespace: "aws:elbv2:listener:443",
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
OptionName: "SSLCertificateArns",
|
|
741
|
+
Value: certArn,
|
|
742
|
+
Namespace: "aws:elbv2:listener:443",
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
OptionName: "SSLPolicy",
|
|
746
|
+
Value: "ELBSecurityPolicy-FS-1-2-Res-2020-10",
|
|
747
|
+
Namespace: "aws:elbv2:listener:443",
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
OptionName: "DefaultProcess",
|
|
751
|
+
Value: "default",
|
|
752
|
+
Namespace: "aws:elbv2:listener:443",
|
|
753
|
+
},
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
OptionSettings.push(...secretSettings);
|
|
758
|
+
|
|
759
|
+
if (hasKeyPair) {
|
|
760
|
+
OptionSettings.push({
|
|
761
|
+
OptionName: "EC2KeyName",
|
|
762
|
+
Value: "id_rsa_prev",
|
|
763
|
+
Namespace: "aws:autoscaling:launchconfiguration",
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
environment =
|
|
768
|
+
await module.exports.elasticbeanstalk.createElasticBeanstalkEnvironment(
|
|
769
|
+
name,
|
|
770
|
+
{ framework, availability, platform, language },
|
|
771
|
+
repo,
|
|
772
|
+
OptionSettings,
|
|
773
|
+
CNAMEPrefix,
|
|
774
|
+
tags,
|
|
775
|
+
);
|
|
232
776
|
}
|
|
233
777
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
778
|
+
// Add RDS security group ingress rule from EB security group
|
|
779
|
+
const rdsSlugForIngress = module.exports.slug(
|
|
780
|
+
repo,
|
|
781
|
+
process.env.AWS_PROFILE,
|
|
782
|
+
branch,
|
|
783
|
+
"rds",
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
try {
|
|
787
|
+
const rdsSg = await module.exports.ec2.findSecurityGroupByName(
|
|
788
|
+
`${rdsSlugForIngress}-rds`,
|
|
242
789
|
);
|
|
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
|
+
}
|
|
809
|
+
|
|
810
|
+
// Add OpenSearch security group ingress rule from EB security group
|
|
811
|
+
if (osSlug) {
|
|
812
|
+
try {
|
|
813
|
+
const osSg = await module.exports.ec2.findSecurityGroupByName(
|
|
814
|
+
`${osSlug}-os`,
|
|
815
|
+
);
|
|
816
|
+
const ebSgForOs = await module.exports.ec2.findSecurityGroupByTag([
|
|
817
|
+
{
|
|
818
|
+
Name: "tag:elasticbeanstalk:environment-name",
|
|
819
|
+
Values: [name],
|
|
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
|
+
}
|
|
835
|
+
}
|
|
243
836
|
|
|
244
837
|
const configurations = eb.merge("configurations", {
|
|
245
838
|
framework,
|
|
@@ -287,28 +880,104 @@ module.exports.fullstack = async (
|
|
|
287
880
|
return config;
|
|
288
881
|
};
|
|
289
882
|
|
|
290
|
-
module.exports.fullstackTerminate = async (
|
|
291
|
-
|
|
883
|
+
module.exports.fullstackTerminate = async (
|
|
884
|
+
name,
|
|
885
|
+
repo,
|
|
886
|
+
branch,
|
|
887
|
+
{
|
|
888
|
+
deleteStorage = false,
|
|
889
|
+
deleteDatabase = false,
|
|
890
|
+
deleteOpenSearch = false,
|
|
891
|
+
deleteAppSecret = false,
|
|
892
|
+
} = {},
|
|
893
|
+
) => {
|
|
894
|
+
if (deleteStorage) {
|
|
895
|
+
const s3Slug = module.exports.slug(
|
|
896
|
+
repo,
|
|
897
|
+
process.env.AWS_PROFILE,
|
|
898
|
+
branch,
|
|
899
|
+
"s3",
|
|
900
|
+
);
|
|
901
|
+
|
|
902
|
+
try {
|
|
903
|
+
await module.exports.iam.removeIAMUser(s3Slug);
|
|
904
|
+
} catch {
|
|
905
|
+
/* empty */
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
try {
|
|
909
|
+
await module.exports.s3.emptyS3Bucket(s3Slug);
|
|
910
|
+
} catch {
|
|
911
|
+
/* empty */
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
try {
|
|
915
|
+
await module.exports.s3.removeS3Bucket(s3Slug);
|
|
916
|
+
} catch {
|
|
917
|
+
/* empty */
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Remove RDS inbound rule referencing EB security group before terminating EB
|
|
922
|
+
const rdsSlug = module.exports.slug(
|
|
292
923
|
repo,
|
|
293
924
|
process.env.AWS_PROFILE,
|
|
294
925
|
branch,
|
|
295
|
-
"
|
|
926
|
+
"rds",
|
|
296
927
|
);
|
|
297
928
|
|
|
298
929
|
try {
|
|
299
|
-
await module.exports.
|
|
930
|
+
const rdsSg = await module.exports.ec2.findSecurityGroupByName(
|
|
931
|
+
`${rdsSlug}-rds`,
|
|
932
|
+
);
|
|
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
|
+
}
|
|
300
949
|
} catch {
|
|
301
950
|
/* empty */
|
|
302
951
|
}
|
|
303
952
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
953
|
+
// Remove OpenSearch inbound rule referencing EB security group before terminating EB
|
|
954
|
+
const osSlug = module.exports.slug(
|
|
955
|
+
repo,
|
|
956
|
+
process.env.AWS_PROFILE,
|
|
957
|
+
branch,
|
|
958
|
+
"os",
|
|
959
|
+
);
|
|
309
960
|
|
|
310
961
|
try {
|
|
311
|
-
await module.exports.
|
|
962
|
+
const osSg = await module.exports.ec2.findSecurityGroupByName(
|
|
963
|
+
`${osSlug}-os`,
|
|
964
|
+
);
|
|
965
|
+
const ebSgForOs = await module.exports.ec2.findSecurityGroupByTag([
|
|
966
|
+
{
|
|
967
|
+
Name: "tag:elasticbeanstalk:environment-name",
|
|
968
|
+
Values: [name],
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
Name: "tag:aws:cloudformation:logical-id",
|
|
972
|
+
Values: ["AWSEBSecurityGroup"],
|
|
973
|
+
},
|
|
974
|
+
]);
|
|
975
|
+
if (osSg && ebSgForOs) {
|
|
976
|
+
await module.exports.ec2.revokeOpenSearchIngress(
|
|
977
|
+
osSg.GroupId,
|
|
978
|
+
ebSgForOs.GroupId,
|
|
979
|
+
);
|
|
980
|
+
}
|
|
312
981
|
} catch {
|
|
313
982
|
/* empty */
|
|
314
983
|
}
|
|
@@ -328,4 +997,87 @@ module.exports.fullstackTerminate = async (name, repo, branch) => {
|
|
|
328
997
|
} catch {
|
|
329
998
|
/* empty */
|
|
330
999
|
}
|
|
1000
|
+
|
|
1001
|
+
try {
|
|
1002
|
+
await module.exports.iam.removeEnvironmentInstanceProfile(name);
|
|
1003
|
+
} catch {
|
|
1004
|
+
/* empty */
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (deleteDatabase) {
|
|
1008
|
+
try {
|
|
1009
|
+
await module.exports.rds.deleteRDSInstance(rdsSlug);
|
|
1010
|
+
await module.exports.rds.waitForRDSDeletion(rdsSlug);
|
|
1011
|
+
} catch {
|
|
1012
|
+
/* empty */
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
try {
|
|
1016
|
+
const sg = await module.exports.ec2.findSecurityGroupByName(
|
|
1017
|
+
`${rdsSlug}-rds`,
|
|
1018
|
+
);
|
|
1019
|
+
if (sg) {
|
|
1020
|
+
await module.exports.ec2.deleteSecurityGroup(sg.GroupId);
|
|
1021
|
+
}
|
|
1022
|
+
} catch {
|
|
1023
|
+
/* empty */
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (deleteOpenSearch) {
|
|
1028
|
+
try {
|
|
1029
|
+
await module.exports.opensearch.deleteOpenSearchDomain(osSlug);
|
|
1030
|
+
await module.exports.opensearch.waitForOpenSearchDeletion(osSlug);
|
|
1031
|
+
} catch {
|
|
1032
|
+
/* empty */
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
try {
|
|
1036
|
+
const sg = await module.exports.ec2.findSecurityGroupByName(
|
|
1037
|
+
`${osSlug}-os`,
|
|
1038
|
+
);
|
|
1039
|
+
if (sg) {
|
|
1040
|
+
await module.exports.ec2.deleteSecurityGroup(sg.GroupId);
|
|
1041
|
+
}
|
|
1042
|
+
} catch {
|
|
1043
|
+
/* empty */
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Delete secrets
|
|
1048
|
+
if (deleteStorage) {
|
|
1049
|
+
try {
|
|
1050
|
+
await module.exports.secretsmanager.deleteSecret(`${name}-storage`);
|
|
1051
|
+
} catch {
|
|
1052
|
+
/* empty */
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (deleteDatabase) {
|
|
1057
|
+
try {
|
|
1058
|
+
await module.exports.secretsmanager.deleteSecret(
|
|
1059
|
+
`${name}-database`,
|
|
1060
|
+
);
|
|
1061
|
+
} catch {
|
|
1062
|
+
/* empty */
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (deleteOpenSearch) {
|
|
1067
|
+
try {
|
|
1068
|
+
await module.exports.secretsmanager.deleteSecret(
|
|
1069
|
+
`${name}-opensearch`,
|
|
1070
|
+
);
|
|
1071
|
+
} catch {
|
|
1072
|
+
/* empty */
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
if (deleteAppSecret) {
|
|
1077
|
+
try {
|
|
1078
|
+
await module.exports.secretsmanager.deleteSecret(`${name}-app`);
|
|
1079
|
+
} catch {
|
|
1080
|
+
/* empty */
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
331
1083
|
};
|