@friggframework/devtools 2.0.0-next.45 → 2.0.0-next.46
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/infrastructure/ARCHITECTURE.md +487 -0
- package/infrastructure/HEALTH.md +468 -0
- package/infrastructure/README.md +51 -0
- package/infrastructure/__tests__/postgres-config.test.js +914 -0
- package/infrastructure/__tests__/template-generation.test.js +687 -0
- package/infrastructure/create-frigg-infrastructure.js +1 -1
- package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
- package/infrastructure/{DEPLOYMENT-INSTRUCTIONS.md → docs/deployment-instructions.md} +3 -3
- package/infrastructure/{IAM-POLICY-TEMPLATES.md → docs/iam-policy-templates.md} +9 -10
- package/infrastructure/domains/database/aurora-builder.js +809 -0
- package/infrastructure/domains/database/aurora-builder.test.js +950 -0
- package/infrastructure/domains/database/aurora-discovery.js +87 -0
- package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
- package/infrastructure/domains/database/aurora-resolver.js +210 -0
- package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
- package/infrastructure/domains/database/migration-builder.js +633 -0
- package/infrastructure/domains/database/migration-builder.test.js +294 -0
- package/infrastructure/domains/database/migration-resolver.js +163 -0
- package/infrastructure/domains/database/migration-resolver.test.js +337 -0
- package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
- package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
- package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
- package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
- package/infrastructure/domains/health/application/ports/index.js +26 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
- package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
- package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
- package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
- package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
- package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
- package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
- package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
- package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
- package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
- package/infrastructure/domains/health/domain/entities/issue.js +299 -0
- package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
- package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
- package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
- package/infrastructure/domains/health/domain/entities/resource.js +159 -0
- package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
- package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
- package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
- package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
- package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
- package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
- package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
- package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
- package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
- package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
- package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
- package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
- package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
- package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
- package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
- package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
- package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
- package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
- package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
- package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
- package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
- package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
- package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
- package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
- package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
- package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +192 -0
- package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +784 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
- package/infrastructure/domains/integration/integration-builder.js +397 -0
- package/infrastructure/domains/integration/integration-builder.test.js +593 -0
- package/infrastructure/domains/integration/integration-resolver.js +170 -0
- package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
- package/infrastructure/domains/integration/websocket-builder.js +69 -0
- package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
- package/infrastructure/domains/networking/vpc-builder.js +1829 -0
- package/infrastructure/domains/networking/vpc-builder.test.js +1262 -0
- package/infrastructure/domains/networking/vpc-discovery.js +177 -0
- package/infrastructure/domains/networking/vpc-discovery.test.js +350 -0
- package/infrastructure/domains/networking/vpc-resolver.js +324 -0
- package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
- package/infrastructure/domains/parameters/ssm-builder.js +79 -0
- package/infrastructure/domains/parameters/ssm-builder.test.js +189 -0
- package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
- package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
- package/infrastructure/{iam-generator.js → domains/security/iam-generator.js} +2 -2
- package/infrastructure/domains/security/kms-builder.js +366 -0
- package/infrastructure/domains/security/kms-builder.test.js +374 -0
- package/infrastructure/domains/security/kms-discovery.js +80 -0
- package/infrastructure/domains/security/kms-discovery.test.js +177 -0
- package/infrastructure/domains/security/kms-resolver.js +96 -0
- package/infrastructure/domains/security/kms-resolver.test.js +216 -0
- package/infrastructure/domains/shared/base-builder.js +112 -0
- package/infrastructure/domains/shared/base-resolver.js +186 -0
- package/infrastructure/domains/shared/base-resolver.test.js +305 -0
- package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
- package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
- package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
- package/infrastructure/domains/shared/cloudformation-discovery.js +375 -0
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +590 -0
- package/infrastructure/domains/shared/environment-builder.js +119 -0
- package/infrastructure/domains/shared/environment-builder.test.js +247 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.js +544 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +377 -0
- package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
- package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
- package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
- package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
- package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
- package/infrastructure/domains/shared/resource-discovery.js +192 -0
- package/infrastructure/domains/shared/resource-discovery.test.js +552 -0
- package/infrastructure/domains/shared/types/app-definition.js +205 -0
- package/infrastructure/domains/shared/types/discovery-result.js +106 -0
- package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
- package/infrastructure/domains/shared/types/index.js +46 -0
- package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
- package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +380 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +248 -0
- package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
- package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +138 -0
- package/infrastructure/{env-validator.js → domains/shared/validation/env-validator.js} +2 -1
- package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
- package/infrastructure/esbuild.config.js +53 -0
- package/infrastructure/infrastructure-composer.js +87 -0
- package/infrastructure/{serverless-template.test.js → infrastructure-composer.test.js} +115 -24
- package/infrastructure/scripts/build-prisma-layer.js +553 -0
- package/infrastructure/scripts/build-prisma-layer.test.js +102 -0
- package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +80 -48
- package/infrastructure/{build-time-discovery.test.js → scripts/build-time-discovery.test.js} +5 -4
- package/layers/prisma/nodejs/package.json +8 -0
- package/management-ui/server/utils/cliIntegration.js +1 -1
- package/management-ui/server/utils/environment/awsParameterStore.js +29 -18
- package/package.json +11 -11
- package/frigg-cli/.eslintrc.js +0 -141
- package/frigg-cli/__tests__/unit/commands/build.test.js +0 -251
- package/frigg-cli/__tests__/unit/commands/db-setup.test.js +0 -548
- package/frigg-cli/__tests__/unit/commands/install.test.js +0 -400
- package/frigg-cli/__tests__/unit/commands/ui.test.js +0 -346
- package/frigg-cli/__tests__/unit/utils/database-validator.test.js +0 -366
- package/frigg-cli/__tests__/unit/utils/error-messages.test.js +0 -304
- package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +0 -486
- package/frigg-cli/__tests__/utils/mock-factory.js +0 -270
- package/frigg-cli/__tests__/utils/prisma-mock.js +0 -194
- package/frigg-cli/__tests__/utils/test-fixtures.js +0 -463
- package/frigg-cli/__tests__/utils/test-setup.js +0 -287
- package/frigg-cli/build-command/index.js +0 -65
- package/frigg-cli/db-setup-command/index.js +0 -193
- package/frigg-cli/deploy-command/index.js +0 -175
- package/frigg-cli/generate-command/__tests__/generate-command.test.js +0 -301
- package/frigg-cli/generate-command/azure-generator.js +0 -43
- package/frigg-cli/generate-command/gcp-generator.js +0 -47
- package/frigg-cli/generate-command/index.js +0 -332
- package/frigg-cli/generate-command/terraform-generator.js +0 -555
- package/frigg-cli/generate-iam-command.js +0 -118
- package/frigg-cli/index.js +0 -75
- package/frigg-cli/index.test.js +0 -158
- package/frigg-cli/init-command/backend-first-handler.js +0 -756
- package/frigg-cli/init-command/index.js +0 -93
- package/frigg-cli/init-command/template-handler.js +0 -143
- package/frigg-cli/install-command/backend-js.js +0 -33
- package/frigg-cli/install-command/commit-changes.js +0 -16
- package/frigg-cli/install-command/environment-variables.js +0 -127
- package/frigg-cli/install-command/environment-variables.test.js +0 -136
- package/frigg-cli/install-command/index.js +0 -54
- package/frigg-cli/install-command/install-package.js +0 -13
- package/frigg-cli/install-command/integration-file.js +0 -30
- package/frigg-cli/install-command/logger.js +0 -12
- package/frigg-cli/install-command/template.js +0 -90
- package/frigg-cli/install-command/validate-package.js +0 -75
- package/frigg-cli/jest.config.js +0 -124
- package/frigg-cli/package.json +0 -54
- package/frigg-cli/start-command/index.js +0 -149
- package/frigg-cli/start-command/start-command.test.js +0 -297
- package/frigg-cli/test/init-command.test.js +0 -180
- package/frigg-cli/test/npm-registry.test.js +0 -319
- package/frigg-cli/ui-command/index.js +0 -154
- package/frigg-cli/utils/app-resolver.js +0 -319
- package/frigg-cli/utils/backend-path.js +0 -25
- package/frigg-cli/utils/database-validator.js +0 -161
- package/frigg-cli/utils/error-messages.js +0 -257
- package/frigg-cli/utils/npm-registry.js +0 -167
- package/frigg-cli/utils/prisma-runner.js +0 -280
- package/frigg-cli/utils/process-manager.js +0 -199
- package/frigg-cli/utils/repo-detection.js +0 -405
- package/infrastructure/aws-discovery.js +0 -1176
- package/infrastructure/aws-discovery.test.js +0 -1220
- package/infrastructure/serverless-template.js +0 -2094
- /package/infrastructure/{WEBSOCKET-CONFIGURATION.md → docs/WEBSOCKET-CONFIGURATION.md} +0 -0
- /package/infrastructure/{GENERATE-IAM-DOCS.md → docs/generate-iam-command.md} +0 -0
- /package/infrastructure/{iam-generator.test.js → domains/security/iam-generator.test.js} +0 -0
- /package/infrastructure/{frigg-deployment-iam-stack.yaml → domains/security/templates/frigg-deployment-iam-stack.yaml} +0 -0
- /package/infrastructure/{iam-policy-basic.json → domains/security/templates/iam-policy-basic.json} +0 -0
- /package/infrastructure/{iam-policy-full.json → domains/security/templates/iam-policy-full.json} +0 -0
- /package/infrastructure/{run-discovery.js → scripts/run-discovery.js} +0 -0
|
@@ -0,0 +1,1829 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VPC Infrastructure Builder
|
|
3
|
+
*
|
|
4
|
+
* Domain Layer - Hexagonal Architecture
|
|
5
|
+
*
|
|
6
|
+
* Responsible for building VPC infrastructure including:
|
|
7
|
+
* - VPC creation or discovery
|
|
8
|
+
* - Subnet management (public/private)
|
|
9
|
+
* - Security groups for Lambda functions
|
|
10
|
+
* - NAT Gateways for private subnet internet access
|
|
11
|
+
* - VPC Endpoints (S3, DynamoDB, KMS, Secrets Manager)
|
|
12
|
+
* - Route tables and routing configuration
|
|
13
|
+
* - Self-healing VPC misconfigurations
|
|
14
|
+
*
|
|
15
|
+
* Supports three management modes:
|
|
16
|
+
* 1. create-new: Creates complete VPC infrastructure from scratch
|
|
17
|
+
* 2. use-existing: Uses explicitly provided VPC/subnet IDs
|
|
18
|
+
* 3. discover (default): Discovers and uses existing AWS resources
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const { InfrastructureBuilder, ValidationResult } = require('../shared/base-builder');
|
|
22
|
+
const VpcResourceResolver = require('./vpc-resolver');
|
|
23
|
+
const { createEmptyDiscoveryResult } = require('../shared/types/discovery-result');
|
|
24
|
+
const { ResourceOwnership } = require('../shared/types/resource-ownership');
|
|
25
|
+
|
|
26
|
+
class VpcBuilder extends InfrastructureBuilder {
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
this.name = 'VpcBuilder';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
shouldExecute(appDefinition) {
|
|
33
|
+
// Skip VPC in local mode (when FRIGG_SKIP_AWS_DISCOVERY is set)
|
|
34
|
+
// VPC is an AWS-specific service that should only be created in production
|
|
35
|
+
if (process.env.FRIGG_SKIP_AWS_DISCOVERY === 'true') {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return appDefinition.vpc?.enable === true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
validate(appDefinition) {
|
|
43
|
+
const result = new ValidationResult();
|
|
44
|
+
|
|
45
|
+
if (!appDefinition.vpc) {
|
|
46
|
+
result.addError('VPC configuration is missing');
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const vpc = appDefinition.vpc;
|
|
51
|
+
|
|
52
|
+
// Validate management mode
|
|
53
|
+
const validModes = ['discover', 'create-new', 'use-existing'];
|
|
54
|
+
const management = vpc.management || 'discover';
|
|
55
|
+
if (!validModes.includes(management)) {
|
|
56
|
+
result.addError(`Invalid vpc.management: "${management}". Must be one of: ${validModes.join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Validate use-existing mode requirements
|
|
60
|
+
if (management === 'use-existing') {
|
|
61
|
+
if (!vpc.vpcId) {
|
|
62
|
+
result.addError('vpc.vpcId is required when management="use-existing"');
|
|
63
|
+
}
|
|
64
|
+
if (!vpc.securityGroupIds || vpc.securityGroupIds.length === 0) {
|
|
65
|
+
result.addWarning('vpc.securityGroupIds not provided - will attempt discovery');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Validate CIDR block format
|
|
70
|
+
if (vpc.cidrBlock) {
|
|
71
|
+
const cidrPattern = /^([0-9]{1,3}\.){3}[0-9]{1,3}\/[0-9]{1,2}$/;
|
|
72
|
+
if (!cidrPattern.test(vpc.cidrBlock)) {
|
|
73
|
+
result.addError(`Invalid CIDR block format: ${vpc.cidrBlock}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Validate subnet configuration
|
|
78
|
+
if (vpc.subnets?.management === 'use-existing') {
|
|
79
|
+
if (!vpc.subnets.ids || vpc.subnets.ids.length < 2) {
|
|
80
|
+
result.addError('At least 2 subnet IDs required when subnets.management="use-existing"');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Warn about ignored options when managementMode='managed'
|
|
89
|
+
*/
|
|
90
|
+
warnIgnoredOptions(appDefinition) {
|
|
91
|
+
const ignoredOptions = [];
|
|
92
|
+
if (appDefinition.vpc?.management) ignoredOptions.push('vpc.management');
|
|
93
|
+
if (appDefinition.vpc?.subnets?.management) ignoredOptions.push('vpc.subnets.management');
|
|
94
|
+
if (appDefinition.vpc?.natGateway?.management) ignoredOptions.push('vpc.natGateway.management');
|
|
95
|
+
if (appDefinition.vpc?.shareAcrossStages !== undefined) ignoredOptions.push('vpc.shareAcrossStages');
|
|
96
|
+
|
|
97
|
+
if (ignoredOptions.length > 0) {
|
|
98
|
+
console.log(` ⚠️ managementMode='managed' ignoring: ${ignoredOptions.join(', ')}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Convert flat discovery result to structured discovery result
|
|
104
|
+
* Provides backwards compatibility for tests using old discovery format
|
|
105
|
+
*
|
|
106
|
+
* @param {Object} flatDiscovery - Flat discovery object
|
|
107
|
+
* @param {Object} appDefinition - App definition (used to detect stack-managed resources)
|
|
108
|
+
*/
|
|
109
|
+
convertFlatDiscoveryToStructured(flatDiscovery, appDefinition = {}) {
|
|
110
|
+
const discovery = createEmptyDiscoveryResult();
|
|
111
|
+
|
|
112
|
+
if (!flatDiscovery) {
|
|
113
|
+
return discovery;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Special case: managementMode='managed' + vpcIsolation='isolated' with existing resources
|
|
117
|
+
// These resources are from a previous deployment of this stack, so they're stack-managed
|
|
118
|
+
const isManagedIsolated = appDefinition.managementMode === 'managed' &&
|
|
119
|
+
(appDefinition.vpcIsolation === 'isolated' || !appDefinition.vpcIsolation);
|
|
120
|
+
const hasExistingStackResources = isManagedIsolated && flatDiscovery.defaultVpcId &&
|
|
121
|
+
typeof flatDiscovery.defaultVpcId === 'string';
|
|
122
|
+
|
|
123
|
+
// Check if this came from CloudFormation stack
|
|
124
|
+
if (flatDiscovery.fromCloudFormationStack || hasExistingStackResources) {
|
|
125
|
+
discovery.fromCloudFormation = true;
|
|
126
|
+
discovery.stackName = flatDiscovery.stackName || 'assumed-stack';
|
|
127
|
+
|
|
128
|
+
// Add resources to stackManaged array
|
|
129
|
+
let existingLogicalIds = flatDiscovery.existingLogicalIds || [];
|
|
130
|
+
|
|
131
|
+
// If hasExistingStackResources but no existingLogicalIds provided,
|
|
132
|
+
// infer logical IDs from presence of physical IDs
|
|
133
|
+
if (hasExistingStackResources && existingLogicalIds.length === 0) {
|
|
134
|
+
existingLogicalIds = [];
|
|
135
|
+
if (flatDiscovery.defaultVpcId) existingLogicalIds.push('FriggVPC');
|
|
136
|
+
if (flatDiscovery.privateSubnetId1) existingLogicalIds.push('FriggPrivateSubnet1');
|
|
137
|
+
if (flatDiscovery.privateSubnetId2) existingLogicalIds.push('FriggPrivateSubnet2');
|
|
138
|
+
if (flatDiscovery.publicSubnetId1) existingLogicalIds.push('FriggPublicSubnet');
|
|
139
|
+
if (flatDiscovery.publicSubnetId2) existingLogicalIds.push('FriggPublicSubnet2');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
existingLogicalIds.forEach(logicalId => {
|
|
143
|
+
// Find the resource type and physical ID
|
|
144
|
+
let resourceType = '';
|
|
145
|
+
let physicalId = '';
|
|
146
|
+
|
|
147
|
+
if (logicalId === 'FriggVPC') {
|
|
148
|
+
resourceType = 'AWS::EC2::VPC';
|
|
149
|
+
physicalId = flatDiscovery.defaultVpcId;
|
|
150
|
+
} else if (logicalId === 'FriggLambdaSecurityGroup') {
|
|
151
|
+
resourceType = 'AWS::EC2::SecurityGroup';
|
|
152
|
+
physicalId = flatDiscovery.defaultSecurityGroupId || flatDiscovery.securityGroupId;
|
|
153
|
+
} else if (logicalId === 'FriggPrivateSubnet1') {
|
|
154
|
+
resourceType = 'AWS::EC2::Subnet';
|
|
155
|
+
physicalId = flatDiscovery.privateSubnetId1;
|
|
156
|
+
} else if (logicalId === 'FriggPrivateSubnet2') {
|
|
157
|
+
resourceType = 'AWS::EC2::Subnet';
|
|
158
|
+
physicalId = flatDiscovery.privateSubnetId2;
|
|
159
|
+
} else if (logicalId === 'FriggNATGateway') {
|
|
160
|
+
resourceType = 'AWS::EC2::NatGateway';
|
|
161
|
+
physicalId = flatDiscovery.existingNatGatewayId;
|
|
162
|
+
} else if (logicalId === 'FriggS3VPCEndpoint') {
|
|
163
|
+
resourceType = 'AWS::EC2::VPCEndpoint';
|
|
164
|
+
physicalId = flatDiscovery.s3VpcEndpointId;
|
|
165
|
+
} else if (logicalId === 'FriggDynamoDBVPCEndpoint') {
|
|
166
|
+
resourceType = 'AWS::EC2::VPCEndpoint';
|
|
167
|
+
physicalId = flatDiscovery.dynamodbVpcEndpointId;
|
|
168
|
+
} else if (logicalId === 'FriggKMSVPCEndpoint') {
|
|
169
|
+
resourceType = 'AWS::EC2::VPCEndpoint';
|
|
170
|
+
physicalId = flatDiscovery.kmsVpcEndpointId;
|
|
171
|
+
} else if (logicalId === 'FriggSecretsManagerVPCEndpoint') {
|
|
172
|
+
resourceType = 'AWS::EC2::VPCEndpoint';
|
|
173
|
+
physicalId = flatDiscovery.secretsManagerVpcEndpointId;
|
|
174
|
+
} else if (logicalId === 'FriggSQSVPCEndpoint') {
|
|
175
|
+
resourceType = 'AWS::EC2::VPCEndpoint';
|
|
176
|
+
physicalId = flatDiscovery.sqsVpcEndpointId;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (physicalId && typeof physicalId === 'string') {
|
|
180
|
+
discovery.stackManaged.push({
|
|
181
|
+
logicalId,
|
|
182
|
+
physicalId,
|
|
183
|
+
resourceType
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
} else {
|
|
188
|
+
// Resources discovered from AWS API (not CloudFormation)
|
|
189
|
+
// These go into external array
|
|
190
|
+
|
|
191
|
+
if (flatDiscovery.defaultVpcId && typeof flatDiscovery.defaultVpcId === 'string') {
|
|
192
|
+
discovery.external.push({
|
|
193
|
+
physicalId: flatDiscovery.defaultVpcId,
|
|
194
|
+
resourceType: 'AWS::EC2::VPC',
|
|
195
|
+
source: 'aws-discovery'
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (flatDiscovery.defaultSecurityGroupId && typeof flatDiscovery.defaultSecurityGroupId === 'string') {
|
|
200
|
+
discovery.external.push({
|
|
201
|
+
physicalId: flatDiscovery.defaultSecurityGroupId,
|
|
202
|
+
resourceType: 'AWS::EC2::SecurityGroup',
|
|
203
|
+
source: 'aws-discovery'
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (flatDiscovery.privateSubnetId1 && typeof flatDiscovery.privateSubnetId1 === 'string') {
|
|
208
|
+
discovery.external.push({
|
|
209
|
+
physicalId: flatDiscovery.privateSubnetId1,
|
|
210
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
211
|
+
source: 'aws-discovery'
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (flatDiscovery.privateSubnetId2 && typeof flatDiscovery.privateSubnetId2 === 'string') {
|
|
216
|
+
discovery.external.push({
|
|
217
|
+
physicalId: flatDiscovery.privateSubnetId2,
|
|
218
|
+
resourceType: 'AWS::EC2::Subnet',
|
|
219
|
+
source: 'aws-discovery'
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Only add NAT Gateway to external if it's NOT in a private subnet (properly placed)
|
|
224
|
+
// If natGatewayInPrivateSubnet is true, we need a new NAT Gateway
|
|
225
|
+
const natIsProperlyPlaced = flatDiscovery.natGatewayInPrivateSubnet !== true;
|
|
226
|
+
|
|
227
|
+
if (flatDiscovery.natGatewayId && typeof flatDiscovery.natGatewayId === 'string' && natIsProperlyPlaced) {
|
|
228
|
+
discovery.external.push({
|
|
229
|
+
physicalId: flatDiscovery.natGatewayId,
|
|
230
|
+
resourceType: 'AWS::EC2::NatGateway',
|
|
231
|
+
source: 'aws-discovery'
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (flatDiscovery.existingNatGatewayId && typeof flatDiscovery.existingNatGatewayId === 'string' && natIsProperlyPlaced) {
|
|
236
|
+
discovery.external.push({
|
|
237
|
+
physicalId: flatDiscovery.existingNatGatewayId,
|
|
238
|
+
resourceType: 'AWS::EC2::NatGateway',
|
|
239
|
+
source: 'aws-discovery'
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// VPC Endpoints
|
|
244
|
+
if (flatDiscovery.s3VpcEndpointId && typeof flatDiscovery.s3VpcEndpointId === 'string') {
|
|
245
|
+
discovery.external.push({
|
|
246
|
+
physicalId: flatDiscovery.s3VpcEndpointId,
|
|
247
|
+
resourceType: 'AWS::EC2::VPCEndpoint',
|
|
248
|
+
source: 'aws-discovery',
|
|
249
|
+
properties: { ServiceName: 's3' }
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (flatDiscovery.dynamodbVpcEndpointId && typeof flatDiscovery.dynamodbVpcEndpointId === 'string') {
|
|
254
|
+
discovery.external.push({
|
|
255
|
+
physicalId: flatDiscovery.dynamodbVpcEndpointId,
|
|
256
|
+
resourceType: 'AWS::EC2::VPCEndpoint',
|
|
257
|
+
source: 'aws-discovery',
|
|
258
|
+
properties: { ServiceName: 'dynamodb' }
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (flatDiscovery.kmsVpcEndpointId && typeof flatDiscovery.kmsVpcEndpointId === 'string') {
|
|
263
|
+
discovery.external.push({
|
|
264
|
+
physicalId: flatDiscovery.kmsVpcEndpointId,
|
|
265
|
+
resourceType: 'AWS::EC2::VPCEndpoint',
|
|
266
|
+
source: 'aws-discovery',
|
|
267
|
+
properties: { ServiceName: 'kms' }
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (flatDiscovery.secretsManagerVpcEndpointId && typeof flatDiscovery.secretsManagerVpcEndpointId === 'string') {
|
|
272
|
+
discovery.external.push({
|
|
273
|
+
physicalId: flatDiscovery.secretsManagerVpcEndpointId,
|
|
274
|
+
resourceType: 'AWS::EC2::VPCEndpoint',
|
|
275
|
+
source: 'aws-discovery',
|
|
276
|
+
properties: { ServiceName: 'secretsmanager' }
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (flatDiscovery.sqsVpcEndpointId && typeof flatDiscovery.sqsVpcEndpointId === 'string') {
|
|
281
|
+
discovery.external.push({
|
|
282
|
+
physicalId: flatDiscovery.sqsVpcEndpointId,
|
|
283
|
+
resourceType: 'AWS::EC2::VPCEndpoint',
|
|
284
|
+
source: 'aws-discovery',
|
|
285
|
+
properties: { ServiceName: 'sqs' }
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return discovery;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Translate legacy configuration (management modes) to new ownership-based configuration
|
|
295
|
+
* Provides backwards compatibility for existing app definitions
|
|
296
|
+
*/
|
|
297
|
+
translateLegacyConfig(appDefinition, discoveredResources) {
|
|
298
|
+
// If already using new ownership schema, return as-is
|
|
299
|
+
if (appDefinition.vpc?.ownership) {
|
|
300
|
+
return appDefinition;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Clone to avoid mutating original
|
|
304
|
+
const translated = JSON.parse(JSON.stringify(appDefinition));
|
|
305
|
+
|
|
306
|
+
// Initialize ownership and external sections
|
|
307
|
+
if (!translated.vpc.ownership) {
|
|
308
|
+
translated.vpc.ownership = {};
|
|
309
|
+
}
|
|
310
|
+
if (!translated.vpc.external) {
|
|
311
|
+
translated.vpc.external = {};
|
|
312
|
+
}
|
|
313
|
+
if (!translated.vpc.config) {
|
|
314
|
+
translated.vpc.config = {};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Handle top-level managementMode
|
|
318
|
+
const globalMode = appDefinition.managementMode || 'discover';
|
|
319
|
+
const vpcIsolation = appDefinition.vpcIsolation || 'shared';
|
|
320
|
+
|
|
321
|
+
if (globalMode === 'managed') {
|
|
322
|
+
this.warnIgnoredOptions(appDefinition);
|
|
323
|
+
|
|
324
|
+
if (vpcIsolation === 'isolated') {
|
|
325
|
+
// Check if CloudFormation stack already has resources
|
|
326
|
+
const hasStackVpc = discoveredResources?.defaultVpcId && typeof discoveredResources.defaultVpcId === 'string';
|
|
327
|
+
|
|
328
|
+
if (hasStackVpc) {
|
|
329
|
+
// Stack has VPC - reuse it
|
|
330
|
+
translated.vpc.ownership.vpc = 'auto';
|
|
331
|
+
translated.vpc.ownership.securityGroup = 'auto';
|
|
332
|
+
translated.vpc.ownership.subnets = 'auto';
|
|
333
|
+
translated.vpc.config.selfHeal = true;
|
|
334
|
+
console.log(` managementMode='managed' + vpcIsolation='isolated' → stack has VPC, reusing`);
|
|
335
|
+
} else {
|
|
336
|
+
// No stack VPC - create new
|
|
337
|
+
translated.vpc.ownership.vpc = 'stack';
|
|
338
|
+
translated.vpc.ownership.securityGroup = 'stack';
|
|
339
|
+
translated.vpc.ownership.subnets = 'stack';
|
|
340
|
+
translated.vpc.ownership.natGateway = 'stack';
|
|
341
|
+
translated.vpc.config.natGateway = { enable: true };
|
|
342
|
+
console.log(` managementMode='managed' + vpcIsolation='isolated' → no stack VPC, creating new`);
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
// Shared VPC
|
|
346
|
+
translated.vpc.ownership.vpc = 'auto';
|
|
347
|
+
translated.vpc.ownership.securityGroup = 'auto';
|
|
348
|
+
translated.vpc.ownership.subnets = 'auto';
|
|
349
|
+
translated.vpc.config.selfHeal = true;
|
|
350
|
+
}
|
|
351
|
+
} else if (globalMode === 'existing') {
|
|
352
|
+
translated.vpc.ownership.vpc = 'external';
|
|
353
|
+
translated.vpc.ownership.securityGroup = 'external';
|
|
354
|
+
translated.vpc.ownership.subnets = 'external';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Handle legacy vpc.management modes
|
|
358
|
+
const vpcManagement = appDefinition.vpc?.management;
|
|
359
|
+
if (vpcManagement === 'create-new') {
|
|
360
|
+
translated.vpc.ownership.vpc = 'stack';
|
|
361
|
+
translated.vpc.ownership.securityGroup = 'stack';
|
|
362
|
+
translated.vpc.ownership.subnets = 'stack';
|
|
363
|
+
} else if (vpcManagement === 'use-existing') {
|
|
364
|
+
translated.vpc.ownership.vpc = 'external';
|
|
365
|
+
translated.vpc.external.vpcId = appDefinition.vpc.vpcId;
|
|
366
|
+
|
|
367
|
+
if (appDefinition.vpc.securityGroupIds) {
|
|
368
|
+
translated.vpc.ownership.securityGroup = 'external';
|
|
369
|
+
translated.vpc.external.securityGroupIds = appDefinition.vpc.securityGroupIds;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (appDefinition.vpc.subnets?.ids) {
|
|
373
|
+
translated.vpc.ownership.subnets = 'external';
|
|
374
|
+
translated.vpc.external.subnetIds = appDefinition.vpc.subnets.ids;
|
|
375
|
+
}
|
|
376
|
+
} else if (vpcManagement === 'discover') {
|
|
377
|
+
// Discover mode - let auto-resolution handle it
|
|
378
|
+
translated.vpc.ownership.vpc = 'auto';
|
|
379
|
+
translated.vpc.ownership.securityGroup = 'auto';
|
|
380
|
+
translated.vpc.ownership.subnets = 'auto';
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Handle legacy shareAcrossStages
|
|
384
|
+
if (appDefinition.vpc?.shareAcrossStages !== undefined) {
|
|
385
|
+
if (appDefinition.vpc.shareAcrossStages) {
|
|
386
|
+
// Shared VPC - discover and reuse
|
|
387
|
+
translated.vpc.ownership.vpc = 'auto';
|
|
388
|
+
translated.vpc.ownership.subnets = 'auto';
|
|
389
|
+
} else {
|
|
390
|
+
// Isolated VPC - create stage-specific
|
|
391
|
+
translated.vpc.ownership.vpc = 'stack';
|
|
392
|
+
translated.vpc.ownership.subnets = 'stack';
|
|
393
|
+
translated.vpc.ownership.natGateway = 'stack';
|
|
394
|
+
translated.vpc.config.natGateway = { enable: true };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Handle legacy NAT Gateway management
|
|
399
|
+
if (appDefinition.vpc?.natGateway?.management === 'createAndManage') {
|
|
400
|
+
// Use 'auto' to allow discovering and reusing properly placed external NAT Gateways
|
|
401
|
+
// The resolver will check if there's a good external NAT Gateway and reuse it,
|
|
402
|
+
// or create a new one if needed (or if the existing one is misplaced)
|
|
403
|
+
translated.vpc.ownership.natGateway = 'auto';
|
|
404
|
+
translated.vpc.config.natGateway = { enable: true };
|
|
405
|
+
} else if (appDefinition.vpc?.natGateway?.id) {
|
|
406
|
+
translated.vpc.ownership.natGateway = 'external';
|
|
407
|
+
translated.vpc.external.natGatewayId = appDefinition.vpc.natGateway.id;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Handle legacy subnet management
|
|
411
|
+
if (appDefinition.vpc?.subnets?.management === 'create') {
|
|
412
|
+
translated.vpc.ownership.subnets = 'stack';
|
|
413
|
+
} else if (appDefinition.vpc?.subnets?.management === 'use-existing' && appDefinition.vpc.subnets.ids) {
|
|
414
|
+
translated.vpc.ownership.subnets = 'external';
|
|
415
|
+
translated.vpc.external.subnetIds = appDefinition.vpc.subnets.ids;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Preserve other VPC config
|
|
419
|
+
if (appDefinition.vpc?.cidrBlock) {
|
|
420
|
+
translated.vpc.config.cidrBlock = appDefinition.vpc.cidrBlock;
|
|
421
|
+
}
|
|
422
|
+
if (appDefinition.vpc?.enableVPCEndpoints !== undefined) {
|
|
423
|
+
translated.vpc.config.enableVpcEndpoints = appDefinition.vpc.enableVPCEndpoints;
|
|
424
|
+
}
|
|
425
|
+
if (appDefinition.vpc?.selfHeal !== undefined) {
|
|
426
|
+
translated.vpc.config.selfHeal = appDefinition.vpc.selfHeal;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return translated;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Build complete VPC infrastructure using ownership-based architecture
|
|
434
|
+
*/
|
|
435
|
+
async build(appDefinition, discoveredResources) {
|
|
436
|
+
console.log(`\n[${this.name}] Building VPC infrastructure...`);
|
|
437
|
+
|
|
438
|
+
// Backwards compatibility: Translate old schema to new ownership schema
|
|
439
|
+
appDefinition = this.translateLegacyConfig(appDefinition, discoveredResources);
|
|
440
|
+
|
|
441
|
+
// Get structured discovery result (or convert flat discovery to structured)
|
|
442
|
+
// Pass appDefinition to help detect stack-managed resources in managementMode='managed'
|
|
443
|
+
const discovery = discoveredResources._structured || this.convertFlatDiscoveryToStructured(discoveredResources, appDefinition);
|
|
444
|
+
|
|
445
|
+
// Use VpcResourceResolver to make ownership decisions
|
|
446
|
+
const resolver = new VpcResourceResolver();
|
|
447
|
+
const decisions = resolver.resolveAll(appDefinition, discovery);
|
|
448
|
+
|
|
449
|
+
console.log('\n 📋 Resource Ownership Decisions:');
|
|
450
|
+
console.log(` VPC: ${decisions.vpc.ownership} - ${decisions.vpc.reason}`);
|
|
451
|
+
console.log(` Security Group: ${decisions.securityGroup.ownership} - ${decisions.securityGroup.reason}`);
|
|
452
|
+
console.log(` Subnets: ${decisions.subnets.ownership} - ${decisions.subnets.reason}`);
|
|
453
|
+
console.log(` NAT Gateway: ${decisions.natGateway.ownership || 'disabled'} - ${decisions.natGateway.reason}`);
|
|
454
|
+
console.log(` VPC Endpoints:`);
|
|
455
|
+
console.log(` S3: ${decisions.vpcEndpoints.s3.ownership || 'disabled'} - ${decisions.vpcEndpoints.s3.reason}`);
|
|
456
|
+
console.log(` DynamoDB: ${decisions.vpcEndpoints.dynamodb.ownership || 'disabled'} - ${decisions.vpcEndpoints.dynamodb.reason}`);
|
|
457
|
+
|
|
458
|
+
// Initialize result
|
|
459
|
+
const result = {
|
|
460
|
+
resources: {},
|
|
461
|
+
vpcConfig: {
|
|
462
|
+
securityGroupIds: [],
|
|
463
|
+
subnetIds: [],
|
|
464
|
+
},
|
|
465
|
+
iamStatements: [],
|
|
466
|
+
outputs: {},
|
|
467
|
+
environment: {},
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// Add IAM permissions for VPC-enabled Lambda functions
|
|
471
|
+
this.addVpcIamPermissions(result);
|
|
472
|
+
|
|
473
|
+
// Build VPC based on ownership decision
|
|
474
|
+
this.buildVpcFromDecision(decisions.vpc, appDefinition, result);
|
|
475
|
+
|
|
476
|
+
// Build Security Group based on ownership decision
|
|
477
|
+
this.buildSecurityGroupFromDecision(decisions.securityGroup, appDefinition, result);
|
|
478
|
+
|
|
479
|
+
// Build Subnets based on ownership decision
|
|
480
|
+
this.buildSubnetsFromDecision(decisions.subnets, appDefinition, discoveredResources, result);
|
|
481
|
+
|
|
482
|
+
// Build NAT Gateway based on ownership decision
|
|
483
|
+
this.buildNatGatewayFromDecision(decisions.natGateway, appDefinition, discoveredResources, result);
|
|
484
|
+
|
|
485
|
+
// Build VPC Endpoints based on ownership decisions
|
|
486
|
+
this.buildVpcEndpointsFromDecisions(decisions.vpcEndpoints, appDefinition, result);
|
|
487
|
+
|
|
488
|
+
// Set VPC_ENABLED environment variable
|
|
489
|
+
result.environment.VPC_ENABLED = 'true';
|
|
490
|
+
|
|
491
|
+
console.log(`\n[${this.name}] ✅ VPC infrastructure built successfully`);
|
|
492
|
+
console.log(` - VPC ID: ${result.vpcId || 'from discovery'}`);
|
|
493
|
+
console.log(` - Subnets: ${result.vpcConfig.subnetIds.length}`);
|
|
494
|
+
console.log(` - Security Groups: ${result.vpcConfig.securityGroupIds.length}`);
|
|
495
|
+
|
|
496
|
+
return result;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Add IAM permissions for VPC-enabled Lambda functions
|
|
501
|
+
*/
|
|
502
|
+
addVpcIamPermissions(result) {
|
|
503
|
+
result.iamStatements.push({
|
|
504
|
+
Effect: 'Allow',
|
|
505
|
+
Action: [
|
|
506
|
+
'ec2:CreateNetworkInterface',
|
|
507
|
+
'ec2:DescribeNetworkInterfaces',
|
|
508
|
+
'ec2:DeleteNetworkInterface',
|
|
509
|
+
'ec2:AttachNetworkInterface',
|
|
510
|
+
'ec2:DetachNetworkInterface',
|
|
511
|
+
],
|
|
512
|
+
Resource: '*',
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Build VPC based on ownership decision
|
|
518
|
+
*
|
|
519
|
+
* For STACK ownership: ALWAYS add definitions to template.
|
|
520
|
+
* CloudFormation idempotency ensures existing resources aren't recreated.
|
|
521
|
+
*/
|
|
522
|
+
buildVpcFromDecision(decision, appDefinition, result) {
|
|
523
|
+
if (decision.ownership === ResourceOwnership.STACK) {
|
|
524
|
+
// For STACK ownership: ALWAYS create definitions (CloudFormation idempotency)
|
|
525
|
+
if (decision.physicalId) {
|
|
526
|
+
console.log(` → Adding VPC definition to template (existing: ${decision.physicalId})`);
|
|
527
|
+
} else {
|
|
528
|
+
console.log(' → Adding VPC definition to template (new)');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const cidrBlock = appDefinition.vpc?.config?.cidrBlock || appDefinition.vpc?.cidrBlock || '10.0.0.0/16';
|
|
532
|
+
|
|
533
|
+
result.resources.FriggVPC = {
|
|
534
|
+
Type: 'AWS::EC2::VPC',
|
|
535
|
+
Properties: {
|
|
536
|
+
CidrBlock: cidrBlock,
|
|
537
|
+
EnableDnsHostnames: true,
|
|
538
|
+
EnableDnsSupport: true,
|
|
539
|
+
Tags: [
|
|
540
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc' },
|
|
541
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
542
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
543
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
544
|
+
],
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
// Internet Gateway
|
|
549
|
+
result.resources.FriggInternetGateway = {
|
|
550
|
+
Type: 'AWS::EC2::InternetGateway',
|
|
551
|
+
Properties: {
|
|
552
|
+
Tags: [
|
|
553
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-igw' },
|
|
554
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
555
|
+
],
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
result.resources.FriggVPCGatewayAttachment = {
|
|
560
|
+
Type: 'AWS::EC2::VPCGatewayAttachment',
|
|
561
|
+
Properties: {
|
|
562
|
+
VpcId: { Ref: 'FriggVPC' },
|
|
563
|
+
InternetGatewayId: { Ref: 'FriggInternetGateway' },
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
// Use Ref for stack-managed VPC
|
|
568
|
+
result.vpcId = { Ref: 'FriggVPC' };
|
|
569
|
+
console.log(' ✅ VPC definition added to template');
|
|
570
|
+
} else if (decision.ownership === ResourceOwnership.EXTERNAL) {
|
|
571
|
+
// Use external VPC ID (no definition in template)
|
|
572
|
+
result.vpcId = decision.physicalId;
|
|
573
|
+
console.log(` ✓ Using external VPC: ${decision.physicalId}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Build Security Group based on ownership decision
|
|
579
|
+
*/
|
|
580
|
+
buildSecurityGroupFromDecision(decision, appDefinition, result) {
|
|
581
|
+
if (decision.ownership === ResourceOwnership.STACK) {
|
|
582
|
+
// Always create security group resource in template
|
|
583
|
+
// CloudFormation handles idempotency if it already exists
|
|
584
|
+
console.log(' → Adding Lambda Security Group to template...');
|
|
585
|
+
|
|
586
|
+
result.resources.FriggLambdaSecurityGroup = {
|
|
587
|
+
Type: 'AWS::EC2::SecurityGroup',
|
|
588
|
+
Properties: {
|
|
589
|
+
GroupDescription: 'Security group for Frigg Lambda functions',
|
|
590
|
+
VpcId: result.vpcId,
|
|
591
|
+
SecurityGroupEgress: [
|
|
592
|
+
{ IpProtocol: 'tcp', FromPort: 443, ToPort: 443, CidrIp: '0.0.0.0/0', Description: 'HTTPS outbound' },
|
|
593
|
+
{ IpProtocol: 'tcp', FromPort: 80, ToPort: 80, CidrIp: '0.0.0.0/0', Description: 'HTTP outbound' },
|
|
594
|
+
{ IpProtocol: 'tcp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS TCP' },
|
|
595
|
+
{ IpProtocol: 'udp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS UDP' },
|
|
596
|
+
{ IpProtocol: 'tcp', FromPort: 5432, ToPort: 5432, CidrIp: '0.0.0.0/0', Description: 'PostgreSQL' },
|
|
597
|
+
{ IpProtocol: 'tcp', FromPort: 27017, ToPort: 27017, CidrIp: '0.0.0.0/0', Description: 'MongoDB' },
|
|
598
|
+
],
|
|
599
|
+
Tags: [
|
|
600
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-sg' },
|
|
601
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
602
|
+
],
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
// Use CloudFormation Ref since resource is in template
|
|
607
|
+
result.vpcConfig.securityGroupIds = [{ Ref: 'FriggLambdaSecurityGroup' }];
|
|
608
|
+
console.log(' ✅ Security Group added to template');
|
|
609
|
+
} else if (decision.ownership === ResourceOwnership.EXTERNAL) {
|
|
610
|
+
// Use external security group IDs
|
|
611
|
+
const sgIds = Array.isArray(decision.physicalId) ? decision.physicalId : [decision.physicalId];
|
|
612
|
+
result.vpcConfig.securityGroupIds = sgIds;
|
|
613
|
+
console.log(` ✓ Using external security group(s): ${sgIds.join(', ')}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Build Subnets based on ownership decision
|
|
619
|
+
*/
|
|
620
|
+
buildSubnetsFromDecision(decision, appDefinition, discoveredResources, result) {
|
|
621
|
+
if (decision.ownership === ResourceOwnership.STACK) {
|
|
622
|
+
// Check if no subnets exist and selfHeal is disabled
|
|
623
|
+
if (!decision.physicalIds || decision.physicalIds.length < 2) {
|
|
624
|
+
const selfHeal = appDefinition.vpc?.config?.selfHeal !== false;
|
|
625
|
+
if (!selfHeal) {
|
|
626
|
+
throw new Error(
|
|
627
|
+
'No subnets discovered. Enable vpc.selfHeal, set subnets.management to "create", or provide subnet IDs.'
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// For STACK ownership: ALWAYS add definitions to template
|
|
633
|
+
// CloudFormation idempotency ensures existing resources won't be recreated
|
|
634
|
+
if (decision.physicalIds && decision.physicalIds.length >= 2) {
|
|
635
|
+
console.log(` → Adding subnet definitions to template (existing: ${decision.physicalIds.join(', ')})`);
|
|
636
|
+
} else {
|
|
637
|
+
console.log(' → Adding subnet definitions to template (new)');
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
this.createSubnetsInTemplate(appDefinition, result, discoveredResources);
|
|
641
|
+
|
|
642
|
+
// Use Refs for stack-managed resources
|
|
643
|
+
result.vpcConfig.subnetIds = [
|
|
644
|
+
{ Ref: 'FriggPrivateSubnet1' },
|
|
645
|
+
{ Ref: 'FriggPrivateSubnet2' }
|
|
646
|
+
];
|
|
647
|
+
} else if (decision.ownership === ResourceOwnership.EXTERNAL) {
|
|
648
|
+
// Use external subnet IDs directly (no definitions in template)
|
|
649
|
+
result.vpcConfig.subnetIds = decision.physicalIds;
|
|
650
|
+
console.log(` ✓ Using external subnets: ${decision.physicalIds.join(', ')}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Create subnet resources in CloudFormation template
|
|
656
|
+
*/
|
|
657
|
+
createSubnetsInTemplate(appDefinition, result, discoveredResources) {
|
|
658
|
+
// Determine VPC ID for subnets
|
|
659
|
+
const vpcId = result.vpcId;
|
|
660
|
+
|
|
661
|
+
// Generate subnet CIDRs
|
|
662
|
+
const cidrs = this.generateSubnetCidrsForNewVpc(vpcId, discoveredResources);
|
|
663
|
+
|
|
664
|
+
// Private Subnet 1
|
|
665
|
+
result.resources.FriggPrivateSubnet1 = {
|
|
666
|
+
Type: 'AWS::EC2::Subnet',
|
|
667
|
+
DeletionPolicy: 'Retain',
|
|
668
|
+
Properties: {
|
|
669
|
+
VpcId: vpcId,
|
|
670
|
+
CidrBlock: cidrs.private1,
|
|
671
|
+
AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
|
|
672
|
+
Tags: [
|
|
673
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-1' },
|
|
674
|
+
{ Key: 'Type', Value: 'Private' },
|
|
675
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
676
|
+
],
|
|
677
|
+
},
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
// Private Subnet 2
|
|
681
|
+
result.resources.FriggPrivateSubnet2 = {
|
|
682
|
+
Type: 'AWS::EC2::Subnet',
|
|
683
|
+
DeletionPolicy: 'Retain',
|
|
684
|
+
Properties: {
|
|
685
|
+
VpcId: vpcId,
|
|
686
|
+
CidrBlock: cidrs.private2,
|
|
687
|
+
AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
|
|
688
|
+
Tags: [
|
|
689
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-2' },
|
|
690
|
+
{ Key: 'Type', Value: 'Private' },
|
|
691
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
692
|
+
],
|
|
693
|
+
},
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
// Public Subnets (for NAT Gateway)
|
|
697
|
+
result.resources.FriggPublicSubnet = {
|
|
698
|
+
Type: 'AWS::EC2::Subnet',
|
|
699
|
+
Properties: {
|
|
700
|
+
VpcId: vpcId,
|
|
701
|
+
CidrBlock: cidrs.public1,
|
|
702
|
+
MapPublicIpOnLaunch: true,
|
|
703
|
+
AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
|
|
704
|
+
Tags: [
|
|
705
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-1' },
|
|
706
|
+
{ Key: 'Type', Value: 'Public' },
|
|
707
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
708
|
+
],
|
|
709
|
+
},
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
result.resources.FriggPublicSubnet2 = {
|
|
713
|
+
Type: 'AWS::EC2::Subnet',
|
|
714
|
+
Properties: {
|
|
715
|
+
VpcId: vpcId,
|
|
716
|
+
CidrBlock: cidrs.public2,
|
|
717
|
+
MapPublicIpOnLaunch: true,
|
|
718
|
+
AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
|
|
719
|
+
Tags: [
|
|
720
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-2' },
|
|
721
|
+
{ Key: 'Type', Value: 'Public' },
|
|
722
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
723
|
+
],
|
|
724
|
+
},
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
result.vpcConfig.subnetIds = [
|
|
728
|
+
{ Ref: 'FriggPrivateSubnet1' },
|
|
729
|
+
{ Ref: 'FriggPrivateSubnet2' },
|
|
730
|
+
];
|
|
731
|
+
|
|
732
|
+
// Map to discovered resources for other builders
|
|
733
|
+
discoveredResources.privateSubnetId1 = { Ref: 'FriggPrivateSubnet1' };
|
|
734
|
+
discoveredResources.privateSubnetId2 = { Ref: 'FriggPrivateSubnet2' };
|
|
735
|
+
discoveredResources.publicSubnetId1 = { Ref: 'FriggPublicSubnet' };
|
|
736
|
+
discoveredResources.publicSubnetId2 = { Ref: 'FriggPublicSubnet2' };
|
|
737
|
+
|
|
738
|
+
console.log(' ✅ Subnet resources added to template');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Generate subnet CIDRs for new VPC or existing VPC
|
|
743
|
+
*/
|
|
744
|
+
generateSubnetCidrsForNewVpc(vpcId, discoveredResources) {
|
|
745
|
+
// If VPC is a Ref (new VPC), use Fn::Cidr
|
|
746
|
+
if (typeof vpcId === 'object' && vpcId.Ref === 'FriggVPC') {
|
|
747
|
+
return {
|
|
748
|
+
private1: { 'Fn::Select': [0, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
|
|
749
|
+
private2: { 'Fn::Select': [1, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
|
|
750
|
+
public1: { 'Fn::Select': [2, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
|
|
751
|
+
public2: { 'Fn::Select': [3, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// For existing VPC, find available CIDRs
|
|
756
|
+
const existingCidrs = new Set();
|
|
757
|
+
if (discoveredResources?.subnets) {
|
|
758
|
+
for (const subnet of discoveredResources.subnets) {
|
|
759
|
+
if (subnet.CidrBlock) {
|
|
760
|
+
existingCidrs.add(subnet.CidrBlock);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const findAvailableCidr = (startOctet, endOctet) => {
|
|
766
|
+
for (let octet = startOctet; octet <= endOctet; octet++) {
|
|
767
|
+
const candidate = `172.31.${octet}.0/24`;
|
|
768
|
+
if (!existingCidrs.has(candidate)) {
|
|
769
|
+
existingCidrs.add(candidate);
|
|
770
|
+
return candidate;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return `172.31.${startOctet}.0/24`;
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
return {
|
|
777
|
+
private1: findAvailableCidr(240, 249),
|
|
778
|
+
private2: findAvailableCidr(240, 249),
|
|
779
|
+
public1: findAvailableCidr(250, 255),
|
|
780
|
+
public2: findAvailableCidr(250, 255),
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Build NAT Gateway based on ownership decision
|
|
786
|
+
*/
|
|
787
|
+
buildNatGatewayFromDecision(decision, appDefinition, discoveredResources, result) {
|
|
788
|
+
if (!decision.ownership) {
|
|
789
|
+
console.log(' ⊝ NAT Gateway disabled');
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (decision.ownership === ResourceOwnership.STACK) {
|
|
794
|
+
if (decision.physicalId) {
|
|
795
|
+
// NAT Gateway exists in stack - CloudFormation will handle it
|
|
796
|
+
console.log(` ✓ NAT Gateway in stack: ${decision.physicalId}`);
|
|
797
|
+
// Still need to ensure route tables are set up
|
|
798
|
+
this.createNatGatewayRouting(appDefinition, discoveredResources, result, { Ref: 'FriggNATGateway' });
|
|
799
|
+
} else {
|
|
800
|
+
// Create new NAT Gateway
|
|
801
|
+
console.log(' → Creating NAT Gateway in template...');
|
|
802
|
+
this.createNatGatewayInTemplate(appDefinition, discoveredResources, result);
|
|
803
|
+
}
|
|
804
|
+
} else if (decision.ownership === ResourceOwnership.EXTERNAL) {
|
|
805
|
+
// Use external NAT Gateway
|
|
806
|
+
console.log(` ✓ Using external NAT Gateway: ${decision.physicalId}`);
|
|
807
|
+
result.natGatewayId = decision.physicalId;
|
|
808
|
+
this.createNatGatewayRouting(appDefinition, discoveredResources, result, decision.physicalId);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Create NAT Gateway resources in CloudFormation template
|
|
814
|
+
*/
|
|
815
|
+
createNatGatewayInTemplate(appDefinition, discoveredResources, result) {
|
|
816
|
+
// Elastic IP for NAT Gateway
|
|
817
|
+
result.resources.FriggNATGatewayEIP = {
|
|
818
|
+
Type: 'AWS::EC2::EIP',
|
|
819
|
+
DeletionPolicy: 'Retain',
|
|
820
|
+
UpdateReplacePolicy: 'Retain',
|
|
821
|
+
Properties: {
|
|
822
|
+
Domain: 'vpc',
|
|
823
|
+
Tags: [
|
|
824
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-eip' },
|
|
825
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
826
|
+
],
|
|
827
|
+
},
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
// NAT Gateway in public subnet
|
|
831
|
+
result.resources.FriggNATGateway = {
|
|
832
|
+
Type: 'AWS::EC2::NatGateway',
|
|
833
|
+
DeletionPolicy: 'Retain',
|
|
834
|
+
UpdateReplacePolicy: 'Retain',
|
|
835
|
+
Properties: {
|
|
836
|
+
AllocationId: { 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'] },
|
|
837
|
+
SubnetId: discoveredResources.publicSubnetId1 || { Ref: 'FriggPublicSubnet' },
|
|
838
|
+
Tags: [
|
|
839
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat' },
|
|
840
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
841
|
+
],
|
|
842
|
+
},
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
// Create public routing
|
|
846
|
+
this.createPublicRouting(appDefinition, discoveredResources, result);
|
|
847
|
+
|
|
848
|
+
// Create NAT routing
|
|
849
|
+
this.createNatGatewayRouting(appDefinition, discoveredResources, result, { Ref: 'FriggNATGateway' });
|
|
850
|
+
|
|
851
|
+
console.log(' ✅ NAT Gateway resources added to template');
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Build VPC Endpoints based on ownership decisions
|
|
856
|
+
*/
|
|
857
|
+
buildVpcEndpointsFromDecisions(decisions, appDefinition, result) {
|
|
858
|
+
const endpointsToCreate = [];
|
|
859
|
+
const endpointsInStack = [];
|
|
860
|
+
const externalEndpoints = [];
|
|
861
|
+
|
|
862
|
+
// Analyze decisions
|
|
863
|
+
Object.entries(decisions).forEach(([type, decision]) => {
|
|
864
|
+
if (decision.ownership === ResourceOwnership.STACK && !decision.physicalId) {
|
|
865
|
+
endpointsToCreate.push(type);
|
|
866
|
+
} else if (decision.ownership === ResourceOwnership.STACK && decision.physicalId) {
|
|
867
|
+
endpointsInStack.push(type);
|
|
868
|
+
} else if (decision.ownership === ResourceOwnership.EXTERNAL) {
|
|
869
|
+
externalEndpoints.push(type);
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
if (endpointsInStack.length > 0) {
|
|
874
|
+
console.log(` ✓ VPC Endpoints in stack: ${endpointsInStack.join(', ')}`);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (externalEndpoints.length > 0) {
|
|
878
|
+
console.log(` ✓ External VPC Endpoints: ${externalEndpoints.join(', ')}`);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (endpointsToCreate.length === 0) {
|
|
882
|
+
if (endpointsInStack.length === 0 && externalEndpoints.length === 0) {
|
|
883
|
+
console.log(' ⊝ VPC Endpoints disabled');
|
|
884
|
+
}
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
console.log(` → Creating VPC Endpoints: ${endpointsToCreate.join(', ')}...`);
|
|
889
|
+
|
|
890
|
+
const vpcId = result.vpcId;
|
|
891
|
+
|
|
892
|
+
// Create route table if needed
|
|
893
|
+
if (!result.resources.FriggLambdaRouteTable) {
|
|
894
|
+
result.resources.FriggLambdaRouteTable = {
|
|
895
|
+
Type: 'AWS::EC2::RouteTable',
|
|
896
|
+
Properties: {
|
|
897
|
+
VpcId: vpcId,
|
|
898
|
+
Tags: [
|
|
899
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
|
|
900
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
901
|
+
],
|
|
902
|
+
},
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Ensure subnet associations
|
|
907
|
+
this.ensureSubnetAssociations(appDefinition, {}, result);
|
|
908
|
+
|
|
909
|
+
// Create endpoints
|
|
910
|
+
if (endpointsToCreate.includes('s3')) {
|
|
911
|
+
result.resources.FriggS3VPCEndpoint = {
|
|
912
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
913
|
+
Properties: {
|
|
914
|
+
VpcId: vpcId,
|
|
915
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.s3',
|
|
916
|
+
VpcEndpointType: 'Gateway',
|
|
917
|
+
RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }],
|
|
918
|
+
},
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (endpointsToCreate.includes('dynamodb')) {
|
|
923
|
+
result.resources.FriggDynamoDBVPCEndpoint = {
|
|
924
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
925
|
+
Properties: {
|
|
926
|
+
VpcId: vpcId,
|
|
927
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.dynamodb',
|
|
928
|
+
VpcEndpointType: 'Gateway',
|
|
929
|
+
RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }],
|
|
930
|
+
},
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Create security group for interface endpoints if needed
|
|
935
|
+
const needsInterfaceEndpoints = endpointsToCreate.some(type => ['kms', 'secretsManager', 'sqs'].includes(type));
|
|
936
|
+
if (needsInterfaceEndpoints) {
|
|
937
|
+
result.resources.FriggVPCEndpointSecurityGroup = {
|
|
938
|
+
Type: 'AWS::EC2::SecurityGroup',
|
|
939
|
+
Properties: {
|
|
940
|
+
GroupDescription: 'Security group for VPC Endpoints',
|
|
941
|
+
VpcId: vpcId,
|
|
942
|
+
SecurityGroupIngress: [
|
|
943
|
+
{
|
|
944
|
+
IpProtocol: 'tcp',
|
|
945
|
+
FromPort: 443,
|
|
946
|
+
ToPort: 443,
|
|
947
|
+
SourceSecurityGroupId: { Ref: 'FriggLambdaSecurityGroup' },
|
|
948
|
+
Description: 'HTTPS from Lambda',
|
|
949
|
+
},
|
|
950
|
+
],
|
|
951
|
+
Tags: [
|
|
952
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg' },
|
|
953
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
954
|
+
],
|
|
955
|
+
},
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (endpointsToCreate.includes('kms')) {
|
|
960
|
+
result.resources.FriggKMSVPCEndpoint = {
|
|
961
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
962
|
+
Properties: {
|
|
963
|
+
VpcId: vpcId,
|
|
964
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.kms',
|
|
965
|
+
VpcEndpointType: 'Interface',
|
|
966
|
+
SubnetIds: result.vpcConfig.subnetIds,
|
|
967
|
+
SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
|
|
968
|
+
PrivateDnsEnabled: true,
|
|
969
|
+
},
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (endpointsToCreate.includes('secretsManager')) {
|
|
974
|
+
result.resources.FriggSecretsManagerVPCEndpoint = {
|
|
975
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
976
|
+
Properties: {
|
|
977
|
+
VpcId: vpcId,
|
|
978
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.secretsmanager',
|
|
979
|
+
VpcEndpointType: 'Interface',
|
|
980
|
+
SubnetIds: result.vpcConfig.subnetIds,
|
|
981
|
+
SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
|
|
982
|
+
PrivateDnsEnabled: true,
|
|
983
|
+
},
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (endpointsToCreate.includes('sqs')) {
|
|
988
|
+
result.resources.FriggSQSVPCEndpoint = {
|
|
989
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
990
|
+
Properties: {
|
|
991
|
+
VpcId: vpcId,
|
|
992
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.sqs',
|
|
993
|
+
VpcEndpointType: 'Interface',
|
|
994
|
+
SubnetIds: result.vpcConfig.subnetIds,
|
|
995
|
+
SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
|
|
996
|
+
PrivateDnsEnabled: true,
|
|
997
|
+
},
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
console.log(` ✅ VPC Endpoint resources added to template`);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Perform self-healing checks and fixes
|
|
1006
|
+
*/
|
|
1007
|
+
performSelfHealing(discoveredResources, appDefinition) {
|
|
1008
|
+
console.log('🔧 VPC Self-healing mode enabled - checking for misconfigurations...');
|
|
1009
|
+
|
|
1010
|
+
const healingReport = {
|
|
1011
|
+
healed: [],
|
|
1012
|
+
warnings: [],
|
|
1013
|
+
errors: [],
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
// Check for NAT Gateway in private subnet
|
|
1017
|
+
if (discoveredResources.natGatewayInPrivateSubnet) {
|
|
1018
|
+
healingReport.warnings.push(
|
|
1019
|
+
`NAT Gateway ${discoveredResources.natGatewayInPrivateSubnet} is in a private subnet`
|
|
1020
|
+
);
|
|
1021
|
+
healingReport.healed.push(
|
|
1022
|
+
'Will create new NAT Gateway in public subnet'
|
|
1023
|
+
);
|
|
1024
|
+
discoveredResources.needsNewNatGateway = true;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Check for orphaned Elastic IPs
|
|
1028
|
+
if (discoveredResources.orphanedElasticIps?.length > 0) {
|
|
1029
|
+
healingReport.warnings.push(
|
|
1030
|
+
`Found ${discoveredResources.orphanedElasticIps.length} orphaned Elastic IPs`
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Check for subnet routing issues
|
|
1035
|
+
if (discoveredResources.privateSubnetsWithWrongRoutes?.length > 0) {
|
|
1036
|
+
healingReport.warnings.push(
|
|
1037
|
+
`Found ${discoveredResources.privateSubnetsWithWrongRoutes.length} subnets with wrong routes`
|
|
1038
|
+
);
|
|
1039
|
+
healingReport.healed.push('Will create correct route tables');
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Log healing report
|
|
1043
|
+
if (healingReport.healed.length > 0) {
|
|
1044
|
+
console.log(' ✅ Self-healing actions:');
|
|
1045
|
+
healingReport.healed.forEach(action => console.log(` - ${action}`));
|
|
1046
|
+
}
|
|
1047
|
+
if (healingReport.warnings.length > 0) {
|
|
1048
|
+
console.log(' ⚠️ Issues detected:');
|
|
1049
|
+
healingReport.warnings.forEach(warning => console.log(` - ${warning}`));
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
return healingReport;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Build new VPC from scratch
|
|
1057
|
+
*/
|
|
1058
|
+
async buildNewVpc(appDefinition, discoveredResources, result) {
|
|
1059
|
+
console.log(' Creating new VPC infrastructure...');
|
|
1060
|
+
|
|
1061
|
+
const cidrBlock = appDefinition.vpc.cidrBlock || '10.0.0.0/16';
|
|
1062
|
+
|
|
1063
|
+
// Main VPC
|
|
1064
|
+
result.resources.FriggVPC = {
|
|
1065
|
+
Type: 'AWS::EC2::VPC',
|
|
1066
|
+
Properties: {
|
|
1067
|
+
CidrBlock: cidrBlock,
|
|
1068
|
+
EnableDnsHostnames: true,
|
|
1069
|
+
EnableDnsSupport: true,
|
|
1070
|
+
Tags: [
|
|
1071
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc' },
|
|
1072
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1073
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
1074
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
1075
|
+
],
|
|
1076
|
+
},
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
// Internet Gateway
|
|
1080
|
+
result.resources.FriggInternetGateway = {
|
|
1081
|
+
Type: 'AWS::EC2::InternetGateway',
|
|
1082
|
+
Properties: {
|
|
1083
|
+
Tags: [
|
|
1084
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-igw' },
|
|
1085
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1086
|
+
],
|
|
1087
|
+
},
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
result.resources.FriggVPCGatewayAttachment = {
|
|
1091
|
+
Type: 'AWS::EC2::VPCGatewayAttachment',
|
|
1092
|
+
Properties: {
|
|
1093
|
+
VpcId: { Ref: 'FriggVPC' },
|
|
1094
|
+
InternetGatewayId: { Ref: 'FriggInternetGateway' },
|
|
1095
|
+
},
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
// Lambda Security Group
|
|
1099
|
+
result.resources.FriggLambdaSecurityGroup = {
|
|
1100
|
+
Type: 'AWS::EC2::SecurityGroup',
|
|
1101
|
+
Properties: {
|
|
1102
|
+
GroupDescription: 'Security group for Frigg Lambda functions',
|
|
1103
|
+
VpcId: { Ref: 'FriggVPC' },
|
|
1104
|
+
SecurityGroupEgress: [
|
|
1105
|
+
{ IpProtocol: 'tcp', FromPort: 443, ToPort: 443, CidrIp: '0.0.0.0/0', Description: 'HTTPS outbound' },
|
|
1106
|
+
{ IpProtocol: 'tcp', FromPort: 80, ToPort: 80, CidrIp: '0.0.0.0/0', Description: 'HTTP outbound' },
|
|
1107
|
+
{ IpProtocol: 'tcp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS TCP' },
|
|
1108
|
+
{ IpProtocol: 'udp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS UDP' },
|
|
1109
|
+
{ IpProtocol: 'tcp', FromPort: 5432, ToPort: 5432, CidrIp: '0.0.0.0/0', Description: 'PostgreSQL' },
|
|
1110
|
+
{ IpProtocol: 'tcp', FromPort: 27017, ToPort: 27017, CidrIp: '0.0.0.0/0', Description: 'MongoDB' },
|
|
1111
|
+
],
|
|
1112
|
+
Tags: [
|
|
1113
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-sg' },
|
|
1114
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1115
|
+
],
|
|
1116
|
+
},
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
result.vpcId = { Ref: 'FriggVPC' };
|
|
1120
|
+
result.vpcConfig.securityGroupIds = [{ Ref: 'FriggLambdaSecurityGroup' }];
|
|
1121
|
+
|
|
1122
|
+
console.log(' ✅ New VPC infrastructure resources created');
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Use existing VPC (explicitly provided)
|
|
1127
|
+
*/
|
|
1128
|
+
async useExistingVpc(appDefinition, discoveredResources, result) {
|
|
1129
|
+
console.log(' Using existing VPC...');
|
|
1130
|
+
|
|
1131
|
+
if (!appDefinition.vpc.vpcId) {
|
|
1132
|
+
throw new Error('vpc.vpcId is required when management="use-existing"');
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
result.vpcId = appDefinition.vpc.vpcId;
|
|
1136
|
+
result.vpcConfig.securityGroupIds = appDefinition.vpc.securityGroupIds ||
|
|
1137
|
+
(discoveredResources.defaultSecurityGroupId ? [discoveredResources.defaultSecurityGroupId] : []);
|
|
1138
|
+
|
|
1139
|
+
console.log(` ✅ Using VPC: ${result.vpcId}`);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Discover existing VPC from AWS
|
|
1144
|
+
*/
|
|
1145
|
+
async discoverVpc(appDefinition, discoveredResources, result) {
|
|
1146
|
+
console.log(' Discovering existing VPC...');
|
|
1147
|
+
|
|
1148
|
+
if (!discoveredResources.defaultVpcId) {
|
|
1149
|
+
throw new Error(
|
|
1150
|
+
'VPC discovery failed: No VPC found. Set vpc.management to "create-new" or provide vpc.vpcId with "use-existing".'
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
result.vpcId = discoveredResources.defaultVpcId;
|
|
1155
|
+
|
|
1156
|
+
// Check if resources came from CloudFormation stack
|
|
1157
|
+
const fromCfStack = discoveredResources.fromCloudFormationStack === true;
|
|
1158
|
+
const existingLogicalIds = discoveredResources.existingLogicalIds || [];
|
|
1159
|
+
|
|
1160
|
+
if (fromCfStack && existingLogicalIds.length > 0) {
|
|
1161
|
+
console.log(` ✓ VPC discovered from CloudFormation stack: ${discoveredResources.stackName}`);
|
|
1162
|
+
console.log(` ✓ Found ${existingLogicalIds.length} existing resources in stack`);
|
|
1163
|
+
console.log(' ℹ Adding resources to template for idempotent deployment');
|
|
1164
|
+
} else {
|
|
1165
|
+
// VPC discovered from AWS API (not from CF stack)
|
|
1166
|
+
console.log(' ℹ VPC discovered from AWS API - will create Lambda security group');
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// Always create Lambda security group in template for idempotent deployments
|
|
1170
|
+
// CloudFormation will recognize it already exists and won't recreate it
|
|
1171
|
+
result.resources.FriggLambdaSecurityGroup = {
|
|
1172
|
+
Type: 'AWS::EC2::SecurityGroup',
|
|
1173
|
+
Properties: {
|
|
1174
|
+
GroupDescription: 'Security group for Frigg Lambda functions',
|
|
1175
|
+
VpcId: result.vpcId,
|
|
1176
|
+
SecurityGroupEgress: [
|
|
1177
|
+
{ IpProtocol: 'tcp', FromPort: 443, ToPort: 443, CidrIp: '0.0.0.0/0', Description: 'HTTPS outbound' },
|
|
1178
|
+
{ IpProtocol: 'tcp', FromPort: 80, ToPort: 80, CidrIp: '0.0.0.0/0', Description: 'HTTP outbound' },
|
|
1179
|
+
{ IpProtocol: 'tcp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS TCP' },
|
|
1180
|
+
{ IpProtocol: 'udp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS UDP' },
|
|
1181
|
+
{ IpProtocol: 'tcp', FromPort: 5432, ToPort: 5432, CidrIp: '0.0.0.0/0', Description: 'PostgreSQL' },
|
|
1182
|
+
{ IpProtocol: 'tcp', FromPort: 27017, ToPort: 27017, CidrIp: '0.0.0.0/0', Description: 'MongoDB' },
|
|
1183
|
+
],
|
|
1184
|
+
Tags: [
|
|
1185
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-sg' },
|
|
1186
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1187
|
+
],
|
|
1188
|
+
},
|
|
1189
|
+
};
|
|
1190
|
+
|
|
1191
|
+
// Always use Ref since resource is in template
|
|
1192
|
+
result.vpcConfig.securityGroupIds = [{ Ref: 'FriggLambdaSecurityGroup' }];
|
|
1193
|
+
|
|
1194
|
+
console.log(` ✅ Discovered VPC: ${result.vpcId}`);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Build subnet infrastructure
|
|
1199
|
+
* @param {Object} vpcManagement - Normalized VPC management mode (passed from build() to ensure consistency)
|
|
1200
|
+
*/
|
|
1201
|
+
async buildSubnets(appDefinition, discoveredResources, result, vpcManagement) {
|
|
1202
|
+
// Default subnet management depends on context:
|
|
1203
|
+
// - Stack-managed subnets discovered: discover (reuse existing)
|
|
1204
|
+
// - use-existing mode with subnet IDs provided: use-existing
|
|
1205
|
+
// - create-new mode: create
|
|
1206
|
+
// - discover mode without stack subnets: create (for stage isolation)
|
|
1207
|
+
let defaultSubnetManagement = 'create';
|
|
1208
|
+
|
|
1209
|
+
// Check if stack-managed subnets were discovered from CloudFormation
|
|
1210
|
+
// Only reuse if they're actual subnet IDs (strings), not CloudFormation Refs (objects)
|
|
1211
|
+
const hasStackManagedSubnets =
|
|
1212
|
+
discoveredResources?.privateSubnetId1 &&
|
|
1213
|
+
discoveredResources?.privateSubnetId2 &&
|
|
1214
|
+
typeof discoveredResources.privateSubnetId1 === 'string' &&
|
|
1215
|
+
typeof discoveredResources.privateSubnetId2 === 'string';
|
|
1216
|
+
|
|
1217
|
+
if (hasStackManagedSubnets) {
|
|
1218
|
+
defaultSubnetManagement = 'discover';
|
|
1219
|
+
} else if (vpcManagement === 'use-existing' && appDefinition.vpc.subnets?.ids?.length >= 2) {
|
|
1220
|
+
defaultSubnetManagement = 'use-existing';
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const subnetManagement = appDefinition.vpc.subnets?.management || defaultSubnetManagement;
|
|
1224
|
+
|
|
1225
|
+
console.log(` Subnet Management Mode: ${subnetManagement} (default: ${defaultSubnetManagement}, explicit: ${appDefinition.vpc.subnets?.management})`);
|
|
1226
|
+
|
|
1227
|
+
switch (subnetManagement) {
|
|
1228
|
+
case 'create':
|
|
1229
|
+
this.createSubnets(appDefinition, discoveredResources, result, vpcManagement);
|
|
1230
|
+
break;
|
|
1231
|
+
case 'use-existing':
|
|
1232
|
+
this.useExistingSubnets(appDefinition, result);
|
|
1233
|
+
break;
|
|
1234
|
+
case 'discover':
|
|
1235
|
+
default:
|
|
1236
|
+
this.discoverSubnets(appDefinition, discoveredResources, result);
|
|
1237
|
+
break;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* Create new subnets
|
|
1243
|
+
*/
|
|
1244
|
+
createSubnets(appDefinition, discoveredResources, result, vpcManagement) {
|
|
1245
|
+
console.log(' Creating new subnets...');
|
|
1246
|
+
|
|
1247
|
+
const subnetVpcId = vpcManagement === 'create-new' ? { Ref: 'FriggVPC' } : result.vpcId;
|
|
1248
|
+
|
|
1249
|
+
// Generate CIDRs - pass discovered resources to avoid conflicts
|
|
1250
|
+
const cidrs = this.generateSubnetCidrs(vpcManagement, discoveredResources);
|
|
1251
|
+
|
|
1252
|
+
// Private Subnet 1
|
|
1253
|
+
result.resources.FriggPrivateSubnet1 = {
|
|
1254
|
+
Type: 'AWS::EC2::Subnet',
|
|
1255
|
+
DeletionPolicy: 'Retain',
|
|
1256
|
+
Properties: {
|
|
1257
|
+
VpcId: subnetVpcId,
|
|
1258
|
+
CidrBlock: cidrs.private1,
|
|
1259
|
+
AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
|
|
1260
|
+
Tags: [
|
|
1261
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-1' },
|
|
1262
|
+
{ Key: 'Type', Value: 'Private' },
|
|
1263
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1264
|
+
],
|
|
1265
|
+
},
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1268
|
+
// Private Subnet 2
|
|
1269
|
+
result.resources.FriggPrivateSubnet2 = {
|
|
1270
|
+
Type: 'AWS::EC2::Subnet',
|
|
1271
|
+
DeletionPolicy: 'Retain',
|
|
1272
|
+
Properties: {
|
|
1273
|
+
VpcId: subnetVpcId,
|
|
1274
|
+
CidrBlock: cidrs.private2,
|
|
1275
|
+
AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
|
|
1276
|
+
Tags: [
|
|
1277
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-2' },
|
|
1278
|
+
{ Key: 'Type', Value: 'Private' },
|
|
1279
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1280
|
+
],
|
|
1281
|
+
},
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
// Public Subnets (for NAT Gateway and Aurora if publicly accessible)
|
|
1285
|
+
result.resources.FriggPublicSubnet = {
|
|
1286
|
+
Type: 'AWS::EC2::Subnet',
|
|
1287
|
+
Properties: {
|
|
1288
|
+
VpcId: subnetVpcId,
|
|
1289
|
+
CidrBlock: cidrs.public1,
|
|
1290
|
+
MapPublicIpOnLaunch: true,
|
|
1291
|
+
AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
|
|
1292
|
+
Tags: [
|
|
1293
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-1' },
|
|
1294
|
+
{ Key: 'Type', Value: 'Public' },
|
|
1295
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1296
|
+
],
|
|
1297
|
+
},
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
result.resources.FriggPublicSubnet2 = {
|
|
1301
|
+
Type: 'AWS::EC2::Subnet',
|
|
1302
|
+
Properties: {
|
|
1303
|
+
VpcId: subnetVpcId,
|
|
1304
|
+
CidrBlock: cidrs.public2,
|
|
1305
|
+
MapPublicIpOnLaunch: true,
|
|
1306
|
+
AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
|
|
1307
|
+
Tags: [
|
|
1308
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-2' },
|
|
1309
|
+
{ Key: 'Type', Value: 'Public' },
|
|
1310
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1311
|
+
],
|
|
1312
|
+
},
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1315
|
+
result.vpcConfig.subnetIds = [
|
|
1316
|
+
{ Ref: 'FriggPrivateSubnet1' },
|
|
1317
|
+
{ Ref: 'FriggPrivateSubnet2' },
|
|
1318
|
+
];
|
|
1319
|
+
|
|
1320
|
+
// Map to discovered resources for other builders (Aurora, etc.)
|
|
1321
|
+
discoveredResources.privateSubnetId1 = { Ref: 'FriggPrivateSubnet1' };
|
|
1322
|
+
discoveredResources.privateSubnetId2 = { Ref: 'FriggPrivateSubnet2' };
|
|
1323
|
+
discoveredResources.publicSubnetId1 = { Ref: 'FriggPublicSubnet' };
|
|
1324
|
+
discoveredResources.publicSubnetId2 = { Ref: 'FriggPublicSubnet2' };
|
|
1325
|
+
|
|
1326
|
+
console.log(' ✅ Subnets created');
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Use existing subnets
|
|
1331
|
+
*/
|
|
1332
|
+
useExistingSubnets(appDefinition, result) {
|
|
1333
|
+
console.log(' Using existing subnets...');
|
|
1334
|
+
|
|
1335
|
+
if (!appDefinition.vpc.subnets?.ids || appDefinition.vpc.subnets.ids.length < 2) {
|
|
1336
|
+
throw new Error(
|
|
1337
|
+
'At least 2 subnet IDs required when subnets.management="use-existing"'
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
result.vpcConfig.subnetIds = appDefinition.vpc.subnets.ids;
|
|
1342
|
+
console.log(` ✅ Using ${result.vpcConfig.subnetIds.length} existing subnets`);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* Discover existing subnets from AWS
|
|
1347
|
+
*/
|
|
1348
|
+
discoverSubnets(appDefinition, discoveredResources, result) {
|
|
1349
|
+
console.log(' Discovering subnets...');
|
|
1350
|
+
|
|
1351
|
+
// Use explicitly provided subnet IDs first
|
|
1352
|
+
if (appDefinition.vpc.subnets?.ids?.length >= 2) {
|
|
1353
|
+
result.vpcConfig.subnetIds = appDefinition.vpc.subnets.ids;
|
|
1354
|
+
console.log(` ✅ Using ${result.vpcConfig.subnetIds.length} provided subnets`);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// User explicitly set subnets.management: 'discover', so use discovered subnets
|
|
1359
|
+
// NOTE: This may cause route table conflicts if multiple stages share subnets
|
|
1360
|
+
// Default behavior is now to create stage-specific subnets (subnets.management: 'create')
|
|
1361
|
+
if (discoveredResources.privateSubnetId1 && discoveredResources.privateSubnetId2) {
|
|
1362
|
+
result.vpcConfig.subnetIds = [
|
|
1363
|
+
discoveredResources.privateSubnetId1,
|
|
1364
|
+
discoveredResources.privateSubnetId2,
|
|
1365
|
+
];
|
|
1366
|
+
console.log(' ✅ Using discovered subnets (backwards compatibility mode)');
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// No subnets found - create if self-heal enabled
|
|
1371
|
+
if (appDefinition.vpc.selfHeal) {
|
|
1372
|
+
console.log(' ⚠️ No subnets found - self-heal will create them');
|
|
1373
|
+
this.createSubnets(appDefinition, discoveredResources, result, 'discover');
|
|
1374
|
+
} else {
|
|
1375
|
+
throw new Error(
|
|
1376
|
+
'No subnets discovered. Enable vpc.selfHeal, set subnets.management to "create", or provide subnet IDs.'
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/**
|
|
1382
|
+
* Generate subnet CIDR blocks
|
|
1383
|
+
* Finds available CIDRs that don't conflict with existing subnets
|
|
1384
|
+
*/
|
|
1385
|
+
generateSubnetCidrs(vpcManagement, discoveredResources) {
|
|
1386
|
+
if (vpcManagement === 'create-new') {
|
|
1387
|
+
// Use CloudFormation Fn::Cidr for dynamic generation
|
|
1388
|
+
return {
|
|
1389
|
+
private1: { 'Fn::Select': [0, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
|
|
1390
|
+
private2: { 'Fn::Select': [1, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
|
|
1391
|
+
public1: { 'Fn::Select': [2, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
|
|
1392
|
+
public2: { 'Fn::Select': [3, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
|
|
1393
|
+
};
|
|
1394
|
+
} else {
|
|
1395
|
+
// Find available CIDRs for existing VPC by checking existing subnets
|
|
1396
|
+
const existingCidrs = new Set();
|
|
1397
|
+
|
|
1398
|
+
// Collect all existing subnet CIDRs
|
|
1399
|
+
if (discoveredResources?.subnets) {
|
|
1400
|
+
for (const subnet of discoveredResources.subnets) {
|
|
1401
|
+
if (subnet.CidrBlock) {
|
|
1402
|
+
existingCidrs.add(subnet.CidrBlock);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
console.log(` Found ${existingCidrs.size} existing subnet CIDRs in VPC`);
|
|
1408
|
+
|
|
1409
|
+
// Generate candidates in the default VPC range (172.31.0.0/16)
|
|
1410
|
+
// Private subnets: 240-249, Public subnets: 250-255
|
|
1411
|
+
const findAvailableCidr = (startOctet, endOctet) => {
|
|
1412
|
+
for (let octet = startOctet; octet <= endOctet; octet++) {
|
|
1413
|
+
const candidate = `172.31.${octet}.0/24`;
|
|
1414
|
+
if (!existingCidrs.has(candidate)) {
|
|
1415
|
+
existingCidrs.add(candidate); // Mark as used immediately
|
|
1416
|
+
return candidate;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
// Fallback if range exhausted
|
|
1420
|
+
return `172.31.${startOctet}.0/24`;
|
|
1421
|
+
};
|
|
1422
|
+
|
|
1423
|
+
const privateRange = { start: 240, end: 249 };
|
|
1424
|
+
const publicRange = { start: 250, end: 255 };
|
|
1425
|
+
|
|
1426
|
+
const cidrs = {
|
|
1427
|
+
private1: findAvailableCidr(privateRange.start, privateRange.end),
|
|
1428
|
+
private2: findAvailableCidr(privateRange.start, privateRange.end),
|
|
1429
|
+
public1: findAvailableCidr(publicRange.start, publicRange.end),
|
|
1430
|
+
public2: findAvailableCidr(publicRange.start, publicRange.end),
|
|
1431
|
+
};
|
|
1432
|
+
|
|
1433
|
+
console.log(` Using available CIDRs: ${Object.values(cidrs).join(', ')}`);
|
|
1434
|
+
|
|
1435
|
+
return cidrs;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* Build NAT Gateway for private subnet internet access
|
|
1441
|
+
*/
|
|
1442
|
+
async buildNatGateway(appDefinition, discoveredResources, result) {
|
|
1443
|
+
const natManagement = appDefinition.vpc.natGateway?.management || 'discover';
|
|
1444
|
+
|
|
1445
|
+
console.log(` NAT Gateway Management: ${natManagement}`);
|
|
1446
|
+
|
|
1447
|
+
// Check if resources came from CloudFormation stack
|
|
1448
|
+
const fromCfStack = discoveredResources.fromCloudFormationStack === true;
|
|
1449
|
+
const existingLogicalIds = discoveredResources.existingLogicalIds || [];
|
|
1450
|
+
|
|
1451
|
+
if (fromCfStack && existingLogicalIds.length > 0) {
|
|
1452
|
+
console.log(' Skipping NAT Gateway - will reuse from CloudFormation stack');
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// Check if we should create NAT Gateway
|
|
1457
|
+
const needsNatGateway = natManagement === 'createAndManage' ||
|
|
1458
|
+
discoveredResources.needsNewNatGateway === true;
|
|
1459
|
+
|
|
1460
|
+
if (!needsNatGateway && natManagement === 'discover') {
|
|
1461
|
+
console.log(' Skipping NAT Gateway (discovery mode)');
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// Check if we should reuse existing
|
|
1466
|
+
if (appDefinition.vpc.natGateway?.id) {
|
|
1467
|
+
console.log(` Using existing NAT Gateway: ${appDefinition.vpc.natGateway.id}`);
|
|
1468
|
+
result.natGatewayId = appDefinition.vpc.natGateway.id;
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
if (discoveredResources.existingNatGatewayId && !discoveredResources.natGatewayInPrivateSubnet) {
|
|
1473
|
+
console.log(` Reusing discovered NAT Gateway: ${discoveredResources.existingNatGatewayId}`);
|
|
1474
|
+
result.natGatewayId = discoveredResources.existingNatGatewayId;
|
|
1475
|
+
|
|
1476
|
+
// Still need to create route table and associations for discovered NAT
|
|
1477
|
+
this.createNatGatewayRouting(appDefinition, discoveredResources, result, discoveredResources.existingNatGatewayId);
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// Create new NAT Gateway
|
|
1482
|
+
console.log(' Creating new NAT Gateway...');
|
|
1483
|
+
|
|
1484
|
+
// Elastic IP for NAT Gateway
|
|
1485
|
+
result.resources.FriggNATGatewayEIP = {
|
|
1486
|
+
Type: 'AWS::EC2::EIP',
|
|
1487
|
+
DeletionPolicy: 'Retain',
|
|
1488
|
+
UpdateReplacePolicy: 'Retain',
|
|
1489
|
+
Properties: {
|
|
1490
|
+
Domain: 'vpc',
|
|
1491
|
+
Tags: [
|
|
1492
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-eip' },
|
|
1493
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1494
|
+
],
|
|
1495
|
+
},
|
|
1496
|
+
};
|
|
1497
|
+
|
|
1498
|
+
// NAT Gateway in public subnet
|
|
1499
|
+
result.resources.FriggNATGateway = {
|
|
1500
|
+
Type: 'AWS::EC2::NatGateway',
|
|
1501
|
+
DeletionPolicy: 'Retain',
|
|
1502
|
+
UpdateReplacePolicy: 'Retain',
|
|
1503
|
+
Properties: {
|
|
1504
|
+
AllocationId: { 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'] },
|
|
1505
|
+
SubnetId: discoveredResources.publicSubnetId1 || { Ref: 'FriggPublicSubnet' },
|
|
1506
|
+
Tags: [
|
|
1507
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat' },
|
|
1508
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1509
|
+
],
|
|
1510
|
+
},
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
// Create public routing (public subnets → Internet Gateway)
|
|
1514
|
+
this.createPublicRouting(appDefinition, discoveredResources, result);
|
|
1515
|
+
|
|
1516
|
+
// Create routing for the new NAT Gateway (private subnets → NAT → IGW)
|
|
1517
|
+
this.createNatGatewayRouting(appDefinition, discoveredResources, result, { Ref: 'FriggNATGateway' });
|
|
1518
|
+
|
|
1519
|
+
console.log(' ✅ NAT Gateway infrastructure created');
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
/**
|
|
1523
|
+
* Create public route table with Internet Gateway route
|
|
1524
|
+
* Required for NAT Gateway to have internet access
|
|
1525
|
+
*/
|
|
1526
|
+
createPublicRouting(appDefinition, discoveredResources, result) {
|
|
1527
|
+
// Public route table with Internet Gateway route
|
|
1528
|
+
result.resources.FriggPublicRouteTable = {
|
|
1529
|
+
Type: 'AWS::EC2::RouteTable',
|
|
1530
|
+
Properties: {
|
|
1531
|
+
VpcId: result.vpcId,
|
|
1532
|
+
Tags: [
|
|
1533
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-rt' },
|
|
1534
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1535
|
+
],
|
|
1536
|
+
},
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1539
|
+
// Route to Internet Gateway
|
|
1540
|
+
result.resources.FriggPublicRoute = {
|
|
1541
|
+
Type: 'AWS::EC2::Route',
|
|
1542
|
+
DependsOn: 'FriggVPCGatewayAttachment',
|
|
1543
|
+
Properties: {
|
|
1544
|
+
RouteTableId: { Ref: 'FriggPublicRouteTable' },
|
|
1545
|
+
DestinationCidrBlock: '0.0.0.0/0',
|
|
1546
|
+
GatewayId: { Ref: 'FriggInternetGateway' },
|
|
1547
|
+
},
|
|
1548
|
+
};
|
|
1549
|
+
|
|
1550
|
+
// Use discovered public subnets or created ones
|
|
1551
|
+
const publicSubnet1 = discoveredResources.publicSubnetId1 || { Ref: 'FriggPublicSubnet' };
|
|
1552
|
+
const publicSubnet2 = discoveredResources.publicSubnetId2 || { Ref: 'FriggPublicSubnet2' };
|
|
1553
|
+
|
|
1554
|
+
// Associate public subnets with public route table
|
|
1555
|
+
result.resources.FriggPublicSubnet1RouteTableAssociation = {
|
|
1556
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1557
|
+
Properties: {
|
|
1558
|
+
SubnetId: publicSubnet1,
|
|
1559
|
+
RouteTableId: { Ref: 'FriggPublicRouteTable' },
|
|
1560
|
+
},
|
|
1561
|
+
};
|
|
1562
|
+
|
|
1563
|
+
result.resources.FriggPublicSubnet2RouteTableAssociation = {
|
|
1564
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1565
|
+
Properties: {
|
|
1566
|
+
SubnetId: publicSubnet2,
|
|
1567
|
+
RouteTableId: { Ref: 'FriggPublicRouteTable' },
|
|
1568
|
+
},
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* Create route table and associations for NAT Gateway
|
|
1574
|
+
*/
|
|
1575
|
+
createNatGatewayRouting(appDefinition, discoveredResources, result, natGatewayId) {
|
|
1576
|
+
// Private route table with NAT Gateway route
|
|
1577
|
+
if (!result.resources.FriggLambdaRouteTable) {
|
|
1578
|
+
result.resources.FriggLambdaRouteTable = {
|
|
1579
|
+
Type: 'AWS::EC2::RouteTable',
|
|
1580
|
+
Properties: {
|
|
1581
|
+
VpcId: result.vpcId,
|
|
1582
|
+
Tags: [
|
|
1583
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
|
|
1584
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1585
|
+
],
|
|
1586
|
+
},
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
result.resources.FriggPrivateRoute = {
|
|
1591
|
+
Type: 'AWS::EC2::Route',
|
|
1592
|
+
Properties: {
|
|
1593
|
+
RouteTableId: { Ref: 'FriggLambdaRouteTable' },
|
|
1594
|
+
DestinationCidrBlock: '0.0.0.0/0',
|
|
1595
|
+
NatGatewayId: natGatewayId,
|
|
1596
|
+
},
|
|
1597
|
+
};
|
|
1598
|
+
|
|
1599
|
+
// Associate route table with private subnets
|
|
1600
|
+
// Use discovered subnet IDs or CloudFormation references
|
|
1601
|
+
const subnet1Id = discoveredResources.privateSubnetId1 || { Ref: 'FriggPrivateSubnet1' };
|
|
1602
|
+
const subnet2Id = discoveredResources.privateSubnetId2 || { Ref: 'FriggPrivateSubnet2' };
|
|
1603
|
+
|
|
1604
|
+
result.resources.FriggPrivateSubnet1RouteTableAssociation = {
|
|
1605
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1606
|
+
Properties: {
|
|
1607
|
+
SubnetId: subnet1Id,
|
|
1608
|
+
RouteTableId: { Ref: 'FriggLambdaRouteTable' },
|
|
1609
|
+
},
|
|
1610
|
+
};
|
|
1611
|
+
|
|
1612
|
+
result.resources.FriggPrivateSubnet2RouteTableAssociation = {
|
|
1613
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1614
|
+
Properties: {
|
|
1615
|
+
SubnetId: subnet2Id,
|
|
1616
|
+
RouteTableId: { Ref: 'FriggLambdaRouteTable' },
|
|
1617
|
+
},
|
|
1618
|
+
};
|
|
1619
|
+
|
|
1620
|
+
console.log(' ✅ Route table and subnet associations created');
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
/**
|
|
1624
|
+
* Ensure subnet associations with route table
|
|
1625
|
+
* Called to heal missing associations when route table exists but associations don't
|
|
1626
|
+
*/
|
|
1627
|
+
ensureSubnetAssociations(appDefinition, discoveredResources, result) {
|
|
1628
|
+
// Skip if associations already created (by NAT Gateway routing)
|
|
1629
|
+
if (result.resources.FriggPrivateSubnet1RouteTableAssociation) {
|
|
1630
|
+
return; // Already handled by NAT Gateway routing
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
const routeTableId = discoveredResources.routeTableId || { Ref: 'FriggLambdaRouteTable' };
|
|
1634
|
+
const subnet1Id = discoveredResources.privateSubnetId1 || { Ref: 'FriggPrivateSubnet1' };
|
|
1635
|
+
const subnet2Id = discoveredResources.privateSubnetId2 || { Ref: 'FriggPrivateSubnet2' };
|
|
1636
|
+
|
|
1637
|
+
result.resources.FriggPrivateSubnet1RouteTableAssociation = {
|
|
1638
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1639
|
+
Properties: {
|
|
1640
|
+
SubnetId: subnet1Id,
|
|
1641
|
+
RouteTableId: routeTableId,
|
|
1642
|
+
},
|
|
1643
|
+
};
|
|
1644
|
+
|
|
1645
|
+
result.resources.FriggPrivateSubnet2RouteTableAssociation = {
|
|
1646
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1647
|
+
Properties: {
|
|
1648
|
+
SubnetId: subnet2Id,
|
|
1649
|
+
RouteTableId: routeTableId,
|
|
1650
|
+
},
|
|
1651
|
+
};
|
|
1652
|
+
|
|
1653
|
+
console.log(' ✓ Ensured subnet associations with route table');
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* Build VPC Endpoints for AWS services
|
|
1658
|
+
*/
|
|
1659
|
+
buildVpcEndpoints(appDefinition, discoveredResources, result, existingEndpoints = {}) {
|
|
1660
|
+
// Check if endpoints are from CloudFormation stack (string IDs)
|
|
1661
|
+
// Stack-managed resources should be reused, not recreated
|
|
1662
|
+
const stackManagedEndpoints = {
|
|
1663
|
+
s3: discoveredResources.s3VpcEndpointId && typeof discoveredResources.s3VpcEndpointId === 'string',
|
|
1664
|
+
dynamodb: discoveredResources.dynamoDbVpcEndpointId && typeof discoveredResources.dynamoDbVpcEndpointId === 'string',
|
|
1665
|
+
kms: discoveredResources.kmsVpcEndpointId && typeof discoveredResources.kmsVpcEndpointId === 'string',
|
|
1666
|
+
secretsManager: discoveredResources.secretsManagerVpcEndpointId && typeof discoveredResources.secretsManagerVpcEndpointId === 'string',
|
|
1667
|
+
sqs: discoveredResources.sqsVpcEndpointId && typeof discoveredResources.sqsVpcEndpointId === 'string',
|
|
1668
|
+
};
|
|
1669
|
+
|
|
1670
|
+
// Build list of what needs creation (not stack-managed, not existing elsewhere)
|
|
1671
|
+
const missing = [];
|
|
1672
|
+
if (!stackManagedEndpoints.s3 && !existingEndpoints.s3) missing.push('S3');
|
|
1673
|
+
if (!stackManagedEndpoints.dynamodb && !existingEndpoints.dynamodb) missing.push('DynamoDB');
|
|
1674
|
+
if (!stackManagedEndpoints.kms && !existingEndpoints.kms && appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') missing.push('KMS');
|
|
1675
|
+
if (!stackManagedEndpoints.secretsManager && !existingEndpoints.secretsManager) missing.push('Secrets Manager');
|
|
1676
|
+
// SQS endpoint needed for job queues and async processing
|
|
1677
|
+
if (!stackManagedEndpoints.sqs && !existingEndpoints.sqs) missing.push('SQS');
|
|
1678
|
+
|
|
1679
|
+
// Log reused stack-managed endpoints
|
|
1680
|
+
const reused = [];
|
|
1681
|
+
if (stackManagedEndpoints.s3) reused.push('S3');
|
|
1682
|
+
if (stackManagedEndpoints.dynamodb) reused.push('DynamoDB');
|
|
1683
|
+
if (stackManagedEndpoints.kms) reused.push('KMS');
|
|
1684
|
+
if (stackManagedEndpoints.secretsManager) reused.push('Secrets Manager');
|
|
1685
|
+
if (stackManagedEndpoints.sqs) reused.push('SQS');
|
|
1686
|
+
|
|
1687
|
+
if (reused.length > 0) {
|
|
1688
|
+
console.log(` ✓ Reusing stack-managed VPC endpoints: ${reused.join(', ')}`);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
if (missing.length > 0) {
|
|
1692
|
+
console.log(` Creating missing VPC Endpoints: ${missing.join(', ')}...`);
|
|
1693
|
+
} else if (reused.length === 0) {
|
|
1694
|
+
console.log(' All required VPC Endpoints already exist - skipping creation');
|
|
1695
|
+
return;
|
|
1696
|
+
} else {
|
|
1697
|
+
// All endpoints are stack-managed, no creation needed
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
const vpcId = result.vpcId || discoveredResources.defaultVpcId;
|
|
1702
|
+
|
|
1703
|
+
// Create route table for VPC endpoints if it doesn't exist
|
|
1704
|
+
// VPC endpoints (S3, DynamoDB) need to reference a route table
|
|
1705
|
+
if (!result.resources.FriggLambdaRouteTable) {
|
|
1706
|
+
result.resources.FriggLambdaRouteTable = {
|
|
1707
|
+
Type: 'AWS::EC2::RouteTable',
|
|
1708
|
+
Properties: {
|
|
1709
|
+
VpcId: vpcId,
|
|
1710
|
+
Tags: [
|
|
1711
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
|
|
1712
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1713
|
+
],
|
|
1714
|
+
},
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// Ensure subnet associations exist (healing for VPC endpoints without NAT Gateway)
|
|
1719
|
+
if (result.resources.FriggLambdaRouteTable || discoveredResources.routeTableId) {
|
|
1720
|
+
this.ensureSubnetAssociations(appDefinition, discoveredResources, result);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// S3 Gateway Endpoint (only if not stack-managed and missing)
|
|
1724
|
+
if (!stackManagedEndpoints.s3 && !existingEndpoints.s3) {
|
|
1725
|
+
result.resources.FriggS3VPCEndpoint = {
|
|
1726
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
1727
|
+
Properties: {
|
|
1728
|
+
VpcId: vpcId,
|
|
1729
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.s3',
|
|
1730
|
+
VpcEndpointType: 'Gateway',
|
|
1731
|
+
RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }],
|
|
1732
|
+
},
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// DynamoDB Gateway Endpoint (only if not stack-managed and missing)
|
|
1737
|
+
if (!stackManagedEndpoints.dynamodb && !existingEndpoints.dynamodb) {
|
|
1738
|
+
result.resources.FriggDynamoDBVPCEndpoint = {
|
|
1739
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
1740
|
+
Properties: {
|
|
1741
|
+
VpcId: vpcId,
|
|
1742
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.dynamodb',
|
|
1743
|
+
VpcEndpointType: 'Gateway',
|
|
1744
|
+
RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }],
|
|
1745
|
+
},
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
// VPC Endpoint Security Group (only if KMS, Secrets Manager, or SQS are not stack-managed and missing)
|
|
1750
|
+
const needsSecurityGroup =
|
|
1751
|
+
(!stackManagedEndpoints.kms && !existingEndpoints.kms && appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') ||
|
|
1752
|
+
(!stackManagedEndpoints.secretsManager && !existingEndpoints.secretsManager) ||
|
|
1753
|
+
(!stackManagedEndpoints.sqs && !existingEndpoints.sqs);
|
|
1754
|
+
|
|
1755
|
+
if (needsSecurityGroup) {
|
|
1756
|
+
result.resources.FriggVPCEndpointSecurityGroup = {
|
|
1757
|
+
Type: 'AWS::EC2::SecurityGroup',
|
|
1758
|
+
Properties: {
|
|
1759
|
+
GroupDescription: 'Security group for VPC Endpoints',
|
|
1760
|
+
VpcId: vpcId,
|
|
1761
|
+
SecurityGroupIngress: [
|
|
1762
|
+
{
|
|
1763
|
+
IpProtocol: 'tcp',
|
|
1764
|
+
FromPort: 443,
|
|
1765
|
+
ToPort: 443,
|
|
1766
|
+
SourceSecurityGroupId: { Ref: 'FriggLambdaSecurityGroup' },
|
|
1767
|
+
Description: 'HTTPS from Lambda',
|
|
1768
|
+
},
|
|
1769
|
+
],
|
|
1770
|
+
Tags: [
|
|
1771
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg' },
|
|
1772
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1773
|
+
],
|
|
1774
|
+
},
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// KMS Interface Endpoint (only if not stack-managed, missing, AND KMS encryption is enabled)
|
|
1779
|
+
if (!stackManagedEndpoints.kms && !existingEndpoints.kms && appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
|
|
1780
|
+
result.resources.FriggKMSVPCEndpoint = {
|
|
1781
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
1782
|
+
Properties: {
|
|
1783
|
+
VpcId: vpcId,
|
|
1784
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.kms',
|
|
1785
|
+
VpcEndpointType: 'Interface',
|
|
1786
|
+
SubnetIds: result.vpcConfig.subnetIds,
|
|
1787
|
+
SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
|
|
1788
|
+
PrivateDnsEnabled: true,
|
|
1789
|
+
},
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// Secrets Manager Interface Endpoint (only if not stack-managed and missing)
|
|
1794
|
+
if (!stackManagedEndpoints.secretsManager && !existingEndpoints.secretsManager) {
|
|
1795
|
+
result.resources.FriggSecretsManagerVPCEndpoint = {
|
|
1796
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
1797
|
+
Properties: {
|
|
1798
|
+
VpcId: vpcId,
|
|
1799
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.secretsmanager',
|
|
1800
|
+
VpcEndpointType: 'Interface',
|
|
1801
|
+
SubnetIds: result.vpcConfig.subnetIds,
|
|
1802
|
+
SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
|
|
1803
|
+
PrivateDnsEnabled: true,
|
|
1804
|
+
},
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
// SQS Interface Endpoint (only if not stack-managed and missing)
|
|
1809
|
+
// Used for job queues and async processing (not just database migrations)
|
|
1810
|
+
if (!stackManagedEndpoints.sqs && !existingEndpoints.sqs) {
|
|
1811
|
+
result.resources.FriggSQSVPCEndpoint = {
|
|
1812
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
1813
|
+
Properties: {
|
|
1814
|
+
VpcId: vpcId,
|
|
1815
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.sqs',
|
|
1816
|
+
VpcEndpointType: 'Interface',
|
|
1817
|
+
SubnetIds: result.vpcConfig.subnetIds,
|
|
1818
|
+
SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
|
|
1819
|
+
PrivateDnsEnabled: true,
|
|
1820
|
+
},
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
console.log(` ✅ Created ${missing.length} VPC endpoint(s): ${missing.join(', ')}`);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
module.exports = { VpcBuilder };
|
|
1829
|
+
|