@friggframework/devtools 2.0.0--canary.490.feacde9.0 → 2.0.0--canary.497.a3f25f9.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.
Files changed (26) hide show
  1. package/frigg-cli/deploy-command/index.js +3 -9
  2. package/infrastructure/README.md +0 -28
  3. package/infrastructure/domains/database/migration-builder.js +13 -19
  4. package/infrastructure/domains/database/migration-builder.test.js +0 -57
  5. package/infrastructure/domains/integration/integration-builder.js +14 -19
  6. package/infrastructure/domains/integration/integration-builder.test.js +74 -0
  7. package/infrastructure/domains/networking/vpc-builder.js +18 -240
  8. package/infrastructure/domains/networking/vpc-builder.test.js +13 -711
  9. package/infrastructure/domains/networking/vpc-resolver.js +40 -221
  10. package/infrastructure/domains/networking/vpc-resolver.test.js +18 -318
  11. package/infrastructure/domains/security/kms-builder.js +6 -55
  12. package/infrastructure/domains/security/kms-builder.test.js +1 -19
  13. package/infrastructure/domains/shared/cloudformation-discovery.js +13 -310
  14. package/infrastructure/domains/shared/cloudformation-discovery.test.js +0 -395
  15. package/infrastructure/domains/shared/providers/aws-provider-adapter.js +6 -41
  16. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +0 -39
  17. package/infrastructure/domains/shared/resource-discovery.js +5 -17
  18. package/infrastructure/domains/shared/resource-discovery.test.js +0 -36
  19. package/infrastructure/domains/shared/utilities/base-definition-factory.js +17 -27
  20. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +0 -73
  21. package/infrastructure/infrastructure-composer.js +3 -11
  22. package/infrastructure/scripts/build-prisma-layer.js +81 -8
  23. package/infrastructure/scripts/build-prisma-layer.test.js +53 -1
  24. package/infrastructure/scripts/verify-prisma-layer.js +72 -0
  25. package/package.json +7 -7
  26. package/layers/prisma/.build-complete +0 -3
@@ -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.lambdaSecurityGroupId || flatDiscovery.defaultSecurityGroupId || flatDiscovery.securityGroupId;
152
+ physicalId = flatDiscovery.defaultSecurityGroupId || flatDiscovery.securityGroupId;
153
153
  } else if (logicalId === 'FriggPrivateSubnet1') {
154
154
  resourceType = 'AWS::EC2::Subnet';
155
155
  physicalId = flatDiscovery.privateSubnetId1;
@@ -159,27 +159,21 @@ 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 === 'FriggLambdaRouteTable') {
163
- resourceType = 'AWS::EC2::RouteTable';
164
- physicalId = flatDiscovery.routeTableId;
165
- } else if (logicalId === 'FriggS3VPCEndpoint' || logicalId === 'VPCEndpointS3') {
162
+ } else if (logicalId === 'FriggS3VPCEndpoint') {
166
163
  resourceType = 'AWS::EC2::VPCEndpoint';
167
164
  physicalId = flatDiscovery.s3VpcEndpointId;
168
- } else if (logicalId === 'FriggDynamoDBVPCEndpoint' || logicalId === 'VPCEndpointDynamoDB') {
165
+ } else if (logicalId === 'FriggDynamoDBVPCEndpoint') {
169
166
  resourceType = 'AWS::EC2::VPCEndpoint';
170
167
  physicalId = flatDiscovery.dynamodbVpcEndpointId;
171
- } else if (logicalId === 'FriggKMSVPCEndpoint' || logicalId === 'VPCEndpointKMS') {
168
+ } else if (logicalId === 'FriggKMSVPCEndpoint') {
172
169
  resourceType = 'AWS::EC2::VPCEndpoint';
173
170
  physicalId = flatDiscovery.kmsVpcEndpointId;
174
- } else if (logicalId === 'FriggSecretsManagerVPCEndpoint' || logicalId === 'VPCEndpointSecretsManager') {
171
+ } else if (logicalId === 'FriggSecretsManagerVPCEndpoint') {
175
172
  resourceType = 'AWS::EC2::VPCEndpoint';
176
173
  physicalId = flatDiscovery.secretsManagerVpcEndpointId;
177
- } else if (logicalId === 'FriggSQSVPCEndpoint' || logicalId === 'VPCEndpointSQS') {
174
+ } else if (logicalId === 'FriggSQSVPCEndpoint') {
178
175
  resourceType = 'AWS::EC2::VPCEndpoint';
179
176
  physicalId = flatDiscovery.sqsVpcEndpointId;
180
- } else if (logicalId === 'FriggNATRoute' || logicalId === 'FriggPrivateRoute') {
181
- resourceType = 'AWS::EC2::Route';
182
- physicalId = flatDiscovery.natRoute;
183
177
  }
184
178
 
185
179
  if (physicalId && typeof physicalId === 'string') {
@@ -190,11 +184,6 @@ class VpcBuilder extends InfrastructureBuilder {
190
184
  });
191
185
  }
192
186
  });
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);
198
187
  } else {
199
188
  // Resources discovered from AWS API (not CloudFormation)
200
189
  // These go into external array
@@ -298,70 +287,9 @@ class VpcBuilder extends InfrastructureBuilder {
298
287
  }
299
288
  }
300
289
 
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
-
310
290
  return discovery;
311
291
  }
312
292
 
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
-
365
293
  /**
366
294
  * Translate legacy configuration (management modes) to new ownership-based configuration
367
295
  * Provides backwards compatibility for existing app definitions
@@ -537,7 +465,6 @@ class VpcBuilder extends InfrastructureBuilder {
537
465
  iamStatements: [],
538
466
  outputs: {},
539
467
  environment: {},
540
- discovery: discoveredResources, // Store for backwards compatibility checks
541
468
  };
542
469
 
543
470
  // Add IAM permissions for VPC-enabled Lambda functions
@@ -556,7 +483,7 @@ class VpcBuilder extends InfrastructureBuilder {
556
483
  this.buildNatGatewayFromDecision(decisions.natGateway, appDefinition, discoveredResources, result);
557
484
 
558
485
  // Build VPC Endpoints based on ownership decisions
559
- this.buildVpcEndpointsFromDecisions(decisions.vpcEndpoints, decisions.securityGroup, appDefinition, discoveredResources, result);
486
+ this.buildVpcEndpointsFromDecisions(decisions.vpcEndpoints, appDefinition, result);
560
487
 
561
488
  // Set VPC_ENABLED environment variable
562
489
  result.environment.VPC_ENABLED = 'true';
@@ -590,10 +517,11 @@ class VpcBuilder extends InfrastructureBuilder {
590
517
  * Build VPC based on ownership decision
591
518
  *
592
519
  * For STACK ownership: ALWAYS add definitions to template.
520
+ * CloudFormation idempotency ensures existing resources aren't recreated.
593
521
  */
594
522
  buildVpcFromDecision(decision, appDefinition, result) {
595
523
  if (decision.ownership === ResourceOwnership.STACK) {
596
- // For STACK ownership: ALWAYS create definitions
524
+ // For STACK ownership: ALWAYS create definitions (CloudFormation idempotency)
597
525
  if (decision.physicalId) {
598
526
  console.log(` → Adding VPC definition to template (existing: ${decision.physicalId})`);
599
527
  } else {
@@ -652,6 +580,7 @@ class VpcBuilder extends InfrastructureBuilder {
652
580
  buildSecurityGroupFromDecision(decision, appDefinition, result) {
653
581
  if (decision.ownership === ResourceOwnership.STACK) {
654
582
  // Always create security group resource in template
583
+ // CloudFormation handles idempotency if it already exists
655
584
  console.log(' → Adding Lambda Security Group to template...');
656
585
 
657
586
  result.resources.FriggLambdaSecurityGroup = {
@@ -701,6 +630,7 @@ class VpcBuilder extends InfrastructureBuilder {
701
630
  }
702
631
 
703
632
  // For STACK ownership: ALWAYS add definitions to template
633
+ // CloudFormation idempotency ensures existing resources won't be recreated
704
634
  if (decision.physicalIds && decision.physicalIds.length >= 2) {
705
635
  console.log(` → Adding subnet definitions to template (existing: ${decision.physicalIds.join(', ')})`);
706
636
  } else {
@@ -924,8 +854,7 @@ class VpcBuilder extends InfrastructureBuilder {
924
854
  /**
925
855
  * Build VPC Endpoints based on ownership decisions
926
856
  */
927
- buildVpcEndpointsFromDecisions(endpointDecisions, securityGroupDecision, appDefinition, discoveredResources, result) {
928
- const decisions = endpointDecisions; // For backwards compatibility with existing code
857
+ buildVpcEndpointsFromDecisions(decisions, appDefinition, result) {
929
858
  const endpointsToCreate = [];
930
859
  const endpointsInStack = [];
931
860
  const externalEndpoints = [];
@@ -943,8 +872,6 @@ class VpcBuilder extends InfrastructureBuilder {
943
872
 
944
873
  if (endpointsInStack.length > 0) {
945
874
  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);
948
875
  }
949
876
 
950
877
  if (externalEndpoints.length > 0) {
@@ -1007,15 +934,6 @@ class VpcBuilder extends InfrastructureBuilder {
1007
934
  // Create security group for interface endpoints if needed
1008
935
  const needsInterfaceEndpoints = endpointsToCreate.some(type => ['kms', 'secretsManager', 'sqs'].includes(type));
1009
936
  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
-
1019
937
  result.resources.FriggVPCEndpointSecurityGroup = {
1020
938
  Type: 'AWS::EC2::SecurityGroup',
1021
939
  Properties: {
@@ -1026,7 +944,7 @@ class VpcBuilder extends InfrastructureBuilder {
1026
944
  IpProtocol: 'tcp',
1027
945
  FromPort: 443,
1028
946
  ToPort: 443,
1029
- SourceSecurityGroupId: sourceSgId,
947
+ SourceSecurityGroupId: { Ref: 'FriggLambdaSecurityGroup' },
1030
948
  Description: 'HTTPS from Lambda',
1031
949
  },
1032
950
  ],
@@ -1134,119 +1052,6 @@ class VpcBuilder extends InfrastructureBuilder {
1134
1052
  return healingReport;
1135
1053
  }
1136
1054
 
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
-
1250
1055
  /**
1251
1056
  * Build new VPC from scratch
1252
1057
  */
@@ -1766,29 +1571,8 @@ class VpcBuilder extends InfrastructureBuilder {
1766
1571
 
1767
1572
  /**
1768
1573
  * 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
1771
1574
  */
1772
1575
  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
-
1792
1576
  // Private route table with NAT Gateway route
1793
1577
  if (!result.resources.FriggLambdaRouteTable) {
1794
1578
  result.resources.FriggLambdaRouteTable = {
@@ -1803,7 +1587,7 @@ class VpcBuilder extends InfrastructureBuilder {
1803
1587
  };
1804
1588
  }
1805
1589
 
1806
- result.resources[routeLogicalId] = {
1590
+ result.resources.FriggPrivateRoute = {
1807
1591
  Type: 'AWS::EC2::Route',
1808
1592
  Properties: {
1809
1593
  RouteTableId: { Ref: 'FriggLambdaRouteTable' },
@@ -1817,18 +1601,16 @@ class VpcBuilder extends InfrastructureBuilder {
1817
1601
  const subnet1Id = discoveredResources.privateSubnetId1 || { Ref: 'FriggPrivateSubnet1' };
1818
1602
  const subnet2Id = discoveredResources.privateSubnetId2 || { Ref: 'FriggPrivateSubnet2' };
1819
1603
 
1820
- result.resources[subnet1AssocLogicalId] = {
1604
+ result.resources.FriggPrivateSubnet1RouteTableAssociation = {
1821
1605
  Type: 'AWS::EC2::SubnetRouteTableAssociation',
1822
- UpdateReplacePolicy: 'Delete',
1823
1606
  Properties: {
1824
1607
  SubnetId: subnet1Id,
1825
1608
  RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1826
1609
  },
1827
1610
  };
1828
1611
 
1829
- result.resources[subnet2AssocLogicalId] = {
1612
+ result.resources.FriggPrivateSubnet2RouteTableAssociation = {
1830
1613
  Type: 'AWS::EC2::SubnetRouteTableAssociation',
1831
- UpdateReplacePolicy: 'Delete',
1832
1614
  Properties: {
1833
1615
  SubnetId: subnet2Id,
1834
1616
  RouteTableId: { Ref: 'FriggLambdaRouteTable' },
@@ -1844,9 +1626,7 @@ class VpcBuilder extends InfrastructureBuilder {
1844
1626
  */
1845
1627
  ensureSubnetAssociations(appDefinition, discoveredResources, result) {
1846
1628
  // Skip if associations already created (by NAT Gateway routing)
1847
- // Check for both old and new logical ID patterns
1848
- if (result.resources.FriggPrivateSubnet1RouteTableAssociation ||
1849
- result.resources.FriggSubnet1RouteAssociation) {
1629
+ if (result.resources.FriggPrivateSubnet1RouteTableAssociation) {
1850
1630
  return; // Already handled by NAT Gateway routing
1851
1631
  }
1852
1632
 
@@ -1856,7 +1636,6 @@ class VpcBuilder extends InfrastructureBuilder {
1856
1636
 
1857
1637
  result.resources.FriggPrivateSubnet1RouteTableAssociation = {
1858
1638
  Type: 'AWS::EC2::SubnetRouteTableAssociation',
1859
- UpdateReplacePolicy: 'Delete',
1860
1639
  Properties: {
1861
1640
  SubnetId: subnet1Id,
1862
1641
  RouteTableId: routeTableId,
@@ -1865,7 +1644,6 @@ class VpcBuilder extends InfrastructureBuilder {
1865
1644
 
1866
1645
  result.resources.FriggPrivateSubnet2RouteTableAssociation = {
1867
1646
  Type: 'AWS::EC2::SubnetRouteTableAssociation',
1868
- UpdateReplacePolicy: 'Delete',
1869
1647
  Properties: {
1870
1648
  SubnetId: subnet2Id,
1871
1649
  RouteTableId: routeTableId,
@@ -1883,7 +1661,7 @@ class VpcBuilder extends InfrastructureBuilder {
1883
1661
  // Stack-managed resources should be reused, not recreated
1884
1662
  const stackManagedEndpoints = {
1885
1663
  s3: discoveredResources.s3VpcEndpointId && typeof discoveredResources.s3VpcEndpointId === 'string',
1886
- dynamodb: discoveredResources.dynamodbVpcEndpointId && typeof discoveredResources.dynamodbVpcEndpointId === 'string',
1664
+ dynamodb: discoveredResources.dynamoDbVpcEndpointId && typeof discoveredResources.dynamoDbVpcEndpointId === 'string',
1887
1665
  kms: discoveredResources.kmsVpcEndpointId && typeof discoveredResources.kmsVpcEndpointId === 'string',
1888
1666
  secretsManager: discoveredResources.secretsManagerVpcEndpointId && typeof discoveredResources.secretsManagerVpcEndpointId === 'string',
1889
1667
  sqs: discoveredResources.sqsVpcEndpointId && typeof discoveredResources.sqsVpcEndpointId === 'string',