@friggframework/devtools 2.0.0--canary.428.4fa6f20.0 → 2.0.0--canary.428.08c3f6f.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.
|
@@ -229,6 +229,18 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
229
229
|
Key: 'Name',
|
|
230
230
|
Value: '${self:service}-${self:provider.stage}-vpc',
|
|
231
231
|
},
|
|
232
|
+
{
|
|
233
|
+
Key: 'ManagedBy',
|
|
234
|
+
Value: 'Frigg',
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
Key: 'Service',
|
|
238
|
+
Value: '${self:service}',
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
Key: 'Stage',
|
|
242
|
+
Value: '${self:provider.stage}',
|
|
243
|
+
},
|
|
232
244
|
],
|
|
233
245
|
},
|
|
234
246
|
},
|
|
@@ -242,6 +254,18 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
242
254
|
Key: 'Name',
|
|
243
255
|
Value: '${self:service}-${self:provider.stage}-igw',
|
|
244
256
|
},
|
|
257
|
+
{
|
|
258
|
+
Key: 'ManagedBy',
|
|
259
|
+
Value: 'Frigg',
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
Key: 'Service',
|
|
263
|
+
Value: '${self:service}',
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
Key: 'Stage',
|
|
267
|
+
Value: '${self:provider.stage}',
|
|
268
|
+
},
|
|
245
269
|
],
|
|
246
270
|
},
|
|
247
271
|
},
|
|
@@ -268,6 +292,22 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
268
292
|
Key: 'Name',
|
|
269
293
|
Value: '${self:service}-${self:provider.stage}-public-subnet',
|
|
270
294
|
},
|
|
295
|
+
{
|
|
296
|
+
Key: 'ManagedBy',
|
|
297
|
+
Value: 'Frigg',
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
Key: 'Service',
|
|
301
|
+
Value: '${self:service}',
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
Key: 'Stage',
|
|
305
|
+
Value: '${self:provider.stage}',
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
Key: 'Type',
|
|
309
|
+
Value: 'Public',
|
|
310
|
+
},
|
|
271
311
|
],
|
|
272
312
|
},
|
|
273
313
|
},
|
|
@@ -284,6 +324,22 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
284
324
|
Key: 'Name',
|
|
285
325
|
Value: '${self:service}-${self:provider.stage}-private-subnet-1',
|
|
286
326
|
},
|
|
327
|
+
{
|
|
328
|
+
Key: 'ManagedBy',
|
|
329
|
+
Value: 'Frigg',
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
Key: 'Service',
|
|
333
|
+
Value: '${self:service}',
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
Key: 'Stage',
|
|
337
|
+
Value: '${self:provider.stage}',
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
Key: 'Type',
|
|
341
|
+
Value: 'Private',
|
|
342
|
+
},
|
|
287
343
|
],
|
|
288
344
|
},
|
|
289
345
|
},
|
|
@@ -300,6 +356,22 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
300
356
|
Key: 'Name',
|
|
301
357
|
Value: '${self:service}-${self:provider.stage}-private-subnet-2',
|
|
302
358
|
},
|
|
359
|
+
{
|
|
360
|
+
Key: 'ManagedBy',
|
|
361
|
+
Value: 'Frigg',
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
Key: 'Service',
|
|
365
|
+
Value: '${self:service}',
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
Key: 'Stage',
|
|
369
|
+
Value: '${self:provider.stage}',
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
Key: 'Type',
|
|
373
|
+
Value: 'Private',
|
|
374
|
+
},
|
|
303
375
|
],
|
|
304
376
|
},
|
|
305
377
|
},
|
|
@@ -314,6 +386,18 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
314
386
|
Key: 'Name',
|
|
315
387
|
Value: '${self:service}-${self:provider.stage}-nat-eip',
|
|
316
388
|
},
|
|
389
|
+
{
|
|
390
|
+
Key: 'ManagedBy',
|
|
391
|
+
Value: 'Frigg',
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
Key: 'Service',
|
|
395
|
+
Value: '${self:service}',
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
Key: 'Stage',
|
|
399
|
+
Value: '${self:provider.stage}',
|
|
400
|
+
},
|
|
317
401
|
],
|
|
318
402
|
},
|
|
319
403
|
DependsOn: 'FriggVPCGatewayAttachment',
|
|
@@ -332,6 +416,18 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
332
416
|
Key: 'Name',
|
|
333
417
|
Value: '${self:service}-${self:provider.stage}-nat-gateway',
|
|
334
418
|
},
|
|
419
|
+
{
|
|
420
|
+
Key: 'ManagedBy',
|
|
421
|
+
Value: 'Frigg',
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
Key: 'Service',
|
|
425
|
+
Value: '${self:service}',
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
Key: 'Stage',
|
|
429
|
+
Value: '${self:provider.stage}',
|
|
430
|
+
},
|
|
335
431
|
],
|
|
336
432
|
},
|
|
337
433
|
},
|
|
@@ -346,6 +442,22 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
346
442
|
Key: 'Name',
|
|
347
443
|
Value: '${self:service}-${self:provider.stage}-public-rt',
|
|
348
444
|
},
|
|
445
|
+
{
|
|
446
|
+
Key: 'ManagedBy',
|
|
447
|
+
Value: 'Frigg',
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
Key: 'Service',
|
|
451
|
+
Value: '${self:service}',
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
Key: 'Stage',
|
|
455
|
+
Value: '${self:provider.stage}',
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
Key: 'Type',
|
|
459
|
+
Value: 'Public',
|
|
460
|
+
},
|
|
349
461
|
],
|
|
350
462
|
},
|
|
351
463
|
},
|
|
@@ -380,6 +492,22 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
380
492
|
Key: 'Name',
|
|
381
493
|
Value: '${self:service}-${self:provider.stage}-private-rt',
|
|
382
494
|
},
|
|
495
|
+
{
|
|
496
|
+
Key: 'ManagedBy',
|
|
497
|
+
Value: 'Frigg',
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
Key: 'Service',
|
|
501
|
+
Value: '${self:service}',
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
Key: 'Stage',
|
|
505
|
+
Value: '${self:provider.stage}',
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
Key: 'Type',
|
|
509
|
+
Value: 'Private',
|
|
510
|
+
},
|
|
383
511
|
],
|
|
384
512
|
},
|
|
385
513
|
},
|
|
@@ -460,6 +588,18 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
460
588
|
Key: 'Name',
|
|
461
589
|
Value: '${self:service}-${self:provider.stage}-lambda-sg',
|
|
462
590
|
},
|
|
591
|
+
{
|
|
592
|
+
Key: 'ManagedBy',
|
|
593
|
+
Value: 'Frigg',
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
Key: 'Service',
|
|
597
|
+
Value: '${self:service}',
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
Key: 'Stage',
|
|
601
|
+
Value: '${self:provider.stage}',
|
|
602
|
+
},
|
|
463
603
|
],
|
|
464
604
|
},
|
|
465
605
|
},
|
|
@@ -548,6 +688,22 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
548
688
|
Key: 'Name',
|
|
549
689
|
Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg',
|
|
550
690
|
},
|
|
691
|
+
{
|
|
692
|
+
Key: 'ManagedBy',
|
|
693
|
+
Value: 'Frigg',
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
Key: 'Service',
|
|
697
|
+
Value: '${self:service}',
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
Key: 'Stage',
|
|
701
|
+
Value: '${self:provider.stage}',
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
Key: 'Type',
|
|
705
|
+
Value: 'VPCEndpoint',
|
|
706
|
+
},
|
|
551
707
|
],
|
|
552
708
|
},
|
|
553
709
|
};
|
|
@@ -1007,6 +1163,100 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
1007
1163
|
}
|
|
1008
1164
|
}
|
|
1009
1165
|
|
|
1166
|
+
/**
|
|
1167
|
+
* Heals VPC configuration issues by fixing common misconfigurations
|
|
1168
|
+
* @param {Object} discoveredResources - Resources discovered from AWS
|
|
1169
|
+
* @param {Object} AppDefinition - Application definition with VPC settings
|
|
1170
|
+
* @returns {Object} Healing report with actions taken and recommendations
|
|
1171
|
+
*/
|
|
1172
|
+
const healVPCConfiguration = (discoveredResources, AppDefinition) => {
|
|
1173
|
+
const healingReport = {
|
|
1174
|
+
healed: [],
|
|
1175
|
+
warnings: [],
|
|
1176
|
+
errors: [],
|
|
1177
|
+
recommendations: []
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
// Only heal if selfHeal is explicitly enabled
|
|
1181
|
+
if (!AppDefinition.vpc?.selfHeal) {
|
|
1182
|
+
return healingReport;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
console.log('🔧 Self-healing mode enabled - checking for VPC misconfigurations...');
|
|
1186
|
+
|
|
1187
|
+
// Check NAT Gateway placement
|
|
1188
|
+
if (discoveredResources.natGatewayInPrivateSubnet) {
|
|
1189
|
+
healingReport.warnings.push(
|
|
1190
|
+
`NAT Gateway ${discoveredResources.natGatewayInPrivateSubnet} is in a private subnet`
|
|
1191
|
+
);
|
|
1192
|
+
healingReport.recommendations.push(
|
|
1193
|
+
'NAT Gateway should be recreated in a public subnet for proper internet connectivity'
|
|
1194
|
+
);
|
|
1195
|
+
|
|
1196
|
+
// Mark that we need to create a new NAT Gateway
|
|
1197
|
+
discoveredResources.needsNewNatGateway = true;
|
|
1198
|
+
healingReport.healed.push('Marked NAT Gateway for recreation in public subnet');
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Check if EIP is already associated
|
|
1202
|
+
if (discoveredResources.elasticIpAlreadyAssociated) {
|
|
1203
|
+
healingReport.warnings.push(
|
|
1204
|
+
`Elastic IP ${discoveredResources.existingElasticIp} is already associated`
|
|
1205
|
+
);
|
|
1206
|
+
|
|
1207
|
+
// In self-heal mode, we'll try to reuse or create a new one
|
|
1208
|
+
if (discoveredResources.existingNatGatewayId) {
|
|
1209
|
+
healingReport.healed.push(
|
|
1210
|
+
'Will reuse existing NAT Gateway instead of creating a new one'
|
|
1211
|
+
);
|
|
1212
|
+
discoveredResources.reuseExistingNatGateway = true;
|
|
1213
|
+
} else {
|
|
1214
|
+
healingReport.healed.push(
|
|
1215
|
+
'Will allocate a new Elastic IP for NAT Gateway'
|
|
1216
|
+
);
|
|
1217
|
+
discoveredResources.allocateNewElasticIp = true;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Check route table associations
|
|
1222
|
+
if (discoveredResources.privateSubnetsWithWrongRoutes) {
|
|
1223
|
+
healingReport.warnings.push(
|
|
1224
|
+
`Found ${discoveredResources.privateSubnetsWithWrongRoutes.length} private subnets with incorrect routes`
|
|
1225
|
+
);
|
|
1226
|
+
healingReport.healed.push(
|
|
1227
|
+
'Route tables will be corrected during deployment'
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// Check for orphaned resources
|
|
1232
|
+
if (discoveredResources.orphanedElasticIps?.length > 0) {
|
|
1233
|
+
healingReport.warnings.push(
|
|
1234
|
+
`Found ${discoveredResources.orphanedElasticIps.length} orphaned Elastic IPs`
|
|
1235
|
+
);
|
|
1236
|
+
healingReport.recommendations.push(
|
|
1237
|
+
'Consider releasing orphaned Elastic IPs to avoid charges'
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// Log healing report
|
|
1242
|
+
if (healingReport.healed.length > 0) {
|
|
1243
|
+
console.log('✅ Self-healing actions:');
|
|
1244
|
+
healingReport.healed.forEach(action => console.log(` - ${action}`));
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
if (healingReport.warnings.length > 0) {
|
|
1248
|
+
console.log('⚠️ Issues detected:');
|
|
1249
|
+
healingReport.warnings.forEach(warning => console.log(` - ${warning}`));
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
if (healingReport.recommendations.length > 0) {
|
|
1253
|
+
console.log('💡 Recommendations:');
|
|
1254
|
+
healingReport.recommendations.forEach(rec => console.log(` - ${rec}`));
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
return healingReport;
|
|
1258
|
+
};
|
|
1259
|
+
|
|
1010
1260
|
// VPC Configuration based on App Definition
|
|
1011
1261
|
if (AppDefinition.vpc?.enable === true) {
|
|
1012
1262
|
// Add VPC-related IAM permissions
|
|
@@ -1022,107 +1272,383 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
1022
1272
|
Resource: '*',
|
|
1023
1273
|
});
|
|
1024
1274
|
|
|
1025
|
-
//
|
|
1026
|
-
if (
|
|
1027
|
-
|
|
1028
|
-
const vpcConfig = {};
|
|
1275
|
+
// Run healing if enabled and we have discovered resources
|
|
1276
|
+
if (discoveredResources && Object.keys(discoveredResources).length > 0) {
|
|
1277
|
+
const healingReport = healVPCConfiguration(discoveredResources, AppDefinition);
|
|
1029
1278
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
} else {
|
|
1034
|
-
// Use auto-created security group
|
|
1035
|
-
vpcConfig.securityGroupIds = [
|
|
1036
|
-
{ Ref: 'FriggLambdaSecurityGroup' },
|
|
1037
|
-
];
|
|
1279
|
+
// If healing failed critically, throw an error unless selfHeal is true
|
|
1280
|
+
if (healingReport.errors.length > 0 && !AppDefinition.vpc?.selfHeal) {
|
|
1281
|
+
throw new Error(`VPC configuration errors detected: ${healingReport.errors.join(', ')}`);
|
|
1038
1282
|
}
|
|
1283
|
+
}
|
|
1039
1284
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
{ Ref: 'FriggPrivateSubnet2' },
|
|
1048
|
-
];
|
|
1049
|
-
}
|
|
1285
|
+
// STEP 1: Determine VPC (create, discover, or use existing)
|
|
1286
|
+
const vpcManagement = AppDefinition.vpc.management || 'discover';
|
|
1287
|
+
let vpcId = null;
|
|
1288
|
+
let vpcConfig = {
|
|
1289
|
+
securityGroupIds: [],
|
|
1290
|
+
subnetIds: []
|
|
1291
|
+
};
|
|
1050
1292
|
|
|
1051
|
-
|
|
1052
|
-
definition.provider.vpc = vpcConfig;
|
|
1293
|
+
console.log(`VPC Management Mode: ${vpcManagement}`);
|
|
1053
1294
|
|
|
1054
|
-
|
|
1295
|
+
// First, establish VPC context
|
|
1296
|
+
if (vpcManagement === 'create-new') {
|
|
1297
|
+
// Create new VPC infrastructure
|
|
1055
1298
|
const vpcResources = createVPCInfrastructure(AppDefinition);
|
|
1056
1299
|
Object.assign(definition.resources.Resources, vpcResources);
|
|
1300
|
+
vpcId = { Ref: 'FriggVPC' }; // Reference to created VPC
|
|
1301
|
+
|
|
1302
|
+
// Default security group for new VPC
|
|
1303
|
+
vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds || [
|
|
1304
|
+
{ Ref: 'FriggLambdaSecurityGroup' }
|
|
1305
|
+
];
|
|
1306
|
+
} else if (vpcManagement === 'use-existing') {
|
|
1307
|
+
// Use explicitly provided VPC
|
|
1308
|
+
if (!AppDefinition.vpc.vpcId) {
|
|
1309
|
+
throw new Error('VPC management is set to "use-existing" but no vpcId was provided');
|
|
1310
|
+
}
|
|
1311
|
+
vpcId = AppDefinition.vpc.vpcId;
|
|
1312
|
+
// Use provided security groups or try to discover default security group for the VPC
|
|
1313
|
+
vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds ||
|
|
1314
|
+
(discoveredResources.defaultSecurityGroupId ? [discoveredResources.defaultSecurityGroupId] : []);
|
|
1315
|
+
} else {
|
|
1316
|
+
// Discover VPC
|
|
1317
|
+
if (!discoveredResources.defaultVpcId) {
|
|
1318
|
+
throw new Error(
|
|
1319
|
+
'VPC discovery failed: No VPC found. ' +
|
|
1320
|
+
'Either set vpc.management to "create-new" or provide vpc.vpcId with "use-existing".'
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
vpcId = discoveredResources.defaultVpcId;
|
|
1324
|
+
vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds ||
|
|
1325
|
+
(discoveredResources.defaultSecurityGroupId ? [discoveredResources.defaultSecurityGroupId] : []);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// STEP 2: Handle Subnet Management (independent of VPC management)
|
|
1329
|
+
// When creating a new VPC, default to creating subnets unless explicitly specified
|
|
1330
|
+
const defaultSubnetManagement = vpcManagement === 'create-new' ? 'create' : 'discover';
|
|
1331
|
+
const subnetManagement = AppDefinition.vpc.subnets?.management || defaultSubnetManagement;
|
|
1332
|
+
console.log(`Subnet Management Mode: ${subnetManagement}`);
|
|
1333
|
+
|
|
1334
|
+
// Ensure we have a valid VPC ID for subnet operations
|
|
1335
|
+
const effectiveVpcId = vpcId || discoveredResources.defaultVpcId;
|
|
1336
|
+
if (!effectiveVpcId) {
|
|
1337
|
+
throw new Error('Cannot manage subnets without a VPC ID');
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// Subnet decision tree
|
|
1341
|
+
if (subnetManagement === 'create') {
|
|
1342
|
+
// Create new subnets in the VPC (either new or existing)
|
|
1343
|
+
console.log('Creating new subnets...');
|
|
1344
|
+
|
|
1345
|
+
// Determine VpcId based on VPC management mode
|
|
1346
|
+
const subnetVpcId = vpcManagement === 'create-new' ? { Ref: 'FriggVPC' } : effectiveVpcId;
|
|
1347
|
+
|
|
1348
|
+
// Generate CIDR blocks based on VPC type
|
|
1349
|
+
// For new VPC: use Fn::Cidr to generate from 10.0.0.0/16
|
|
1350
|
+
// For existing VPC: use safer high-range /24 blocks less likely to conflict
|
|
1351
|
+
let subnet1Cidr, subnet2Cidr, publicSubnetCidr;
|
|
1352
|
+
|
|
1353
|
+
if (vpcManagement === 'create-new') {
|
|
1354
|
+
// Use Fn::Cidr to generate 3 /24 subnets from the VPC CIDR
|
|
1355
|
+
// This creates [10.0.0.0/24, 10.0.1.0/24, 10.0.2.0/24]
|
|
1356
|
+
const generatedCidrs = {
|
|
1357
|
+
'Fn::Cidr': ['10.0.0.0/16', 3, 8] // 3 subnets with /24 (256-8=248 bits)
|
|
1358
|
+
};
|
|
1359
|
+
subnet1Cidr = { 'Fn::Select': [0, generatedCidrs] }; // 10.0.0.0/24
|
|
1360
|
+
subnet2Cidr = { 'Fn::Select': [1, generatedCidrs] }; // 10.0.1.0/24
|
|
1361
|
+
publicSubnetCidr = { 'Fn::Select': [2, generatedCidrs] }; // 10.0.2.0/24
|
|
1362
|
+
} else {
|
|
1363
|
+
// For existing VPCs, use high-range /24 blocks less likely to conflict
|
|
1364
|
+
// These are in the 172.31.x.x range for default VPC or high ranges for custom VPCs
|
|
1365
|
+
subnet1Cidr = '172.31.240.0/24';
|
|
1366
|
+
subnet2Cidr = '172.31.241.0/24';
|
|
1367
|
+
publicSubnetCidr = '172.31.250.0/24';
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Create private subnets
|
|
1371
|
+
definition.resources.Resources.FriggPrivateSubnet1 = {
|
|
1372
|
+
Type: 'AWS::EC2::Subnet',
|
|
1373
|
+
Properties: {
|
|
1374
|
+
VpcId: subnetVpcId,
|
|
1375
|
+
CidrBlock: subnet1Cidr,
|
|
1376
|
+
AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
|
|
1377
|
+
Tags: [
|
|
1378
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-1' },
|
|
1379
|
+
{ Key: 'Type', Value: 'Private' },
|
|
1380
|
+
{ Key: 'ManagedBy', Value: 'Frigg' }
|
|
1381
|
+
]
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
|
|
1385
|
+
definition.resources.Resources.FriggPrivateSubnet2 = {
|
|
1386
|
+
Type: 'AWS::EC2::Subnet',
|
|
1387
|
+
Properties: {
|
|
1388
|
+
VpcId: subnetVpcId,
|
|
1389
|
+
CidrBlock: subnet2Cidr,
|
|
1390
|
+
AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
|
|
1391
|
+
Tags: [
|
|
1392
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-2' },
|
|
1393
|
+
{ Key: 'Type', Value: 'Private' },
|
|
1394
|
+
{ Key: 'ManagedBy', Value: 'Frigg' }
|
|
1395
|
+
]
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1399
|
+
// Create public subnet for NAT
|
|
1400
|
+
definition.resources.Resources.FriggPublicSubnet = {
|
|
1401
|
+
Type: 'AWS::EC2::Subnet',
|
|
1402
|
+
Properties: {
|
|
1403
|
+
VpcId: subnetVpcId,
|
|
1404
|
+
CidrBlock: publicSubnetCidr,
|
|
1405
|
+
MapPublicIpOnLaunch: true,
|
|
1406
|
+
AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
|
|
1407
|
+
Tags: [
|
|
1408
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-public' },
|
|
1409
|
+
{ Key: 'Type', Value: 'Public' },
|
|
1410
|
+
{ Key: 'ManagedBy', Value: 'Frigg' }
|
|
1411
|
+
]
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
|
|
1415
|
+
vpcConfig.subnetIds = [
|
|
1416
|
+
{ Ref: 'FriggPrivateSubnet1' },
|
|
1417
|
+
{ Ref: 'FriggPrivateSubnet2' }
|
|
1418
|
+
];
|
|
1419
|
+
|
|
1420
|
+
// IMPORTANT: Create route tables even without NAT Gateway management
|
|
1421
|
+
// Otherwise subnets won't have proper routing
|
|
1422
|
+
if (!AppDefinition.vpc.natGateway || AppDefinition.vpc.natGateway.management === 'discover') {
|
|
1423
|
+
// Need to ensure public subnet has IGW route
|
|
1424
|
+
if (vpcManagement === 'create-new' || !discoveredResources.internetGatewayId) {
|
|
1425
|
+
// Create or reference IGW for public subnet
|
|
1426
|
+
if (!definition.resources.Resources.FriggInternetGateway) {
|
|
1427
|
+
definition.resources.Resources.FriggInternetGateway = {
|
|
1428
|
+
Type: 'AWS::EC2::InternetGateway',
|
|
1429
|
+
Properties: {
|
|
1430
|
+
Tags: [
|
|
1431
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-igw' },
|
|
1432
|
+
{ Key: 'ManagedBy', Value: 'Frigg' }
|
|
1433
|
+
]
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
definition.resources.Resources.FriggIGWAttachment = {
|
|
1438
|
+
Type: 'AWS::EC2::VPCGatewayAttachment',
|
|
1439
|
+
Properties: {
|
|
1440
|
+
VpcId: subnetVpcId,
|
|
1441
|
+
InternetGatewayId: { Ref: 'FriggInternetGateway' }
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// Create public route table with IGW route
|
|
1448
|
+
definition.resources.Resources.FriggPublicRouteTable = {
|
|
1449
|
+
Type: 'AWS::EC2::RouteTable',
|
|
1450
|
+
Properties: {
|
|
1451
|
+
VpcId: subnetVpcId,
|
|
1452
|
+
Tags: [
|
|
1453
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-rt' },
|
|
1454
|
+
{ Key: 'ManagedBy', Value: 'Frigg' }
|
|
1455
|
+
]
|
|
1456
|
+
}
|
|
1457
|
+
};
|
|
1458
|
+
|
|
1459
|
+
definition.resources.Resources.FriggPublicRoute = {
|
|
1460
|
+
Type: 'AWS::EC2::Route',
|
|
1461
|
+
DependsOn: vpcManagement === 'create-new' ? 'FriggIGWAttachment' : undefined,
|
|
1462
|
+
Properties: {
|
|
1463
|
+
RouteTableId: { Ref: 'FriggPublicRouteTable' },
|
|
1464
|
+
DestinationCidrBlock: '0.0.0.0/0',
|
|
1465
|
+
GatewayId: discoveredResources.internetGatewayId || { Ref: 'FriggInternetGateway' }
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
|
|
1469
|
+
// Associate public subnet with public route table
|
|
1470
|
+
definition.resources.Resources.FriggPublicSubnetRouteTableAssociation = {
|
|
1471
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1472
|
+
Properties: {
|
|
1473
|
+
SubnetId: { Ref: 'FriggPublicSubnet' },
|
|
1474
|
+
RouteTableId: { Ref: 'FriggPublicRouteTable' }
|
|
1475
|
+
}
|
|
1476
|
+
};
|
|
1477
|
+
|
|
1478
|
+
// Create private route table for Lambda subnets
|
|
1479
|
+
definition.resources.Resources.FriggLambdaRouteTable = {
|
|
1480
|
+
Type: 'AWS::EC2::RouteTable',
|
|
1481
|
+
Properties: {
|
|
1482
|
+
VpcId: subnetVpcId,
|
|
1483
|
+
Tags: [
|
|
1484
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
|
|
1485
|
+
{ Key: 'ManagedBy', Value: 'Frigg' }
|
|
1486
|
+
]
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
// Associate private subnets with route table
|
|
1491
|
+
definition.resources.Resources.FriggPrivateSubnet1RouteTableAssociation = {
|
|
1492
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1493
|
+
Properties: {
|
|
1494
|
+
SubnetId: { Ref: 'FriggPrivateSubnet1' },
|
|
1495
|
+
RouteTableId: { Ref: 'FriggLambdaRouteTable' }
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1498
|
+
|
|
1499
|
+
definition.resources.Resources.FriggPrivateSubnet2RouteTableAssociation = {
|
|
1500
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1501
|
+
Properties: {
|
|
1502
|
+
SubnetId: { Ref: 'FriggPrivateSubnet2' },
|
|
1503
|
+
RouteTableId: { Ref: 'FriggLambdaRouteTable' }
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
} else if (subnetManagement === 'use-existing') {
|
|
1508
|
+
// Use explicitly provided subnet IDs
|
|
1509
|
+
if (!AppDefinition.vpc.subnets?.ids || AppDefinition.vpc.subnets.ids.length < 2) {
|
|
1510
|
+
throw new Error(
|
|
1511
|
+
'Subnet management is "use-existing" but less than 2 subnet IDs provided. ' +
|
|
1512
|
+
'Provide at least 2 subnet IDs in vpc.subnets.ids.'
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
vpcConfig.subnetIds = AppDefinition.vpc.subnets.ids;
|
|
1057
1516
|
} else {
|
|
1058
|
-
//
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
(discoveredResources.defaultSecurityGroupId
|
|
1064
|
-
? [discoveredResources.defaultSecurityGroupId]
|
|
1065
|
-
: []),
|
|
1066
|
-
subnetIds:
|
|
1067
|
-
AppDefinition.vpc.subnetIds ||
|
|
1068
|
-
(discoveredResources.privateSubnetId1 &&
|
|
1517
|
+
// Discover mode (default)
|
|
1518
|
+
vpcConfig.subnetIds =
|
|
1519
|
+
AppDefinition.vpc.subnets?.ids?.length > 0
|
|
1520
|
+
? AppDefinition.vpc.subnets.ids
|
|
1521
|
+
: (discoveredResources.privateSubnetId1 &&
|
|
1069
1522
|
discoveredResources.privateSubnetId2
|
|
1070
1523
|
? [
|
|
1071
1524
|
discoveredResources.privateSubnetId1,
|
|
1072
1525
|
discoveredResources.privateSubnetId2,
|
|
1073
1526
|
]
|
|
1074
|
-
: [])
|
|
1075
|
-
|
|
1527
|
+
: []);
|
|
1528
|
+
|
|
1529
|
+
if (vpcConfig.subnetIds.length < 2) {
|
|
1530
|
+
if (AppDefinition.vpc.selfHeal) {
|
|
1531
|
+
console.log('No subnets found but self-heal enabled - creating minimal subnet setup');
|
|
1532
|
+
// Fall back to creating subnets
|
|
1533
|
+
subnetManagement = 'create';
|
|
1534
|
+
// Recursion would be complex here, so just set flag
|
|
1535
|
+
discoveredResources.createSubnets = true;
|
|
1536
|
+
} else {
|
|
1537
|
+
throw new Error(
|
|
1538
|
+
'No subnets discovered and subnets.management is "discover". ' +
|
|
1539
|
+
'Either enable vpc.selfHeal, set subnets.management to "create", or provide subnet IDs.'
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1076
1544
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1545
|
+
// Set VPC config for Lambda functions only if we have valid subnet IDs
|
|
1546
|
+
if (
|
|
1547
|
+
vpcConfig.subnetIds.length >= 2 &&
|
|
1548
|
+
vpcConfig.securityGroupIds.length > 0
|
|
1549
|
+
) {
|
|
1550
|
+
definition.provider.vpc = vpcConfig;
|
|
1083
1551
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1552
|
+
// ALWAYS manage NAT Gateway through CloudFormation for self-healing
|
|
1553
|
+
// This ensures NAT Gateway is always in the correct subnet with proper configuration
|
|
1086
1554
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1555
|
+
console.log('AppDefinition.vpc.natGateway', AppDefinition.vpc.natGateway);
|
|
1556
|
+
const natGatewayManagement =
|
|
1557
|
+
AppDefinition.vpc.natGateway?.management || 'discover';
|
|
1558
|
+
console.log('natGatewayManagement', natGatewayManagement);
|
|
1559
|
+
let needsNewNatGateway =
|
|
1560
|
+
natGatewayManagement === 'createAndManage' ||
|
|
1561
|
+
discoveredResources.needsNewNatGateway === true; // Use healing flag
|
|
1093
1562
|
|
|
1094
|
-
|
|
1563
|
+
console.log('needsNewNatGateway', needsNewNatGateway);
|
|
1095
1564
|
|
|
1096
|
-
|
|
1097
|
-
const isValidPublicSubnet = (subnetId, discoveredResources) => {
|
|
1098
|
-
// Basic validation - in production, AWSDiscovery should check route tables for IGW routes
|
|
1099
|
-
return (
|
|
1100
|
-
discoveredResources.publicSubnetHasIgwRoute !== false
|
|
1101
|
-
);
|
|
1102
|
-
};
|
|
1565
|
+
// Remove unused helper function - validation is done in discovery
|
|
1103
1566
|
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
'Create mode: Creating dedicated EIP, public subnet, and NAT Gateway...'
|
|
1108
|
-
);
|
|
1567
|
+
// Variables to track NAT Gateway and EIP reuse
|
|
1568
|
+
let reuseExistingNatGateway = false;
|
|
1569
|
+
let useExistingEip = false;
|
|
1109
1570
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1571
|
+
if (needsNewNatGateway) {
|
|
1572
|
+
// Always create new dedicated resources in create mode to avoid confusion with existing ones
|
|
1573
|
+
console.log(
|
|
1574
|
+
'Create mode: Creating dedicated EIP, public subnet, and NAT Gateway...'
|
|
1575
|
+
);
|
|
1576
|
+
|
|
1577
|
+
// Check if we can reuse existing NAT Gateway and EIP to avoid conflicts
|
|
1578
|
+
|
|
1579
|
+
// Check if we have a Frigg-managed NAT Gateway that we can reuse
|
|
1580
|
+
if (discoveredResources.existingNatGatewayId &&
|
|
1581
|
+
discoveredResources.existingElasticIpAllocationId) {
|
|
1582
|
+
// We have both NAT Gateway and EIP
|
|
1583
|
+
console.log('Found existing Frigg-managed NAT Gateway and EIP');
|
|
1584
|
+
|
|
1585
|
+
// CRITICAL: Check if NAT Gateway is in correct (public) subnet
|
|
1586
|
+
if (!discoveredResources.natGatewayInPrivateSubnet) {
|
|
1587
|
+
// NAT Gateway is properly configured, reuse it
|
|
1588
|
+
console.log('✅ Existing NAT Gateway is in PUBLIC subnet, will reuse it');
|
|
1589
|
+
reuseExistingNatGateway = true;
|
|
1590
|
+
} else {
|
|
1591
|
+
// NAT Gateway is in PRIVATE subnet - NEVER reuse it
|
|
1592
|
+
console.log('❌ NAT Gateway is in PRIVATE subnet - MUST create new one in PUBLIC subnet');
|
|
1593
|
+
|
|
1594
|
+
if (AppDefinition.vpc.selfHeal) {
|
|
1595
|
+
console.log('Self-heal enabled: Creating new NAT Gateway in PUBLIC subnet');
|
|
1596
|
+
// Force creation of new NAT in public subnet
|
|
1597
|
+
reuseExistingNatGateway = false;
|
|
1598
|
+
// Cannot reuse the EIP since it's associated with wrong NAT
|
|
1599
|
+
useExistingEip = false;
|
|
1600
|
+
// Mark for cleanup recommendations
|
|
1601
|
+
discoveredResources.needsCleanup = true;
|
|
1602
|
+
} else {
|
|
1603
|
+
throw new Error(
|
|
1604
|
+
'CRITICAL: NAT Gateway is in PRIVATE subnet (will not work!). ' +
|
|
1605
|
+
'Enable vpc.selfHeal to auto-fix or set natGateway.management to "createAndManage".'
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
} else if (discoveredResources.existingElasticIpAllocationId &&
|
|
1610
|
+
!discoveredResources.existingNatGatewayId) {
|
|
1611
|
+
// We have an EIP but no NAT Gateway - can reuse the EIP
|
|
1612
|
+
console.log('Found orphaned EIP, will reuse it for new NAT Gateway in PUBLIC subnet');
|
|
1613
|
+
useExistingEip = true;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// Skip all resource creation if reusing existing NAT Gateway
|
|
1617
|
+
if (reuseExistingNatGateway) {
|
|
1618
|
+
console.log('Reusing existing NAT Gateway - skipping resource creation');
|
|
1619
|
+
// The existing NAT Gateway will be used for routing
|
|
1620
|
+
// No new resources need to be created
|
|
1621
|
+
} else {
|
|
1622
|
+
// Only create EIP if we're not reusing an existing one
|
|
1623
|
+
if (!useExistingEip) {
|
|
1624
|
+
definition.resources.Resources.FriggNATGatewayEIP = {
|
|
1625
|
+
Type: 'AWS::EC2::EIP',
|
|
1626
|
+
Properties: {
|
|
1627
|
+
Domain: 'vpc',
|
|
1628
|
+
Tags: [
|
|
1629
|
+
{
|
|
1630
|
+
Key: 'Name',
|
|
1631
|
+
Value: '${self:service}-${self:provider.stage}-nat-eip',
|
|
1632
|
+
},
|
|
1633
|
+
{
|
|
1634
|
+
Key: 'ManagedBy',
|
|
1635
|
+
Value: 'Frigg',
|
|
1636
|
+
},
|
|
1637
|
+
{
|
|
1638
|
+
Key: 'Service',
|
|
1639
|
+
Value: '${self:service}',
|
|
1640
|
+
},
|
|
1641
|
+
{
|
|
1642
|
+
Key: 'Stage',
|
|
1643
|
+
Value: '${self:provider.stage}',
|
|
1644
|
+
},
|
|
1645
|
+
],
|
|
1646
|
+
},
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1123
1649
|
|
|
1124
|
-
// Create public subnet
|
|
1125
|
-
if (!discoveredResources.publicSubnetId) {
|
|
1650
|
+
// Create public subnet if needed (for NAT Gateway placement)
|
|
1651
|
+
if (!discoveredResources.publicSubnetId || discoveredResources.createPublicSubnet) {
|
|
1126
1652
|
console.log(
|
|
1127
1653
|
'No public subnet found, creating one for NAT Gateway placement...'
|
|
1128
1654
|
);
|
|
@@ -1222,55 +1748,103 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
1222
1748
|
};
|
|
1223
1749
|
}
|
|
1224
1750
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1751
|
+
// Create NAT Gateway only if not reusing existing one
|
|
1752
|
+
definition.resources.Resources.FriggNATGateway = {
|
|
1753
|
+
Type: 'AWS::EC2::NatGateway',
|
|
1754
|
+
Properties: {
|
|
1755
|
+
AllocationId: useExistingEip ?
|
|
1756
|
+
discoveredResources.existingElasticIpAllocationId :
|
|
1757
|
+
{
|
|
1758
|
+
'Fn::GetAtt': [
|
|
1759
|
+
'FriggNATGatewayEIP',
|
|
1760
|
+
'AllocationId',
|
|
1761
|
+
],
|
|
1762
|
+
},
|
|
1763
|
+
SubnetId: discoveredResources.publicSubnetId || {
|
|
1764
|
+
Ref: 'FriggPublicSubnet',
|
|
1765
|
+
},
|
|
1766
|
+
Tags: [
|
|
1767
|
+
{
|
|
1768
|
+
Key: 'Name',
|
|
1769
|
+
Value: '${self:service}-${self:provider.stage}-nat-gateway',
|
|
1770
|
+
},
|
|
1771
|
+
{
|
|
1772
|
+
Key: 'ManagedBy',
|
|
1773
|
+
Value: 'Frigg',
|
|
1774
|
+
},
|
|
1775
|
+
{
|
|
1776
|
+
Key: 'Service',
|
|
1777
|
+
Value: '${self:service}',
|
|
1778
|
+
},
|
|
1779
|
+
{
|
|
1780
|
+
Key: 'Stage',
|
|
1781
|
+
Value: '${self:provider.stage}',
|
|
1782
|
+
},
|
|
1233
1783
|
],
|
|
1234
1784
|
},
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
} else if (natGatewayManagement === 'discover' || natGatewayManagement === 'useExisting') {
|
|
1788
|
+
// Discover or use existing NAT Gateway
|
|
1789
|
+
if (natGatewayManagement === 'useExisting' && AppDefinition.vpc.natGateway?.id) {
|
|
1790
|
+
// Use explicitly provided NAT Gateway ID
|
|
1791
|
+
console.log(`Using explicitly provided NAT Gateway: ${AppDefinition.vpc.natGateway.id}`);
|
|
1792
|
+
discoveredResources.existingNatGatewayId = AppDefinition.vpc.natGateway.id;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
if (discoveredResources.existingNatGatewayId) {
|
|
1796
|
+
console.log('discoveredResources.existingNatGatewayId', discoveredResources.existingNatGatewayId);
|
|
1797
|
+
|
|
1798
|
+
// CRITICAL: Verify NAT Gateway is in PUBLIC subnet
|
|
1799
|
+
if (discoveredResources.natGatewayInPrivateSubnet) {
|
|
1800
|
+
// NAT is in PRIVATE subnet - CANNOT use it
|
|
1801
|
+
console.log('❌ CRITICAL: NAT Gateway is in PRIVATE subnet - Internet connectivity will NOT work!');
|
|
1802
|
+
|
|
1803
|
+
if (AppDefinition.vpc.selfHeal === true) {
|
|
1804
|
+
console.log('Self-heal enabled: Will create new NAT Gateway in PUBLIC subnet');
|
|
1805
|
+
// Force creation of new NAT Gateway in public subnet
|
|
1806
|
+
needsNewNatGateway = true;
|
|
1807
|
+
discoveredResources.existingNatGatewayId = null; // Don't use the misconfigured NAT
|
|
1808
|
+
// Ensure we have a public subnet for the NAT
|
|
1809
|
+
if (!discoveredResources.publicSubnetId) {
|
|
1810
|
+
console.log('No public subnet found - will create one for NAT Gateway');
|
|
1811
|
+
discoveredResources.createPublicSubnet = true;
|
|
1812
|
+
}
|
|
1813
|
+
} else {
|
|
1814
|
+
throw new Error(
|
|
1815
|
+
'CRITICAL: NAT Gateway is in PRIVATE subnet and will NOT provide internet connectivity! ' +
|
|
1816
|
+
'Options: 1) Enable vpc.selfHeal to auto-create proper NAT, ' +
|
|
1817
|
+
'2) Set natGateway.management to "createAndManage", or ' +
|
|
1818
|
+
'3) Manually fix the NAT Gateway placement.'
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
} else {
|
|
1822
|
+
// NAT is correctly in public subnet
|
|
1823
|
+
console.log('✅ NAT Gateway is correctly placed in PUBLIC subnet');
|
|
1824
|
+
}
|
|
1264
1825
|
} else {
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1826
|
+
// No existing NAT Gateway found
|
|
1827
|
+
if (natGatewayManagement === 'useExisting') {
|
|
1828
|
+
throw new Error(
|
|
1829
|
+
'NAT Gateway management set to "useExisting" but no NAT Gateway found. ' +
|
|
1830
|
+
'Either provide natGateway.id or change management to "discover" or "createAndManage".'
|
|
1831
|
+
);
|
|
1832
|
+
} else if (AppDefinition.vpc.selfHeal === true) {
|
|
1833
|
+
// Self-healing enabled, create a new NAT Gateway
|
|
1834
|
+
console.log('No NAT Gateway found but self-healing enabled - creating new NAT Gateway in PUBLIC subnet');
|
|
1835
|
+
needsNewNatGateway = true;
|
|
1836
|
+
// Ensure we have a public subnet for the NAT
|
|
1837
|
+
if (!discoveredResources.publicSubnetId) {
|
|
1838
|
+
console.log('No public subnet found - will create one for NAT Gateway');
|
|
1839
|
+
discoveredResources.createPublicSubnet = true;
|
|
1840
|
+
}
|
|
1841
|
+
} else {
|
|
1842
|
+
throw new Error(
|
|
1843
|
+
'No existing NAT Gateway found in discovery mode. ' +
|
|
1844
|
+
'Set natGateway.management to "createAndManage" to create a new NAT Gateway.'
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1268
1847
|
}
|
|
1269
|
-
} else {
|
|
1270
|
-
// No NAT and not in create mode: Error out to prevent isolated subnets
|
|
1271
|
-
throw new Error(
|
|
1272
|
-
'No existing NAT Gateway found and createAndManage not enabled. Update appDefinition.vpc.natGateway.method or ensure discovery finds a valid NAT.'
|
|
1273
|
-
);
|
|
1274
1848
|
}
|
|
1275
1849
|
|
|
1276
1850
|
// Always add route table and routes (referencing the NAT, whether new or existing)
|
|
@@ -1289,47 +1863,96 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
1289
1863
|
},
|
|
1290
1864
|
};
|
|
1291
1865
|
|
|
1292
|
-
|
|
1293
|
-
|
|
1866
|
+
// Determine which NAT Gateway ID to use for routing
|
|
1867
|
+
let natGatewayIdForRoute;
|
|
1868
|
+
|
|
1869
|
+
if (reuseExistingNatGateway) {
|
|
1870
|
+
// Use the existing NAT Gateway that we're reusing
|
|
1871
|
+
natGatewayIdForRoute = discoveredResources.existingNatGatewayId;
|
|
1872
|
+
} else if (needsNewNatGateway && !reuseExistingNatGateway) {
|
|
1873
|
+
// Reference the new NAT Gateway being created
|
|
1874
|
+
natGatewayIdForRoute = { Ref: 'FriggNATGateway' };
|
|
1875
|
+
} else if (discoveredResources.existingNatGatewayId) {
|
|
1876
|
+
// Use the existing NAT Gateway ID
|
|
1877
|
+
natGatewayIdForRoute = discoveredResources.existingNatGatewayId;
|
|
1878
|
+
} else if (AppDefinition.vpc.natGateway?.id) {
|
|
1879
|
+
// Use explicitly provided NAT Gateway ID
|
|
1880
|
+
natGatewayIdForRoute = AppDefinition.vpc.natGateway.id;
|
|
1881
|
+
} else if (AppDefinition.vpc.selfHeal === true) {
|
|
1882
|
+
// Self-healing enabled but no NAT Gateway - skip NAT route
|
|
1883
|
+
natGatewayIdForRoute = null;
|
|
1884
|
+
} else {
|
|
1885
|
+
throw new Error(
|
|
1886
|
+
'Unable to determine NAT Gateway ID for routing. ' +
|
|
1887
|
+
'Please check your configuration.'
|
|
1888
|
+
);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
// Only create NAT route if we have a NAT Gateway
|
|
1892
|
+
if (natGatewayIdForRoute) {
|
|
1893
|
+
definition.resources.Resources.FriggNATRoute = {
|
|
1294
1894
|
Type: 'AWS::EC2::Route',
|
|
1295
1895
|
Properties: {
|
|
1296
1896
|
RouteTableId: { Ref: 'FriggLambdaRouteTable' },
|
|
1297
1897
|
DestinationCidrBlock: '0.0.0.0/0',
|
|
1298
|
-
NatGatewayId:
|
|
1898
|
+
NatGatewayId: natGatewayIdForRoute,
|
|
1299
1899
|
},
|
|
1300
1900
|
};
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// Associate Lambda subnets with NAT Gateway route table
|
|
1904
|
+
// CRITICAL: This fixes the "NAT Gateway in private subnet" issue by ensuring correct routing
|
|
1905
|
+
if (AppDefinition.vpc.selfHeal === true) {
|
|
1906
|
+
console.log('✅ Self-healing: Ensuring subnets have correct route table associations');
|
|
1907
|
+
// In self-heal mode, we force the associations even if they might conflict
|
|
1908
|
+
// CloudFormation will automatically disassociate from old route table first
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// Only create associations for discovered subnets (not for Refs)
|
|
1912
|
+
if (typeof vpcConfig.subnetIds[0] === 'string') {
|
|
1913
|
+
definition.resources.Resources.FriggSubnet1RouteAssociation = {
|
|
1914
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1304
1915
|
Properties: {
|
|
1916
|
+
SubnetId: vpcConfig.subnetIds[0],
|
|
1305
1917
|
RouteTableId: { Ref: 'FriggLambdaRouteTable' },
|
|
1306
|
-
DestinationCidrBlock: '0.0.0.0/0',
|
|
1307
|
-
NatGatewayId:
|
|
1308
|
-
discoveredResources.existingNatGatewayId,
|
|
1309
1918
|
},
|
|
1919
|
+
DependsOn: 'FriggLambdaRouteTable',
|
|
1310
1920
|
};
|
|
1311
1921
|
}
|
|
1312
1922
|
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
};
|
|
1923
|
+
if (typeof vpcConfig.subnetIds[1] === 'string') {
|
|
1924
|
+
definition.resources.Resources.FriggSubnet2RouteAssociation = {
|
|
1925
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1926
|
+
Properties: {
|
|
1927
|
+
SubnetId: vpcConfig.subnetIds[1],
|
|
1928
|
+
RouteTableId: { Ref: 'FriggLambdaRouteTable' },
|
|
1929
|
+
},
|
|
1930
|
+
DependsOn: 'FriggLambdaRouteTable',
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1324
1933
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1934
|
+
// If subnets are CloudFormation refs (newly created), associate them
|
|
1935
|
+
if (typeof vpcConfig.subnetIds[0] === 'object' && vpcConfig.subnetIds[0].Ref) {
|
|
1936
|
+
definition.resources.Resources.FriggNewSubnet1RouteAssociation = {
|
|
1937
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1938
|
+
Properties: {
|
|
1939
|
+
SubnetId: vpcConfig.subnetIds[0],
|
|
1940
|
+
RouteTableId: { Ref: 'FriggLambdaRouteTable' },
|
|
1941
|
+
},
|
|
1942
|
+
DependsOn: ['FriggLambdaRouteTable', vpcConfig.subnetIds[0].Ref],
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
if (typeof vpcConfig.subnetIds[1] === 'object' && vpcConfig.subnetIds[1].Ref) {
|
|
1947
|
+
definition.resources.Resources.FriggNewSubnet2RouteAssociation = {
|
|
1948
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1949
|
+
Properties: {
|
|
1950
|
+
SubnetId: vpcConfig.subnetIds[1],
|
|
1951
|
+
RouteTableId: { Ref: 'FriggLambdaRouteTable' },
|
|
1952
|
+
},
|
|
1953
|
+
DependsOn: ['FriggLambdaRouteTable', vpcConfig.subnetIds[1].Ref],
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1333
1956
|
|
|
1334
1957
|
// Add VPC endpoints for AWS service optimization (optional but recommended)
|
|
1335
1958
|
if (AppDefinition.vpc.enableVPCEndpoints !== false) {
|
|
@@ -1430,109 +2053,109 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
1430
2053
|
}
|
|
1431
2054
|
}
|
|
1432
2055
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
2056
|
+
// SSM Parameter Store Configuration based on App Definition
|
|
2057
|
+
if (AppDefinition.ssm?.enable === true) {
|
|
2058
|
+
// Add AWS Parameters and Secrets Lambda Extension layer
|
|
2059
|
+
definition.provider.layers = [
|
|
2060
|
+
'arn:aws:lambda:${self:provider.region}:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11',
|
|
2061
|
+
];
|
|
1439
2062
|
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
2063
|
+
// Add SSM IAM permissions
|
|
2064
|
+
definition.provider.iamRoleStatements.push({
|
|
2065
|
+
Effect: 'Allow',
|
|
2066
|
+
Action: [
|
|
2067
|
+
'ssm:GetParameter',
|
|
2068
|
+
'ssm:GetParameters',
|
|
2069
|
+
'ssm:GetParametersByPath',
|
|
2070
|
+
],
|
|
2071
|
+
Resource: [
|
|
2072
|
+
'arn:aws:ssm:${self:provider.region}:*:parameter/${self:service}/${self:provider.stage}/*',
|
|
2073
|
+
],
|
|
2074
|
+
});
|
|
1452
2075
|
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
2076
|
+
// Add environment variable for SSM parameter prefix
|
|
2077
|
+
definition.provider.environment.SSM_PARAMETER_PREFIX =
|
|
2078
|
+
'/${self:service}/${self:provider.stage}';
|
|
2079
|
+
}
|
|
1457
2080
|
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
2081
|
+
// Add integration-specific functions and resources
|
|
2082
|
+
if (
|
|
2083
|
+
AppDefinition.integrations &&
|
|
2084
|
+
Array.isArray(AppDefinition.integrations)
|
|
2085
|
+
) {
|
|
2086
|
+
console.log(`Processing ${AppDefinition.integrations.length} integrations...`);
|
|
2087
|
+
for (const integration of AppDefinition.integrations) {
|
|
2088
|
+
if (
|
|
2089
|
+
!integration ||
|
|
2090
|
+
!integration.Definition ||
|
|
2091
|
+
!integration.Definition.name
|
|
2092
|
+
) {
|
|
2093
|
+
throw new Error(
|
|
2094
|
+
'Invalid integration: missing Definition or name'
|
|
2095
|
+
);
|
|
2096
|
+
}
|
|
2097
|
+
const integrationName = integration.Definition.name;
|
|
1474
2098
|
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
},
|
|
2099
|
+
// Add function for the integration
|
|
2100
|
+
definition.functions[integrationName] = {
|
|
2101
|
+
handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
|
|
2102
|
+
events: [
|
|
2103
|
+
{
|
|
2104
|
+
httpApi: {
|
|
2105
|
+
path: `/api/${integrationName}-integration/{proxy+}`,
|
|
2106
|
+
method: 'ANY',
|
|
1484
2107
|
},
|
|
1485
|
-
|
|
1486
|
-
|
|
2108
|
+
},
|
|
2109
|
+
],
|
|
2110
|
+
};
|
|
1487
2111
|
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
},
|
|
2112
|
+
// Add SQS Queue for the integration
|
|
2113
|
+
const queueReference = `${integrationName.charAt(0).toUpperCase() +
|
|
2114
|
+
integrationName.slice(1)
|
|
2115
|
+
}Queue`;
|
|
2116
|
+
const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
|
|
2117
|
+
definition.resources.Resources[queueReference] = {
|
|
2118
|
+
Type: 'AWS::SQS::Queue',
|
|
2119
|
+
Properties: {
|
|
2120
|
+
QueueName: `\${self:custom.${queueReference}}`,
|
|
2121
|
+
MessageRetentionPeriod: 60,
|
|
2122
|
+
VisibilityTimeout: 1800, // 30 minutes
|
|
2123
|
+
RedrivePolicy: {
|
|
2124
|
+
maxReceiveCount: 1,
|
|
2125
|
+
deadLetterTargetArn: {
|
|
2126
|
+
'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
|
|
1504
2127
|
},
|
|
1505
2128
|
},
|
|
1506
|
-
}
|
|
2129
|
+
},
|
|
2130
|
+
};
|
|
1507
2131
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
},
|
|
1519
|
-
batchSize: 1,
|
|
2132
|
+
// Add Queue Worker for the integration
|
|
2133
|
+
const queueWorkerName = `${integrationName}QueueWorker`;
|
|
2134
|
+
definition.functions[queueWorkerName] = {
|
|
2135
|
+
handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
|
|
2136
|
+
reservedConcurrency: 5,
|
|
2137
|
+
events: [
|
|
2138
|
+
{
|
|
2139
|
+
sqs: {
|
|
2140
|
+
arn: {
|
|
2141
|
+
'Fn::GetAtt': [queueReference, 'Arn'],
|
|
1520
2142
|
},
|
|
2143
|
+
batchSize: 1,
|
|
1521
2144
|
},
|
|
1522
|
-
],
|
|
1523
|
-
timeout: 600,
|
|
1524
|
-
};
|
|
1525
|
-
|
|
1526
|
-
// Add Queue URL for the integration to the ENVironment variables
|
|
1527
|
-
definition.provider.environment = {
|
|
1528
|
-
...definition.provider.environment,
|
|
1529
|
-
[`${integrationName.toUpperCase()}_QUEUE_URL`]: {
|
|
1530
|
-
Ref: queueReference,
|
|
1531
2145
|
},
|
|
1532
|
-
|
|
2146
|
+
],
|
|
2147
|
+
timeout: 600,
|
|
2148
|
+
};
|
|
1533
2149
|
|
|
1534
|
-
|
|
1535
|
-
|
|
2150
|
+
// Add Queue URL for the integration to the ENVironment variables
|
|
2151
|
+
definition.provider.environment = {
|
|
2152
|
+
...definition.provider.environment,
|
|
2153
|
+
[`${integrationName.toUpperCase()}_QUEUE_URL`]: {
|
|
2154
|
+
Ref: queueReference,
|
|
2155
|
+
},
|
|
2156
|
+
};
|
|
2157
|
+
|
|
2158
|
+
definition.custom[queueReference] = queueName;
|
|
1536
2159
|
}
|
|
1537
2160
|
}
|
|
1538
2161
|
|