@friggframework/devtools 2.0.0--canary.428.ade056e.0 → 2.0.0--canary.428.3bab734.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.
@@ -372,7 +372,7 @@ const createVPCInfrastructure = (AppDefinition) => {
372
372
  },
373
373
  },
374
374
 
375
- // Private Route Table
375
+ // Private Route Table for Private Subnets
376
376
  FriggPrivateRouteTable: {
377
377
  Type: 'AWS::EC2::RouteTable',
378
378
  Properties: {
@@ -976,7 +976,7 @@ const composeServerlessDefinition = async (AppDefinition) => {
976
976
 
977
977
  // Configure KMS grants to reference the created key
978
978
  definition.custom.kmsGrants = {
979
- kmsKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] }
979
+ kmsKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
980
980
  };
981
981
  } else {
982
982
  // No key found and createIfNoneFound is not enabled - error
@@ -1086,54 +1086,70 @@ const composeServerlessDefinition = async (AppDefinition) => {
1086
1086
  // ALWAYS manage NAT Gateway through CloudFormation for self-healing
1087
1087
  // This ensures NAT Gateway is always in the correct subnet with proper configuration
1088
1088
 
1089
- // Check if we found a valid NAT Gateway (will be null if in private subnet)
1090
- const needsNewNatGateway = !discoveredResources.existingNatGatewayId;
1089
+ const natGatewayMethod =
1090
+ AppDefinition.vpc.natGateway?.method || 'useExisting';
1091
+ const needsNewNatGateway =
1092
+ natGatewayMethod === 'createAndManage';
1093
+
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
+ );
1100
+ };
1091
1101
 
1092
1102
  if (needsNewNatGateway) {
1093
- console.log('Creating CloudFormation-managed NAT Gateway resources...');
1094
- console.log('Note: Any existing misconfigured NAT Gateways will be replaced by CloudFormation');
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...'
1106
+ );
1095
1107
 
1096
- // Only create EIP if we don't have an existing one available
1097
- if (!discoveredResources.existingElasticIpAllocationId) {
1098
- definition.resources.Resources.FriggNATGatewayEIP = {
1099
- Type: 'AWS::EC2::EIP',
1100
- Properties: {
1101
- Domain: 'vpc',
1102
- Tags: [
1103
- {
1104
- Key: 'Name',
1105
- Value: '${self:service}-${self:provider.stage}-nat-eip',
1106
- },
1107
- ],
1108
- },
1109
- };
1110
- }
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
+ };
1111
1121
 
1112
- // If no public subnet exists, create one for NAT Gateway placement
1122
+ // Create public subnet (ignore any discovered; ensure it's in a matching AZ)
1113
1123
  if (!discoveredResources.publicSubnetId) {
1114
- console.log('No public subnet found, creating one for NAT Gateway placement...');
1124
+ console.log(
1125
+ 'No public subnet found, creating one for NAT Gateway placement...'
1126
+ );
1115
1127
 
1116
1128
  // Check if Internet Gateway exists or create one
1117
1129
  if (!discoveredResources.internetGatewayId) {
1118
- definition.resources.Resources.FriggInternetGateway = {
1119
- Type: 'AWS::EC2::InternetGateway',
1120
- Properties: {
1121
- Tags: [
1122
- {
1123
- Key: 'Name',
1124
- Value: '${self:service}-${self:provider.stage}-igw',
1125
- },
1126
- ],
1127
- },
1128
- };
1130
+ 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
+ };
1129
1142
 
1130
- definition.resources.Resources.FriggIGWAttachment = {
1131
- Type: 'AWS::EC2::VPCGatewayAttachment',
1132
- Properties: {
1133
- VpcId: discoveredResources.defaultVpcId,
1134
- InternetGatewayId: { Ref: 'FriggInternetGateway' },
1135
- },
1136
- };
1143
+ definition.resources.Resources.FriggIGWAttachment =
1144
+ {
1145
+ Type: 'AWS::EC2::VPCGatewayAttachment',
1146
+ Properties: {
1147
+ VpcId: discoveredResources.defaultVpcId,
1148
+ InternetGatewayId: {
1149
+ Ref: 'FriggInternetGateway',
1150
+ },
1151
+ },
1152
+ };
1137
1153
  }
1138
1154
 
1139
1155
  // Create a small public subnet for NAT Gateway
@@ -1141,8 +1157,12 @@ const composeServerlessDefinition = async (AppDefinition) => {
1141
1157
  Type: 'AWS::EC2::Subnet',
1142
1158
  Properties: {
1143
1159
  VpcId: discoveredResources.defaultVpcId,
1144
- CidrBlock: '${self:custom.publicSubnetCidr, "172.31.250.0/24"}', // Small /24 subnet
1145
- AvailabilityZone: '${self:provider.region}a',
1160
+ CidrBlock:
1161
+ AppDefinition.vpc.natGateway
1162
+ ?.publicSubnetCidr || '172.31.250.0/24',
1163
+ AvailabilityZone: {
1164
+ 'Fn::Select': [0, { 'Fn::GetAZs': '' }],
1165
+ },
1146
1166
  MapPublicIpOnLaunch: true,
1147
1167
  Tags: [
1148
1168
  {
@@ -1174,36 +1194,45 @@ const composeServerlessDefinition = async (AppDefinition) => {
1174
1194
  // Add route to Internet Gateway
1175
1195
  definition.resources.Resources.FriggPublicRoute = {
1176
1196
  Type: 'AWS::EC2::Route',
1177
- DependsOn: discoveredResources.internetGatewayId ? [] : 'FriggIGWAttachment',
1197
+ DependsOn: discoveredResources.internetGatewayId
1198
+ ? []
1199
+ : 'FriggIGWAttachment',
1178
1200
  Properties: {
1179
1201
  RouteTableId: { Ref: 'FriggPublicRouteTable' },
1180
1202
  DestinationCidrBlock: '0.0.0.0/0',
1181
- GatewayId: discoveredResources.internetGatewayId || { Ref: 'FriggInternetGateway' },
1203
+ GatewayId:
1204
+ discoveredResources.internetGatewayId || {
1205
+ Ref: 'FriggInternetGateway',
1206
+ },
1182
1207
  },
1183
1208
  };
1184
1209
 
1185
1210
  // Associate public subnet with public route table
1186
- definition.resources.Resources.FriggPublicSubnetRouteTableAssociation = {
1187
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1188
- Properties: {
1189
- SubnetId: { Ref: 'FriggPublicSubnet' },
1190
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
1191
- },
1192
- };
1211
+ definition.resources.Resources.FriggPublicSubnetRouteTableAssociation =
1212
+ {
1213
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1214
+ Properties: {
1215
+ SubnetId: { Ref: 'FriggPublicSubnet' },
1216
+ RouteTableId: {
1217
+ Ref: 'FriggPublicRouteTable',
1218
+ },
1219
+ },
1220
+ };
1193
1221
  }
1194
1222
 
1195
- // ALWAYS create NAT Gateway in CloudFormation for management and self-healing
1223
+ // Create NAT Gateway using the new resources
1196
1224
  definition.resources.Resources.FriggNATGateway = {
1197
1225
  Type: 'AWS::EC2::NatGateway',
1198
1226
  Properties: {
1199
- AllocationId:
1200
- discoveredResources.existingElasticIpAllocationId || {
1201
- 'Fn::GetAtt': [
1202
- 'FriggNATGatewayEIP',
1203
- 'AllocationId',
1204
- ],
1205
- },
1206
- SubnetId: discoveredResources.publicSubnetId || { Ref: 'FriggPublicSubnet' },
1227
+ AllocationId: {
1228
+ 'Fn::GetAtt': [
1229
+ 'FriggNATGatewayEIP',
1230
+ 'AllocationId',
1231
+ ],
1232
+ },
1233
+ SubnetId: discoveredResources.publicSubnetId || {
1234
+ Ref: 'FriggPublicSubnet',
1235
+ },
1207
1236
  Tags: [
1208
1237
  {
1209
1238
  Key: 'Name',
@@ -1216,15 +1245,32 @@ const composeServerlessDefinition = async (AppDefinition) => {
1216
1245
  ],
1217
1246
  },
1218
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
1261
+ } 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
+ );
1265
+ }
1219
1266
  } else {
1220
- // We have an existing valid NAT Gateway - import it into CloudFormation management
1221
- console.log('Found existing NAT Gateway - importing into CloudFormation management for self-healing...');
1222
-
1223
- // Note: CloudFormation will detect if a NAT Gateway already exists with these properties
1224
- // and will adopt it rather than creating a duplicate
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
+ );
1225
1271
  }
1226
1272
 
1227
- // Create route table for Lambda subnets to use NAT Gateway
1273
+ // Always add route table and routes (referencing the NAT, whether new or existing)
1228
1274
  definition.resources.Resources.FriggLambdaRouteTable = {
1229
1275
  Type: 'AWS::EC2::RouteTable',
1230
1276
  Properties: {
@@ -1240,14 +1286,26 @@ const composeServerlessDefinition = async (AppDefinition) => {
1240
1286
  },
1241
1287
  };
1242
1288
 
1243
- definition.resources.Resources.FriggNATRoute = {
1244
- Type: 'AWS::EC2::Route',
1245
- Properties: {
1246
- RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1247
- DestinationCidrBlock: '0.0.0.0/0',
1248
- NatGatewayId: { Ref: 'FriggNATGateway' }, // Always use CloudFormation-managed NAT Gateway
1249
- },
1250
- };
1289
+ if (needsNewNatGateway) {
1290
+ definition.resources.Resources.FriggNATRoute = {
1291
+ Type: 'AWS::EC2::Route',
1292
+ Properties: {
1293
+ RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1294
+ DestinationCidrBlock: '0.0.0.0/0',
1295
+ NatGatewayId: { Ref: 'FriggNATGateway' },
1296
+ },
1297
+ };
1298
+ } else {
1299
+ definition.resources.Resources.FriggNATRoute = {
1300
+ Type: 'AWS::EC2::Route',
1301
+ Properties: {
1302
+ RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1303
+ DestinationCidrBlock: '0.0.0.0/0',
1304
+ NatGatewayId:
1305
+ discoveredResources.existingNatGatewayId,
1306
+ },
1307
+ };
1308
+ }
1251
1309
 
1252
1310
  // Associate Lambda subnets with NAT Gateway route table
1253
1311
  // Note: This will only work if the subnets aren't already associated with another route table
@@ -1295,37 +1353,48 @@ const composeServerlessDefinition = async (AppDefinition) => {
1295
1353
  };
1296
1354
 
1297
1355
  // Add KMS VPC endpoint if using KMS encryption
1298
- if (AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
1356
+ if (
1357
+ AppDefinition.encryption?.fieldLevelEncryptionMethod ===
1358
+ 'kms'
1359
+ ) {
1299
1360
  // Create security group for VPC endpoints if it doesn't exist
1300
- if (!definition.resources.Resources.VPCEndpointSecurityGroup) {
1301
- definition.resources.Resources.VPCEndpointSecurityGroup = {
1302
- Type: 'AWS::EC2::SecurityGroup',
1303
- Properties: {
1304
- GroupDescription: 'Security group for VPC endpoints',
1305
- VpcId: discoveredResources.defaultVpcId,
1306
- SecurityGroupIngress: [
1307
- {
1308
- IpProtocol: 'tcp',
1309
- FromPort: 443,
1310
- ToPort: 443,
1311
- CidrIp: '172.31.0.0/16', // VPC CIDR
1312
- },
1313
- ],
1314
- Tags: [
1315
- {
1316
- Key: 'Name',
1317
- Value: '${self:service}-${self:provider.stage}-vpc-endpoints-sg',
1318
- },
1319
- ],
1320
- },
1321
- };
1361
+ if (
1362
+ !definition.resources.Resources
1363
+ .VPCEndpointSecurityGroup
1364
+ ) {
1365
+ 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
+ };
1322
1390
  }
1323
1391
 
1324
1392
  definition.resources.Resources.VPCEndpointKMS = {
1325
1393
  Type: 'AWS::EC2::VPCEndpoint',
1326
1394
  Properties: {
1327
1395
  VpcId: discoveredResources.defaultVpcId,
1328
- ServiceName: 'com.amazonaws.${self:provider.region}.kms',
1396
+ ServiceName:
1397
+ 'com.amazonaws.${self:provider.region}.kms',
1329
1398
  VpcEndpointType: 'Interface',
1330
1399
  SubnetIds: vpcConfig.subnetIds,
1331
1400
  SecurityGroupIds: [
@@ -1337,19 +1406,21 @@ const composeServerlessDefinition = async (AppDefinition) => {
1337
1406
 
1338
1407
  // Also add Secrets Manager endpoint if using Secrets Manager
1339
1408
  if (AppDefinition.secretsManager?.enable === true) {
1340
- definition.resources.Resources.VPCEndpointSecretsManager = {
1341
- Type: 'AWS::EC2::VPCEndpoint',
1342
- Properties: {
1343
- VpcId: discoveredResources.defaultVpcId,
1344
- ServiceName: 'com.amazonaws.${self:provider.region}.secretsmanager',
1345
- VpcEndpointType: 'Interface',
1346
- SubnetIds: vpcConfig.subnetIds,
1347
- SecurityGroupIds: [
1348
- { Ref: 'VPCEndpointSecurityGroup' },
1349
- ],
1350
- PrivateDnsEnabled: true,
1351
- },
1352
- };
1409
+ 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
+ };
1353
1424
  }
1354
1425
  }
1355
1426
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/devtools",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.428.ade056e.0",
4
+ "version": "2.0.0--canary.428.3bab734.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-ec2": "^3.835.0",
7
7
  "@aws-sdk/client-kms": "^3.835.0",
@@ -9,8 +9,8 @@
9
9
  "@babel/eslint-parser": "^7.18.9",
10
10
  "@babel/parser": "^7.25.3",
11
11
  "@babel/traverse": "^7.25.3",
12
- "@friggframework/schemas": "2.0.0--canary.428.ade056e.0",
13
- "@friggframework/test": "2.0.0--canary.428.ade056e.0",
12
+ "@friggframework/schemas": "2.0.0--canary.428.3bab734.0",
13
+ "@friggframework/test": "2.0.0--canary.428.3bab734.0",
14
14
  "@hapi/boom": "^10.0.1",
15
15
  "@inquirer/prompts": "^5.3.8",
16
16
  "axios": "^1.7.2",
@@ -32,8 +32,8 @@
32
32
  "serverless-http": "^2.7.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@friggframework/eslint-config": "2.0.0--canary.428.ade056e.0",
36
- "@friggframework/prettier-config": "2.0.0--canary.428.ade056e.0",
35
+ "@friggframework/eslint-config": "2.0.0--canary.428.3bab734.0",
36
+ "@friggframework/prettier-config": "2.0.0--canary.428.3bab734.0",
37
37
  "jest": "^30.1.3",
38
38
  "prettier": "^2.7.1",
39
39
  "serverless": "3.39.0",
@@ -66,5 +66,5 @@
66
66
  "publishConfig": {
67
67
  "access": "public"
68
68
  },
69
- "gitHead": "ade056ef7772afeb2d7eae1b90e698feae9cb4c9"
69
+ "gitHead": "3bab734d2f509d43177d80959ac59df495b3df7b"
70
70
  }