@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.
@@ -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
+