@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.
- package/frigg-cli/README.md +13 -14
- package/frigg-cli/__tests__/unit/commands/db-setup.test.js +267 -166
- package/frigg-cli/__tests__/unit/utils/database-validator.test.js +45 -14
- package/frigg-cli/__tests__/unit/utils/error-messages.test.js +44 -3
- package/frigg-cli/db-setup-command/index.js +75 -22
- package/frigg-cli/deploy-command/index.js +6 -3
- package/frigg-cli/utils/database-validator.js +18 -5
- package/frigg-cli/utils/error-messages.js +84 -12
- package/infrastructure/README.md +28 -0
- package/infrastructure/domains/database/migration-builder.js +26 -20
- package/infrastructure/domains/database/migration-builder.test.js +27 -0
- package/infrastructure/domains/integration/integration-builder.js +17 -13
- package/infrastructure/domains/integration/integration-builder.test.js +23 -0
- package/infrastructure/domains/networking/vpc-builder.js +240 -18
- package/infrastructure/domains/networking/vpc-builder.test.js +711 -13
- package/infrastructure/domains/networking/vpc-resolver.js +221 -40
- package/infrastructure/domains/networking/vpc-resolver.test.js +318 -18
- package/infrastructure/domains/security/kms-builder.js +55 -6
- package/infrastructure/domains/security/kms-builder.test.js +19 -1
- package/infrastructure/domains/shared/cloudformation-discovery.js +310 -13
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +395 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.js +41 -6
- package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +39 -0
- package/infrastructure/domains/shared/resource-discovery.js +17 -5
- package/infrastructure/domains/shared/resource-discovery.test.js +36 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +30 -20
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +43 -0
- package/infrastructure/infrastructure-composer.js +11 -3
- package/infrastructure/scripts/build-prisma-layer.js +153 -78
- package/infrastructure/scripts/build-prisma-layer.test.js +27 -11
- package/layers/prisma/.build-complete +3 -0
- 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 === '
|
|
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
|
|
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(
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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',
|