@friggframework/devtools 2.0.0--canary.463.62579dd.0 → 2.0.0--canary.461.ec909cf.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frigg-cli/__tests__/unit/commands/db-setup.test.js +1 -1
- package/frigg-cli/db-setup-command/index.js +1 -1
- package/infrastructure/POSTGRES-CONFIGURATION.md +630 -0
- package/infrastructure/README.md +51 -0
- package/infrastructure/__tests__/postgres-config.test.js +914 -0
- package/infrastructure/aws-discovery.js +549 -21
- package/infrastructure/aws-discovery.test.js +447 -1
- package/infrastructure/domains/database/aurora-builder.js +307 -0
- package/infrastructure/domains/database/aurora-builder.test.js +482 -0
- package/infrastructure/domains/networking/vpc-builder.js +718 -0
- package/infrastructure/domains/networking/vpc-builder.test.js +772 -0
- package/infrastructure/domains/networking/vpc-discovery.js +159 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.js +445 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +385 -0
- package/infrastructure/domains/shared/utilities/handler-path-resolver.js +129 -0
- package/infrastructure/infrastructure-composer.test.js +1895 -0
- package/infrastructure/scripts/build-prisma-layer.js +534 -0
- package/infrastructure/serverless-template.js +790 -84
- package/infrastructure/serverless-template.test.js +94 -1
- package/package.json +8 -6
- package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +0 -486
- package/frigg-cli/utils/prisma-runner.js +0 -280
|
@@ -0,0 +1,718 @@
|
|
|
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
|
+
|
|
23
|
+
class VpcBuilder extends InfrastructureBuilder {
|
|
24
|
+
constructor() {
|
|
25
|
+
super();
|
|
26
|
+
this.name = 'VpcBuilder';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
shouldExecute(appDefinition) {
|
|
30
|
+
// Skip VPC in local mode (when FRIGG_SKIP_AWS_DISCOVERY is set)
|
|
31
|
+
// VPC is an AWS-specific service that should only be created in production
|
|
32
|
+
if (process.env.FRIGG_SKIP_AWS_DISCOVERY === 'true') {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return appDefinition.vpc?.enable === true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
validate(appDefinition) {
|
|
40
|
+
const result = new ValidationResult();
|
|
41
|
+
|
|
42
|
+
if (!appDefinition.vpc) {
|
|
43
|
+
result.addError('VPC configuration is missing');
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const vpc = appDefinition.vpc;
|
|
48
|
+
|
|
49
|
+
// Validate management mode
|
|
50
|
+
const validModes = ['discover', 'create-new', 'use-existing'];
|
|
51
|
+
const management = vpc.management || 'discover';
|
|
52
|
+
if (!validModes.includes(management)) {
|
|
53
|
+
result.addError(`Invalid vpc.management: "${management}". Must be one of: ${validModes.join(', ')}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Validate use-existing mode requirements
|
|
57
|
+
if (management === 'use-existing') {
|
|
58
|
+
if (!vpc.vpcId) {
|
|
59
|
+
result.addError('vpc.vpcId is required when management="use-existing"');
|
|
60
|
+
}
|
|
61
|
+
if (!vpc.securityGroupIds || vpc.securityGroupIds.length === 0) {
|
|
62
|
+
result.addWarning('vpc.securityGroupIds not provided - will attempt discovery');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Validate CIDR block format
|
|
67
|
+
if (vpc.cidrBlock) {
|
|
68
|
+
const cidrPattern = /^([0-9]{1,3}\.){3}[0-9]{1,3}\/[0-9]{1,2}$/;
|
|
69
|
+
if (!cidrPattern.test(vpc.cidrBlock)) {
|
|
70
|
+
result.addError(`Invalid CIDR block format: ${vpc.cidrBlock}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Validate subnet configuration
|
|
75
|
+
if (vpc.subnets?.management === 'use-existing') {
|
|
76
|
+
if (!vpc.subnets.ids || vpc.subnets.ids.length < 2) {
|
|
77
|
+
result.addError('At least 2 subnet IDs required when subnets.management="use-existing"');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build complete VPC infrastructure based on management mode
|
|
86
|
+
*/
|
|
87
|
+
async build(appDefinition, discoveredResources) {
|
|
88
|
+
console.log(`\n[${this.name}] Building VPC infrastructure...`);
|
|
89
|
+
|
|
90
|
+
const result = {
|
|
91
|
+
resources: {},
|
|
92
|
+
vpcConfig: {
|
|
93
|
+
securityGroupIds: [],
|
|
94
|
+
subnetIds: [],
|
|
95
|
+
},
|
|
96
|
+
iamStatements: [],
|
|
97
|
+
outputs: {},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Add IAM permissions for VPC-enabled Lambda functions
|
|
101
|
+
result.iamStatements.push({
|
|
102
|
+
Effect: 'Allow',
|
|
103
|
+
Action: [
|
|
104
|
+
'ec2:CreateNetworkInterface',
|
|
105
|
+
'ec2:DescribeNetworkInterfaces',
|
|
106
|
+
'ec2:DeleteNetworkInterface',
|
|
107
|
+
'ec2:AttachNetworkInterface',
|
|
108
|
+
'ec2:DetachNetworkInterface',
|
|
109
|
+
],
|
|
110
|
+
Resource: '*',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const management = appDefinition.vpc.management || 'discover';
|
|
114
|
+
console.log(` VPC Management Mode: ${management}`);
|
|
115
|
+
|
|
116
|
+
// Handle self-healing if enabled
|
|
117
|
+
if (appDefinition.vpc.selfHeal) {
|
|
118
|
+
this.performSelfHealing(discoveredResources, appDefinition);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Build VPC based on management mode
|
|
122
|
+
switch (management) {
|
|
123
|
+
case 'create-new':
|
|
124
|
+
await this.buildNewVpc(appDefinition, discoveredResources, result);
|
|
125
|
+
break;
|
|
126
|
+
case 'use-existing':
|
|
127
|
+
await this.useExistingVpc(appDefinition, discoveredResources, result);
|
|
128
|
+
break;
|
|
129
|
+
case 'discover':
|
|
130
|
+
default:
|
|
131
|
+
await this.discoverVpc(appDefinition, discoveredResources, result);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Build subnets
|
|
136
|
+
await this.buildSubnets(appDefinition, discoveredResources, result);
|
|
137
|
+
|
|
138
|
+
// Build NAT Gateway if needed
|
|
139
|
+
await this.buildNatGateway(appDefinition, discoveredResources, result);
|
|
140
|
+
|
|
141
|
+
// Build VPC Endpoints if enabled
|
|
142
|
+
const vpcManagement = appDefinition.vpc.management || 'discover';
|
|
143
|
+
const selfHeal = appDefinition.vpc.selfHeal !== false;
|
|
144
|
+
const vpcEndpointsExist = discoveredResources.s3VpcEndpointId && discoveredResources.dynamodbVpcEndpointId;
|
|
145
|
+
|
|
146
|
+
if (appDefinition.vpc.enableVPCEndpoints !== false) {
|
|
147
|
+
if (vpcManagement === 'create-new') {
|
|
148
|
+
// Always create in create-new mode
|
|
149
|
+
this.buildVpcEndpoints(appDefinition, discoveredResources, result);
|
|
150
|
+
} else if (vpcManagement === 'discover') {
|
|
151
|
+
if (vpcEndpointsExist) {
|
|
152
|
+
console.log(' VPC endpoints already exist - skipping creation');
|
|
153
|
+
} else if (selfHeal) {
|
|
154
|
+
console.log(' No VPC endpoints found - selfHeal creating them');
|
|
155
|
+
this.buildVpcEndpoints(appDefinition, discoveredResources, result);
|
|
156
|
+
} else {
|
|
157
|
+
console.log(' VPC endpoints not found and selfHeal disabled - skipping');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(`[${this.name}] ✅ VPC infrastructure built successfully`);
|
|
163
|
+
console.log(` - VPC ID: ${result.vpcId || 'from discovery'}`);
|
|
164
|
+
console.log(` - Subnets: ${result.vpcConfig.subnetIds.length}`);
|
|
165
|
+
console.log(` - Security Groups: ${result.vpcConfig.securityGroupIds.length}`);
|
|
166
|
+
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Perform self-healing checks and fixes
|
|
172
|
+
*/
|
|
173
|
+
performSelfHealing(discoveredResources, appDefinition) {
|
|
174
|
+
console.log('🔧 VPC Self-healing mode enabled - checking for misconfigurations...');
|
|
175
|
+
|
|
176
|
+
const healingReport = {
|
|
177
|
+
healed: [],
|
|
178
|
+
warnings: [],
|
|
179
|
+
errors: [],
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Check for NAT Gateway in private subnet
|
|
183
|
+
if (discoveredResources.natGatewayInPrivateSubnet) {
|
|
184
|
+
healingReport.warnings.push(
|
|
185
|
+
`NAT Gateway ${discoveredResources.natGatewayInPrivateSubnet} is in a private subnet`
|
|
186
|
+
);
|
|
187
|
+
healingReport.healed.push(
|
|
188
|
+
'Will create new NAT Gateway in public subnet'
|
|
189
|
+
);
|
|
190
|
+
discoveredResources.needsNewNatGateway = true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check for orphaned Elastic IPs
|
|
194
|
+
if (discoveredResources.orphanedElasticIps?.length > 0) {
|
|
195
|
+
healingReport.warnings.push(
|
|
196
|
+
`Found ${discoveredResources.orphanedElasticIps.length} orphaned Elastic IPs`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check for subnet routing issues
|
|
201
|
+
if (discoveredResources.privateSubnetsWithWrongRoutes?.length > 0) {
|
|
202
|
+
healingReport.warnings.push(
|
|
203
|
+
`Found ${discoveredResources.privateSubnetsWithWrongRoutes.length} subnets with wrong routes`
|
|
204
|
+
);
|
|
205
|
+
healingReport.healed.push('Will create correct route tables');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Log healing report
|
|
209
|
+
if (healingReport.healed.length > 0) {
|
|
210
|
+
console.log(' ✅ Self-healing actions:');
|
|
211
|
+
healingReport.healed.forEach(action => console.log(` - ${action}`));
|
|
212
|
+
}
|
|
213
|
+
if (healingReport.warnings.length > 0) {
|
|
214
|
+
console.log(' ⚠️ Issues detected:');
|
|
215
|
+
healingReport.warnings.forEach(warning => console.log(` - ${warning}`));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return healingReport;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Build new VPC from scratch
|
|
223
|
+
*/
|
|
224
|
+
async buildNewVpc(appDefinition, discoveredResources, result) {
|
|
225
|
+
console.log(' Creating new VPC infrastructure...');
|
|
226
|
+
|
|
227
|
+
const cidrBlock = appDefinition.vpc.cidrBlock || '10.0.0.0/16';
|
|
228
|
+
|
|
229
|
+
// Main VPC
|
|
230
|
+
result.resources.FriggVPC = {
|
|
231
|
+
Type: 'AWS::EC2::VPC',
|
|
232
|
+
Properties: {
|
|
233
|
+
CidrBlock: cidrBlock,
|
|
234
|
+
EnableDnsHostnames: true,
|
|
235
|
+
EnableDnsSupport: true,
|
|
236
|
+
Tags: [
|
|
237
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc' },
|
|
238
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
239
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
240
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
241
|
+
],
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Internet Gateway
|
|
246
|
+
result.resources.FriggInternetGateway = {
|
|
247
|
+
Type: 'AWS::EC2::InternetGateway',
|
|
248
|
+
Properties: {
|
|
249
|
+
Tags: [
|
|
250
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-igw' },
|
|
251
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
252
|
+
],
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
result.resources.FriggVPCGatewayAttachment = {
|
|
257
|
+
Type: 'AWS::EC2::VPCGatewayAttachment',
|
|
258
|
+
Properties: {
|
|
259
|
+
VpcId: { Ref: 'FriggVPC' },
|
|
260
|
+
InternetGatewayId: { Ref: 'FriggInternetGateway' },
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Lambda Security Group
|
|
265
|
+
result.resources.FriggLambdaSecurityGroup = {
|
|
266
|
+
Type: 'AWS::EC2::SecurityGroup',
|
|
267
|
+
Properties: {
|
|
268
|
+
GroupDescription: 'Security group for Frigg Lambda functions',
|
|
269
|
+
VpcId: { Ref: 'FriggVPC' },
|
|
270
|
+
SecurityGroupEgress: [
|
|
271
|
+
{ IpProtocol: 'tcp', FromPort: 443, ToPort: 443, CidrIp: '0.0.0.0/0', Description: 'HTTPS outbound' },
|
|
272
|
+
{ IpProtocol: 'tcp', FromPort: 80, ToPort: 80, CidrIp: '0.0.0.0/0', Description: 'HTTP outbound' },
|
|
273
|
+
{ IpProtocol: 'tcp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS TCP' },
|
|
274
|
+
{ IpProtocol: 'udp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS UDP' },
|
|
275
|
+
{ IpProtocol: 'tcp', FromPort: 5432, ToPort: 5432, CidrIp: '0.0.0.0/0', Description: 'PostgreSQL' },
|
|
276
|
+
{ IpProtocol: 'tcp', FromPort: 27017, ToPort: 27017, CidrIp: '0.0.0.0/0', Description: 'MongoDB' },
|
|
277
|
+
],
|
|
278
|
+
Tags: [
|
|
279
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-sg' },
|
|
280
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
result.vpcId = { Ref: 'FriggVPC' };
|
|
286
|
+
result.vpcConfig.securityGroupIds = [{ Ref: 'FriggLambdaSecurityGroup' }];
|
|
287
|
+
|
|
288
|
+
console.log(' ✅ New VPC infrastructure resources created');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Use existing VPC (explicitly provided)
|
|
293
|
+
*/
|
|
294
|
+
async useExistingVpc(appDefinition, discoveredResources, result) {
|
|
295
|
+
console.log(' Using existing VPC...');
|
|
296
|
+
|
|
297
|
+
if (!appDefinition.vpc.vpcId) {
|
|
298
|
+
throw new Error('vpc.vpcId is required when management="use-existing"');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
result.vpcId = appDefinition.vpc.vpcId;
|
|
302
|
+
result.vpcConfig.securityGroupIds = appDefinition.vpc.securityGroupIds ||
|
|
303
|
+
(discoveredResources.defaultSecurityGroupId ? [discoveredResources.defaultSecurityGroupId] : []);
|
|
304
|
+
|
|
305
|
+
console.log(` ✅ Using VPC: ${result.vpcId}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Discover existing VPC from AWS
|
|
310
|
+
*/
|
|
311
|
+
async discoverVpc(appDefinition, discoveredResources, result) {
|
|
312
|
+
console.log(' Discovering existing VPC...');
|
|
313
|
+
|
|
314
|
+
if (!discoveredResources.defaultVpcId) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
'VPC discovery failed: No VPC found. Set vpc.management to "create-new" or provide vpc.vpcId with "use-existing".'
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
result.vpcId = discoveredResources.defaultVpcId;
|
|
321
|
+
|
|
322
|
+
// Create a Lambda security group in the discovered VPC
|
|
323
|
+
// This is needed even in discover mode so other resources can reference it
|
|
324
|
+
result.resources.FriggLambdaSecurityGroup = {
|
|
325
|
+
Type: 'AWS::EC2::SecurityGroup',
|
|
326
|
+
Properties: {
|
|
327
|
+
GroupDescription: 'Security group for Frigg Lambda functions',
|
|
328
|
+
VpcId: result.vpcId,
|
|
329
|
+
SecurityGroupEgress: [
|
|
330
|
+
{ IpProtocol: 'tcp', FromPort: 443, ToPort: 443, CidrIp: '0.0.0.0/0', Description: 'HTTPS outbound' },
|
|
331
|
+
{ IpProtocol: 'tcp', FromPort: 80, ToPort: 80, CidrIp: '0.0.0.0/0', Description: 'HTTP outbound' },
|
|
332
|
+
{ IpProtocol: 'tcp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS TCP' },
|
|
333
|
+
{ IpProtocol: 'udp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS UDP' },
|
|
334
|
+
{ IpProtocol: 'tcp', FromPort: 5432, ToPort: 5432, CidrIp: '0.0.0.0/0', Description: 'PostgreSQL' },
|
|
335
|
+
{ IpProtocol: 'tcp', FromPort: 27017, ToPort: 27017, CidrIp: '0.0.0.0/0', Description: 'MongoDB' },
|
|
336
|
+
],
|
|
337
|
+
Tags: [
|
|
338
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-sg' },
|
|
339
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
result.vpcConfig.securityGroupIds = [{ Ref: 'FriggLambdaSecurityGroup' }];
|
|
345
|
+
|
|
346
|
+
console.log(` ✅ Discovered VPC: ${result.vpcId}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Build subnet infrastructure
|
|
351
|
+
*/
|
|
352
|
+
async buildSubnets(appDefinition, discoveredResources, result) {
|
|
353
|
+
const vpcManagement = appDefinition.vpc.management || 'discover';
|
|
354
|
+
const defaultSubnetManagement = vpcManagement === 'create-new' ? 'create' : 'discover';
|
|
355
|
+
const subnetManagement = appDefinition.vpc.subnets?.management || defaultSubnetManagement;
|
|
356
|
+
|
|
357
|
+
console.log(` Subnet Management Mode: ${subnetManagement}`);
|
|
358
|
+
|
|
359
|
+
switch (subnetManagement) {
|
|
360
|
+
case 'create':
|
|
361
|
+
this.createSubnets(appDefinition, discoveredResources, result, vpcManagement);
|
|
362
|
+
break;
|
|
363
|
+
case 'use-existing':
|
|
364
|
+
this.useExistingSubnets(appDefinition, result);
|
|
365
|
+
break;
|
|
366
|
+
case 'discover':
|
|
367
|
+
default:
|
|
368
|
+
this.discoverSubnets(appDefinition, discoveredResources, result);
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Create new subnets
|
|
375
|
+
*/
|
|
376
|
+
createSubnets(appDefinition, discoveredResources, result, vpcManagement) {
|
|
377
|
+
console.log(' Creating new subnets...');
|
|
378
|
+
|
|
379
|
+
const subnetVpcId = vpcManagement === 'create-new' ? { Ref: 'FriggVPC' } : result.vpcId;
|
|
380
|
+
|
|
381
|
+
// Generate CIDRs
|
|
382
|
+
const cidrs = this.generateSubnetCidrs(vpcManagement);
|
|
383
|
+
|
|
384
|
+
// Private Subnet 1
|
|
385
|
+
result.resources.FriggPrivateSubnet1 = {
|
|
386
|
+
Type: 'AWS::EC2::Subnet',
|
|
387
|
+
Properties: {
|
|
388
|
+
VpcId: subnetVpcId,
|
|
389
|
+
CidrBlock: cidrs.private1,
|
|
390
|
+
AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
|
|
391
|
+
Tags: [
|
|
392
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-1' },
|
|
393
|
+
{ Key: 'Type', Value: 'Private' },
|
|
394
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
395
|
+
],
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// Private Subnet 2
|
|
400
|
+
result.resources.FriggPrivateSubnet2 = {
|
|
401
|
+
Type: 'AWS::EC2::Subnet',
|
|
402
|
+
Properties: {
|
|
403
|
+
VpcId: subnetVpcId,
|
|
404
|
+
CidrBlock: cidrs.private2,
|
|
405
|
+
AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
|
|
406
|
+
Tags: [
|
|
407
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-2' },
|
|
408
|
+
{ Key: 'Type', Value: 'Private' },
|
|
409
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
410
|
+
],
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// Public Subnets (for NAT Gateway and Aurora if publicly accessible)
|
|
415
|
+
result.resources.FriggPublicSubnet = {
|
|
416
|
+
Type: 'AWS::EC2::Subnet',
|
|
417
|
+
Properties: {
|
|
418
|
+
VpcId: subnetVpcId,
|
|
419
|
+
CidrBlock: cidrs.public1,
|
|
420
|
+
MapPublicIpOnLaunch: true,
|
|
421
|
+
AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
|
|
422
|
+
Tags: [
|
|
423
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-1' },
|
|
424
|
+
{ Key: 'Type', Value: 'Public' },
|
|
425
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
426
|
+
],
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
result.resources.FriggPublicSubnet2 = {
|
|
431
|
+
Type: 'AWS::EC2::Subnet',
|
|
432
|
+
Properties: {
|
|
433
|
+
VpcId: subnetVpcId,
|
|
434
|
+
CidrBlock: cidrs.public2,
|
|
435
|
+
MapPublicIpOnLaunch: true,
|
|
436
|
+
AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
|
|
437
|
+
Tags: [
|
|
438
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-2' },
|
|
439
|
+
{ Key: 'Type', Value: 'Public' },
|
|
440
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
441
|
+
],
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
result.vpcConfig.subnetIds = [
|
|
446
|
+
{ Ref: 'FriggPrivateSubnet1' },
|
|
447
|
+
{ Ref: 'FriggPrivateSubnet2' },
|
|
448
|
+
];
|
|
449
|
+
|
|
450
|
+
// Map to discovered resources for other builders (Aurora, etc.)
|
|
451
|
+
discoveredResources.privateSubnetId1 = { Ref: 'FriggPrivateSubnet1' };
|
|
452
|
+
discoveredResources.privateSubnetId2 = { Ref: 'FriggPrivateSubnet2' };
|
|
453
|
+
discoveredResources.publicSubnetId1 = { Ref: 'FriggPublicSubnet' };
|
|
454
|
+
discoveredResources.publicSubnetId2 = { Ref: 'FriggPublicSubnet2' };
|
|
455
|
+
|
|
456
|
+
console.log(' ✅ Subnets created');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Use existing subnets
|
|
461
|
+
*/
|
|
462
|
+
useExistingSubnets(appDefinition, result) {
|
|
463
|
+
console.log(' Using existing subnets...');
|
|
464
|
+
|
|
465
|
+
if (!appDefinition.vpc.subnets?.ids || appDefinition.vpc.subnets.ids.length < 2) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
'At least 2 subnet IDs required when subnets.management="use-existing"'
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
result.vpcConfig.subnetIds = appDefinition.vpc.subnets.ids;
|
|
472
|
+
console.log(` ✅ Using ${result.vpcConfig.subnetIds.length} existing subnets`);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Discover existing subnets from AWS
|
|
477
|
+
*/
|
|
478
|
+
discoverSubnets(appDefinition, discoveredResources, result) {
|
|
479
|
+
console.log(' Discovering subnets...');
|
|
480
|
+
|
|
481
|
+
// Use explicitly provided subnet IDs first
|
|
482
|
+
if (appDefinition.vpc.subnets?.ids?.length >= 2) {
|
|
483
|
+
result.vpcConfig.subnetIds = appDefinition.vpc.subnets.ids;
|
|
484
|
+
console.log(` ✅ Using ${result.vpcConfig.subnetIds.length} provided subnets`);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Use discovered subnets
|
|
489
|
+
if (discoveredResources.privateSubnetId1 && discoveredResources.privateSubnetId2) {
|
|
490
|
+
result.vpcConfig.subnetIds = [
|
|
491
|
+
discoveredResources.privateSubnetId1,
|
|
492
|
+
discoveredResources.privateSubnetId2,
|
|
493
|
+
];
|
|
494
|
+
console.log(' ✅ Discovered 2 private subnets');
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Fallback: create if self-heal enabled
|
|
499
|
+
if (appDefinition.vpc.selfHeal) {
|
|
500
|
+
console.log(' ⚠️ No subnets found - self-heal will create them');
|
|
501
|
+
this.createSubnets(appDefinition, discoveredResources, result, 'discover');
|
|
502
|
+
} else {
|
|
503
|
+
throw new Error(
|
|
504
|
+
'No subnets discovered. Enable vpc.selfHeal, set subnets.management to "create", or provide subnet IDs.'
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Generate subnet CIDR blocks
|
|
511
|
+
*/
|
|
512
|
+
generateSubnetCidrs(vpcManagement) {
|
|
513
|
+
if (vpcManagement === 'create-new') {
|
|
514
|
+
// Use CloudFormation Fn::Cidr for dynamic generation
|
|
515
|
+
return {
|
|
516
|
+
private1: { 'Fn::Select': [0, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
|
|
517
|
+
private2: { 'Fn::Select': [1, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
|
|
518
|
+
public1: { 'Fn::Select': [2, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
|
|
519
|
+
public2: { 'Fn::Select': [3, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
|
|
520
|
+
};
|
|
521
|
+
} else {
|
|
522
|
+
// Static CIDRs for existing VPC (default VPC range)
|
|
523
|
+
return {
|
|
524
|
+
private1: '172.31.240.0/24',
|
|
525
|
+
private2: '172.31.241.0/24',
|
|
526
|
+
public1: '172.31.250.0/24',
|
|
527
|
+
public2: '172.31.251.0/24',
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Build NAT Gateway for private subnet internet access
|
|
534
|
+
*/
|
|
535
|
+
async buildNatGateway(appDefinition, discoveredResources, result) {
|
|
536
|
+
const natManagement = appDefinition.vpc.natGateway?.management || 'discover';
|
|
537
|
+
|
|
538
|
+
console.log(` NAT Gateway Management: ${natManagement}`);
|
|
539
|
+
|
|
540
|
+
// Check if we should create NAT Gateway
|
|
541
|
+
const needsNatGateway = natManagement === 'createAndManage' ||
|
|
542
|
+
discoveredResources.needsNewNatGateway === true;
|
|
543
|
+
|
|
544
|
+
if (!needsNatGateway && natManagement === 'discover') {
|
|
545
|
+
console.log(' Skipping NAT Gateway (discovery mode)');
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Check if we should reuse existing
|
|
550
|
+
if (appDefinition.vpc.natGateway?.id) {
|
|
551
|
+
console.log(` Using existing NAT Gateway: ${appDefinition.vpc.natGateway.id}`);
|
|
552
|
+
result.natGatewayId = appDefinition.vpc.natGateway.id;
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (discoveredResources.existingNatGatewayId && !discoveredResources.natGatewayInPrivateSubnet) {
|
|
557
|
+
console.log(` Reusing discovered NAT Gateway: ${discoveredResources.existingNatGatewayId}`);
|
|
558
|
+
result.natGatewayId = discoveredResources.existingNatGatewayId;
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Create new NAT Gateway
|
|
563
|
+
console.log(' Creating new NAT Gateway...');
|
|
564
|
+
|
|
565
|
+
// Elastic IP for NAT Gateway
|
|
566
|
+
result.resources.FriggNATGatewayEIP = {
|
|
567
|
+
Type: 'AWS::EC2::EIP',
|
|
568
|
+
DeletionPolicy: 'Retain',
|
|
569
|
+
UpdateReplacePolicy: 'Retain',
|
|
570
|
+
Properties: {
|
|
571
|
+
Domain: 'vpc',
|
|
572
|
+
Tags: [
|
|
573
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-eip' },
|
|
574
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
575
|
+
],
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
// NAT Gateway in public subnet
|
|
580
|
+
result.resources.FriggNATGateway = {
|
|
581
|
+
Type: 'AWS::EC2::NatGateway',
|
|
582
|
+
DeletionPolicy: 'Retain',
|
|
583
|
+
UpdateReplacePolicy: 'Retain',
|
|
584
|
+
Properties: {
|
|
585
|
+
AllocationId: { 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'] },
|
|
586
|
+
SubnetId: discoveredResources.publicSubnetId1 || { Ref: 'FriggPublicSubnet' },
|
|
587
|
+
Tags: [
|
|
588
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat' },
|
|
589
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
590
|
+
],
|
|
591
|
+
},
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
// Private route table with NAT Gateway route
|
|
595
|
+
result.resources.FriggLambdaRouteTable = {
|
|
596
|
+
Type: 'AWS::EC2::RouteTable',
|
|
597
|
+
Properties: {
|
|
598
|
+
VpcId: result.vpcId,
|
|
599
|
+
Tags: [
|
|
600
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
|
|
601
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
602
|
+
],
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
result.resources.FriggPrivateRoute = {
|
|
607
|
+
Type: 'AWS::EC2::Route',
|
|
608
|
+
Properties: {
|
|
609
|
+
RouteTableId: { Ref: 'FriggLambdaRouteTable' },
|
|
610
|
+
DestinationCidrBlock: '0.0.0.0/0',
|
|
611
|
+
NatGatewayId: { Ref: 'FriggNATGateway' },
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
console.log(' ✅ NAT Gateway infrastructure created');
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Build VPC Endpoints for AWS services
|
|
620
|
+
*/
|
|
621
|
+
buildVpcEndpoints(appDefinition, discoveredResources, result) {
|
|
622
|
+
console.log(' Creating VPC Endpoints...');
|
|
623
|
+
|
|
624
|
+
const vpcId = result.vpcId || discoveredResources.defaultVpcId;
|
|
625
|
+
|
|
626
|
+
// Create route table for VPC endpoints if it doesn't exist
|
|
627
|
+
// VPC endpoints (S3, DynamoDB) need to reference a route table
|
|
628
|
+
if (!result.resources.FriggLambdaRouteTable) {
|
|
629
|
+
result.resources.FriggLambdaRouteTable = {
|
|
630
|
+
Type: 'AWS::EC2::RouteTable',
|
|
631
|
+
Properties: {
|
|
632
|
+
VpcId: vpcId,
|
|
633
|
+
Tags: [
|
|
634
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
|
|
635
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
636
|
+
],
|
|
637
|
+
},
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// S3 Gateway Endpoint
|
|
642
|
+
result.resources.FriggS3VPCEndpoint = {
|
|
643
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
644
|
+
Properties: {
|
|
645
|
+
VpcId: vpcId,
|
|
646
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.s3',
|
|
647
|
+
VpcEndpointType: 'Gateway',
|
|
648
|
+
RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }],
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
// DynamoDB Gateway Endpoint
|
|
653
|
+
result.resources.FriggDynamoDBVPCEndpoint = {
|
|
654
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
655
|
+
Properties: {
|
|
656
|
+
VpcId: vpcId,
|
|
657
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.dynamodb',
|
|
658
|
+
VpcEndpointType: 'Gateway',
|
|
659
|
+
RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }],
|
|
660
|
+
},
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
// VPC Endpoint Security Group
|
|
664
|
+
result.resources.FriggVPCEndpointSecurityGroup = {
|
|
665
|
+
Type: 'AWS::EC2::SecurityGroup',
|
|
666
|
+
Properties: {
|
|
667
|
+
GroupDescription: 'Security group for VPC Endpoints',
|
|
668
|
+
VpcId: vpcId,
|
|
669
|
+
SecurityGroupIngress: [
|
|
670
|
+
{
|
|
671
|
+
IpProtocol: 'tcp',
|
|
672
|
+
FromPort: 443,
|
|
673
|
+
ToPort: 443,
|
|
674
|
+
SourceSecurityGroupId: { Ref: 'FriggLambdaSecurityGroup' },
|
|
675
|
+
Description: 'HTTPS from Lambda',
|
|
676
|
+
},
|
|
677
|
+
],
|
|
678
|
+
Tags: [
|
|
679
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg' },
|
|
680
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
681
|
+
],
|
|
682
|
+
},
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
// KMS Interface Endpoint (if KMS encryption enabled)
|
|
686
|
+
if (appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
|
|
687
|
+
result.resources.FriggKMSVPCEndpoint = {
|
|
688
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
689
|
+
Properties: {
|
|
690
|
+
VpcId: vpcId,
|
|
691
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.kms',
|
|
692
|
+
VpcEndpointType: 'Interface',
|
|
693
|
+
SubnetIds: result.vpcConfig.subnetIds,
|
|
694
|
+
SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
|
|
695
|
+
PrivateDnsEnabled: true,
|
|
696
|
+
},
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Secrets Manager Interface Endpoint
|
|
701
|
+
result.resources.FriggSecretsManagerVPCEndpoint = {
|
|
702
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
703
|
+
Properties: {
|
|
704
|
+
VpcId: vpcId,
|
|
705
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.secretsmanager',
|
|
706
|
+
VpcEndpointType: 'Interface',
|
|
707
|
+
SubnetIds: result.vpcConfig.subnetIds,
|
|
708
|
+
SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
|
|
709
|
+
PrivateDnsEnabled: true,
|
|
710
|
+
},
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
console.log(' ✅ VPC Endpoints created (S3, DynamoDB, KMS, Secrets Manager)');
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
module.exports = { VpcBuilder };
|
|
718
|
+
|