@friggframework/devtools 2.0.0--canary.428.3bab734.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.
@@ -60,15 +60,13 @@ const getAppEnvironmentVars = (AppDefinition) => {
60
60
 
61
61
  if (envKeys.length > 0) {
62
62
  console.log(
63
- ` Found ${
64
- envKeys.length
63
+ ` Found ${envKeys.length
65
64
  } environment variables: ${envKeys.join(', ')}`
66
65
  );
67
66
  }
68
67
  if (skippedKeys.length > 0) {
69
68
  console.log(
70
- ` ⚠️ Skipped ${
71
- skippedKeys.length
69
+ ` ⚠️ Skipped ${skippedKeys.length
72
70
  } reserved AWS Lambda variables: ${skippedKeys.join(', ')}`
73
71
  );
74
72
  }
@@ -231,6 +229,18 @@ const createVPCInfrastructure = (AppDefinition) => {
231
229
  Key: 'Name',
232
230
  Value: '${self:service}-${self:provider.stage}-vpc',
233
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
+ },
234
244
  ],
235
245
  },
236
246
  },
@@ -244,6 +254,18 @@ const createVPCInfrastructure = (AppDefinition) => {
244
254
  Key: 'Name',
245
255
  Value: '${self:service}-${self:provider.stage}-igw',
246
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
+ },
247
269
  ],
248
270
  },
249
271
  },
@@ -270,6 +292,22 @@ const createVPCInfrastructure = (AppDefinition) => {
270
292
  Key: 'Name',
271
293
  Value: '${self:service}-${self:provider.stage}-public-subnet',
272
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
+ },
273
311
  ],
274
312
  },
275
313
  },
@@ -286,6 +324,22 @@ const createVPCInfrastructure = (AppDefinition) => {
286
324
  Key: 'Name',
287
325
  Value: '${self:service}-${self:provider.stage}-private-subnet-1',
288
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
+ },
289
343
  ],
290
344
  },
291
345
  },
@@ -302,6 +356,22 @@ const createVPCInfrastructure = (AppDefinition) => {
302
356
  Key: 'Name',
303
357
  Value: '${self:service}-${self:provider.stage}-private-subnet-2',
304
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
+ },
305
375
  ],
306
376
  },
307
377
  },
@@ -316,6 +386,18 @@ const createVPCInfrastructure = (AppDefinition) => {
316
386
  Key: 'Name',
317
387
  Value: '${self:service}-${self:provider.stage}-nat-eip',
318
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
+ },
319
401
  ],
320
402
  },
321
403
  DependsOn: 'FriggVPCGatewayAttachment',
@@ -334,6 +416,18 @@ const createVPCInfrastructure = (AppDefinition) => {
334
416
  Key: 'Name',
335
417
  Value: '${self:service}-${self:provider.stage}-nat-gateway',
336
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
+ },
337
431
  ],
338
432
  },
339
433
  },
@@ -348,6 +442,22 @@ const createVPCInfrastructure = (AppDefinition) => {
348
442
  Key: 'Name',
349
443
  Value: '${self:service}-${self:provider.stage}-public-rt',
350
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
+ },
351
461
  ],
352
462
  },
353
463
  },
@@ -382,6 +492,22 @@ const createVPCInfrastructure = (AppDefinition) => {
382
492
  Key: 'Name',
383
493
  Value: '${self:service}-${self:provider.stage}-private-rt',
384
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
+ },
385
511
  ],
386
512
  },
387
513
  },
@@ -462,6 +588,18 @@ const createVPCInfrastructure = (AppDefinition) => {
462
588
  Key: 'Name',
463
589
  Value: '${self:service}-${self:provider.stage}-lambda-sg',
464
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
+ },
465
603
  ],
466
604
  },
467
605
  },
@@ -550,6 +688,22 @@ const createVPCInfrastructure = (AppDefinition) => {
550
688
  Key: 'Name',
551
689
  Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg',
552
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
+ },
553
707
  ],
554
708
  },
555
709
  };
@@ -572,6 +726,7 @@ const createVPCInfrastructure = (AppDefinition) => {
572
726
  * @returns {Object} Complete serverless framework configuration
573
727
  */
574
728
  const composeServerlessDefinition = async (AppDefinition) => {
729
+ console.log('composeServerlessDefinition', AppDefinition);
575
730
  // Store discovered resources
576
731
  let discoveredResources = {};
577
732
 
@@ -942,10 +1097,9 @@ const composeServerlessDefinition = async (AppDefinition) => {
942
1097
  Resource: '*',
943
1098
  Condition: {
944
1099
  StringEquals: {
945
- 'kms:ViaService': `lambda.${
946
- process.env.AWS_REGION ||
1100
+ 'kms:ViaService': `lambda.${process.env.AWS_REGION ||
947
1101
  'us-east-1'
948
- }.amazonaws.com`,
1102
+ }.amazonaws.com`,
949
1103
  },
950
1104
  },
951
1105
  },
@@ -982,7 +1136,7 @@ const composeServerlessDefinition = async (AppDefinition) => {
982
1136
  // No key found and createIfNoneFound is not enabled - error
983
1137
  throw new Error(
984
1138
  'KMS field-level encryption is enabled but no KMS key was found. ' +
985
- 'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
1139
+ 'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
986
1140
  );
987
1141
  }
988
1142
  }
@@ -1009,6 +1163,100 @@ const composeServerlessDefinition = async (AppDefinition) => {
1009
1163
  }
1010
1164
  }
1011
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
+
1012
1260
  // VPC Configuration based on App Definition
1013
1261
  if (AppDefinition.vpc?.enable === true) {
1014
1262
  // Add VPC-related IAM permissions
@@ -1024,103 +1272,383 @@ const composeServerlessDefinition = async (AppDefinition) => {
1024
1272
  Resource: '*',
1025
1273
  });
1026
1274
 
1027
- // Default approach: Use AWS Discovery to find existing VPC resources
1028
- if (AppDefinition.vpc.createNew === true) {
1029
- // Option 1: Create new VPC infrastructure (explicit opt-in)
1030
- 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);
1031
1278
 
1032
- if (AppDefinition.vpc.securityGroupIds) {
1033
- // User provided custom security groups
1034
- vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds;
1035
- } else {
1036
- // Use auto-created security group
1037
- vpcConfig.securityGroupIds = [
1038
- { Ref: 'FriggLambdaSecurityGroup' },
1039
- ];
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(', ')}`);
1040
1282
  }
1283
+ }
1041
1284
 
1042
- if (AppDefinition.vpc.subnetIds) {
1043
- // User provided custom subnets
1044
- vpcConfig.subnetIds = AppDefinition.vpc.subnetIds;
1045
- } else {
1046
- // Use auto-created private subnets
1047
- vpcConfig.subnetIds = [
1048
- { Ref: 'FriggPrivateSubnet1' },
1049
- { Ref: 'FriggPrivateSubnet2' },
1050
- ];
1051
- }
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
+ };
1052
1292
 
1053
- // Set VPC config for Lambda functions
1054
- definition.provider.vpc = vpcConfig;
1293
+ console.log(`VPC Management Mode: ${vpcManagement}`);
1055
1294
 
1056
- // Add VPC infrastructure resources to CloudFormation
1295
+ // First, establish VPC context
1296
+ if (vpcManagement === 'create-new') {
1297
+ // Create new VPC infrastructure
1057
1298
  const vpcResources = createVPCInfrastructure(AppDefinition);
1058
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] : []);
1059
1315
  } else {
1060
- // Option 2: Use AWS Discovery (default behavior)
1061
- // VPC configuration using discovered or explicitly provided resources
1062
- const vpcConfig = {
1063
- securityGroupIds:
1064
- AppDefinition.vpc.securityGroupIds ||
1065
- (discoveredResources.defaultSecurityGroupId
1066
- ? [discoveredResources.defaultSecurityGroupId]
1067
- : []),
1068
- subnetIds:
1069
- AppDefinition.vpc.subnetIds ||
1070
- (discoveredResources.privateSubnetId1 &&
1071
- discoveredResources.privateSubnetId2
1072
- ? [
1073
- discoveredResources.privateSubnetId1,
1074
- discoveredResources.privateSubnetId2,
1075
- ]
1076
- : []),
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
+ }
1077
1383
  };
1078
1384
 
1079
- // Set VPC config for Lambda functions only if we have valid subnet IDs
1080
- if (
1081
- vpcConfig.subnetIds.length >= 2 &&
1082
- vpcConfig.securityGroupIds.length > 0
1083
- ) {
1084
- definition.provider.vpc = vpcConfig;
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
+ };
1085
1414
 
1086
- // ALWAYS manage NAT Gateway through CloudFormation for self-healing
1087
- // This ensures NAT Gateway is always in the correct subnet with proper configuration
1415
+ vpcConfig.subnetIds = [
1416
+ { Ref: 'FriggPrivateSubnet1' },
1417
+ { Ref: 'FriggPrivateSubnet2' }
1418
+ ];
1088
1419
 
1089
- const natGatewayMethod =
1090
- AppDefinition.vpc.natGateway?.method || 'useExisting';
1091
- const needsNewNatGateway =
1092
- natGatewayMethod === 'createAndManage';
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
+ };
1093
1436
 
1094
- // Helper function to validate discovered public subnet
1095
- const isValidPublicSubnet = (subnetId, discoveredResources) => {
1096
- // Basic validation - in production, AWSDiscovery should check route tables for IGW routes
1097
- return (
1098
- discoveredResources.publicSubnetHasIgwRoute !== false
1099
- );
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
+ }
1100
1457
  };
1101
1458
 
1102
- if (needsNewNatGateway) {
1103
- // Always create new dedicated resources in create mode to avoid confusion with existing ones
1104
- console.log(
1105
- 'Create mode: Creating dedicated EIP, public subnet, and NAT Gateway...'
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;
1516
+ } else {
1517
+ // Discover mode (default)
1518
+ vpcConfig.subnetIds =
1519
+ AppDefinition.vpc.subnets?.ids?.length > 0
1520
+ ? AppDefinition.vpc.subnets.ids
1521
+ : (discoveredResources.privateSubnetId1 &&
1522
+ discoveredResources.privateSubnetId2
1523
+ ? [
1524
+ discoveredResources.privateSubnetId1,
1525
+ discoveredResources.privateSubnetId2,
1526
+ ]
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.'
1106
1540
  );
1541
+ }
1542
+ }
1543
+ }
1107
1544
 
1108
- // Create EIP (ignore any discovered)
1109
- definition.resources.Resources.FriggNATGatewayEIP = {
1110
- Type: 'AWS::EC2::EIP',
1111
- Properties: {
1112
- Domain: 'vpc',
1113
- Tags: [
1114
- {
1115
- Key: 'Name',
1116
- Value: '${self:service}-${self:provider.stage}-nat-eip',
1117
- },
1118
- ],
1119
- },
1120
- };
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;
1551
+
1552
+ // ALWAYS manage NAT Gateway through CloudFormation for self-healing
1553
+ // This ensures NAT Gateway is always in the correct subnet with proper configuration
1554
+
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
1562
+
1563
+ console.log('needsNewNatGateway', needsNewNatGateway);
1121
1564
 
1122
- // Create public subnet (ignore any discovered; ensure it's in a matching AZ)
1123
- if (!discoveredResources.publicSubnetId) {
1565
+ // Remove unused helper function - validation is done in discovery
1566
+
1567
+ // Variables to track NAT Gateway and EIP reuse
1568
+ let reuseExistingNatGateway = false;
1569
+ let useExistingEip = false;
1570
+
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
+ }
1649
+
1650
+ // Create public subnet if needed (for NAT Gateway placement)
1651
+ if (!discoveredResources.publicSubnetId || discoveredResources.createPublicSubnet) {
1124
1652
  console.log(
1125
1653
  'No public subnet found, creating one for NAT Gateway placement...'
1126
1654
  );
@@ -1128,28 +1656,28 @@ const composeServerlessDefinition = async (AppDefinition) => {
1128
1656
  // Check if Internet Gateway exists or create one
1129
1657
  if (!discoveredResources.internetGatewayId) {
1130
1658
  definition.resources.Resources.FriggInternetGateway =
1131
- {
1132
- Type: 'AWS::EC2::InternetGateway',
1133
- Properties: {
1134
- Tags: [
1135
- {
1136
- Key: 'Name',
1137
- Value: '${self:service}-${self:provider.stage}-igw',
1138
- },
1139
- ],
1140
- },
1141
- };
1659
+ {
1660
+ Type: 'AWS::EC2::InternetGateway',
1661
+ Properties: {
1662
+ Tags: [
1663
+ {
1664
+ Key: 'Name',
1665
+ Value: '${self:service}-${self:provider.stage}-igw',
1666
+ },
1667
+ ],
1668
+ },
1669
+ };
1142
1670
 
1143
1671
  definition.resources.Resources.FriggIGWAttachment =
1144
- {
1145
- Type: 'AWS::EC2::VPCGatewayAttachment',
1146
- Properties: {
1147
- VpcId: discoveredResources.defaultVpcId,
1148
- InternetGatewayId: {
1149
- Ref: 'FriggInternetGateway',
1150
- },
1672
+ {
1673
+ Type: 'AWS::EC2::VPCGatewayAttachment',
1674
+ Properties: {
1675
+ VpcId: discoveredResources.defaultVpcId,
1676
+ InternetGatewayId: {
1677
+ Ref: 'FriggInternetGateway',
1151
1678
  },
1152
- };
1679
+ },
1680
+ };
1153
1681
  }
1154
1682
 
1155
1683
  // Create a small public subnet for NAT Gateway
@@ -1209,65 +1737,114 @@ const composeServerlessDefinition = async (AppDefinition) => {
1209
1737
 
1210
1738
  // Associate public subnet with public route table
1211
1739
  definition.resources.Resources.FriggPublicSubnetRouteTableAssociation =
1212
- {
1213
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1214
- Properties: {
1215
- SubnetId: { Ref: 'FriggPublicSubnet' },
1216
- RouteTableId: {
1217
- Ref: 'FriggPublicRouteTable',
1218
- },
1740
+ {
1741
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1742
+ Properties: {
1743
+ SubnetId: { Ref: 'FriggPublicSubnet' },
1744
+ RouteTableId: {
1745
+ Ref: 'FriggPublicRouteTable',
1219
1746
  },
1220
- };
1747
+ },
1748
+ };
1221
1749
  }
1222
1750
 
1223
- // Create NAT Gateway using the new resources
1224
- definition.resources.Resources.FriggNATGateway = {
1225
- Type: 'AWS::EC2::NatGateway',
1226
- Properties: {
1227
- AllocationId: {
1228
- 'Fn::GetAtt': [
1229
- 'FriggNATGatewayEIP',
1230
- 'AllocationId',
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
+ },
1231
1783
  ],
1232
1784
  },
1233
- SubnetId: discoveredResources.publicSubnetId || {
1234
- Ref: 'FriggPublicSubnet',
1235
- },
1236
- Tags: [
1237
- {
1238
- Key: 'Name',
1239
- Value: '${self:service}-${self:provider.stage}-nat-gateway',
1240
- },
1241
- {
1242
- Key: 'ManagedBy',
1243
- Value: 'CloudFormation',
1244
- },
1245
- ],
1246
- },
1247
- };
1248
- } else if (discoveredResources.existingNatGatewayId) {
1249
- // Reuse mode: Use existing NAT, but validate first
1250
- if (
1251
- discoveredResources.publicSubnetId &&
1252
- isValidPublicSubnet(
1253
- discoveredResources.publicSubnetId,
1254
- discoveredResources
1255
- )
1256
- ) {
1257
- console.log(
1258
- 'Reuse mode: Valid existing NAT found; adding routes...'
1259
- );
1260
- // No new NAT creation; just add routes referencing existingNatGatewayId
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
+ }
1261
1825
  } else {
1262
- throw new Error(
1263
- 'Existing NAT discovered but public subnet is invalid or missing. Set method to "createAndManage" or fix subnet configuration.'
1264
- );
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
+ }
1265
1847
  }
1266
- } else {
1267
- // No NAT and not in create mode: Error out to prevent isolated subnets
1268
- throw new Error(
1269
- 'No existing NAT Gateway found and createAndManage not enabled. Update appDefinition.vpc.natGateway.method or ensure discovery finds a valid NAT.'
1270
- );
1271
1848
  }
1272
1849
 
1273
1850
  // Always add route table and routes (referencing the NAT, whether new or existing)
@@ -1286,47 +1863,96 @@ const composeServerlessDefinition = async (AppDefinition) => {
1286
1863
  },
1287
1864
  };
1288
1865
 
1289
- if (needsNewNatGateway) {
1290
- definition.resources.Resources.FriggNATRoute = {
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 = {
1291
1894
  Type: 'AWS::EC2::Route',
1292
1895
  Properties: {
1293
1896
  RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1294
1897
  DestinationCidrBlock: '0.0.0.0/0',
1295
- NatGatewayId: { Ref: 'FriggNATGateway' },
1898
+ NatGatewayId: natGatewayIdForRoute,
1296
1899
  },
1297
1900
  };
1298
- } else {
1299
- definition.resources.Resources.FriggNATRoute = {
1300
- Type: 'AWS::EC2::Route',
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',
1301
1915
  Properties: {
1916
+ SubnetId: vpcConfig.subnetIds[0],
1302
1917
  RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1303
- DestinationCidrBlock: '0.0.0.0/0',
1304
- NatGatewayId:
1305
- discoveredResources.existingNatGatewayId,
1306
1918
  },
1919
+ DependsOn: 'FriggLambdaRouteTable',
1307
1920
  };
1308
1921
  }
1309
1922
 
1310
- // Associate Lambda subnets with NAT Gateway route table
1311
- // Note: This will only work if the subnets aren't already associated with another route table
1312
- // If deployment fails, manually associate the subnets with the correct route table in AWS Console
1313
- definition.resources.Resources.FriggSubnet1RouteAssociation = {
1314
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1315
- Properties: {
1316
- SubnetId: vpcConfig.subnetIds[0],
1317
- RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1318
- },
1319
- DependsOn: 'FriggLambdaRouteTable',
1320
- };
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
+ }
1321
1933
 
1322
- definition.resources.Resources.FriggSubnet2RouteAssociation = {
1323
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1324
- Properties: {
1325
- SubnetId: vpcConfig.subnetIds[1],
1326
- RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1327
- },
1328
- DependsOn: 'FriggLambdaRouteTable',
1329
- };
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
+ }
1330
1956
 
1331
1957
  // Add VPC endpoints for AWS service optimization (optional but recommended)
1332
1958
  if (AppDefinition.vpc.enableVPCEndpoints !== false) {
@@ -1363,30 +1989,30 @@ const composeServerlessDefinition = async (AppDefinition) => {
1363
1989
  .VPCEndpointSecurityGroup
1364
1990
  ) {
1365
1991
  definition.resources.Resources.VPCEndpointSecurityGroup =
1366
- {
1367
- Type: 'AWS::EC2::SecurityGroup',
1368
- Properties: {
1369
- GroupDescription:
1370
- 'Security group for VPC endpoints',
1371
- VpcId: discoveredResources.defaultVpcId,
1372
- SecurityGroupIngress: [
1373
- {
1374
- IpProtocol: 'tcp',
1375
- FromPort: 443,
1376
- ToPort: 443,
1377
- CidrIp:
1378
- discoveredResources.vpcCidr ||
1379
- '10.0.0.0/16', // Dynamic VPC CIDR
1380
- },
1381
- ],
1382
- Tags: [
1383
- {
1384
- Key: 'Name',
1385
- Value: '${self:service}-${self:provider.stage}-vpc-endpoints-sg',
1386
- },
1387
- ],
1388
- },
1389
- };
1992
+ {
1993
+ Type: 'AWS::EC2::SecurityGroup',
1994
+ Properties: {
1995
+ GroupDescription:
1996
+ 'Security group for VPC endpoints',
1997
+ VpcId: discoveredResources.defaultVpcId,
1998
+ SecurityGroupIngress: [
1999
+ {
2000
+ IpProtocol: 'tcp',
2001
+ FromPort: 443,
2002
+ ToPort: 443,
2003
+ CidrIp:
2004
+ discoveredResources.vpcCidr ||
2005
+ '10.0.0.0/16', // Dynamic VPC CIDR
2006
+ },
2007
+ ],
2008
+ Tags: [
2009
+ {
2010
+ Key: 'Name',
2011
+ Value: '${self:service}-${self:provider.stage}-vpc-endpoints-sg',
2012
+ },
2013
+ ],
2014
+ },
2015
+ };
1390
2016
  }
1391
2017
 
1392
2018
  definition.resources.Resources.VPCEndpointKMS = {
@@ -1407,130 +2033,129 @@ const composeServerlessDefinition = async (AppDefinition) => {
1407
2033
  // Also add Secrets Manager endpoint if using Secrets Manager
1408
2034
  if (AppDefinition.secretsManager?.enable === true) {
1409
2035
  definition.resources.Resources.VPCEndpointSecretsManager =
1410
- {
1411
- Type: 'AWS::EC2::VPCEndpoint',
1412
- Properties: {
1413
- VpcId: discoveredResources.defaultVpcId,
1414
- ServiceName:
1415
- 'com.amazonaws.${self:provider.region}.secretsmanager',
1416
- VpcEndpointType: 'Interface',
1417
- SubnetIds: vpcConfig.subnetIds,
1418
- SecurityGroupIds: [
1419
- { Ref: 'VPCEndpointSecurityGroup' },
1420
- ],
1421
- PrivateDnsEnabled: true,
1422
- },
1423
- };
2036
+ {
2037
+ Type: 'AWS::EC2::VPCEndpoint',
2038
+ Properties: {
2039
+ VpcId: discoveredResources.defaultVpcId,
2040
+ ServiceName:
2041
+ 'com.amazonaws.${self:provider.region}.secretsmanager',
2042
+ VpcEndpointType: 'Interface',
2043
+ SubnetIds: vpcConfig.subnetIds,
2044
+ SecurityGroupIds: [
2045
+ { Ref: 'VPCEndpointSecurityGroup' },
2046
+ ],
2047
+ PrivateDnsEnabled: true,
2048
+ },
2049
+ };
1424
2050
  }
1425
2051
  }
1426
2052
  }
1427
2053
  }
1428
2054
  }
1429
2055
 
1430
- // SSM Parameter Store Configuration based on App Definition
1431
- if (AppDefinition.ssm?.enable === true) {
1432
- // Add AWS Parameters and Secrets Lambda Extension layer
1433
- definition.provider.layers = [
1434
- 'arn:aws:lambda:${self:provider.region}:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11',
1435
- ];
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
+ ];
1436
2062
 
1437
- // Add SSM IAM permissions
1438
- definition.provider.iamRoleStatements.push({
1439
- Effect: 'Allow',
1440
- Action: [
1441
- 'ssm:GetParameter',
1442
- 'ssm:GetParameters',
1443
- 'ssm:GetParametersByPath',
1444
- ],
1445
- Resource: [
1446
- 'arn:aws:ssm:${self:provider.region}:*:parameter/${self:service}/${self:provider.stage}/*',
1447
- ],
1448
- });
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
+ });
1449
2075
 
1450
- // Add environment variable for SSM parameter prefix
1451
- definition.provider.environment.SSM_PARAMETER_PREFIX =
1452
- '/${self:service}/${self:provider.stage}';
1453
- }
2076
+ // Add environment variable for SSM parameter prefix
2077
+ definition.provider.environment.SSM_PARAMETER_PREFIX =
2078
+ '/${self:service}/${self:provider.stage}';
2079
+ }
1454
2080
 
1455
- // Add integration-specific functions and resources
1456
- if (
1457
- AppDefinition.integrations &&
1458
- Array.isArray(AppDefinition.integrations)
1459
- ) {
1460
- for (const integration of AppDefinition.integrations) {
1461
- if (
1462
- !integration ||
1463
- !integration.Definition ||
1464
- !integration.Definition.name
1465
- ) {
1466
- throw new Error(
1467
- 'Invalid integration: missing Definition or name'
1468
- );
1469
- }
1470
- const integrationName = integration.Definition.name;
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;
1471
2098
 
1472
- // Add function for the integration
1473
- definition.functions[integrationName] = {
1474
- handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
1475
- events: [
1476
- {
1477
- httpApi: {
1478
- path: `/api/${integrationName}-integration/{proxy+}`,
1479
- method: 'ANY',
1480
- },
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',
1481
2107
  },
1482
- ],
1483
- };
2108
+ },
2109
+ ],
2110
+ };
1484
2111
 
1485
- // Add SQS Queue for the integration
1486
- const queueReference = `${
1487
- integrationName.charAt(0).toUpperCase() +
1488
- integrationName.slice(1)
2112
+ // Add SQS Queue for the integration
2113
+ const queueReference = `${integrationName.charAt(0).toUpperCase() +
2114
+ integrationName.slice(1)
1489
2115
  }Queue`;
1490
- const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
1491
- definition.resources.Resources[queueReference] = {
1492
- Type: 'AWS::SQS::Queue',
1493
- Properties: {
1494
- QueueName: `\${self:custom.${queueReference}}`,
1495
- MessageRetentionPeriod: 60,
1496
- VisibilityTimeout: 1800, // 30 minutes
1497
- RedrivePolicy: {
1498
- maxReceiveCount: 1,
1499
- deadLetterTargetArn: {
1500
- 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
1501
- },
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'],
1502
2127
  },
1503
2128
  },
1504
- };
2129
+ },
2130
+ };
1505
2131
 
1506
- // Add Queue Worker for the integration
1507
- const queueWorkerName = `${integrationName}QueueWorker`;
1508
- definition.functions[queueWorkerName] = {
1509
- handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
1510
- reservedConcurrency: 5,
1511
- events: [
1512
- {
1513
- sqs: {
1514
- arn: {
1515
- 'Fn::GetAtt': [queueReference, 'Arn'],
1516
- },
1517
- 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'],
1518
2142
  },
2143
+ batchSize: 1,
1519
2144
  },
1520
- ],
1521
- timeout: 600,
1522
- };
1523
-
1524
- // Add Queue URL for the integration to the ENVironment variables
1525
- definition.provider.environment = {
1526
- ...definition.provider.environment,
1527
- [`${integrationName.toUpperCase()}_QUEUE_URL`]: {
1528
- Ref: queueReference,
1529
2145
  },
1530
- };
2146
+ ],
2147
+ timeout: 600,
2148
+ };
1531
2149
 
1532
- definition.custom[queueReference] = queueName;
1533
- }
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;
1534
2159
  }
1535
2160
  }
1536
2161