@friggframework/devtools 2.0.0--canary.461.8cf93ae.0 → 2.0.0--canary.474.213c7d9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/infrastructure/ARCHITECTURE.md +487 -0
- package/infrastructure/domains/database/aurora-builder.js +234 -57
- package/infrastructure/domains/database/aurora-builder.test.js +7 -2
- package/infrastructure/domains/database/aurora-resolver.js +210 -0
- package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
- package/infrastructure/domains/database/migration-builder.js +256 -215
- package/infrastructure/domains/database/migration-builder.test.js +5 -111
- package/infrastructure/domains/database/migration-resolver.js +163 -0
- package/infrastructure/domains/database/migration-resolver.test.js +337 -0
- package/infrastructure/domains/integration/integration-builder.js +258 -84
- package/infrastructure/domains/integration/integration-resolver.js +170 -0
- package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
- package/infrastructure/domains/networking/vpc-builder.js +856 -135
- package/infrastructure/domains/networking/vpc-builder.test.js +10 -6
- package/infrastructure/domains/networking/vpc-resolver.js +324 -0
- package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
- package/infrastructure/domains/security/kms-builder.js +179 -22
- package/infrastructure/domains/security/kms-resolver.js +96 -0
- package/infrastructure/domains/security/kms-resolver.test.js +216 -0
- package/infrastructure/domains/shared/base-resolver.js +186 -0
- package/infrastructure/domains/shared/base-resolver.test.js +305 -0
- package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +26 -1
- package/infrastructure/domains/shared/types/app-definition.js +205 -0
- package/infrastructure/domains/shared/types/discovery-result.js +106 -0
- package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
- package/infrastructure/domains/shared/types/index.js +46 -0
- package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
- package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
- package/package.json +6 -6
- package/infrastructure/REFACTOR.md +0 -532
- package/infrastructure/TRANSFORMATION-VISUAL.md +0 -239
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
const { InfrastructureBuilder, ValidationResult } = require('../shared/base-builder');
|
|
22
|
+
const VpcResourceResolver = require('./vpc-resolver');
|
|
23
|
+
const { createEmptyDiscoveryResult } = require('../shared/types/discovery-result');
|
|
24
|
+
const { ResourceOwnership } = require('../shared/types/resource-ownership');
|
|
22
25
|
|
|
23
26
|
class VpcBuilder extends InfrastructureBuilder {
|
|
24
27
|
constructor() {
|
|
@@ -97,11 +100,362 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
/**
|
|
100
|
-
*
|
|
103
|
+
* Convert flat discovery result to structured discovery result
|
|
104
|
+
* Provides backwards compatibility for tests using old discovery format
|
|
105
|
+
*
|
|
106
|
+
* @param {Object} flatDiscovery - Flat discovery object
|
|
107
|
+
* @param {Object} appDefinition - App definition (used to detect stack-managed resources)
|
|
108
|
+
*/
|
|
109
|
+
convertFlatDiscoveryToStructured(flatDiscovery, appDefinition = {}) {
|
|
110
|
+
const discovery = createEmptyDiscoveryResult();
|
|
111
|
+
|
|
112
|
+
if (!flatDiscovery) {
|
|
113
|
+
return discovery;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Special case: managementMode='managed' + vpcIsolation='isolated' with existing resources
|
|
117
|
+
// These resources are from a previous deployment of this stack, so they're stack-managed
|
|
118
|
+
const isManagedIsolated = appDefinition.managementMode === 'managed' &&
|
|
119
|
+
(appDefinition.vpcIsolation === 'isolated' || !appDefinition.vpcIsolation);
|
|
120
|
+
const hasExistingStackResources = isManagedIsolated && flatDiscovery.defaultVpcId &&
|
|
121
|
+
typeof flatDiscovery.defaultVpcId === 'string';
|
|
122
|
+
|
|
123
|
+
// Check if this came from CloudFormation stack
|
|
124
|
+
if (flatDiscovery.fromCloudFormationStack || hasExistingStackResources) {
|
|
125
|
+
discovery.fromCloudFormation = true;
|
|
126
|
+
discovery.stackName = flatDiscovery.stackName || 'assumed-stack';
|
|
127
|
+
|
|
128
|
+
// Add resources to stackManaged array
|
|
129
|
+
let existingLogicalIds = flatDiscovery.existingLogicalIds || [];
|
|
130
|
+
|
|
131
|
+
// If hasExistingStackResources but no existingLogicalIds provided,
|
|
132
|
+
// infer logical IDs from presence of physical IDs
|
|
133
|
+
if (hasExistingStackResources && existingLogicalIds.length === 0) {
|
|
134
|
+
existingLogicalIds = [];
|
|
135
|
+
if (flatDiscovery.defaultVpcId) existingLogicalIds.push('FriggVPC');
|
|
136
|
+
if (flatDiscovery.privateSubnetId1) existingLogicalIds.push('FriggPrivateSubnet1');
|
|
137
|
+
if (flatDiscovery.privateSubnetId2) existingLogicalIds.push('FriggPrivateSubnet2');
|
|
138
|
+
if (flatDiscovery.publicSubnetId1) existingLogicalIds.push('FriggPublicSubnet');
|
|
139
|
+
if (flatDiscovery.publicSubnetId2) existingLogicalIds.push('FriggPublicSubnet2');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
existingLogicalIds.forEach(logicalId => {
|
|
143
|
+
// Find the resource type and physical ID
|
|
144
|
+
let resourceType = '';
|
|
145
|
+
let physicalId = '';
|
|
146
|
+
|
|
147
|
+
if (logicalId === 'FriggVPC') {
|
|
148
|
+
resourceType = 'AWS::EC2::VPC';
|
|
149
|
+
physicalId = flatDiscovery.defaultVpcId;
|
|
150
|
+
} else if (logicalId === 'FriggLambdaSecurityGroup') {
|
|
151
|
+
resourceType = 'AWS::EC2::SecurityGroup';
|
|
152
|
+
physicalId = flatDiscovery.defaultSecurityGroupId || flatDiscovery.securityGroupId;
|
|
153
|
+
} else if (logicalId === 'FriggPrivateSubnet1') {
|
|
154
|
+
resourceType = 'AWS::EC2::Subnet';
|
|
155
|
+
physicalId = flatDiscovery.privateSubnetId1;
|
|
156
|
+
} else if (logicalId === 'FriggPrivateSubnet2') {
|
|
157
|
+
resourceType = 'AWS::EC2::Subnet';
|
|
158
|
+
physicalId = flatDiscovery.privateSubnetId2;
|
|
159
|
+
} else if (logicalId === 'FriggNATGateway') {
|
|
160
|
+
resourceType = 'AWS::EC2::NatGateway';
|
|
161
|
+
physicalId = flatDiscovery.existingNatGatewayId;
|
|
162
|
+
} else if (logicalId === 'FriggS3VPCEndpoint') {
|
|
163
|
+
resourceType = 'AWS::EC2::VPCEndpoint';
|
|
164
|
+
physicalId = flatDiscovery.s3VpcEndpointId;
|
|
165
|
+
} else if (logicalId === 'FriggDynamoDBVPCEndpoint') {
|
|
166
|
+
resourceType = 'AWS::EC2::VPCEndpoint';
|
|
167
|
+
physicalId = flatDiscovery.dynamodbVpcEndpointId;
|
|
168
|
+
} else if (logicalId === 'FriggKMSVPCEndpoint') {
|
|
169
|
+
resourceType = 'AWS::EC2::VPCEndpoint';
|
|
170
|
+
physicalId = flatDiscovery.kmsVpcEndpointId;
|
|
171
|
+
} else if (logicalId === 'FriggSecretsManagerVPCEndpoint') {
|
|
172
|
+
resourceType = 'AWS::EC2::VPCEndpoint';
|
|
173
|
+
physicalId = flatDiscovery.secretsManagerVpcEndpointId;
|
|
174
|
+
} else if (logicalId === 'FriggSQSVPCEndpoint') {
|
|
175
|
+
resourceType = 'AWS::EC2::VPCEndpoint';
|
|
176
|
+
physicalId = flatDiscovery.sqsVpcEndpointId;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (physicalId && typeof physicalId === 'string') {
|
|
180
|
+
discovery.stackManaged.push({
|
|
181
|
+
logicalId,
|
|
182
|
+
physicalId,
|
|
183
|
+
resourceType
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
} else {
|
|
188
|
+
// Resources discovered from AWS API (not CloudFormation)
|
|
189
|
+
// These go into external array
|
|
190
|
+
|
|
191
|
+
if (flatDiscovery.defaultVpcId && typeof flatDiscovery.defaultVpcId === 'string') {
|
|
192
|
+
discovery.external.push({
|
|
193
|
+
physicalId: flatDiscovery.defaultVpcId,
|
|
194
|
+
resourceType: 'AWS::EC2::VPC',
|
|
195
|
+
source: 'aws-discovery'
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (flatDiscovery.defaultSecurityGroupId && typeof flatDiscovery.defaultSecurityGroupId === 'string') {
|
|
200
|
+
discovery.external.push({
|
|
201
|
+
physicalId: flatDiscovery.defaultSecurityGroupId,
|
|
202
|
+
resourceType: 'AWS::EC2::SecurityGroup',
|
|
203
|
+
source: 'aws-discovery'
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (flatDiscovery.privateSubnetId1 && typeof flatDiscovery.privateSubnetId1 === 'string') {
|
|
208
|
+
discovery.external.push({
|
|
209
|
+
physicalId: flatDiscovery.privateSubnetId1,
|
|
210
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
211
|
+
source: 'aws-discovery'
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (flatDiscovery.privateSubnetId2 && typeof flatDiscovery.privateSubnetId2 === 'string') {
|
|
216
|
+
discovery.external.push({
|
|
217
|
+
physicalId: flatDiscovery.privateSubnetId2,
|
|
218
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
219
|
+
source: 'aws-discovery'
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Only add NAT Gateway to external if it's NOT in a private subnet (properly placed)
|
|
224
|
+
// If natGatewayInPrivateSubnet is true, we need a new NAT Gateway
|
|
225
|
+
const natIsProperlyPlaced = flatDiscovery.natGatewayInPrivateSubnet !== true;
|
|
226
|
+
|
|
227
|
+
if (flatDiscovery.natGatewayId && typeof flatDiscovery.natGatewayId === 'string' && natIsProperlyPlaced) {
|
|
228
|
+
discovery.external.push({
|
|
229
|
+
physicalId: flatDiscovery.natGatewayId,
|
|
230
|
+
resourceType: 'AWS::EC2::NatGateway',
|
|
231
|
+
source: 'aws-discovery'
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (flatDiscovery.existingNatGatewayId && typeof flatDiscovery.existingNatGatewayId === 'string' && natIsProperlyPlaced) {
|
|
236
|
+
discovery.external.push({
|
|
237
|
+
physicalId: flatDiscovery.existingNatGatewayId,
|
|
238
|
+
resourceType: 'AWS::EC2::NatGateway',
|
|
239
|
+
source: 'aws-discovery'
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// VPC Endpoints
|
|
244
|
+
if (flatDiscovery.s3VpcEndpointId && typeof flatDiscovery.s3VpcEndpointId === 'string') {
|
|
245
|
+
discovery.external.push({
|
|
246
|
+
physicalId: flatDiscovery.s3VpcEndpointId,
|
|
247
|
+
resourceType: 'AWS::EC2::VPCEndpoint',
|
|
248
|
+
source: 'aws-discovery',
|
|
249
|
+
properties: { ServiceName: 's3' }
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (flatDiscovery.dynamodbVpcEndpointId && typeof flatDiscovery.dynamodbVpcEndpointId === 'string') {
|
|
254
|
+
discovery.external.push({
|
|
255
|
+
physicalId: flatDiscovery.dynamodbVpcEndpointId,
|
|
256
|
+
resourceType: 'AWS::EC2::VPCEndpoint',
|
|
257
|
+
source: 'aws-discovery',
|
|
258
|
+
properties: { ServiceName: 'dynamodb' }
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (flatDiscovery.kmsVpcEndpointId && typeof flatDiscovery.kmsVpcEndpointId === 'string') {
|
|
263
|
+
discovery.external.push({
|
|
264
|
+
physicalId: flatDiscovery.kmsVpcEndpointId,
|
|
265
|
+
resourceType: 'AWS::EC2::VPCEndpoint',
|
|
266
|
+
source: 'aws-discovery',
|
|
267
|
+
properties: { ServiceName: 'kms' }
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (flatDiscovery.secretsManagerVpcEndpointId && typeof flatDiscovery.secretsManagerVpcEndpointId === 'string') {
|
|
272
|
+
discovery.external.push({
|
|
273
|
+
physicalId: flatDiscovery.secretsManagerVpcEndpointId,
|
|
274
|
+
resourceType: 'AWS::EC2::VPCEndpoint',
|
|
275
|
+
source: 'aws-discovery',
|
|
276
|
+
properties: { ServiceName: 'secretsmanager' }
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (flatDiscovery.sqsVpcEndpointId && typeof flatDiscovery.sqsVpcEndpointId === 'string') {
|
|
281
|
+
discovery.external.push({
|
|
282
|
+
physicalId: flatDiscovery.sqsVpcEndpointId,
|
|
283
|
+
resourceType: 'AWS::EC2::VPCEndpoint',
|
|
284
|
+
source: 'aws-discovery',
|
|
285
|
+
properties: { ServiceName: 'sqs' }
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return discovery;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Translate legacy configuration (management modes) to new ownership-based configuration
|
|
295
|
+
* Provides backwards compatibility for existing app definitions
|
|
296
|
+
*/
|
|
297
|
+
translateLegacyConfig(appDefinition, discoveredResources) {
|
|
298
|
+
// If already using new ownership schema, return as-is
|
|
299
|
+
if (appDefinition.vpc?.ownership) {
|
|
300
|
+
return appDefinition;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Clone to avoid mutating original
|
|
304
|
+
const translated = JSON.parse(JSON.stringify(appDefinition));
|
|
305
|
+
|
|
306
|
+
// Initialize ownership and external sections
|
|
307
|
+
if (!translated.vpc.ownership) {
|
|
308
|
+
translated.vpc.ownership = {};
|
|
309
|
+
}
|
|
310
|
+
if (!translated.vpc.external) {
|
|
311
|
+
translated.vpc.external = {};
|
|
312
|
+
}
|
|
313
|
+
if (!translated.vpc.config) {
|
|
314
|
+
translated.vpc.config = {};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Handle top-level managementMode
|
|
318
|
+
const globalMode = appDefinition.managementMode || 'discover';
|
|
319
|
+
const vpcIsolation = appDefinition.vpcIsolation || 'shared';
|
|
320
|
+
|
|
321
|
+
if (globalMode === 'managed') {
|
|
322
|
+
this.warnIgnoredOptions(appDefinition);
|
|
323
|
+
|
|
324
|
+
if (vpcIsolation === 'isolated') {
|
|
325
|
+
// Check if CloudFormation stack already has resources
|
|
326
|
+
const hasStackVpc = discoveredResources?.defaultVpcId && typeof discoveredResources.defaultVpcId === 'string';
|
|
327
|
+
|
|
328
|
+
if (hasStackVpc) {
|
|
329
|
+
// Stack has VPC - reuse it
|
|
330
|
+
translated.vpc.ownership.vpc = 'auto';
|
|
331
|
+
translated.vpc.ownership.securityGroup = 'auto';
|
|
332
|
+
translated.vpc.ownership.subnets = 'auto';
|
|
333
|
+
translated.vpc.config.selfHeal = true;
|
|
334
|
+
console.log(` managementMode='managed' + vpcIsolation='isolated' → stack has VPC, reusing`);
|
|
335
|
+
} else {
|
|
336
|
+
// No stack VPC - create new
|
|
337
|
+
translated.vpc.ownership.vpc = 'stack';
|
|
338
|
+
translated.vpc.ownership.securityGroup = 'stack';
|
|
339
|
+
translated.vpc.ownership.subnets = 'stack';
|
|
340
|
+
translated.vpc.ownership.natGateway = 'stack';
|
|
341
|
+
translated.vpc.config.natGateway = { enable: true };
|
|
342
|
+
console.log(` managementMode='managed' + vpcIsolation='isolated' → no stack VPC, creating new`);
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
// Shared VPC
|
|
346
|
+
translated.vpc.ownership.vpc = 'auto';
|
|
347
|
+
translated.vpc.ownership.securityGroup = 'auto';
|
|
348
|
+
translated.vpc.ownership.subnets = 'auto';
|
|
349
|
+
translated.vpc.config.selfHeal = true;
|
|
350
|
+
}
|
|
351
|
+
} else if (globalMode === 'existing') {
|
|
352
|
+
translated.vpc.ownership.vpc = 'external';
|
|
353
|
+
translated.vpc.ownership.securityGroup = 'external';
|
|
354
|
+
translated.vpc.ownership.subnets = 'external';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Handle legacy vpc.management modes
|
|
358
|
+
const vpcManagement = appDefinition.vpc?.management;
|
|
359
|
+
if (vpcManagement === 'create-new') {
|
|
360
|
+
translated.vpc.ownership.vpc = 'stack';
|
|
361
|
+
translated.vpc.ownership.securityGroup = 'stack';
|
|
362
|
+
translated.vpc.ownership.subnets = 'stack';
|
|
363
|
+
} else if (vpcManagement === 'use-existing') {
|
|
364
|
+
translated.vpc.ownership.vpc = 'external';
|
|
365
|
+
translated.vpc.external.vpcId = appDefinition.vpc.vpcId;
|
|
366
|
+
|
|
367
|
+
if (appDefinition.vpc.securityGroupIds) {
|
|
368
|
+
translated.vpc.ownership.securityGroup = 'external';
|
|
369
|
+
translated.vpc.external.securityGroupIds = appDefinition.vpc.securityGroupIds;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (appDefinition.vpc.subnets?.ids) {
|
|
373
|
+
translated.vpc.ownership.subnets = 'external';
|
|
374
|
+
translated.vpc.external.subnetIds = appDefinition.vpc.subnets.ids;
|
|
375
|
+
}
|
|
376
|
+
} else if (vpcManagement === 'discover') {
|
|
377
|
+
// Discover mode - let auto-resolution handle it
|
|
378
|
+
translated.vpc.ownership.vpc = 'auto';
|
|
379
|
+
translated.vpc.ownership.securityGroup = 'auto';
|
|
380
|
+
translated.vpc.ownership.subnets = 'auto';
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Handle legacy shareAcrossStages
|
|
384
|
+
if (appDefinition.vpc?.shareAcrossStages !== undefined) {
|
|
385
|
+
if (appDefinition.vpc.shareAcrossStages) {
|
|
386
|
+
// Shared VPC - discover and reuse
|
|
387
|
+
translated.vpc.ownership.vpc = 'auto';
|
|
388
|
+
translated.vpc.ownership.subnets = 'auto';
|
|
389
|
+
} else {
|
|
390
|
+
// Isolated VPC - create stage-specific
|
|
391
|
+
translated.vpc.ownership.vpc = 'stack';
|
|
392
|
+
translated.vpc.ownership.subnets = 'stack';
|
|
393
|
+
translated.vpc.ownership.natGateway = 'stack';
|
|
394
|
+
translated.vpc.config.natGateway = { enable: true };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Handle legacy NAT Gateway management
|
|
399
|
+
if (appDefinition.vpc?.natGateway?.management === 'createAndManage') {
|
|
400
|
+
// Use 'auto' to allow discovering and reusing properly placed external NAT Gateways
|
|
401
|
+
// The resolver will check if there's a good external NAT Gateway and reuse it,
|
|
402
|
+
// or create a new one if needed (or if the existing one is misplaced)
|
|
403
|
+
translated.vpc.ownership.natGateway = 'auto';
|
|
404
|
+
translated.vpc.config.natGateway = { enable: true };
|
|
405
|
+
} else if (appDefinition.vpc?.natGateway?.id) {
|
|
406
|
+
translated.vpc.ownership.natGateway = 'external';
|
|
407
|
+
translated.vpc.external.natGatewayId = appDefinition.vpc.natGateway.id;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Handle legacy subnet management
|
|
411
|
+
if (appDefinition.vpc?.subnets?.management === 'create') {
|
|
412
|
+
translated.vpc.ownership.subnets = 'stack';
|
|
413
|
+
} else if (appDefinition.vpc?.subnets?.management === 'use-existing' && appDefinition.vpc.subnets.ids) {
|
|
414
|
+
translated.vpc.ownership.subnets = 'external';
|
|
415
|
+
translated.vpc.external.subnetIds = appDefinition.vpc.subnets.ids;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Preserve other VPC config
|
|
419
|
+
if (appDefinition.vpc?.cidrBlock) {
|
|
420
|
+
translated.vpc.config.cidrBlock = appDefinition.vpc.cidrBlock;
|
|
421
|
+
}
|
|
422
|
+
if (appDefinition.vpc?.enableVPCEndpoints !== undefined) {
|
|
423
|
+
translated.vpc.config.enableVpcEndpoints = appDefinition.vpc.enableVPCEndpoints;
|
|
424
|
+
}
|
|
425
|
+
if (appDefinition.vpc?.selfHeal !== undefined) {
|
|
426
|
+
translated.vpc.config.selfHeal = appDefinition.vpc.selfHeal;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return translated;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Build complete VPC infrastructure using ownership-based architecture
|
|
101
434
|
*/
|
|
102
435
|
async build(appDefinition, discoveredResources) {
|
|
103
436
|
console.log(`\n[${this.name}] Building VPC infrastructure...`);
|
|
104
437
|
|
|
438
|
+
// Backwards compatibility: Translate old schema to new ownership schema
|
|
439
|
+
appDefinition = this.translateLegacyConfig(appDefinition, discoveredResources);
|
|
440
|
+
|
|
441
|
+
// Get structured discovery result (or convert flat discovery to structured)
|
|
442
|
+
// Pass appDefinition to help detect stack-managed resources in managementMode='managed'
|
|
443
|
+
const discovery = discoveredResources._structured || this.convertFlatDiscoveryToStructured(discoveredResources, appDefinition);
|
|
444
|
+
|
|
445
|
+
// Use VpcResourceResolver to make ownership decisions
|
|
446
|
+
const resolver = new VpcResourceResolver();
|
|
447
|
+
const decisions = resolver.resolveAll(appDefinition, discovery);
|
|
448
|
+
|
|
449
|
+
console.log('\n 📋 Resource Ownership Decisions:');
|
|
450
|
+
console.log(` VPC: ${decisions.vpc.ownership} - ${decisions.vpc.reason}`);
|
|
451
|
+
console.log(` Security Group: ${decisions.securityGroup.ownership} - ${decisions.securityGroup.reason}`);
|
|
452
|
+
console.log(` Subnets: ${decisions.subnets.ownership} - ${decisions.subnets.reason}`);
|
|
453
|
+
console.log(` NAT Gateway: ${decisions.natGateway.ownership || 'disabled'} - ${decisions.natGateway.reason}`);
|
|
454
|
+
console.log(` VPC Endpoints:`);
|
|
455
|
+
console.log(` S3: ${decisions.vpcEndpoints.s3.ownership || 'disabled'} - ${decisions.vpcEndpoints.s3.reason}`);
|
|
456
|
+
console.log(` DynamoDB: ${decisions.vpcEndpoints.dynamodb.ownership || 'disabled'} - ${decisions.vpcEndpoints.dynamodb.reason}`);
|
|
457
|
+
|
|
458
|
+
// Initialize result
|
|
105
459
|
const result = {
|
|
106
460
|
resources: {},
|
|
107
461
|
vpcConfig: {
|
|
@@ -110,9 +464,42 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
110
464
|
},
|
|
111
465
|
iamStatements: [],
|
|
112
466
|
outputs: {},
|
|
467
|
+
environment: {},
|
|
113
468
|
};
|
|
114
469
|
|
|
115
470
|
// Add IAM permissions for VPC-enabled Lambda functions
|
|
471
|
+
this.addVpcIamPermissions(result);
|
|
472
|
+
|
|
473
|
+
// Build VPC based on ownership decision
|
|
474
|
+
this.buildVpcFromDecision(decisions.vpc, appDefinition, result);
|
|
475
|
+
|
|
476
|
+
// Build Security Group based on ownership decision
|
|
477
|
+
this.buildSecurityGroupFromDecision(decisions.securityGroup, appDefinition, result);
|
|
478
|
+
|
|
479
|
+
// Build Subnets based on ownership decision
|
|
480
|
+
this.buildSubnetsFromDecision(decisions.subnets, appDefinition, discoveredResources, result);
|
|
481
|
+
|
|
482
|
+
// Build NAT Gateway based on ownership decision
|
|
483
|
+
this.buildNatGatewayFromDecision(decisions.natGateway, appDefinition, discoveredResources, result);
|
|
484
|
+
|
|
485
|
+
// Build VPC Endpoints based on ownership decisions
|
|
486
|
+
this.buildVpcEndpointsFromDecisions(decisions.vpcEndpoints, appDefinition, result);
|
|
487
|
+
|
|
488
|
+
// Set VPC_ENABLED environment variable
|
|
489
|
+
result.environment.VPC_ENABLED = 'true';
|
|
490
|
+
|
|
491
|
+
console.log(`\n[${this.name}] ✅ VPC infrastructure built successfully`);
|
|
492
|
+
console.log(` - VPC ID: ${result.vpcId || 'from discovery'}`);
|
|
493
|
+
console.log(` - Subnets: ${result.vpcConfig.subnetIds.length}`);
|
|
494
|
+
console.log(` - Security Groups: ${result.vpcConfig.securityGroupIds.length}`);
|
|
495
|
+
|
|
496
|
+
return result;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Add IAM permissions for VPC-enabled Lambda functions
|
|
501
|
+
*/
|
|
502
|
+
addVpcIamPermissions(result) {
|
|
116
503
|
result.iamStatements.push({
|
|
117
504
|
Effect: 'Allow',
|
|
118
505
|
Action: [
|
|
@@ -124,153 +511,494 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
124
511
|
],
|
|
125
512
|
Resource: '*',
|
|
126
513
|
});
|
|
514
|
+
}
|
|
127
515
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
516
|
+
/**
|
|
517
|
+
* Build VPC based on ownership decision
|
|
518
|
+
*
|
|
519
|
+
* For STACK ownership: ALWAYS add definitions to template.
|
|
520
|
+
* CloudFormation idempotency ensures existing resources aren't recreated.
|
|
521
|
+
*/
|
|
522
|
+
buildVpcFromDecision(decision, appDefinition, result) {
|
|
523
|
+
if (decision.ownership === ResourceOwnership.STACK) {
|
|
524
|
+
// For STACK ownership: ALWAYS create definitions (CloudFormation idempotency)
|
|
525
|
+
if (decision.physicalId) {
|
|
526
|
+
console.log(` → Adding VPC definition to template (existing: ${decision.physicalId})`);
|
|
527
|
+
} else {
|
|
528
|
+
console.log(' → Adding VPC definition to template (new)');
|
|
529
|
+
}
|
|
131
530
|
|
|
132
|
-
|
|
133
|
-
console.log(` 🔍 DEBUG: globalMode = '${globalMode}', vpcIsolation = '${vpcIsolation}'`);
|
|
134
|
-
console.log(` 🔍 DEBUG: discoveredResources.defaultVpcId = ${discoveredResources?.defaultVpcId}`);
|
|
135
|
-
console.log(` 🔍 DEBUG: discoveredResources keys = ${Object.keys(discoveredResources || {}).join(', ')}`);
|
|
531
|
+
const cidrBlock = appDefinition.vpc?.config?.cidrBlock || appDefinition.vpc?.cidrBlock || '10.0.0.0/16';
|
|
136
532
|
|
|
137
|
-
|
|
533
|
+
result.resources.FriggVPC = {
|
|
534
|
+
Type: 'AWS::EC2::VPC',
|
|
535
|
+
Properties: {
|
|
536
|
+
CidrBlock: cidrBlock,
|
|
537
|
+
EnableDnsHostnames: true,
|
|
538
|
+
EnableDnsSupport: true,
|
|
539
|
+
Tags: [
|
|
540
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc' },
|
|
541
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
542
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
543
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
544
|
+
],
|
|
545
|
+
},
|
|
546
|
+
};
|
|
138
547
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
548
|
+
// Internet Gateway
|
|
549
|
+
result.resources.FriggInternetGateway = {
|
|
550
|
+
Type: 'AWS::EC2::InternetGateway',
|
|
551
|
+
Properties: {
|
|
552
|
+
Tags: [
|
|
553
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-igw' },
|
|
554
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
555
|
+
],
|
|
556
|
+
},
|
|
557
|
+
};
|
|
142
558
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
559
|
+
result.resources.FriggVPCGatewayAttachment = {
|
|
560
|
+
Type: 'AWS::EC2::VPCGatewayAttachment',
|
|
561
|
+
Properties: {
|
|
562
|
+
VpcId: { Ref: 'FriggVPC' },
|
|
563
|
+
InternetGatewayId: { Ref: 'FriggInternetGateway' },
|
|
564
|
+
},
|
|
565
|
+
};
|
|
148
566
|
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
567
|
+
// Use Ref for stack-managed VPC
|
|
568
|
+
result.vpcId = { Ref: 'FriggVPC' };
|
|
569
|
+
console.log(' ✅ VPC definition added to template');
|
|
570
|
+
} else if (decision.ownership === ResourceOwnership.EXTERNAL) {
|
|
571
|
+
// Use external VPC ID (no definition in template)
|
|
572
|
+
result.vpcId = decision.physicalId;
|
|
573
|
+
console.log(` ✓ Using external VPC: ${decision.physicalId}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
154
576
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
577
|
+
/**
|
|
578
|
+
* Build Security Group based on ownership decision
|
|
579
|
+
*/
|
|
580
|
+
buildSecurityGroupFromDecision(decision, appDefinition, result) {
|
|
581
|
+
if (decision.ownership === ResourceOwnership.STACK) {
|
|
582
|
+
// Always create security group resource in template
|
|
583
|
+
// CloudFormation handles idempotency if it already exists
|
|
584
|
+
console.log(' → Adding Lambda Security Group to template...');
|
|
158
585
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
586
|
+
result.resources.FriggLambdaSecurityGroup = {
|
|
587
|
+
Type: 'AWS::EC2::SecurityGroup',
|
|
588
|
+
Properties: {
|
|
589
|
+
GroupDescription: 'Security group for Frigg Lambda functions',
|
|
590
|
+
VpcId: result.vpcId,
|
|
591
|
+
SecurityGroupEgress: [
|
|
592
|
+
{ IpProtocol: 'tcp', FromPort: 443, ToPort: 443, CidrIp: '0.0.0.0/0', Description: 'HTTPS outbound' },
|
|
593
|
+
{ IpProtocol: 'tcp', FromPort: 80, ToPort: 80, CidrIp: '0.0.0.0/0', Description: 'HTTP outbound' },
|
|
594
|
+
{ IpProtocol: 'tcp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS TCP' },
|
|
595
|
+
{ IpProtocol: 'udp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS UDP' },
|
|
596
|
+
{ IpProtocol: 'tcp', FromPort: 5432, ToPort: 5432, CidrIp: '0.0.0.0/0', Description: 'PostgreSQL' },
|
|
597
|
+
{ IpProtocol: 'tcp', FromPort: 27017, ToPort: 27017, CidrIp: '0.0.0.0/0', Description: 'MongoDB' },
|
|
598
|
+
],
|
|
599
|
+
Tags: [
|
|
600
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-sg' },
|
|
601
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
602
|
+
],
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
// Use CloudFormation Ref since resource is in template
|
|
607
|
+
result.vpcConfig.securityGroupIds = [{ Ref: 'FriggLambdaSecurityGroup' }];
|
|
608
|
+
console.log(' ✅ Security Group added to template');
|
|
609
|
+
} else if (decision.ownership === ResourceOwnership.EXTERNAL) {
|
|
610
|
+
// Use external security group IDs
|
|
611
|
+
const sgIds = Array.isArray(decision.physicalId) ? decision.physicalId : [decision.physicalId];
|
|
612
|
+
result.vpcConfig.securityGroupIds = sgIds;
|
|
613
|
+
console.log(` ✓ Using external security group(s): ${sgIds.join(', ')}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Build Subnets based on ownership decision
|
|
619
|
+
*/
|
|
620
|
+
buildSubnetsFromDecision(decision, appDefinition, discoveredResources, result) {
|
|
621
|
+
if (decision.ownership === ResourceOwnership.STACK) {
|
|
622
|
+
// Check if no subnets exist and selfHeal is disabled
|
|
623
|
+
if (!decision.physicalIds || decision.physicalIds.length < 2) {
|
|
624
|
+
const selfHeal = appDefinition.vpc?.config?.selfHeal !== false;
|
|
625
|
+
if (!selfHeal) {
|
|
626
|
+
throw new Error(
|
|
627
|
+
'No subnets discovered. Enable vpc.selfHeal, set subnets.management to "create", or provide subnet IDs.'
|
|
628
|
+
);
|
|
170
629
|
}
|
|
171
|
-
} else {
|
|
172
|
-
management = 'discover';
|
|
173
|
-
appDefinition.vpc.selfHeal = true;
|
|
174
|
-
console.log(` managementMode='managed' + vpcIsolation='shared' → discovering VPC`);
|
|
175
630
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (!appDefinition.vpc.shareAcrossStages && !appDefinition.vpc.natGateway?.management) {
|
|
184
|
-
appDefinition.vpc.natGateway = appDefinition.vpc.natGateway || {};
|
|
185
|
-
appDefinition.vpc.natGateway.management = 'createAndManage';
|
|
186
|
-
console.log(` NAT Gateway: creating isolated NAT (shareAcrossStages=false)`);
|
|
631
|
+
|
|
632
|
+
// For STACK ownership: ALWAYS add definitions to template
|
|
633
|
+
// CloudFormation idempotency ensures existing resources won't be recreated
|
|
634
|
+
if (decision.physicalIds && decision.physicalIds.length >= 2) {
|
|
635
|
+
console.log(` → Adding subnet definitions to template (existing: ${decision.physicalIds.join(', ')})`);
|
|
636
|
+
} else {
|
|
637
|
+
console.log(' → Adding subnet definitions to template (new)');
|
|
187
638
|
}
|
|
188
|
-
|
|
189
|
-
|
|
639
|
+
|
|
640
|
+
this.createSubnetsInTemplate(appDefinition, result, discoveredResources);
|
|
641
|
+
|
|
642
|
+
// Use Refs for stack-managed resources
|
|
643
|
+
result.vpcConfig.subnetIds = [
|
|
644
|
+
{ Ref: 'FriggPrivateSubnet1' },
|
|
645
|
+
{ Ref: 'FriggPrivateSubnet2' }
|
|
646
|
+
];
|
|
647
|
+
} else if (decision.ownership === ResourceOwnership.EXTERNAL) {
|
|
648
|
+
// Use external subnet IDs directly (no definitions in template)
|
|
649
|
+
result.vpcConfig.subnetIds = decision.physicalIds;
|
|
650
|
+
console.log(` ✓ Using external subnets: ${decision.physicalIds.join(', ')}`);
|
|
190
651
|
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Create subnet resources in CloudFormation template
|
|
656
|
+
*/
|
|
657
|
+
createSubnetsInTemplate(appDefinition, result, discoveredResources) {
|
|
658
|
+
// Determine VPC ID for subnets
|
|
659
|
+
const vpcId = result.vpcId;
|
|
191
660
|
|
|
192
|
-
|
|
661
|
+
// Generate subnet CIDRs
|
|
662
|
+
const cidrs = this.generateSubnetCidrsForNewVpc(vpcId, discoveredResources);
|
|
193
663
|
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
664
|
+
// Private Subnet 1
|
|
665
|
+
result.resources.FriggPrivateSubnet1 = {
|
|
666
|
+
Type: 'AWS::EC2::Subnet',
|
|
667
|
+
DeletionPolicy: 'Retain',
|
|
668
|
+
Properties: {
|
|
669
|
+
VpcId: vpcId,
|
|
670
|
+
CidrBlock: cidrs.private1,
|
|
671
|
+
AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
|
|
672
|
+
Tags: [
|
|
673
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-1' },
|
|
674
|
+
{ Key: 'Type', Value: 'Private' },
|
|
675
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
676
|
+
],
|
|
677
|
+
},
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
// Private Subnet 2
|
|
681
|
+
result.resources.FriggPrivateSubnet2 = {
|
|
682
|
+
Type: 'AWS::EC2::Subnet',
|
|
683
|
+
DeletionPolicy: 'Retain',
|
|
684
|
+
Properties: {
|
|
685
|
+
VpcId: vpcId,
|
|
686
|
+
CidrBlock: cidrs.private2,
|
|
687
|
+
AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
|
|
688
|
+
Tags: [
|
|
689
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-2' },
|
|
690
|
+
{ Key: 'Type', Value: 'Private' },
|
|
691
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
692
|
+
],
|
|
693
|
+
},
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
// Public Subnets (for NAT Gateway)
|
|
697
|
+
result.resources.FriggPublicSubnet = {
|
|
698
|
+
Type: 'AWS::EC2::Subnet',
|
|
699
|
+
Properties: {
|
|
700
|
+
VpcId: vpcId,
|
|
701
|
+
CidrBlock: cidrs.public1,
|
|
702
|
+
MapPublicIpOnLaunch: true,
|
|
703
|
+
AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
|
|
704
|
+
Tags: [
|
|
705
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-1' },
|
|
706
|
+
{ Key: 'Type', Value: 'Public' },
|
|
707
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
708
|
+
],
|
|
709
|
+
},
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
result.resources.FriggPublicSubnet2 = {
|
|
713
|
+
Type: 'AWS::EC2::Subnet',
|
|
714
|
+
Properties: {
|
|
715
|
+
VpcId: vpcId,
|
|
716
|
+
CidrBlock: cidrs.public2,
|
|
717
|
+
MapPublicIpOnLaunch: true,
|
|
718
|
+
AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
|
|
719
|
+
Tags: [
|
|
720
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-2' },
|
|
721
|
+
{ Key: 'Type', Value: 'Public' },
|
|
722
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
723
|
+
],
|
|
724
|
+
},
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
result.vpcConfig.subnetIds = [
|
|
728
|
+
{ Ref: 'FriggPrivateSubnet1' },
|
|
729
|
+
{ Ref: 'FriggPrivateSubnet2' },
|
|
730
|
+
];
|
|
731
|
+
|
|
732
|
+
// Map to discovered resources for other builders
|
|
733
|
+
discoveredResources.privateSubnetId1 = { Ref: 'FriggPrivateSubnet1' };
|
|
734
|
+
discoveredResources.privateSubnetId2 = { Ref: 'FriggPrivateSubnet2' };
|
|
735
|
+
discoveredResources.publicSubnetId1 = { Ref: 'FriggPublicSubnet' };
|
|
736
|
+
discoveredResources.publicSubnetId2 = { Ref: 'FriggPublicSubnet2' };
|
|
737
|
+
|
|
738
|
+
console.log(' ✅ Subnet resources added to template');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Generate subnet CIDRs for new VPC or existing VPC
|
|
743
|
+
*/
|
|
744
|
+
generateSubnetCidrsForNewVpc(vpcId, discoveredResources) {
|
|
745
|
+
// If VPC is a Ref (new VPC), use Fn::Cidr
|
|
746
|
+
if (typeof vpcId === 'object' && vpcId.Ref === 'FriggVPC') {
|
|
747
|
+
return {
|
|
748
|
+
private1: { 'Fn::Select': [0, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
|
|
749
|
+
private2: { 'Fn::Select': [1, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
|
|
750
|
+
public1: { 'Fn::Select': [2, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
|
|
751
|
+
public2: { 'Fn::Select': [3, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
|
|
752
|
+
};
|
|
197
753
|
}
|
|
198
754
|
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
case 'discover':
|
|
208
|
-
default:
|
|
209
|
-
await this.discoverVpc(appDefinition, discoveredResources, result);
|
|
210
|
-
break;
|
|
755
|
+
// For existing VPC, find available CIDRs
|
|
756
|
+
const existingCidrs = new Set();
|
|
757
|
+
if (discoveredResources?.subnets) {
|
|
758
|
+
for (const subnet of discoveredResources.subnets) {
|
|
759
|
+
if (subnet.CidrBlock) {
|
|
760
|
+
existingCidrs.add(subnet.CidrBlock);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
211
763
|
}
|
|
212
764
|
|
|
213
|
-
|
|
214
|
-
|
|
765
|
+
const findAvailableCidr = (startOctet, endOctet) => {
|
|
766
|
+
for (let octet = startOctet; octet <= endOctet; octet++) {
|
|
767
|
+
const candidate = `172.31.${octet}.0/24`;
|
|
768
|
+
if (!existingCidrs.has(candidate)) {
|
|
769
|
+
existingCidrs.add(candidate);
|
|
770
|
+
return candidate;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return `172.31.${startOctet}.0/24`;
|
|
774
|
+
};
|
|
215
775
|
|
|
216
|
-
|
|
217
|
-
|
|
776
|
+
return {
|
|
777
|
+
private1: findAvailableCidr(240, 249),
|
|
778
|
+
private2: findAvailableCidr(240, 249),
|
|
779
|
+
public1: findAvailableCidr(250, 255),
|
|
780
|
+
public2: findAvailableCidr(250, 255),
|
|
781
|
+
};
|
|
782
|
+
}
|
|
218
783
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
784
|
+
/**
|
|
785
|
+
* Build NAT Gateway based on ownership decision
|
|
786
|
+
*/
|
|
787
|
+
buildNatGatewayFromDecision(decision, appDefinition, discoveredResources, result) {
|
|
788
|
+
if (!decision.ownership) {
|
|
789
|
+
console.log(' ⊝ NAT Gateway disabled');
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (decision.ownership === ResourceOwnership.STACK) {
|
|
794
|
+
if (decision.physicalId) {
|
|
795
|
+
// NAT Gateway exists in stack - CloudFormation will handle it
|
|
796
|
+
console.log(` ✓ NAT Gateway in stack: ${decision.physicalId}`);
|
|
797
|
+
// Still need to ensure route tables are set up
|
|
798
|
+
this.createNatGatewayRouting(appDefinition, discoveredResources, result, { Ref: 'FriggNATGateway' });
|
|
799
|
+
} else {
|
|
800
|
+
// Create new NAT Gateway
|
|
801
|
+
console.log(' → Creating NAT Gateway in template...');
|
|
802
|
+
this.createNatGatewayInTemplate(appDefinition, discoveredResources, result);
|
|
803
|
+
}
|
|
804
|
+
} else if (decision.ownership === ResourceOwnership.EXTERNAL) {
|
|
805
|
+
// Use external NAT Gateway
|
|
806
|
+
console.log(` ✓ Using external NAT Gateway: ${decision.physicalId}`);
|
|
807
|
+
result.natGatewayId = decision.physicalId;
|
|
808
|
+
this.createNatGatewayRouting(appDefinition, discoveredResources, result, decision.physicalId);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Create NAT Gateway resources in CloudFormation template
|
|
814
|
+
*/
|
|
815
|
+
createNatGatewayInTemplate(appDefinition, discoveredResources, result) {
|
|
816
|
+
// Elastic IP for NAT Gateway
|
|
817
|
+
result.resources.FriggNATGatewayEIP = {
|
|
818
|
+
Type: 'AWS::EC2::EIP',
|
|
819
|
+
DeletionPolicy: 'Retain',
|
|
820
|
+
UpdateReplacePolicy: 'Retain',
|
|
821
|
+
Properties: {
|
|
822
|
+
Domain: 'vpc',
|
|
823
|
+
Tags: [
|
|
824
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-eip' },
|
|
825
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
826
|
+
],
|
|
827
|
+
},
|
|
229
828
|
};
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
829
|
+
|
|
830
|
+
// NAT Gateway in public subnet
|
|
831
|
+
result.resources.FriggNATGateway = {
|
|
832
|
+
Type: 'AWS::EC2::NatGateway',
|
|
833
|
+
DeletionPolicy: 'Retain',
|
|
834
|
+
UpdateReplacePolicy: 'Retain',
|
|
835
|
+
Properties: {
|
|
836
|
+
AllocationId: { 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'] },
|
|
837
|
+
SubnetId: discoveredResources.publicSubnetId1 || { Ref: 'FriggPublicSubnet' },
|
|
838
|
+
Tags: [
|
|
839
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat' },
|
|
840
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
841
|
+
],
|
|
842
|
+
},
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
// Create public routing
|
|
846
|
+
this.createPublicRouting(appDefinition, discoveredResources, result);
|
|
847
|
+
|
|
848
|
+
// Create NAT routing
|
|
849
|
+
this.createNatGatewayRouting(appDefinition, discoveredResources, result, { Ref: 'FriggNATGateway' });
|
|
850
|
+
|
|
851
|
+
console.log(' ✅ NAT Gateway resources added to template');
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Build VPC Endpoints based on ownership decisions
|
|
856
|
+
*/
|
|
857
|
+
buildVpcEndpointsFromDecisions(decisions, appDefinition, result) {
|
|
858
|
+
const endpointsToCreate = [];
|
|
859
|
+
const endpointsInStack = [];
|
|
860
|
+
const externalEndpoints = [];
|
|
861
|
+
|
|
862
|
+
// Analyze decisions
|
|
863
|
+
Object.entries(decisions).forEach(([type, decision]) => {
|
|
864
|
+
if (decision.ownership === ResourceOwnership.STACK && !decision.physicalId) {
|
|
865
|
+
endpointsToCreate.push(type);
|
|
866
|
+
} else if (decision.ownership === ResourceOwnership.STACK && decision.physicalId) {
|
|
867
|
+
endpointsInStack.push(type);
|
|
868
|
+
} else if (decision.ownership === ResourceOwnership.EXTERNAL) {
|
|
869
|
+
externalEndpoints.push(type);
|
|
259
870
|
}
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
if (endpointsInStack.length > 0) {
|
|
874
|
+
console.log(` ✓ VPC Endpoints in stack: ${endpointsInStack.join(', ')}`);
|
|
260
875
|
}
|
|
261
876
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
result.environment = {};
|
|
877
|
+
if (externalEndpoints.length > 0) {
|
|
878
|
+
console.log(` ✓ External VPC Endpoints: ${externalEndpoints.join(', ')}`);
|
|
265
879
|
}
|
|
266
|
-
result.environment.VPC_ENABLED = 'true';
|
|
267
880
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
881
|
+
if (endpointsToCreate.length === 0) {
|
|
882
|
+
if (endpointsInStack.length === 0 && externalEndpoints.length === 0) {
|
|
883
|
+
console.log(' ⊝ VPC Endpoints disabled');
|
|
884
|
+
}
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
272
887
|
|
|
273
|
-
|
|
888
|
+
console.log(` → Creating VPC Endpoints: ${endpointsToCreate.join(', ')}...`);
|
|
889
|
+
|
|
890
|
+
const vpcId = result.vpcId;
|
|
891
|
+
|
|
892
|
+
// Create route table if needed
|
|
893
|
+
if (!result.resources.FriggLambdaRouteTable) {
|
|
894
|
+
result.resources.FriggLambdaRouteTable = {
|
|
895
|
+
Type: 'AWS::EC2::RouteTable',
|
|
896
|
+
Properties: {
|
|
897
|
+
VpcId: vpcId,
|
|
898
|
+
Tags: [
|
|
899
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
|
|
900
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
901
|
+
],
|
|
902
|
+
},
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Ensure subnet associations
|
|
907
|
+
this.ensureSubnetAssociations(appDefinition, {}, result);
|
|
908
|
+
|
|
909
|
+
// Create endpoints
|
|
910
|
+
if (endpointsToCreate.includes('s3')) {
|
|
911
|
+
result.resources.FriggS3VPCEndpoint = {
|
|
912
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
913
|
+
Properties: {
|
|
914
|
+
VpcId: vpcId,
|
|
915
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.s3',
|
|
916
|
+
VpcEndpointType: 'Gateway',
|
|
917
|
+
RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }],
|
|
918
|
+
},
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (endpointsToCreate.includes('dynamodb')) {
|
|
923
|
+
result.resources.FriggDynamoDBVPCEndpoint = {
|
|
924
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
925
|
+
Properties: {
|
|
926
|
+
VpcId: vpcId,
|
|
927
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.dynamodb',
|
|
928
|
+
VpcEndpointType: 'Gateway',
|
|
929
|
+
RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }],
|
|
930
|
+
},
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Create security group for interface endpoints if needed
|
|
935
|
+
const needsInterfaceEndpoints = endpointsToCreate.some(type => ['kms', 'secretsManager', 'sqs'].includes(type));
|
|
936
|
+
if (needsInterfaceEndpoints) {
|
|
937
|
+
result.resources.FriggVPCEndpointSecurityGroup = {
|
|
938
|
+
Type: 'AWS::EC2::SecurityGroup',
|
|
939
|
+
Properties: {
|
|
940
|
+
GroupDescription: 'Security group for VPC Endpoints',
|
|
941
|
+
VpcId: vpcId,
|
|
942
|
+
SecurityGroupIngress: [
|
|
943
|
+
{
|
|
944
|
+
IpProtocol: 'tcp',
|
|
945
|
+
FromPort: 443,
|
|
946
|
+
ToPort: 443,
|
|
947
|
+
SourceSecurityGroupId: { Ref: 'FriggLambdaSecurityGroup' },
|
|
948
|
+
Description: 'HTTPS from Lambda',
|
|
949
|
+
},
|
|
950
|
+
],
|
|
951
|
+
Tags: [
|
|
952
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg' },
|
|
953
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
954
|
+
],
|
|
955
|
+
},
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (endpointsToCreate.includes('kms')) {
|
|
960
|
+
result.resources.FriggKMSVPCEndpoint = {
|
|
961
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
962
|
+
Properties: {
|
|
963
|
+
VpcId: vpcId,
|
|
964
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.kms',
|
|
965
|
+
VpcEndpointType: 'Interface',
|
|
966
|
+
SubnetIds: result.vpcConfig.subnetIds,
|
|
967
|
+
SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
|
|
968
|
+
PrivateDnsEnabled: true,
|
|
969
|
+
},
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (endpointsToCreate.includes('secretsManager')) {
|
|
974
|
+
result.resources.FriggSecretsManagerVPCEndpoint = {
|
|
975
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
976
|
+
Properties: {
|
|
977
|
+
VpcId: vpcId,
|
|
978
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.secretsmanager',
|
|
979
|
+
VpcEndpointType: 'Interface',
|
|
980
|
+
SubnetIds: result.vpcConfig.subnetIds,
|
|
981
|
+
SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
|
|
982
|
+
PrivateDnsEnabled: true,
|
|
983
|
+
},
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (endpointsToCreate.includes('sqs')) {
|
|
988
|
+
result.resources.FriggSQSVPCEndpoint = {
|
|
989
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
990
|
+
Properties: {
|
|
991
|
+
VpcId: vpcId,
|
|
992
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.sqs',
|
|
993
|
+
VpcEndpointType: 'Interface',
|
|
994
|
+
SubnetIds: result.vpcConfig.subnetIds,
|
|
995
|
+
SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
|
|
996
|
+
PrivateDnsEnabled: true,
|
|
997
|
+
},
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
console.log(` ✅ VPC Endpoint resources added to template`);
|
|
274
1002
|
}
|
|
275
1003
|
|
|
276
1004
|
/**
|
|
@@ -432,22 +1160,14 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
432
1160
|
if (fromCfStack && existingLogicalIds.length > 0) {
|
|
433
1161
|
console.log(` ✓ VPC discovered from CloudFormation stack: ${discoveredResources.stackName}`);
|
|
434
1162
|
console.log(` ✓ Found ${existingLogicalIds.length} existing resources in stack`);
|
|
435
|
-
console.log(' ℹ
|
|
436
|
-
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
result.vpcConfig.securityGroupIds = [discoveredResources.securityGroupId];
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// Don't create any new resources - they already exist in the CF stack
|
|
443
|
-
return;
|
|
1163
|
+
console.log(' ℹ Adding resources to template for idempotent deployment');
|
|
1164
|
+
} else {
|
|
1165
|
+
// VPC discovered from AWS API (not from CF stack)
|
|
1166
|
+
console.log(' ℹ VPC discovered from AWS API - will create Lambda security group');
|
|
444
1167
|
}
|
|
445
1168
|
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
// Create a Lambda security group in the discovered VPC
|
|
450
|
-
// This is needed even in discover mode so other resources can reference it
|
|
1169
|
+
// Always create Lambda security group in template for idempotent deployments
|
|
1170
|
+
// CloudFormation will recognize it already exists and won't recreate it
|
|
451
1171
|
result.resources.FriggLambdaSecurityGroup = {
|
|
452
1172
|
Type: 'AWS::EC2::SecurityGroup',
|
|
453
1173
|
Properties: {
|
|
@@ -468,6 +1188,7 @@ class VpcBuilder extends InfrastructureBuilder {
|
|
|
468
1188
|
},
|
|
469
1189
|
};
|
|
470
1190
|
|
|
1191
|
+
// Always use Ref since resource is in template
|
|
471
1192
|
result.vpcConfig.securityGroupIds = [{ Ref: 'FriggLambdaSecurityGroup' }];
|
|
472
1193
|
|
|
473
1194
|
console.log(` ✅ Discovered VPC: ${result.vpcId}`);
|