@fishawack/lab-env 5.7.0-beta.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 +16 -0
- package/_Test/provision.js +3 -1
- package/commands/create/cmds/deprovision.js +7 -0
- package/commands/create/cmds/provision.js +182 -48
- package/commands/create/cmds/sync-secrets.js +2 -1
- package/commands/create/libs/utilities.js +2 -0
- package/commands/create/libs/vars.js +53 -53
- package/commands/create/services/aws/acm.js +59 -0
- package/commands/create/services/aws/ec2.js +576 -28
- package/commands/create/services/aws/elasticbeanstalk.js +16 -0
- package/commands/create/services/aws/iam.js +254 -0
- package/commands/create/services/aws/index.js +462 -67
- package/commands/create/services/aws/opensearch.js +147 -0
- package/commands/create/services/aws/rds.js +38 -0
- package/commands/create/services/aws/s3.js +41 -7
- package/commands/create/services/aws/secretsmanager.js +16 -0
- package/package.json +3 -1
|
@@ -6,8 +6,20 @@ const {
|
|
|
6
6
|
RevokeSecurityGroupIngressCommand,
|
|
7
7
|
DeleteSecurityGroupCommand,
|
|
8
8
|
DescribeSecurityGroupsCommand,
|
|
9
|
+
DescribeVpcsCommand,
|
|
10
|
+
DescribeSubnetsCommand,
|
|
11
|
+
CreateSubnetCommand,
|
|
12
|
+
CreateRouteTableCommand,
|
|
13
|
+
CreateRouteCommand,
|
|
14
|
+
AssociateRouteTableCommand,
|
|
15
|
+
CreateNatGatewayCommand,
|
|
16
|
+
DescribeNatGatewaysCommand,
|
|
17
|
+
AllocateAddressCommand,
|
|
18
|
+
DescribeAvailabilityZonesCommand,
|
|
19
|
+
DescribeRouteTablesCommand,
|
|
20
|
+
DescribeInternetGatewaysCommand,
|
|
9
21
|
} = require("@aws-sdk/client-ec2");
|
|
10
|
-
const { Spinner } = require("../../libs/utilities");
|
|
22
|
+
const { Spinner, poll } = require("../../libs/utilities");
|
|
11
23
|
|
|
12
24
|
module.exports.getKeyPair = async (KeyName) => {
|
|
13
25
|
const client = new EC2Client({});
|
|
@@ -29,34 +41,43 @@ module.exports.createRDSSecurityGroup = async (name, tags = []) => {
|
|
|
29
41
|
|
|
30
42
|
const groupName = `${name}-rds`;
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
44
|
+
try {
|
|
45
|
+
const sg = await Spinner.prototype.simple(
|
|
46
|
+
`Creating security group ${groupName}`,
|
|
47
|
+
() => {
|
|
48
|
+
return client.send(
|
|
49
|
+
new CreateSecurityGroupCommand({
|
|
50
|
+
GroupName: groupName,
|
|
51
|
+
Description: `RDS access for ${name}`,
|
|
52
|
+
TagSpecifications: [
|
|
53
|
+
{
|
|
54
|
+
ResourceType: "security-group",
|
|
55
|
+
Tags: [
|
|
56
|
+
{
|
|
57
|
+
Key: "Name",
|
|
58
|
+
Value: groupName,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
Key: "client",
|
|
62
|
+
Value: process.env.AWS_PROFILE,
|
|
63
|
+
},
|
|
64
|
+
].concat(tags),
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
);
|
|
58
71
|
|
|
59
|
-
|
|
72
|
+
return sg.GroupId;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
if (e.Code === "InvalidGroup.Duplicate") {
|
|
75
|
+
const existing =
|
|
76
|
+
await module.exports.findSecurityGroupByName(groupName);
|
|
77
|
+
return existing.GroupId;
|
|
78
|
+
}
|
|
79
|
+
throw e;
|
|
80
|
+
}
|
|
60
81
|
};
|
|
61
82
|
|
|
62
83
|
module.exports.authorizeRDSIngress = async (
|
|
@@ -121,6 +142,112 @@ module.exports.revokeRDSIngress = async (
|
|
|
121
142
|
);
|
|
122
143
|
};
|
|
123
144
|
|
|
145
|
+
module.exports.createOpenSearchSecurityGroup = async (name, tags = []) => {
|
|
146
|
+
const client = new EC2Client({});
|
|
147
|
+
|
|
148
|
+
const groupName = `${name}-os`;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const sg = await Spinner.prototype.simple(
|
|
152
|
+
`Creating security group ${groupName}`,
|
|
153
|
+
() => {
|
|
154
|
+
return client.send(
|
|
155
|
+
new CreateSecurityGroupCommand({
|
|
156
|
+
GroupName: groupName,
|
|
157
|
+
Description: `OpenSearch access for ${name}`,
|
|
158
|
+
TagSpecifications: [
|
|
159
|
+
{
|
|
160
|
+
ResourceType: "security-group",
|
|
161
|
+
Tags: [
|
|
162
|
+
{
|
|
163
|
+
Key: "Name",
|
|
164
|
+
Value: groupName,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
Key: "client",
|
|
168
|
+
Value: process.env.AWS_PROFILE,
|
|
169
|
+
},
|
|
170
|
+
].concat(tags),
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return sg.GroupId;
|
|
179
|
+
} catch (e) {
|
|
180
|
+
if (e.Code === "InvalidGroup.Duplicate") {
|
|
181
|
+
const existing =
|
|
182
|
+
await module.exports.findSecurityGroupByName(groupName);
|
|
183
|
+
return existing.GroupId;
|
|
184
|
+
}
|
|
185
|
+
throw e;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
module.exports.authorizeOpenSearchIngress = async (
|
|
190
|
+
osSecurityGroupId,
|
|
191
|
+
sourceSecurityGroupId,
|
|
192
|
+
) => {
|
|
193
|
+
const client = new EC2Client({});
|
|
194
|
+
|
|
195
|
+
await Spinner.prototype.simple(
|
|
196
|
+
`Adding HTTPS inbound rule from EB security group`,
|
|
197
|
+
() => {
|
|
198
|
+
return client.send(
|
|
199
|
+
new AuthorizeSecurityGroupIngressCommand({
|
|
200
|
+
GroupId: osSecurityGroupId,
|
|
201
|
+
IpPermissions: [
|
|
202
|
+
{
|
|
203
|
+
IpProtocol: "tcp",
|
|
204
|
+
FromPort: 443,
|
|
205
|
+
ToPort: 443,
|
|
206
|
+
UserIdGroupPairs: [
|
|
207
|
+
{
|
|
208
|
+
GroupId: sourceSecurityGroupId,
|
|
209
|
+
Description:
|
|
210
|
+
"HTTPS access for EB instances",
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
module.exports.revokeOpenSearchIngress = async (
|
|
222
|
+
osSecurityGroupId,
|
|
223
|
+
sourceSecurityGroupId,
|
|
224
|
+
) => {
|
|
225
|
+
const client = new EC2Client({});
|
|
226
|
+
|
|
227
|
+
await Spinner.prototype.simple(
|
|
228
|
+
`Removing HTTPS inbound rule from OpenSearch security group`,
|
|
229
|
+
() => {
|
|
230
|
+
return client.send(
|
|
231
|
+
new RevokeSecurityGroupIngressCommand({
|
|
232
|
+
GroupId: osSecurityGroupId,
|
|
233
|
+
IpPermissions: [
|
|
234
|
+
{
|
|
235
|
+
IpProtocol: "tcp",
|
|
236
|
+
FromPort: 443,
|
|
237
|
+
ToPort: 443,
|
|
238
|
+
UserIdGroupPairs: [
|
|
239
|
+
{
|
|
240
|
+
GroupId: sourceSecurityGroupId,
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
};
|
|
250
|
+
|
|
124
251
|
module.exports.findSecurityGroupByTag = async (filters = []) => {
|
|
125
252
|
const client = new EC2Client({});
|
|
126
253
|
|
|
@@ -164,3 +291,424 @@ module.exports.deleteSecurityGroup = async (groupId) => {
|
|
|
164
291
|
);
|
|
165
292
|
});
|
|
166
293
|
};
|
|
294
|
+
|
|
295
|
+
const discoverPublicSubnets = async (client, vpcId, excludeIds, azs) => {
|
|
296
|
+
const { Subnets: allSubnets } = await client.send(
|
|
297
|
+
new DescribeSubnetsCommand({
|
|
298
|
+
Filters: [{ Name: "vpc-id", Values: [vpcId] }],
|
|
299
|
+
}),
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
const { RouteTables } = await client.send(
|
|
303
|
+
new DescribeRouteTablesCommand({
|
|
304
|
+
Filters: [{ Name: "vpc-id", Values: [vpcId] }],
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const publicByAz = {};
|
|
309
|
+
for (const subnet of allSubnets) {
|
|
310
|
+
if (excludeIds.includes(subnet.SubnetId)) continue;
|
|
311
|
+
if (!azs.includes(subnet.AvailabilityZone)) continue;
|
|
312
|
+
|
|
313
|
+
let rt = RouteTables.find((r) =>
|
|
314
|
+
r.Associations.some((a) => a.SubnetId === subnet.SubnetId),
|
|
315
|
+
);
|
|
316
|
+
if (!rt) {
|
|
317
|
+
rt = RouteTables.find((r) => r.Associations.some((a) => a.Main));
|
|
318
|
+
}
|
|
319
|
+
if (!rt) continue;
|
|
320
|
+
|
|
321
|
+
const hasIgwRoute = rt.Routes.some(
|
|
322
|
+
(route) =>
|
|
323
|
+
route.GatewayId &&
|
|
324
|
+
route.GatewayId.startsWith("igw-") &&
|
|
325
|
+
route.DestinationCidrBlock === "0.0.0.0/0",
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
if (hasIgwRoute && !publicByAz[subnet.AvailabilityZone]) {
|
|
329
|
+
publicByAz[subnet.AvailabilityZone] = subnet.SubnetId;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return azs.map((az) => publicByAz[az]).filter(Boolean);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
module.exports.ensurePrivateSubnets = async () => {
|
|
337
|
+
const client = new EC2Client({});
|
|
338
|
+
|
|
339
|
+
// Get default VPC
|
|
340
|
+
const { Vpcs } = await client.send(
|
|
341
|
+
new DescribeVpcsCommand({
|
|
342
|
+
Filters: [{ Name: "isDefault", Values: ["true"] }],
|
|
343
|
+
}),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
if (!Vpcs.length) {
|
|
347
|
+
throw new Error("No default VPC found in this region");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const vpc = Vpcs[0];
|
|
351
|
+
const vpcId = vpc.VpcId;
|
|
352
|
+
|
|
353
|
+
// Preflight: check all private networking resources
|
|
354
|
+
const { Subnets: existingSubnets } = await client.send(
|
|
355
|
+
new DescribeSubnetsCommand({
|
|
356
|
+
Filters: [
|
|
357
|
+
{ Name: "vpc-id", Values: [vpcId] },
|
|
358
|
+
{ Name: "tag:private", Values: ["true"] },
|
|
359
|
+
],
|
|
360
|
+
}),
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const { NatGateways: allNats } = await client.send(
|
|
364
|
+
new DescribeNatGatewaysCommand({
|
|
365
|
+
Filter: [
|
|
366
|
+
{ Name: "vpc-id", Values: [vpcId] },
|
|
367
|
+
{ Name: "tag:private", Values: ["true"] },
|
|
368
|
+
],
|
|
369
|
+
}),
|
|
370
|
+
);
|
|
371
|
+
const existingNats = allNats.filter(
|
|
372
|
+
(n) => n.State !== "deleted" && n.State !== "deleting",
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
const { RouteTables: existingRtbs } = await client.send(
|
|
376
|
+
new DescribeRouteTablesCommand({
|
|
377
|
+
Filters: [
|
|
378
|
+
{ Name: "vpc-id", Values: [vpcId] },
|
|
379
|
+
{ Name: "tag:private", Values: ["true"] },
|
|
380
|
+
],
|
|
381
|
+
}),
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
// Check for inconsistencies (exactly 1 of any resource type)
|
|
385
|
+
const counts = {
|
|
386
|
+
"private subnets": existingSubnets.length,
|
|
387
|
+
"NAT gateways": existingNats.length,
|
|
388
|
+
"route tables": existingRtbs.length,
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const inconsistent = Object.entries(counts).filter(
|
|
392
|
+
([, count]) => count === 1,
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
if (inconsistent.length) {
|
|
396
|
+
const detail = inconsistent
|
|
397
|
+
.map(([name, count]) => `${count} ${name}`)
|
|
398
|
+
.join(", ");
|
|
399
|
+
throw new Error(
|
|
400
|
+
`Inconsistent private networking state: ${detail}. Expected 0 or 2 of each resource tagged private=true. Please resolve manually.`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Determine what needs creation
|
|
405
|
+
const needsSubnets = existingSubnets.length < 2;
|
|
406
|
+
const needsNats = existingNats.length < 2;
|
|
407
|
+
const needsRtbs = existingRtbs.length < 2;
|
|
408
|
+
|
|
409
|
+
// All resources exist — nothing to do
|
|
410
|
+
if (!needsSubnets && !needsNats && !needsRtbs) {
|
|
411
|
+
const publicSubnetIds = await discoverPublicSubnets(
|
|
412
|
+
client,
|
|
413
|
+
vpcId,
|
|
414
|
+
existingSubnets.map((s) => s.SubnetId),
|
|
415
|
+
existingSubnets.slice(0, 2).map((s) => s.AvailabilityZone),
|
|
416
|
+
);
|
|
417
|
+
return {
|
|
418
|
+
privateSubnetIds: existingSubnets.map((s) => s.SubnetId),
|
|
419
|
+
publicSubnetIds,
|
|
420
|
+
vpcId,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const spinner = new Spinner("Setting up private subnets", true);
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
// Discover AZs — use existing subnets' AZs if available, otherwise first 2
|
|
428
|
+
let azs;
|
|
429
|
+
if (!needsSubnets) {
|
|
430
|
+
azs = existingSubnets.slice(0, 2).map((s) => s.AvailabilityZone);
|
|
431
|
+
} else {
|
|
432
|
+
spinner.ora.text = "Discovering availability zones";
|
|
433
|
+
const { AvailabilityZones } = await client.send(
|
|
434
|
+
new DescribeAvailabilityZonesCommand({
|
|
435
|
+
Filters: [{ Name: "state", Values: ["available"] }],
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
438
|
+
azs = AvailabilityZones.slice(0, 2).map((az) => az.ZoneName);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (azs.length < 2) {
|
|
442
|
+
throw new Error("Need at least 2 availability zones");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Step 1: Subnets
|
|
446
|
+
let privateSubnetIds;
|
|
447
|
+
if (needsSubnets) {
|
|
448
|
+
spinner.ora.text = "Discovering available CIDR blocks";
|
|
449
|
+
const { Subnets: allSubnets } = await client.send(
|
|
450
|
+
new DescribeSubnetsCommand({
|
|
451
|
+
Filters: [{ Name: "vpc-id", Values: [vpcId] }],
|
|
452
|
+
}),
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const usedBlocks = allSubnets.map((s) => {
|
|
456
|
+
const [ip, prefix] = s.CidrBlock.split("/");
|
|
457
|
+
const octets = ip.split(".").map(Number);
|
|
458
|
+
const start =
|
|
459
|
+
(octets[0] << 24) |
|
|
460
|
+
(octets[1] << 16) |
|
|
461
|
+
(octets[2] << 8) |
|
|
462
|
+
octets[3];
|
|
463
|
+
const size = 1 << (32 - Number(prefix));
|
|
464
|
+
return { start, end: start + size };
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const vpcOctets = vpc.CidrBlock.split("/")[0]
|
|
468
|
+
.split(".")
|
|
469
|
+
.map(Number);
|
|
470
|
+
const vpcStart =
|
|
471
|
+
(vpcOctets[0] << 24) |
|
|
472
|
+
(vpcOctets[1] << 16) |
|
|
473
|
+
(vpcOctets[2] << 8) |
|
|
474
|
+
vpcOctets[3];
|
|
475
|
+
const vpcSize = 1 << (32 - Number(vpc.CidrBlock.split("/")[1]));
|
|
476
|
+
const vpcEnd = vpcStart + vpcSize;
|
|
477
|
+
const blockSize = 4096; // /20
|
|
478
|
+
|
|
479
|
+
const availableBlocks = [];
|
|
480
|
+
for (
|
|
481
|
+
let candidate = vpcStart;
|
|
482
|
+
candidate + blockSize <= vpcEnd && availableBlocks.length < 2;
|
|
483
|
+
candidate += blockSize
|
|
484
|
+
) {
|
|
485
|
+
const candidateEnd = candidate + blockSize;
|
|
486
|
+
const overlaps = usedBlocks.some(
|
|
487
|
+
(b) => candidate < b.end && candidateEnd > b.start,
|
|
488
|
+
);
|
|
489
|
+
if (!overlaps) {
|
|
490
|
+
const o1 = (candidate >>> 24) & 0xff;
|
|
491
|
+
const o2 = (candidate >>> 16) & 0xff;
|
|
492
|
+
const o3 = (candidate >>> 8) & 0xff;
|
|
493
|
+
const o4 = candidate & 0xff;
|
|
494
|
+
availableBlocks.push(`${o1}.${o2}.${o3}.${o4}/20`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (availableBlocks.length < 2) {
|
|
499
|
+
throw new Error(
|
|
500
|
+
"Could not find 2 available /20 CIDR blocks in the default VPC",
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
privateSubnetIds = [];
|
|
505
|
+
for (let i = 0; i < 2; i++) {
|
|
506
|
+
spinner.ora.text = `Creating private subnet in ${azs[i]}`;
|
|
507
|
+
const subnet = await client.send(
|
|
508
|
+
new CreateSubnetCommand({
|
|
509
|
+
VpcId: vpcId,
|
|
510
|
+
CidrBlock: availableBlocks[i],
|
|
511
|
+
AvailabilityZone: azs[i],
|
|
512
|
+
TagSpecifications: [
|
|
513
|
+
{
|
|
514
|
+
ResourceType: "subnet",
|
|
515
|
+
Tags: [
|
|
516
|
+
{
|
|
517
|
+
Key: "Name",
|
|
518
|
+
Value: `fw-auto-private-subnet-${azs[i]}`,
|
|
519
|
+
},
|
|
520
|
+
{ Key: "private", Value: "true" },
|
|
521
|
+
],
|
|
522
|
+
},
|
|
523
|
+
],
|
|
524
|
+
}),
|
|
525
|
+
);
|
|
526
|
+
privateSubnetIds.push(subnet.Subnet.SubnetId);
|
|
527
|
+
}
|
|
528
|
+
} else {
|
|
529
|
+
privateSubnetIds = existingSubnets.map((s) => s.SubnetId);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Step 2: NAT gateways (+ Elastic IPs)
|
|
533
|
+
let natGatewayIds;
|
|
534
|
+
if (needsNats) {
|
|
535
|
+
// Find public subnets per AZ
|
|
536
|
+
spinner.ora.text = "Identifying public subnets";
|
|
537
|
+
const { InternetGateways } = await client.send(
|
|
538
|
+
new DescribeInternetGatewaysCommand({
|
|
539
|
+
Filters: [{ Name: "attachment.vpc-id", Values: [vpcId] }],
|
|
540
|
+
}),
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
if (!InternetGateways.length) {
|
|
544
|
+
throw new Error("No Internet Gateway found on default VPC");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const { Subnets: vpcSubnets } = await client.send(
|
|
548
|
+
new DescribeSubnetsCommand({
|
|
549
|
+
Filters: [{ Name: "vpc-id", Values: [vpcId] }],
|
|
550
|
+
}),
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
const { RouteTables } = await client.send(
|
|
554
|
+
new DescribeRouteTablesCommand({
|
|
555
|
+
Filters: [{ Name: "vpc-id", Values: [vpcId] }],
|
|
556
|
+
}),
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
const publicSubnetsByAz = {};
|
|
560
|
+
for (const subnet of vpcSubnets) {
|
|
561
|
+
if (privateSubnetIds.includes(subnet.SubnetId)) continue;
|
|
562
|
+
|
|
563
|
+
let rt = RouteTables.find((r) =>
|
|
564
|
+
r.Associations.some((a) => a.SubnetId === subnet.SubnetId),
|
|
565
|
+
);
|
|
566
|
+
if (!rt) {
|
|
567
|
+
rt = RouteTables.find((r) =>
|
|
568
|
+
r.Associations.some((a) => a.Main),
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
if (!rt) continue;
|
|
572
|
+
|
|
573
|
+
const hasIgwRoute = rt.Routes.some(
|
|
574
|
+
(route) =>
|
|
575
|
+
route.GatewayId &&
|
|
576
|
+
route.GatewayId.startsWith("igw-") &&
|
|
577
|
+
route.DestinationCidrBlock === "0.0.0.0/0",
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
if (
|
|
581
|
+
hasIgwRoute &&
|
|
582
|
+
!publicSubnetsByAz[subnet.AvailabilityZone]
|
|
583
|
+
) {
|
|
584
|
+
publicSubnetsByAz[subnet.AvailabilityZone] =
|
|
585
|
+
subnet.SubnetId;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
for (const az of azs) {
|
|
590
|
+
if (!publicSubnetsByAz[az]) {
|
|
591
|
+
throw new Error(
|
|
592
|
+
`No public subnet found in ${az} to host NAT gateway`,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
natGatewayIds = [];
|
|
598
|
+
for (let i = 0; i < 2; i++) {
|
|
599
|
+
spinner.ora.text = `Allocating Elastic IP for NAT gateway in ${azs[i]}`;
|
|
600
|
+
const eip = await client.send(
|
|
601
|
+
new AllocateAddressCommand({
|
|
602
|
+
Domain: "vpc",
|
|
603
|
+
TagSpecifications: [
|
|
604
|
+
{
|
|
605
|
+
ResourceType: "elastic-ip",
|
|
606
|
+
Tags: [
|
|
607
|
+
{
|
|
608
|
+
Key: "Name",
|
|
609
|
+
Value: `fw-auto-nat-eip-${azs[i]}`,
|
|
610
|
+
},
|
|
611
|
+
{ Key: "private", Value: "true" },
|
|
612
|
+
],
|
|
613
|
+
},
|
|
614
|
+
],
|
|
615
|
+
}),
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
spinner.ora.text = `Creating NAT gateway in ${azs[i]}`;
|
|
619
|
+
const nat = await client.send(
|
|
620
|
+
new CreateNatGatewayCommand({
|
|
621
|
+
SubnetId: publicSubnetsByAz[azs[i]],
|
|
622
|
+
AllocationId: eip.AllocationId,
|
|
623
|
+
TagSpecifications: [
|
|
624
|
+
{
|
|
625
|
+
ResourceType: "natgateway",
|
|
626
|
+
Tags: [
|
|
627
|
+
{
|
|
628
|
+
Key: "Name",
|
|
629
|
+
Value: `fw-auto-nat-gateway-${azs[i]}`,
|
|
630
|
+
},
|
|
631
|
+
{ Key: "private", Value: "true" },
|
|
632
|
+
],
|
|
633
|
+
},
|
|
634
|
+
],
|
|
635
|
+
}),
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
natGatewayIds.push(nat.NatGateway.NatGatewayId);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
for (let i = 0; i < 2; i++) {
|
|
642
|
+
spinner.ora.text = `Waiting for NAT gateway in ${azs[i]} to become available`;
|
|
643
|
+
await poll(
|
|
644
|
+
async () => {
|
|
645
|
+
const res = await client.send(
|
|
646
|
+
new DescribeNatGatewaysCommand({
|
|
647
|
+
NatGatewayIds: [natGatewayIds[i]],
|
|
648
|
+
}),
|
|
649
|
+
);
|
|
650
|
+
return res.NatGateways[0];
|
|
651
|
+
},
|
|
652
|
+
(nat) => nat.State !== "available",
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
} else {
|
|
656
|
+
natGatewayIds = existingNats.map((n) => n.NatGatewayId);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Step 3: Route tables
|
|
660
|
+
if (needsRtbs) {
|
|
661
|
+
for (let i = 0; i < 2; i++) {
|
|
662
|
+
spinner.ora.text = `Creating route table for ${azs[i]}`;
|
|
663
|
+
const rt = await client.send(
|
|
664
|
+
new CreateRouteTableCommand({
|
|
665
|
+
VpcId: vpcId,
|
|
666
|
+
TagSpecifications: [
|
|
667
|
+
{
|
|
668
|
+
ResourceType: "route-table",
|
|
669
|
+
Tags: [
|
|
670
|
+
{
|
|
671
|
+
Key: "Name",
|
|
672
|
+
Value: `fw-auto-private-rtb-${azs[i]}`,
|
|
673
|
+
},
|
|
674
|
+
{ Key: "private", Value: "true" },
|
|
675
|
+
],
|
|
676
|
+
},
|
|
677
|
+
],
|
|
678
|
+
}),
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
spinner.ora.text = `Adding NAT gateway route for ${azs[i]}`;
|
|
682
|
+
await client.send(
|
|
683
|
+
new CreateRouteCommand({
|
|
684
|
+
RouteTableId: rt.RouteTable.RouteTableId,
|
|
685
|
+
DestinationCidrBlock: "0.0.0.0/0",
|
|
686
|
+
NatGatewayId: natGatewayIds[i],
|
|
687
|
+
}),
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
spinner.ora.text = `Associating route table with private subnet in ${azs[i]}`;
|
|
691
|
+
await client.send(
|
|
692
|
+
new AssociateRouteTableCommand({
|
|
693
|
+
RouteTableId: rt.RouteTable.RouteTableId,
|
|
694
|
+
SubnetId: privateSubnetIds[i],
|
|
695
|
+
}),
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
spinner.ora.succeed("Private subnets configured");
|
|
701
|
+
|
|
702
|
+
const publicSubnetIds = await discoverPublicSubnets(
|
|
703
|
+
client,
|
|
704
|
+
vpcId,
|
|
705
|
+
privateSubnetIds,
|
|
706
|
+
azs,
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
return { privateSubnetIds, publicSubnetIds, vpcId };
|
|
710
|
+
} catch (e) {
|
|
711
|
+
spinner.ora.fail("Failed to set up private subnets");
|
|
712
|
+
throw e;
|
|
713
|
+
}
|
|
714
|
+
};
|
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
UpdateEnvironmentCommand,
|
|
15
15
|
CreateApplicationVersionCommand,
|
|
16
16
|
ListAvailableSolutionStacksCommand,
|
|
17
|
+
RestartAppServerCommand,
|
|
17
18
|
} = require("@aws-sdk/client-elastic-beanstalk");
|
|
18
19
|
const { Spinner, poll } = require("../../libs/utilities");
|
|
19
20
|
const { eb } = require("../../libs/vars");
|
|
@@ -284,3 +285,18 @@ module.exports.updateEnvironmentSettings = async (
|
|
|
284
285
|
},
|
|
285
286
|
);
|
|
286
287
|
};
|
|
288
|
+
|
|
289
|
+
module.exports.restartAppServer = async (environmentName) => {
|
|
290
|
+
const client = new ElasticBeanstalkClient({});
|
|
291
|
+
|
|
292
|
+
await Spinner.prototype.simple(
|
|
293
|
+
`Restarting app server for ${environmentName}`,
|
|
294
|
+
() => {
|
|
295
|
+
return client.send(
|
|
296
|
+
new RestartAppServerCommand({
|
|
297
|
+
EnvironmentName: environmentName,
|
|
298
|
+
}),
|
|
299
|
+
);
|
|
300
|
+
},
|
|
301
|
+
);
|
|
302
|
+
};
|