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