@friggframework/devtools 2.0.0-next.53 → 2.0.0-next.55

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.
Files changed (32) hide show
  1. package/frigg-cli/README.md +13 -14
  2. package/frigg-cli/__tests__/unit/commands/db-setup.test.js +267 -166
  3. package/frigg-cli/__tests__/unit/utils/database-validator.test.js +45 -14
  4. package/frigg-cli/__tests__/unit/utils/error-messages.test.js +44 -3
  5. package/frigg-cli/db-setup-command/index.js +75 -22
  6. package/frigg-cli/deploy-command/index.js +6 -3
  7. package/frigg-cli/utils/database-validator.js +18 -5
  8. package/frigg-cli/utils/error-messages.js +84 -12
  9. package/infrastructure/README.md +28 -0
  10. package/infrastructure/domains/database/migration-builder.js +26 -20
  11. package/infrastructure/domains/database/migration-builder.test.js +27 -0
  12. package/infrastructure/domains/integration/integration-builder.js +17 -13
  13. package/infrastructure/domains/integration/integration-builder.test.js +23 -0
  14. package/infrastructure/domains/networking/vpc-builder.js +240 -18
  15. package/infrastructure/domains/networking/vpc-builder.test.js +711 -13
  16. package/infrastructure/domains/networking/vpc-resolver.js +221 -40
  17. package/infrastructure/domains/networking/vpc-resolver.test.js +318 -18
  18. package/infrastructure/domains/security/kms-builder.js +55 -6
  19. package/infrastructure/domains/security/kms-builder.test.js +19 -1
  20. package/infrastructure/domains/shared/cloudformation-discovery.js +310 -13
  21. package/infrastructure/domains/shared/cloudformation-discovery.test.js +395 -0
  22. package/infrastructure/domains/shared/providers/aws-provider-adapter.js +41 -6
  23. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +39 -0
  24. package/infrastructure/domains/shared/resource-discovery.js +17 -5
  25. package/infrastructure/domains/shared/resource-discovery.test.js +36 -0
  26. package/infrastructure/domains/shared/utilities/base-definition-factory.js +30 -20
  27. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +43 -0
  28. package/infrastructure/infrastructure-composer.js +11 -3
  29. package/infrastructure/scripts/build-prisma-layer.js +153 -78
  30. package/infrastructure/scripts/build-prisma-layer.test.js +27 -11
  31. package/layers/prisma/.build-complete +3 -0
  32. package/package.json +7 -7
@@ -149,7 +149,7 @@ class VpcBuilder extends InfrastructureBuilder {
149
149
  physicalId = flatDiscovery.defaultVpcId;
150
150
  } else if (logicalId === 'FriggLambdaSecurityGroup') {
151
151
  resourceType = 'AWS::EC2::SecurityGroup';
152
- physicalId = flatDiscovery.defaultSecurityGroupId || flatDiscovery.securityGroupId;
152
+ physicalId = flatDiscovery.lambdaSecurityGroupId || flatDiscovery.defaultSecurityGroupId || flatDiscovery.securityGroupId;
153
153
  } else if (logicalId === 'FriggPrivateSubnet1') {
154
154
  resourceType = 'AWS::EC2::Subnet';
155
155
  physicalId = flatDiscovery.privateSubnetId1;
@@ -159,21 +159,27 @@ class VpcBuilder extends InfrastructureBuilder {
159
159
  } else if (logicalId === 'FriggNATGateway') {
160
160
  resourceType = 'AWS::EC2::NatGateway';
161
161
  physicalId = flatDiscovery.existingNatGatewayId;
162
- } else if (logicalId === 'FriggS3VPCEndpoint') {
162
+ } else if (logicalId === 'FriggLambdaRouteTable') {
163
+ resourceType = 'AWS::EC2::RouteTable';
164
+ physicalId = flatDiscovery.routeTableId;
165
+ } else if (logicalId === 'FriggS3VPCEndpoint' || logicalId === 'VPCEndpointS3') {
163
166
  resourceType = 'AWS::EC2::VPCEndpoint';
164
167
  physicalId = flatDiscovery.s3VpcEndpointId;
165
- } else if (logicalId === 'FriggDynamoDBVPCEndpoint') {
168
+ } else if (logicalId === 'FriggDynamoDBVPCEndpoint' || logicalId === 'VPCEndpointDynamoDB') {
166
169
  resourceType = 'AWS::EC2::VPCEndpoint';
167
170
  physicalId = flatDiscovery.dynamodbVpcEndpointId;
168
- } else if (logicalId === 'FriggKMSVPCEndpoint') {
171
+ } else if (logicalId === 'FriggKMSVPCEndpoint' || logicalId === 'VPCEndpointKMS') {
169
172
  resourceType = 'AWS::EC2::VPCEndpoint';
170
173
  physicalId = flatDiscovery.kmsVpcEndpointId;
171
- } else if (logicalId === 'FriggSecretsManagerVPCEndpoint') {
174
+ } else if (logicalId === 'FriggSecretsManagerVPCEndpoint' || logicalId === 'VPCEndpointSecretsManager') {
172
175
  resourceType = 'AWS::EC2::VPCEndpoint';
173
176
  physicalId = flatDiscovery.secretsManagerVpcEndpointId;
174
- } else if (logicalId === 'FriggSQSVPCEndpoint') {
177
+ } else if (logicalId === 'FriggSQSVPCEndpoint' || logicalId === 'VPCEndpointSQS') {
175
178
  resourceType = 'AWS::EC2::VPCEndpoint';
176
179
  physicalId = flatDiscovery.sqsVpcEndpointId;
180
+ } else if (logicalId === 'FriggNATRoute' || logicalId === 'FriggPrivateRoute') {
181
+ resourceType = 'AWS::EC2::Route';
182
+ physicalId = flatDiscovery.natRoute;
177
183
  }
178
184
 
179
185
  if (physicalId && typeof physicalId === 'string') {
@@ -184,6 +190,11 @@ class VpcBuilder extends InfrastructureBuilder {
184
190
  });
185
191
  }
186
192
  });
193
+
194
+ // Also check for external resources extracted via CloudFormation queries
195
+ // (e.g., VPC ID from security group query, subnets from route table associations)
196
+ // These are NOT in the stack but were discovered through stack resources
197
+ this._addExternalResourcesFromCloudFormationQueries(flatDiscovery, discovery, existingLogicalIds);
187
198
  } else {
188
199
  // Resources discovered from AWS API (not CloudFormation)
189
200
  // These go into external array
@@ -287,9 +298,70 @@ class VpcBuilder extends InfrastructureBuilder {
287
298
  }
288
299
  }
289
300
 
301
+ // Add flat discovery properties directly to discovery object for resolver access
302
+ // The resolver checks both discovery.defaultSecurityGroupId and discovery.external array
303
+ discovery.defaultVpcId = flatDiscovery.defaultVpcId;
304
+ discovery.defaultSecurityGroupId = flatDiscovery.defaultSecurityGroupId;
305
+ discovery.privateSubnetId1 = flatDiscovery.privateSubnetId1;
306
+ discovery.privateSubnetId2 = flatDiscovery.privateSubnetId2;
307
+ discovery.natGatewayId = flatDiscovery.natGatewayId;
308
+ discovery.lambdaSecurityGroupId = flatDiscovery.lambdaSecurityGroupId;
309
+
290
310
  return discovery;
291
311
  }
292
312
 
313
+ /**
314
+ * Add external resources that were discovered via CloudFormation queries
315
+ * (e.g., VPC ID extracted from security group, subnets from route table associations)
316
+ *
317
+ * @private
318
+ */
319
+ _addExternalResourcesFromCloudFormationQueries(flatDiscovery, discovery, existingLogicalIds) {
320
+ // VPC ID extracted from SG or route table (NOT a stack resource)
321
+ if (flatDiscovery.defaultVpcId &&
322
+ typeof flatDiscovery.defaultVpcId === 'string' &&
323
+ !existingLogicalIds.includes('FriggVPC')) {
324
+ discovery.external.push({
325
+ physicalId: flatDiscovery.defaultVpcId,
326
+ resourceType: 'AWS::EC2::VPC',
327
+ source: 'cloudformation-query'
328
+ });
329
+ }
330
+
331
+ // Subnets extracted from route table associations (NOT stack resources)
332
+ if (flatDiscovery.privateSubnetId1 &&
333
+ typeof flatDiscovery.privateSubnetId1 === 'string' &&
334
+ !existingLogicalIds.includes('FriggPrivateSubnet1')) {
335
+ discovery.external.push({
336
+ physicalId: flatDiscovery.privateSubnetId1,
337
+ resourceType: 'AWS::EC2::Subnet',
338
+ source: 'cloudformation-query'
339
+ });
340
+ }
341
+
342
+ if (flatDiscovery.privateSubnetId2 &&
343
+ typeof flatDiscovery.privateSubnetId2 === 'string' &&
344
+ !existingLogicalIds.includes('FriggPrivateSubnet2')) {
345
+ discovery.external.push({
346
+ physicalId: flatDiscovery.privateSubnetId2,
347
+ resourceType: 'AWS::EC2::Subnet',
348
+ source: 'cloudformation-query'
349
+ });
350
+ }
351
+
352
+ // NAT Gateway extracted from route table routes
353
+ if (flatDiscovery.existingNatGatewayId &&
354
+ typeof flatDiscovery.existingNatGatewayId === 'string' &&
355
+ !existingLogicalIds.includes('FriggNATGateway') &&
356
+ !existingLogicalIds.includes('FriggNatGateway')) {
357
+ discovery.external.push({
358
+ physicalId: flatDiscovery.existingNatGatewayId,
359
+ resourceType: 'AWS::EC2::NatGateway',
360
+ source: 'cloudformation-query'
361
+ });
362
+ }
363
+ }
364
+
293
365
  /**
294
366
  * Translate legacy configuration (management modes) to new ownership-based configuration
295
367
  * Provides backwards compatibility for existing app definitions
@@ -465,6 +537,7 @@ class VpcBuilder extends InfrastructureBuilder {
465
537
  iamStatements: [],
466
538
  outputs: {},
467
539
  environment: {},
540
+ discovery: discoveredResources, // Store for backwards compatibility checks
468
541
  };
469
542
 
470
543
  // Add IAM permissions for VPC-enabled Lambda functions
@@ -483,7 +556,7 @@ class VpcBuilder extends InfrastructureBuilder {
483
556
  this.buildNatGatewayFromDecision(decisions.natGateway, appDefinition, discoveredResources, result);
484
557
 
485
558
  // Build VPC Endpoints based on ownership decisions
486
- this.buildVpcEndpointsFromDecisions(decisions.vpcEndpoints, appDefinition, result);
559
+ this.buildVpcEndpointsFromDecisions(decisions.vpcEndpoints, decisions.securityGroup, appDefinition, discoveredResources, result);
487
560
 
488
561
  // Set VPC_ENABLED environment variable
489
562
  result.environment.VPC_ENABLED = 'true';
@@ -517,11 +590,10 @@ class VpcBuilder extends InfrastructureBuilder {
517
590
  * Build VPC based on ownership decision
518
591
  *
519
592
  * For STACK ownership: ALWAYS add definitions to template.
520
- * CloudFormation idempotency ensures existing resources aren't recreated.
521
593
  */
522
594
  buildVpcFromDecision(decision, appDefinition, result) {
523
595
  if (decision.ownership === ResourceOwnership.STACK) {
524
- // For STACK ownership: ALWAYS create definitions (CloudFormation idempotency)
596
+ // For STACK ownership: ALWAYS create definitions
525
597
  if (decision.physicalId) {
526
598
  console.log(` → Adding VPC definition to template (existing: ${decision.physicalId})`);
527
599
  } else {
@@ -580,7 +652,6 @@ class VpcBuilder extends InfrastructureBuilder {
580
652
  buildSecurityGroupFromDecision(decision, appDefinition, result) {
581
653
  if (decision.ownership === ResourceOwnership.STACK) {
582
654
  // Always create security group resource in template
583
- // CloudFormation handles idempotency if it already exists
584
655
  console.log(' → Adding Lambda Security Group to template...');
585
656
 
586
657
  result.resources.FriggLambdaSecurityGroup = {
@@ -630,7 +701,6 @@ class VpcBuilder extends InfrastructureBuilder {
630
701
  }
631
702
 
632
703
  // For STACK ownership: ALWAYS add definitions to template
633
- // CloudFormation idempotency ensures existing resources won't be recreated
634
704
  if (decision.physicalIds && decision.physicalIds.length >= 2) {
635
705
  console.log(` → Adding subnet definitions to template (existing: ${decision.physicalIds.join(', ')})`);
636
706
  } else {
@@ -854,7 +924,8 @@ class VpcBuilder extends InfrastructureBuilder {
854
924
  /**
855
925
  * Build VPC Endpoints based on ownership decisions
856
926
  */
857
- buildVpcEndpointsFromDecisions(decisions, appDefinition, result) {
927
+ buildVpcEndpointsFromDecisions(endpointDecisions, securityGroupDecision, appDefinition, discoveredResources, result) {
928
+ const decisions = endpointDecisions; // For backwards compatibility with existing code
858
929
  const endpointsToCreate = [];
859
930
  const endpointsInStack = [];
860
931
  const externalEndpoints = [];
@@ -872,6 +943,8 @@ class VpcBuilder extends InfrastructureBuilder {
872
943
 
873
944
  if (endpointsInStack.length > 0) {
874
945
  console.log(` ✓ VPC Endpoints in stack: ${endpointsInStack.join(', ')}`);
946
+ // CRITICAL: Must add stack-managed endpoints back to template or CloudFormation will DELETE them!
947
+ this._addStackManagedEndpointsToTemplate(decisions, securityGroupDecision, discoveredResources, result);
875
948
  }
876
949
 
877
950
  if (externalEndpoints.length > 0) {
@@ -934,6 +1007,15 @@ class VpcBuilder extends InfrastructureBuilder {
934
1007
  // Create security group for interface endpoints if needed
935
1008
  const needsInterfaceEndpoints = endpointsToCreate.some(type => ['kms', 'secretsManager', 'sqs'].includes(type));
936
1009
  if (needsInterfaceEndpoints) {
1010
+ // Determine source security group for ingress rule
1011
+ let sourceSgId;
1012
+ if (securityGroupDecision.ownership === ResourceOwnership.STACK) {
1013
+ sourceSgId = { Ref: 'FriggLambdaSecurityGroup' };
1014
+ } else {
1015
+ // External - use the physical ID
1016
+ sourceSgId = securityGroupDecision.physicalIds[0];
1017
+ }
1018
+
937
1019
  result.resources.FriggVPCEndpointSecurityGroup = {
938
1020
  Type: 'AWS::EC2::SecurityGroup',
939
1021
  Properties: {
@@ -944,7 +1026,7 @@ class VpcBuilder extends InfrastructureBuilder {
944
1026
  IpProtocol: 'tcp',
945
1027
  FromPort: 443,
946
1028
  ToPort: 443,
947
- SourceSecurityGroupId: { Ref: 'FriggLambdaSecurityGroup' },
1029
+ SourceSecurityGroupId: sourceSgId,
948
1030
  Description: 'HTTPS from Lambda',
949
1031
  },
950
1032
  ],
@@ -1052,6 +1134,119 @@ class VpcBuilder extends InfrastructureBuilder {
1052
1134
  return healingReport;
1053
1135
  }
1054
1136
 
1137
+ /**
1138
+ * Add stack-managed VPC endpoints back to template
1139
+ *
1140
+ * CRITICAL: CloudFormation will DELETE resources that exist in the previous template
1141
+ * but are missing from the new template. We must re-add discovered stack-managed
1142
+ * endpoints to prevent CloudFormation from deleting them.
1143
+ *
1144
+ * @private
1145
+ */
1146
+ _addStackManagedEndpointsToTemplate(endpointDecisions, securityGroupDecision, discoveredResources, result) {
1147
+ const decisions = endpointDecisions; // For backwards compatibility
1148
+ const vpcId = result.vpcId;
1149
+
1150
+ // Determine logical IDs based on what exists in stack for backwards compatibility
1151
+ // CRITICAL: Frontify production uses OLD naming (VPCEndpointS3, not FriggS3VPCEndpoint)
1152
+ const existingLogicalIds = discoveredResources?.existingLogicalIds || [];
1153
+
1154
+
1155
+ const logicalIdMap = {
1156
+ s3: existingLogicalIds.includes('VPCEndpointS3') ? 'VPCEndpointS3' : 'FriggS3VPCEndpoint',
1157
+ dynamodb: existingLogicalIds.includes('VPCEndpointDynamoDB') ? 'VPCEndpointDynamoDB' : 'FriggDynamoDBVPCEndpoint',
1158
+ kms: existingLogicalIds.includes('VPCEndpointKMS') ? 'VPCEndpointKMS' : 'FriggKMSVPCEndpoint',
1159
+ secretsManager: existingLogicalIds.includes('VPCEndpointSecretsManager') ? 'VPCEndpointSecretsManager' : 'FriggSecretsManagerVPCEndpoint',
1160
+ sqs: existingLogicalIds.includes('VPCEndpointSQS') ? 'VPCEndpointSQS' : 'FriggSQSVPCEndpoint'
1161
+ };
1162
+
1163
+ Object.entries(decisions).forEach(([type, decision]) => {
1164
+ if (decision.ownership === ResourceOwnership.STACK) {
1165
+ const logicalId = logicalIdMap[type];
1166
+
1167
+ // Determine endpoint type and properties based on service
1168
+ if (type === 's3') {
1169
+ result.resources[logicalId] = {
1170
+ Type: 'AWS::EC2::VPCEndpoint',
1171
+ Properties: {
1172
+ VpcId: vpcId,
1173
+ ServiceName: 'com.amazonaws.${self:provider.region}.s3',
1174
+ VpcEndpointType: 'Gateway',
1175
+ RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }]
1176
+ }
1177
+ };
1178
+ } else if (type === 'dynamodb') {
1179
+ result.resources[logicalId] = {
1180
+ Type: 'AWS::EC2::VPCEndpoint',
1181
+ Properties: {
1182
+ VpcId: vpcId,
1183
+ ServiceName: 'com.amazonaws.${self:provider.region}.dynamodb',
1184
+ VpcEndpointType: 'Gateway',
1185
+ RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }]
1186
+ }
1187
+ };
1188
+ } else {
1189
+ // Interface endpoints (KMS, Secrets Manager, SQS)
1190
+ const serviceMap = {
1191
+ kms: 'kms',
1192
+ secretsManager: 'secretsmanager',
1193
+ sqs: 'sqs'
1194
+ };
1195
+
1196
+ result.resources[logicalId] = {
1197
+ Type: 'AWS::EC2::VPCEndpoint',
1198
+ Properties: {
1199
+ VpcId: vpcId,
1200
+ ServiceName: `com.amazonaws.\${self:provider.region}.${serviceMap[type]}`,
1201
+ VpcEndpointType: 'Interface',
1202
+ SubnetIds: result.vpcConfig.subnetIds,
1203
+ SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
1204
+ PrivateDnsEnabled: true
1205
+ }
1206
+ };
1207
+ }
1208
+ }
1209
+ });
1210
+
1211
+ // If any interface endpoints exist, ensure security group is in template
1212
+ const hasInterfaceEndpoints = ['kms', 'secretsManager', 'sqs'].some(
1213
+ type => decisions[type]?.ownership === ResourceOwnership.STACK && decisions[type]?.physicalId
1214
+ );
1215
+
1216
+ if (hasInterfaceEndpoints && !result.resources.FriggVPCEndpointSecurityGroup) {
1217
+ // Determine source security group for ingress rule
1218
+ // If Lambda SG is stack-managed, use CloudFormation Ref
1219
+ // If Lambda SG is external, use the physical ID directly
1220
+ let sourceSgId;
1221
+ if (securityGroupDecision.ownership === ResourceOwnership.STACK) {
1222
+ sourceSgId = { Ref: 'FriggLambdaSecurityGroup' };
1223
+ } else {
1224
+ // External - use the physical ID
1225
+ sourceSgId = securityGroupDecision.physicalIds[0];
1226
+ }
1227
+
1228
+ result.resources.FriggVPCEndpointSecurityGroup = {
1229
+ Type: 'AWS::EC2::SecurityGroup',
1230
+ Properties: {
1231
+ GroupDescription: 'Security group for VPC Endpoints',
1232
+ VpcId: vpcId,
1233
+ SecurityGroupIngress: [
1234
+ {
1235
+ IpProtocol: 'tcp',
1236
+ FromPort: 443,
1237
+ ToPort: 443,
1238
+ SourceSecurityGroupId: sourceSgId
1239
+ }
1240
+ ],
1241
+ Tags: [
1242
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg' },
1243
+ { Key: 'ManagedBy', Value: 'Frigg' }
1244
+ ]
1245
+ }
1246
+ };
1247
+ }
1248
+ }
1249
+
1055
1250
  /**
1056
1251
  * Build new VPC from scratch
1057
1252
  */
@@ -1571,8 +1766,29 @@ class VpcBuilder extends InfrastructureBuilder {
1571
1766
 
1572
1767
  /**
1573
1768
  * Create route table and associations for NAT Gateway
1769
+ * Always adds to template - CloudFormation handles idempotency
1770
+ * Uses existing logical IDs from stack to prevent AlreadyExists errors
1574
1771
  */
1575
1772
  createNatGatewayRouting(appDefinition, discoveredResources, result, natGatewayId) {
1773
+ // Note: We always add routing resources to the template.
1774
+ // CloudFormation's idempotency ensures existing resources are updated, not recreated.
1775
+ // Removing resources from the template causes CloudFormation to try CREATE on next deploy → AlreadyExists error
1776
+
1777
+ // Determine which logical ID to use for the NAT route based on what exists in stack
1778
+ // Older stacks use 'FriggNATRoute', newer ones use 'FriggPrivateRoute'
1779
+ // CRITICAL: Must check existingLogicalIds to avoid AlreadyExists errors on logical ID mismatch
1780
+ const existingLogicalIds = discoveredResources?.existingLogicalIds || [];
1781
+
1782
+ const routeLogicalId = existingLogicalIds.includes('FriggNATRoute')
1783
+ ? 'FriggNATRoute' // Use existing logical ID from stack (backwards compatibility)
1784
+ : 'FriggPrivateRoute'; // Default for new stacks
1785
+
1786
+ // Always use new logical IDs to force recreation and fix drift
1787
+ // Old IDs (FriggSubnet1RouteAssociation) may have drifted from CloudFormation state
1788
+ // Using new IDs forces CloudFormation to delete old and create new associations
1789
+ const subnet1AssocLogicalId = 'FriggPrivateSubnet1RouteTableAssociation';
1790
+ const subnet2AssocLogicalId = 'FriggPrivateSubnet2RouteTableAssociation';
1791
+
1576
1792
  // Private route table with NAT Gateway route
1577
1793
  if (!result.resources.FriggLambdaRouteTable) {
1578
1794
  result.resources.FriggLambdaRouteTable = {
@@ -1587,7 +1803,7 @@ class VpcBuilder extends InfrastructureBuilder {
1587
1803
  };
1588
1804
  }
1589
1805
 
1590
- result.resources.FriggPrivateRoute = {
1806
+ result.resources[routeLogicalId] = {
1591
1807
  Type: 'AWS::EC2::Route',
1592
1808
  Properties: {
1593
1809
  RouteTableId: { Ref: 'FriggLambdaRouteTable' },
@@ -1601,16 +1817,18 @@ class VpcBuilder extends InfrastructureBuilder {
1601
1817
  const subnet1Id = discoveredResources.privateSubnetId1 || { Ref: 'FriggPrivateSubnet1' };
1602
1818
  const subnet2Id = discoveredResources.privateSubnetId2 || { Ref: 'FriggPrivateSubnet2' };
1603
1819
 
1604
- result.resources.FriggPrivateSubnet1RouteTableAssociation = {
1820
+ result.resources[subnet1AssocLogicalId] = {
1605
1821
  Type: 'AWS::EC2::SubnetRouteTableAssociation',
1822
+ UpdateReplacePolicy: 'Delete',
1606
1823
  Properties: {
1607
1824
  SubnetId: subnet1Id,
1608
1825
  RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1609
1826
  },
1610
1827
  };
1611
1828
 
1612
- result.resources.FriggPrivateSubnet2RouteTableAssociation = {
1829
+ result.resources[subnet2AssocLogicalId] = {
1613
1830
  Type: 'AWS::EC2::SubnetRouteTableAssociation',
1831
+ UpdateReplacePolicy: 'Delete',
1614
1832
  Properties: {
1615
1833
  SubnetId: subnet2Id,
1616
1834
  RouteTableId: { Ref: 'FriggLambdaRouteTable' },
@@ -1626,7 +1844,9 @@ class VpcBuilder extends InfrastructureBuilder {
1626
1844
  */
1627
1845
  ensureSubnetAssociations(appDefinition, discoveredResources, result) {
1628
1846
  // Skip if associations already created (by NAT Gateway routing)
1629
- if (result.resources.FriggPrivateSubnet1RouteTableAssociation) {
1847
+ // Check for both old and new logical ID patterns
1848
+ if (result.resources.FriggPrivateSubnet1RouteTableAssociation ||
1849
+ result.resources.FriggSubnet1RouteAssociation) {
1630
1850
  return; // Already handled by NAT Gateway routing
1631
1851
  }
1632
1852
 
@@ -1636,6 +1856,7 @@ class VpcBuilder extends InfrastructureBuilder {
1636
1856
 
1637
1857
  result.resources.FriggPrivateSubnet1RouteTableAssociation = {
1638
1858
  Type: 'AWS::EC2::SubnetRouteTableAssociation',
1859
+ UpdateReplacePolicy: 'Delete',
1639
1860
  Properties: {
1640
1861
  SubnetId: subnet1Id,
1641
1862
  RouteTableId: routeTableId,
@@ -1644,6 +1865,7 @@ class VpcBuilder extends InfrastructureBuilder {
1644
1865
 
1645
1866
  result.resources.FriggPrivateSubnet2RouteTableAssociation = {
1646
1867
  Type: 'AWS::EC2::SubnetRouteTableAssociation',
1868
+ UpdateReplacePolicy: 'Delete',
1647
1869
  Properties: {
1648
1870
  SubnetId: subnet2Id,
1649
1871
  RouteTableId: routeTableId,
@@ -1661,7 +1883,7 @@ class VpcBuilder extends InfrastructureBuilder {
1661
1883
  // Stack-managed resources should be reused, not recreated
1662
1884
  const stackManagedEndpoints = {
1663
1885
  s3: discoveredResources.s3VpcEndpointId && typeof discoveredResources.s3VpcEndpointId === 'string',
1664
- dynamodb: discoveredResources.dynamoDbVpcEndpointId && typeof discoveredResources.dynamoDbVpcEndpointId === 'string',
1886
+ dynamodb: discoveredResources.dynamodbVpcEndpointId && typeof discoveredResources.dynamodbVpcEndpointId === 'string',
1665
1887
  kms: discoveredResources.kmsVpcEndpointId && typeof discoveredResources.kmsVpcEndpointId === 'string',
1666
1888
  secretsManager: discoveredResources.secretsManagerVpcEndpointId && typeof discoveredResources.secretsManagerVpcEndpointId === 'string',
1667
1889
  sqs: discoveredResources.sqsVpcEndpointId && typeof discoveredResources.sqsVpcEndpointId === 'string',