@friggframework/devtools 2.0.0--canary.428.1a6e465.0 → 2.0.0--canary.428.2abc64a.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.
@@ -2,28 +2,13 @@ const path = require('path');
2
2
  const fs = require('fs');
3
3
  const { AWSDiscovery } = require('./aws-discovery');
4
4
 
5
- /**
6
- * Check if AWS discovery should run based on AppDefinition
7
- * @param {Object} AppDefinition - Application definition
8
- * @returns {boolean} True if discovery should run
9
- */
10
- const shouldRunDiscovery = (AppDefinition) => {
11
- return (
12
- AppDefinition.vpc?.enable === true ||
13
- AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
14
- AppDefinition.ssm?.enable === true
15
- );
16
- };
5
+ const shouldRunDiscovery = (AppDefinition) =>
6
+ AppDefinition.vpc?.enable === true ||
7
+ AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
8
+ AppDefinition.ssm?.enable === true;
17
9
 
18
- /**
19
- * Extract environment variables from AppDefinition
20
- * @param {Object} AppDefinition - Application definition
21
- * @returns {Object} Environment variables to set in serverless
22
- */
23
10
  const getAppEnvironmentVars = (AppDefinition) => {
24
11
  const envVars = {};
25
-
26
- // AWS Lambda reserved environment variables that cannot be set (from official AWS docs)
27
12
  const reservedVars = new Set([
28
13
  '_HANDLER',
29
14
  '_X_AMZN_TRACE_ID',
@@ -42,115 +27,80 @@ const getAppEnvironmentVars = (AppDefinition) => {
42
27
  'AWS_SESSION_TOKEN',
43
28
  ]);
44
29
 
45
- if (AppDefinition.environment) {
46
- console.log('📋 Loading environment variables from appDefinition...');
47
- const envKeys = [];
48
- const skippedKeys = [];
30
+ if (!AppDefinition.environment) {
31
+ return envVars;
32
+ }
49
33
 
50
- for (const [key, value] of Object.entries(AppDefinition.environment)) {
51
- if (value === true) {
52
- if (reservedVars.has(key)) {
53
- skippedKeys.push(key);
54
- } else {
55
- envVars[key] = `\${env:${key}, ''}`;
56
- envKeys.push(key);
57
- }
58
- }
59
- }
34
+ console.log('📋 Loading environment variables from appDefinition...');
35
+ const envKeys = [];
36
+ const skippedKeys = [];
60
37
 
61
- if (envKeys.length > 0) {
62
- console.log(
63
- ` Found ${envKeys.length
64
- } environment variables: ${envKeys.join(', ')}`
65
- );
66
- }
67
- if (skippedKeys.length > 0) {
68
- console.log(
69
- ` ⚠️ Skipped ${skippedKeys.length
70
- } reserved AWS Lambda variables: ${skippedKeys.join(', ')}`
71
- );
38
+ for (const [key, value] of Object.entries(AppDefinition.environment)) {
39
+ if (value !== true) continue;
40
+ if (reservedVars.has(key)) {
41
+ skippedKeys.push(key);
42
+ continue;
72
43
  }
44
+ envVars[key] = `\${env:${key}, ''}`;
45
+ envKeys.push(key);
46
+ }
47
+
48
+ if (envKeys.length > 0) {
49
+ console.log(` Found ${envKeys.length} environment variables: ${envKeys.join(', ')}`);
50
+ }
51
+ if (skippedKeys.length > 0) {
52
+ console.log(
53
+ ` ⚠️ Skipped ${skippedKeys.length} reserved AWS Lambda variables: ${skippedKeys.join(', ')}`
54
+ );
73
55
  }
74
56
 
75
57
  return envVars;
76
58
  };
77
59
 
78
- /**
79
- * Find the actual path to node_modules directory
80
- * Tries multiple methods to locate node_modules:
81
- * 1. Traversing up from current directory
82
- * 2. Using npm root command
83
- * 3. Looking for package.json and adjacent node_modules
84
- * @returns {string} Path to node_modules directory
85
- */
86
60
  const findNodeModulesPath = () => {
87
61
  try {
88
- // Method 1: Try to find node_modules by traversing up from current directory
89
62
  let currentDir = process.cwd();
90
63
  let nodeModulesPath = null;
91
64
 
92
- // Traverse up to 5 levels to find node_modules
93
65
  for (let i = 0; i < 5; i++) {
94
66
  const potentialPath = path.join(currentDir, 'node_modules');
95
67
  if (fs.existsSync(potentialPath)) {
96
68
  nodeModulesPath = potentialPath;
97
- console.log(
98
- `Found node_modules at: ${nodeModulesPath} (method 1)`
99
- );
69
+ console.log(`Found node_modules at: ${nodeModulesPath} (method 1)`);
100
70
  break;
101
71
  }
102
- // Move up one directory
103
72
  const parentDir = path.dirname(currentDir);
104
- if (parentDir === currentDir) {
105
- // We've reached the root
106
- break;
107
- }
73
+ if (parentDir === currentDir) break;
108
74
  currentDir = parentDir;
109
75
  }
110
76
 
111
- // Method 2: If method 1 fails, try using npm root command
112
77
  if (!nodeModulesPath) {
113
78
  try {
114
- // This requires child_process, so let's require it here
115
79
  const { execSync } = require('node:child_process');
116
- const npmRoot = execSync('npm root', {
117
- encoding: 'utf8',
118
- }).trim();
80
+ const npmRoot = execSync('npm root', { encoding: 'utf8' }).trim();
119
81
  if (fs.existsSync(npmRoot)) {
120
82
  nodeModulesPath = npmRoot;
121
- console.log(
122
- `Found node_modules at: ${nodeModulesPath} (method 2)`
123
- );
83
+ console.log(`Found node_modules at: ${nodeModulesPath} (method 2)`);
124
84
  }
125
85
  } catch (npmError) {
126
86
  console.error('Error executing npm root:', npmError);
127
87
  }
128
88
  }
129
89
 
130
- // Method 3: If all else fails, check for a package.json and assume node_modules is adjacent
131
90
  if (!nodeModulesPath) {
132
91
  currentDir = process.cwd();
133
92
  for (let i = 0; i < 5; i++) {
134
93
  const packageJsonPath = path.join(currentDir, 'package.json');
135
94
  if (fs.existsSync(packageJsonPath)) {
136
- const potentialNodeModules = path.join(
137
- currentDir,
138
- 'node_modules'
139
- );
95
+ const potentialNodeModules = path.join(currentDir, 'node_modules');
140
96
  if (fs.existsSync(potentialNodeModules)) {
141
97
  nodeModulesPath = potentialNodeModules;
142
- console.log(
143
- `Found node_modules at: ${nodeModulesPath} (method 3)`
144
- );
98
+ console.log(`Found node_modules at: ${nodeModulesPath} (method 3)`);
145
99
  break;
146
100
  }
147
101
  }
148
- // Move up one directory
149
102
  const parentDir = path.dirname(currentDir);
150
- if (parentDir === currentDir) {
151
- // We've reached the root
152
- break;
153
- }
103
+ if (parentDir === currentDir) break;
154
104
  currentDir = parentDir;
155
105
  }
156
106
  }
@@ -159,9 +109,7 @@ const findNodeModulesPath = () => {
159
109
  return nodeModulesPath;
160
110
  }
161
111
 
162
- console.warn(
163
- 'Could not find node_modules path, falling back to default'
164
- );
112
+ console.warn('Could not find node_modules path, falling back to default');
165
113
  return path.resolve(process.cwd(), '../node_modules');
166
114
  } catch (error) {
167
115
  console.error('Error finding node_modules path:', error);
@@ -169,14 +117,7 @@ const findNodeModulesPath = () => {
169
117
  }
170
118
  };
171
119
 
172
- /**
173
- * Modify handler paths to point to the correct node_modules location
174
- * Only modifies paths when running in offline mode
175
- * @param {Object} functions - Serverless functions configuration object
176
- * @returns {Object} Modified functions object with updated handler paths
177
- */
178
120
  const modifyHandlerPaths = (functions) => {
179
- // Check if we're running in offline mode
180
121
  const isOffline = process.argv.includes('offline');
181
122
  console.log('isOffline', isOffline);
182
123
 
@@ -192,32 +133,17 @@ const modifyHandlerPaths = (functions) => {
192
133
  console.log('functionName', functionName);
193
134
  const functionDef = modifiedFunctions[functionName];
194
135
  if (functionDef?.handler?.includes('node_modules/')) {
195
- // Replace node_modules/ with the actual path to node_modules/
196
136
  const relativePath = path.relative(process.cwd(), nodeModulesPath);
197
- functionDef.handler = functionDef.handler.replace(
198
- 'node_modules/',
199
- `${relativePath}/`
200
- );
201
- console.log(
202
- `Updated handler for ${functionName}: ${functionDef.handler}`
203
- );
137
+ functionDef.handler = functionDef.handler.replace('node_modules/', `${relativePath}/`);
138
+ console.log(`Updated handler for ${functionName}: ${functionDef.handler}`);
204
139
  }
205
140
  }
206
141
 
207
142
  return modifiedFunctions;
208
143
  };
209
144
 
210
- /**
211
- * Create VPC infrastructure resources for CloudFormation
212
- * Creates VPC, subnets, NAT gateway, route tables, and security groups
213
- * @param {Object} AppDefinition - Application definition object
214
- * @param {Object} AppDefinition.vpc - VPC configuration
215
- * @param {string} [AppDefinition.vpc.cidrBlock='10.0.0.0/16'] - CIDR block for VPC
216
- * @returns {Object} CloudFormation resources for VPC infrastructure
217
- */
218
145
  const createVPCInfrastructure = (AppDefinition) => {
219
146
  const vpcResources = {
220
- // VPC
221
147
  FriggVPC: {
222
148
  Type: 'AWS::EC2::VPC',
223
149
  Properties: {
@@ -225,52 +151,24 @@ const createVPCInfrastructure = (AppDefinition) => {
225
151
  EnableDnsHostnames: true,
226
152
  EnableDnsSupport: true,
227
153
  Tags: [
228
- {
229
- Key: 'Name',
230
- Value: '${self:service}-${self:provider.stage}-vpc',
231
- },
232
- {
233
- Key: 'ManagedBy',
234
- Value: 'Frigg',
235
- },
236
- {
237
- Key: 'Service',
238
- Value: '${self:service}',
239
- },
240
- {
241
- Key: 'Stage',
242
- Value: '${self:provider.stage}',
243
- },
154
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc' },
155
+ { Key: 'ManagedBy', Value: 'Frigg' },
156
+ { Key: 'Service', Value: '${self:service}' },
157
+ { Key: 'Stage', Value: '${self:provider.stage}' },
244
158
  ],
245
159
  },
246
160
  },
247
-
248
- // Internet Gateway
249
161
  FriggInternetGateway: {
250
162
  Type: 'AWS::EC2::InternetGateway',
251
163
  Properties: {
252
164
  Tags: [
253
- {
254
- Key: 'Name',
255
- Value: '${self:service}-${self:provider.stage}-igw',
256
- },
257
- {
258
- Key: 'ManagedBy',
259
- Value: 'Frigg',
260
- },
261
- {
262
- Key: 'Service',
263
- Value: '${self:service}',
264
- },
265
- {
266
- Key: 'Stage',
267
- Value: '${self:provider.stage}',
268
- },
165
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-igw' },
166
+ { Key: 'ManagedBy', Value: 'Frigg' },
167
+ { Key: 'Service', Value: '${self:service}' },
168
+ { Key: 'Stage', Value: '${self:provider.stage}' },
269
169
  ],
270
170
  },
271
171
  },
272
-
273
- // Attach Internet Gateway to VPC
274
172
  FriggVPCGatewayAttachment: {
275
173
  Type: 'AWS::EC2::VPCGatewayAttachment',
276
174
  Properties: {
@@ -278,8 +176,6 @@ const createVPCInfrastructure = (AppDefinition) => {
278
176
  InternetGatewayId: { Ref: 'FriggInternetGateway' },
279
177
  },
280
178
  },
281
-
282
- // Public Subnet for NAT Gateway
283
179
  FriggPublicSubnet: {
284
180
  Type: 'AWS::EC2::Subnet',
285
181
  Properties: {
@@ -288,31 +184,14 @@ const createVPCInfrastructure = (AppDefinition) => {
288
184
  AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
289
185
  MapPublicIpOnLaunch: true,
290
186
  Tags: [
291
- {
292
- Key: 'Name',
293
- Value: '${self:service}-${self:provider.stage}-public-subnet',
294
- },
295
- {
296
- Key: 'ManagedBy',
297
- Value: 'Frigg',
298
- },
299
- {
300
- Key: 'Service',
301
- Value: '${self:service}',
302
- },
303
- {
304
- Key: 'Stage',
305
- Value: '${self:provider.stage}',
306
- },
307
- {
308
- Key: 'Type',
309
- Value: 'Public',
310
- },
187
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-subnet' },
188
+ { Key: 'ManagedBy', Value: 'Frigg' },
189
+ { Key: 'Service', Value: '${self:service}' },
190
+ { Key: 'Stage', Value: '${self:provider.stage}' },
191
+ { Key: 'Type', Value: 'Public' },
311
192
  ],
312
193
  },
313
194
  },
314
-
315
- // Private Subnet 1 for Lambda
316
195
  FriggPrivateSubnet1: {
317
196
  Type: 'AWS::EC2::Subnet',
318
197
  Properties: {
@@ -320,31 +199,14 @@ const createVPCInfrastructure = (AppDefinition) => {
320
199
  CidrBlock: '10.0.2.0/24',
321
200
  AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
322
201
  Tags: [
323
- {
324
- Key: 'Name',
325
- Value: '${self:service}-${self:provider.stage}-private-subnet-1',
326
- },
327
- {
328
- Key: 'ManagedBy',
329
- Value: 'Frigg',
330
- },
331
- {
332
- Key: 'Service',
333
- Value: '${self:service}',
334
- },
335
- {
336
- Key: 'Stage',
337
- Value: '${self:provider.stage}',
338
- },
339
- {
340
- Key: 'Type',
341
- Value: 'Private',
342
- },
202
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-subnet-1' },
203
+ { Key: 'ManagedBy', Value: 'Frigg' },
204
+ { Key: 'Service', Value: '${self:service}' },
205
+ { Key: 'Stage', Value: '${self:provider.stage}' },
206
+ { Key: 'Type', Value: 'Private' },
343
207
  ],
344
208
  },
345
209
  },
346
-
347
- // Private Subnet 2 for Lambda (different AZ for redundancy)
348
210
  FriggPrivateSubnet2: {
349
211
  Type: 'AWS::EC2::Subnet',
350
212
  Properties: {
@@ -352,117 +214,53 @@ const createVPCInfrastructure = (AppDefinition) => {
352
214
  CidrBlock: '10.0.3.0/24',
353
215
  AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
354
216
  Tags: [
355
- {
356
- Key: 'Name',
357
- Value: '${self:service}-${self:provider.stage}-private-subnet-2',
358
- },
359
- {
360
- Key: 'ManagedBy',
361
- Value: 'Frigg',
362
- },
363
- {
364
- Key: 'Service',
365
- Value: '${self:service}',
366
- },
367
- {
368
- Key: 'Stage',
369
- Value: '${self:provider.stage}',
370
- },
371
- {
372
- Key: 'Type',
373
- Value: 'Private',
374
- },
217
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-subnet-2' },
218
+ { Key: 'ManagedBy', Value: 'Frigg' },
219
+ { Key: 'Service', Value: '${self:service}' },
220
+ { Key: 'Stage', Value: '${self:provider.stage}' },
221
+ { Key: 'Type', Value: 'Private' },
375
222
  ],
376
223
  },
377
224
  },
378
-
379
- // Elastic IP for NAT Gateway
380
225
  FriggNATGatewayEIP: {
381
226
  Type: 'AWS::EC2::EIP',
382
227
  Properties: {
383
228
  Domain: 'vpc',
384
229
  Tags: [
385
- {
386
- Key: 'Name',
387
- Value: '${self:service}-${self:provider.stage}-nat-eip',
388
- },
389
- {
390
- Key: 'ManagedBy',
391
- Value: 'Frigg',
392
- },
393
- {
394
- Key: 'Service',
395
- Value: '${self:service}',
396
- },
397
- {
398
- Key: 'Stage',
399
- Value: '${self:provider.stage}',
400
- },
230
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-eip' },
231
+ { Key: 'ManagedBy', Value: 'Frigg' },
232
+ { Key: 'Service', Value: '${self:service}' },
233
+ { Key: 'Stage', Value: '${self:provider.stage}' },
401
234
  ],
402
235
  },
403
236
  DependsOn: 'FriggVPCGatewayAttachment',
404
237
  },
405
-
406
- // NAT Gateway for private subnet internet access
407
238
  FriggNATGateway: {
408
239
  Type: 'AWS::EC2::NatGateway',
409
240
  Properties: {
410
- AllocationId: {
411
- 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'],
412
- },
241
+ AllocationId: { 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'] },
413
242
  SubnetId: { Ref: 'FriggPublicSubnet' },
414
243
  Tags: [
415
- {
416
- Key: 'Name',
417
- Value: '${self:service}-${self:provider.stage}-nat-gateway',
418
- },
419
- {
420
- Key: 'ManagedBy',
421
- Value: 'Frigg',
422
- },
423
- {
424
- Key: 'Service',
425
- Value: '${self:service}',
426
- },
427
- {
428
- Key: 'Stage',
429
- Value: '${self:provider.stage}',
430
- },
244
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-gateway' },
245
+ { Key: 'ManagedBy', Value: 'Frigg' },
246
+ { Key: 'Service', Value: '${self:service}' },
247
+ { Key: 'Stage', Value: '${self:provider.stage}' },
431
248
  ],
432
249
  },
433
250
  },
434
-
435
- // Public Route Table
436
251
  FriggPublicRouteTable: {
437
252
  Type: 'AWS::EC2::RouteTable',
438
253
  Properties: {
439
254
  VpcId: { Ref: 'FriggVPC' },
440
255
  Tags: [
441
- {
442
- Key: 'Name',
443
- Value: '${self:service}-${self:provider.stage}-public-rt',
444
- },
445
- {
446
- Key: 'ManagedBy',
447
- Value: 'Frigg',
448
- },
449
- {
450
- Key: 'Service',
451
- Value: '${self:service}',
452
- },
453
- {
454
- Key: 'Stage',
455
- Value: '${self:provider.stage}',
456
- },
457
- {
458
- Key: 'Type',
459
- Value: 'Public',
460
- },
256
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-rt' },
257
+ { Key: 'ManagedBy', Value: 'Frigg' },
258
+ { Key: 'Service', Value: '${self:service}' },
259
+ { Key: 'Stage', Value: '${self:provider.stage}' },
260
+ { Key: 'Type', Value: 'Public' },
461
261
  ],
462
262
  },
463
263
  },
464
-
465
- // Public Route to Internet Gateway
466
264
  FriggPublicRoute: {
467
265
  Type: 'AWS::EC2::Route',
468
266
  Properties: {
@@ -472,8 +270,6 @@ const createVPCInfrastructure = (AppDefinition) => {
472
270
  },
473
271
  DependsOn: 'FriggVPCGatewayAttachment',
474
272
  },
475
-
476
- // Associate Public Subnet with Public Route Table
477
273
  FriggPublicSubnetRouteTableAssociation: {
478
274
  Type: 'AWS::EC2::SubnetRouteTableAssociation',
479
275
  Properties: {
@@ -481,38 +277,19 @@ const createVPCInfrastructure = (AppDefinition) => {
481
277
  RouteTableId: { Ref: 'FriggPublicRouteTable' },
482
278
  },
483
279
  },
484
-
485
- // Private Route Table for Private Subnets
486
280
  FriggPrivateRouteTable: {
487
281
  Type: 'AWS::EC2::RouteTable',
488
282
  Properties: {
489
283
  VpcId: { Ref: 'FriggVPC' },
490
284
  Tags: [
491
- {
492
- Key: 'Name',
493
- Value: '${self:service}-${self:provider.stage}-private-rt',
494
- },
495
- {
496
- Key: 'ManagedBy',
497
- Value: 'Frigg',
498
- },
499
- {
500
- Key: 'Service',
501
- Value: '${self:service}',
502
- },
503
- {
504
- Key: 'Stage',
505
- Value: '${self:provider.stage}',
506
- },
507
- {
508
- Key: 'Type',
509
- Value: 'Private',
510
- },
285
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-rt' },
286
+ { Key: 'ManagedBy', Value: 'Frigg' },
287
+ { Key: 'Service', Value: '${self:service}' },
288
+ { Key: 'Stage', Value: '${self:provider.stage}' },
289
+ { Key: 'Type', Value: 'Private' },
511
290
  ],
512
291
  },
513
292
  },
514
-
515
- // Private Route to NAT Gateway
516
293
  FriggPrivateRoute: {
517
294
  Type: 'AWS::EC2::Route',
518
295
  Properties: {
@@ -521,8 +298,6 @@ const createVPCInfrastructure = (AppDefinition) => {
521
298
  NatGatewayId: { Ref: 'FriggNATGateway' },
522
299
  },
523
300
  },
524
-
525
- // Associate Private Subnet 1 with Private Route Table
526
301
  FriggPrivateSubnet1RouteTableAssociation: {
527
302
  Type: 'AWS::EC2::SubnetRouteTableAssociation',
528
303
  Properties: {
@@ -530,8 +305,6 @@ const createVPCInfrastructure = (AppDefinition) => {
530
305
  RouteTableId: { Ref: 'FriggPrivateRouteTable' },
531
306
  },
532
307
  },
533
-
534
- // Associate Private Subnet 2 with Private Route Table
535
308
  FriggPrivateSubnet2RouteTableAssociation: {
536
309
  Type: 'AWS::EC2::SubnetRouteTableAssociation',
537
310
  Properties: {
@@ -539,75 +312,29 @@ const createVPCInfrastructure = (AppDefinition) => {
539
312
  RouteTableId: { Ref: 'FriggPrivateRouteTable' },
540
313
  },
541
314
  },
542
-
543
- // Security Group for Lambda functions
544
315
  FriggLambdaSecurityGroup: {
545
316
  Type: 'AWS::EC2::SecurityGroup',
546
317
  Properties: {
547
318
  GroupDescription: 'Security group for Frigg Lambda functions',
548
319
  VpcId: { Ref: 'FriggVPC' },
549
320
  SecurityGroupEgress: [
550
- {
551
- IpProtocol: 'tcp',
552
- FromPort: 443,
553
- ToPort: 443,
554
- CidrIp: '0.0.0.0/0',
555
- Description: 'HTTPS outbound',
556
- },
557
- {
558
- IpProtocol: 'tcp',
559
- FromPort: 80,
560
- ToPort: 80,
561
- CidrIp: '0.0.0.0/0',
562
- Description: 'HTTP outbound',
563
- },
564
- {
565
- IpProtocol: 'tcp',
566
- FromPort: 53,
567
- ToPort: 53,
568
- CidrIp: '0.0.0.0/0',
569
- Description: 'DNS TCP',
570
- },
571
- {
572
- IpProtocol: 'udp',
573
- FromPort: 53,
574
- ToPort: 53,
575
- CidrIp: '0.0.0.0/0',
576
- Description: 'DNS UDP',
577
- },
578
- {
579
- IpProtocol: 'tcp',
580
- FromPort: 27017,
581
- ToPort: 27017,
582
- CidrIp: '0.0.0.0/0',
583
- Description: 'MongoDB outbound',
584
- },
321
+ { IpProtocol: 'tcp', FromPort: 443, ToPort: 443, CidrIp: '0.0.0.0/0', Description: 'HTTPS outbound' },
322
+ { IpProtocol: 'tcp', FromPort: 80, ToPort: 80, CidrIp: '0.0.0.0/0', Description: 'HTTP outbound' },
323
+ { IpProtocol: 'tcp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS TCP' },
324
+ { IpProtocol: 'udp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS UDP' },
325
+ { IpProtocol: 'tcp', FromPort: 27017, ToPort: 27017, CidrIp: '0.0.0.0/0', Description: 'MongoDB outbound' },
585
326
  ],
586
327
  Tags: [
587
- {
588
- Key: 'Name',
589
- Value: '${self:service}-${self:provider.stage}-lambda-sg',
590
- },
591
- {
592
- Key: 'ManagedBy',
593
- Value: 'Frigg',
594
- },
595
- {
596
- Key: 'Service',
597
- Value: '${self:service}',
598
- },
599
- {
600
- Key: 'Stage',
601
- Value: '${self:provider.stage}',
602
- },
328
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-sg' },
329
+ { Key: 'ManagedBy', Value: 'Frigg' },
330
+ { Key: 'Service', Value: '${self:service}' },
331
+ { Key: 'Stage', Value: '${self:provider.stage}' },
603
332
  ],
604
333
  },
605
334
  },
606
335
  };
607
336
 
608
- // Add VPC Endpoints for cost optimization
609
337
  if (AppDefinition.vpc.enableVPCEndpoints !== false) {
610
- // S3 Gateway Endpoint (free)
611
338
  vpcResources.FriggS3VPCEndpoint = {
612
339
  Type: 'AWS::EC2::VPCEndpoint',
613
340
  Properties: {
@@ -618,7 +345,6 @@ const createVPCInfrastructure = (AppDefinition) => {
618
345
  },
619
346
  };
620
347
 
621
- // DynamoDB Gateway Endpoint (free)
622
348
  vpcResources.FriggDynamoDBVPCEndpoint = {
623
349
  Type: 'AWS::EC2::VPCEndpoint',
624
350
  Properties: {
@@ -629,7 +355,6 @@ const createVPCInfrastructure = (AppDefinition) => {
629
355
  },
630
356
  };
631
357
 
632
- // KMS Interface Endpoint (paid, but useful if using KMS)
633
358
  if (AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
634
359
  vpcResources.FriggKMSVPCEndpoint = {
635
360
  Type: 'AWS::EC2::VPCEndpoint',
@@ -637,36 +362,25 @@ const createVPCInfrastructure = (AppDefinition) => {
637
362
  VpcId: { Ref: 'FriggVPC' },
638
363
  ServiceName: 'com.amazonaws.${self:provider.region}.kms',
639
364
  VpcEndpointType: 'Interface',
640
- SubnetIds: [
641
- { Ref: 'FriggPrivateSubnet1' },
642
- { Ref: 'FriggPrivateSubnet2' },
643
- ],
644
- SecurityGroupIds: [
645
- { Ref: 'FriggVPCEndpointSecurityGroup' },
646
- ],
365
+ SubnetIds: [{ Ref: 'FriggPrivateSubnet1' }, { Ref: 'FriggPrivateSubnet2' }],
366
+ SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
647
367
  PrivateDnsEnabled: true,
648
368
  },
649
369
  };
650
370
  }
651
371
 
652
- // Secrets Manager Interface Endpoint (paid, but useful for secrets)
653
372
  vpcResources.FriggSecretsManagerVPCEndpoint = {
654
373
  Type: 'AWS::EC2::VPCEndpoint',
655
374
  Properties: {
656
375
  VpcId: { Ref: 'FriggVPC' },
657
- ServiceName:
658
- 'com.amazonaws.${self:provider.region}.secretsmanager',
376
+ ServiceName: 'com.amazonaws.${self:provider.region}.secretsmanager',
659
377
  VpcEndpointType: 'Interface',
660
- SubnetIds: [
661
- { Ref: 'FriggPrivateSubnet1' },
662
- { Ref: 'FriggPrivateSubnet2' },
663
- ],
378
+ SubnetIds: [{ Ref: 'FriggPrivateSubnet1' }, { Ref: 'FriggPrivateSubnet2' }],
664
379
  SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
665
380
  PrivateDnsEnabled: true,
666
381
  },
667
382
  };
668
383
 
669
- // Security Group for VPC Endpoints
670
384
  vpcResources.FriggVPCEndpointSecurityGroup = {
671
385
  Type: 'AWS::EC2::SecurityGroup',
672
386
  Properties: {
@@ -677,13 +391,10 @@ const createVPCInfrastructure = (AppDefinition) => {
677
391
  IpProtocol: 'tcp',
678
392
  FromPort: 443,
679
393
  ToPort: 443,
680
- SourceSecurityGroupId: {
681
- Ref: 'FriggLambdaSecurityGroup',
682
- },
394
+ SourceSecurityGroupId: { Ref: 'FriggLambdaSecurityGroup' },
683
395
  Description: 'HTTPS from Lambda security group',
684
396
  },
685
397
  {
686
- // Also allow from VPC CIDR as fallback
687
398
  IpProtocol: 'tcp',
688
399
  FromPort: 443,
689
400
  ToPort: 443,
@@ -692,30 +403,12 @@ const createVPCInfrastructure = (AppDefinition) => {
692
403
  },
693
404
  ],
694
405
  Tags: [
695
- {
696
- Key: 'Name',
697
- Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg',
698
- },
699
- {
700
- Key: 'ManagedBy',
701
- Value: 'Frigg',
702
- },
703
- {
704
- Key: 'Service',
705
- Value: '${self:service}',
706
- },
707
- {
708
- Key: 'Stage',
709
- Value: '${self:provider.stage}',
710
- },
711
- {
712
- Key: 'Type',
713
- Value: 'VPCEndpoint',
714
- },
715
- {
716
- Key: 'Purpose',
717
- Value: 'Allow Lambda functions to access VPC endpoints',
718
- },
406
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg' },
407
+ { Key: 'ManagedBy', Value: 'Frigg' },
408
+ { Key: 'Service', Value: '${self:service}' },
409
+ { Key: 'Stage', Value: '${self:provider.stage}' },
410
+ { Key: 'Type', Value: 'VPCEndpoint' },
411
+ { Key: 'Purpose', Value: 'Allow Lambda functions to access VPC endpoints' },
719
412
  ],
720
413
  },
721
414
  };
@@ -724,150 +417,105 @@ const createVPCInfrastructure = (AppDefinition) => {
724
417
  return vpcResources;
725
418
  };
726
419
 
727
- /**
728
- * Compose a complete serverless framework configuration from app definition
729
- * @param {Object} AppDefinition - Application definition object
730
- * @param {string} [AppDefinition.name] - Application name
731
- * @param {string} [AppDefinition.provider='aws'] - Cloud provider
732
- * @param {Array} AppDefinition.integrations - Array of integration definitions
733
- * @param {Object} [AppDefinition.vpc] - VPC configuration
734
- * @param {Object} [AppDefinition.encryption] - KMS encryption configuration
735
- * @param {Object} [AppDefinition.ssm] - SSM parameter store configuration
736
- * @param {Object} [AppDefinition.websockets] - WebSocket configuration
737
- * @param {boolean} [AppDefinition.websockets.enable=false] - Enable WebSocket support for live update streaming
738
- * @returns {Object} Complete serverless framework configuration
739
- */
740
- const composeServerlessDefinition = async (AppDefinition) => {
741
- console.log('composeServerlessDefinition', AppDefinition);
742
- // Store discovered resources
743
- let discoveredResources = {};
420
+ const gatherDiscoveredResources = async (AppDefinition) => {
421
+ if (!shouldRunDiscovery(AppDefinition)) {
422
+ return {};
423
+ }
744
424
 
745
- // Run AWS discovery if needed
746
- if (shouldRunDiscovery(AppDefinition)) {
747
- console.log(
748
- '🔍 Running AWS resource discovery for serverless template...'
749
- );
750
- try {
751
- const region = process.env.AWS_REGION || 'us-east-1';
752
- const discovery = new AWSDiscovery(region);
753
-
754
- const config = {
755
- vpc: AppDefinition.vpc || {},
756
- encryption: AppDefinition.encryption || {},
757
- ssm: AppDefinition.ssm || {},
758
- };
425
+ console.log('🔍 Running AWS resource discovery for serverless template...');
426
+ try {
427
+ const region = process.env.AWS_REGION || 'us-east-1';
428
+ const discovery = new AWSDiscovery(region);
429
+ const config = {
430
+ vpc: AppDefinition.vpc || {},
431
+ encryption: AppDefinition.encryption || {},
432
+ ssm: AppDefinition.ssm || {},
433
+ };
759
434
 
760
- discoveredResources = await discovery.discoverResources(config);
435
+ const discoveredResources = await discovery.discoverResources(config);
761
436
 
762
- console.log('✅ AWS discovery completed successfully!');
763
- if (discoveredResources.defaultVpcId) {
764
- console.log(` VPC: ${discoveredResources.defaultVpcId}`);
765
- }
766
- if (
767
- discoveredResources.privateSubnetId1 &&
768
- discoveredResources.privateSubnetId2
769
- ) {
770
- console.log(
771
- ` Subnets: ${discoveredResources.privateSubnetId1}, ${discoveredResources.privateSubnetId2}`
772
- );
773
- }
774
- if (discoveredResources.defaultSecurityGroupId) {
775
- console.log(
776
- ` Security Group: ${discoveredResources.defaultSecurityGroupId}`
777
- );
778
- }
779
- if (discoveredResources.defaultKmsKeyId) {
780
- console.log(
781
- ` KMS Key: ${discoveredResources.defaultKmsKeyId}`
782
- );
783
- }
784
- } catch (error) {
785
- console.error('❌ AWS discovery failed:', error.message);
786
- throw new Error(`AWS discovery failed: ${error.message}`);
437
+ console.log('✅ AWS discovery completed successfully!');
438
+ if (discoveredResources.defaultVpcId) {
439
+ console.log(` VPC: ${discoveredResources.defaultVpcId}`);
440
+ }
441
+ if (discoveredResources.privateSubnetId1 && discoveredResources.privateSubnetId2) {
442
+ console.log(
443
+ ` Subnets: ${discoveredResources.privateSubnetId1}, ${discoveredResources.privateSubnetId2}`
444
+ );
445
+ }
446
+ if (discoveredResources.defaultSecurityGroupId) {
447
+ console.log(` Security Group: ${discoveredResources.defaultSecurityGroupId}`);
448
+ }
449
+ if (discoveredResources.defaultKmsKeyId) {
450
+ console.log(` KMS Key: ${discoveredResources.defaultKmsKeyId}`);
451
+ }
452
+
453
+ return discoveredResources;
454
+ } catch (error) {
455
+ console.error('❌ AWS discovery failed:', error.message);
456
+ throw new Error(`AWS discovery failed: ${error.message}`);
457
+ }
458
+ };
459
+
460
+ const buildEnvironment = (appEnvironmentVars, discoveredResources) => {
461
+ const environment = {
462
+ STAGE: '${opt:stage, "dev"}',
463
+ AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1,
464
+ ...appEnvironmentVars,
465
+ };
466
+
467
+ const discoveryEnvMapping = {
468
+ defaultVpcId: 'AWS_DISCOVERY_VPC_ID',
469
+ defaultSecurityGroupId: 'AWS_DISCOVERY_SECURITY_GROUP_ID',
470
+ privateSubnetId1: 'AWS_DISCOVERY_SUBNET_ID_1',
471
+ privateSubnetId2: 'AWS_DISCOVERY_SUBNET_ID_2',
472
+ publicSubnetId: 'AWS_DISCOVERY_PUBLIC_SUBNET_ID',
473
+ defaultRouteTableId: 'AWS_DISCOVERY_ROUTE_TABLE_ID',
474
+ defaultKmsKeyId: 'AWS_DISCOVERY_KMS_KEY_ID',
475
+ };
476
+
477
+ for (const [key, envKey] of Object.entries(discoveryEnvMapping)) {
478
+ if (discoveredResources[key]) {
479
+ environment[envKey] = discoveredResources[key];
787
480
  }
788
481
  }
789
482
 
790
- // Get environment variables from appDefinition
791
- const appEnvironmentVars = getAppEnvironmentVars(AppDefinition);
483
+ return environment;
484
+ };
485
+
486
+ const createBaseDefinition = (AppDefinition, appEnvironmentVars, discoveredResources) => {
487
+ const region = process.env.AWS_REGION || 'us-east-1';
792
488
 
793
- const definition = {
489
+ return {
794
490
  frameworkVersion: '>=3.17.0',
795
491
  service: AppDefinition.name || 'create-frigg-app',
796
492
  package: {
797
493
  individually: true,
798
- exclude: [
799
- '!**/node_modules/aws-sdk/**',
800
- '!**/node_modules/@aws-sdk/**',
801
- '!package.json',
802
- ],
494
+ exclude: ['!**/node_modules/aws-sdk/**', '!**/node_modules/@aws-sdk/**', '!package.json'],
803
495
  },
804
496
  useDotenv: true,
805
497
  provider: {
806
498
  name: AppDefinition.provider || 'aws',
807
499
  runtime: 'nodejs20.x',
808
500
  timeout: 30,
809
- region: process.env.AWS_REGION || 'us-east-1',
501
+ region,
810
502
  stage: '${opt:stage}',
811
- environment: {
812
- STAGE: '${opt:stage, "dev"}',
813
- AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1,
814
- // Add environment variables from appDefinition
815
- ...appEnvironmentVars,
816
- // Add discovered resources to environment if available
817
- ...(discoveredResources.defaultVpcId && {
818
- AWS_DISCOVERY_VPC_ID: discoveredResources.defaultVpcId,
819
- }),
820
- ...(discoveredResources.defaultSecurityGroupId && {
821
- AWS_DISCOVERY_SECURITY_GROUP_ID:
822
- discoveredResources.defaultSecurityGroupId,
823
- }),
824
- ...(discoveredResources.privateSubnetId1 && {
825
- AWS_DISCOVERY_SUBNET_ID_1:
826
- discoveredResources.privateSubnetId1,
827
- }),
828
- ...(discoveredResources.privateSubnetId2 && {
829
- AWS_DISCOVERY_SUBNET_ID_2:
830
- discoveredResources.privateSubnetId2,
831
- }),
832
- ...(discoveredResources.publicSubnetId && {
833
- AWS_DISCOVERY_PUBLIC_SUBNET_ID:
834
- discoveredResources.publicSubnetId,
835
- }),
836
- ...(discoveredResources.defaultRouteTableId && {
837
- AWS_DISCOVERY_ROUTE_TABLE_ID:
838
- discoveredResources.defaultRouteTableId,
839
- }),
840
- ...(discoveredResources.defaultKmsKeyId && {
841
- AWS_DISCOVERY_KMS_KEY_ID:
842
- discoveredResources.defaultKmsKeyId,
843
- }),
844
- },
503
+ environment: buildEnvironment(appEnvironmentVars, discoveredResources),
845
504
  iamRoleStatements: [
846
505
  {
847
506
  Effect: 'Allow',
848
507
  Action: ['sns:Publish'],
849
- Resource: {
850
- Ref: 'InternalErrorBridgeTopic',
851
- },
508
+ Resource: { Ref: 'InternalErrorBridgeTopic' },
852
509
  },
853
510
  {
854
511
  Effect: 'Allow',
855
- Action: [
856
- 'sqs:SendMessage',
857
- 'sqs:SendMessageBatch',
858
- 'sqs:GetQueueUrl',
859
- 'sqs:GetQueueAttributes',
860
- ],
512
+ Action: ['sqs:SendMessage', 'sqs:SendMessageBatch', 'sqs:GetQueueUrl', 'sqs:GetQueueAttributes'],
861
513
  Resource: [
862
- {
863
- 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
864
- },
514
+ { 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
865
515
  {
866
516
  'Fn::Join': [
867
517
  ':',
868
- [
869
- 'arn:aws:sqs:${self:provider.region}:*:${self:service}--${self:provider.stage}-*Queue',
870
- ],
518
+ ['arn:aws:sqs:${self:provider.region}:*:${self:service}--${self:provider.stage}-*Queue'],
871
519
  ],
872
520
  },
873
521
  ],
@@ -902,7 +550,7 @@ const composeServerlessDefinition = async (AppDefinition) => {
902
550
  autoCreate: false,
903
551
  apiVersion: '2012-11-05',
904
552
  endpoint: 'http://localhost:4566',
905
- region: process.env.AWS_REGION || 'us-east-1',
553
+ region,
906
554
  accessKeyId: 'root',
907
555
  secretAccessKey: 'root',
908
556
  skipCacheInvalidation: false,
@@ -913,57 +561,22 @@ const composeServerlessDefinition = async (AppDefinition) => {
913
561
  },
914
562
  functions: {
915
563
  auth: {
916
- handler:
917
- 'node_modules/@friggframework/core/handlers/routers/auth.handler',
564
+ handler: 'node_modules/@friggframework/core/handlers/routers/auth.handler',
918
565
  events: [
919
- {
920
- httpApi: {
921
- path: '/api/integrations',
922
- method: 'ANY',
923
- },
924
- },
925
- {
926
- httpApi: {
927
- path: '/api/integrations/{proxy+}',
928
- method: 'ANY',
929
- },
930
- },
931
- {
932
- httpApi: {
933
- path: '/api/authorize',
934
- method: 'ANY',
935
- },
936
- },
566
+ { httpApi: { path: '/api/integrations', method: 'ANY' } },
567
+ { httpApi: { path: '/api/integrations/{proxy+}', method: 'ANY' } },
568
+ { httpApi: { path: '/api/authorize', method: 'ANY' } },
937
569
  ],
938
570
  },
939
571
  user: {
940
- handler:
941
- 'node_modules/@friggframework/core/handlers/routers/user.handler',
942
- events: [
943
- {
944
- httpApi: {
945
- path: '/user/{proxy+}',
946
- method: 'ANY',
947
- },
948
- },
949
- ],
572
+ handler: 'node_modules/@friggframework/core/handlers/routers/user.handler',
573
+ events: [{ httpApi: { path: '/user/{proxy+}', method: 'ANY' } }],
950
574
  },
951
575
  health: {
952
- handler:
953
- 'node_modules/@friggframework/core/handlers/routers/health.handler',
576
+ handler: 'node_modules/@friggframework/core/handlers/routers/health.handler',
954
577
  events: [
955
- {
956
- httpApi: {
957
- path: '/health',
958
- method: 'GET',
959
- },
960
- },
961
- {
962
- httpApi: {
963
- path: '/health/{proxy+}',
964
- method: 'GET',
965
- },
966
- },
578
+ { httpApi: { path: '/health', method: 'GET' } },
579
+ { httpApi: { path: '/health/{proxy+}', method: 'GET' } },
967
580
  ],
968
581
  },
969
582
  },
@@ -972,8 +585,7 @@ const composeServerlessDefinition = async (AppDefinition) => {
972
585
  InternalErrorQueue: {
973
586
  Type: 'AWS::SQS::Queue',
974
587
  Properties: {
975
- QueueName:
976
- '${self:service}-internal-error-queue-${self:provider.stage}',
588
+ QueueName: '${self:service}-internal-error-queue-${self:provider.stage}',
977
589
  MessageRetentionPeriod: 300,
978
590
  },
979
591
  },
@@ -983,9 +595,7 @@ const composeServerlessDefinition = async (AppDefinition) => {
983
595
  Subscription: [
984
596
  {
985
597
  Protocol: 'sqs',
986
- Endpoint: {
987
- 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
988
- },
598
+ Endpoint: { 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
989
599
  },
990
600
  ],
991
601
  },
@@ -1000,25 +610,11 @@ const composeServerlessDefinition = async (AppDefinition) => {
1000
610
  {
1001
611
  Sid: 'Allow Dead Letter SNS to publish to SQS',
1002
612
  Effect: 'Allow',
1003
- Principal: {
1004
- Service: 'sns.amazonaws.com',
1005
- },
1006
- Resource: {
1007
- 'Fn::GetAtt': [
1008
- 'InternalErrorQueue',
1009
- 'Arn',
1010
- ],
1011
- },
1012
- Action: [
1013
- 'SQS:SendMessage',
1014
- 'SQS:SendMessageBatch',
1015
- ],
613
+ Principal: { Service: 'sns.amazonaws.com' },
614
+ Resource: { 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
615
+ Action: ['SQS:SendMessage', 'SQS:SendMessageBatch'],
1016
616
  Condition: {
1017
- ArnEquals: {
1018
- 'aws:SourceArn': {
1019
- Ref: 'InternalErrorBridgeTopic',
1020
- },
1021
- },
617
+ ArnEquals: { 'aws:SourceArn': { Ref: 'InternalErrorBridgeTopic' } },
1022
618
  },
1023
619
  },
1024
620
  ],
@@ -1038,1309 +634,989 @@ const composeServerlessDefinition = async (AppDefinition) => {
1038
634
  Period: 60,
1039
635
  AlarmActions: [{ Ref: 'InternalErrorBridgeTopic' }],
1040
636
  Dimensions: [
1041
- {
1042
- Name: 'ApiId',
1043
- Value: { Ref: 'HttpApi' },
1044
- },
1045
- {
1046
- Name: 'Stage',
1047
- Value: '${self:provider.stage}',
1048
- },
637
+ { Name: 'ApiId', Value: { Ref: 'HttpApi' } },
638
+ { Name: 'Stage', Value: '${self:provider.stage}' },
1049
639
  ],
1050
640
  },
1051
641
  },
1052
642
  },
1053
643
  },
1054
644
  };
645
+ };
1055
646
 
1056
- // KMS Configuration based on App Definition
1057
- if (AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
1058
- // Check if a KMS key was discovered
1059
- if (discoveredResources.defaultKmsKeyId) {
1060
- // Use the existing discovered KMS key
1061
- console.log(
1062
- `Using existing KMS key: ${discoveredResources.defaultKmsKeyId}`
1063
- );
647
+ const applyKmsConfiguration = (definition, AppDefinition, discoveredResources) => {
648
+ if (AppDefinition.encryption?.fieldLevelEncryptionMethod !== 'kms') {
649
+ return;
650
+ }
1064
651
 
1065
- // Create a CloudFormation-managed alias to track the discovered key
1066
- // This ensures CloudFormation always has a resource to manage, preventing deletion
1067
- definition.resources.Resources.FriggKMSKeyAlias = {
1068
- Type: 'AWS::KMS::Alias',
1069
- DeletionPolicy: 'Retain',
1070
- Properties: {
1071
- AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
1072
- TargetKeyId: discoveredResources.defaultKmsKeyId
1073
- }
1074
- };
652
+ if (discoveredResources.defaultKmsKeyId) {
653
+ console.log(`Using existing KMS key: ${discoveredResources.defaultKmsKeyId}`);
654
+ definition.resources.Resources.FriggKMSKeyAlias = {
655
+ Type: 'AWS::KMS::Alias',
656
+ DeletionPolicy: 'Retain',
657
+ Properties: {
658
+ AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
659
+ TargetKeyId: discoveredResources.defaultKmsKeyId,
660
+ },
661
+ };
1075
662
 
1076
- definition.provider.iamRoleStatements.push({
1077
- Effect: 'Allow',
1078
- Action: ['kms:GenerateDataKey', 'kms:Decrypt'],
1079
- Resource: [discoveredResources.defaultKmsKeyId],
1080
- });
663
+ definition.provider.iamRoleStatements.push({
664
+ Effect: 'Allow',
665
+ Action: ['kms:GenerateDataKey', 'kms:Decrypt'],
666
+ Resource: [discoveredResources.defaultKmsKeyId],
667
+ });
668
+ } else {
669
+ if (AppDefinition.encryption?.createResourceIfNoneFound !== true) {
670
+ throw new Error(
671
+ 'KMS field-level encryption is enabled but no KMS key was found. ' +
672
+ 'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
673
+ );
674
+ }
1081
675
 
1082
- // KMS_KEY_ARN will be set later from custom.kmsGrants for consistency
1083
- } else {
1084
- // No existing key found - check if we should create one or error
1085
- if (AppDefinition.encryption?.createResourceIfNoneFound === true) {
1086
- // Create a new KMS key
1087
- console.log('No existing KMS key found, creating a new one...');
1088
-
1089
- definition.resources.Resources.FriggKMSKey = {
1090
- Type: 'AWS::KMS::Key',
1091
- DeletionPolicy: 'Retain',
1092
- UpdateReplacePolicy: 'Retain',
1093
- Properties: {
1094
- EnableKeyRotation: true,
1095
- Description: 'Frigg KMS key for field-level encryption',
1096
- KeyPolicy: {
1097
- Version: '2012-10-17',
1098
- Statement: [
1099
- {
1100
- Sid: 'AllowRootAccountAdmin',
1101
- Effect: 'Allow',
1102
- Principal: {
1103
- AWS: {
1104
- 'Fn::Sub':
1105
- 'arn:aws:iam::${AWS::AccountId}:root',
1106
- },
1107
- },
1108
- Action: 'kms:*',
1109
- Resource: '*',
1110
- },
1111
- {
1112
- Sid: 'AllowLambdaService',
1113
- Effect: 'Allow',
1114
- Principal: {
1115
- Service: 'lambda.amazonaws.com',
1116
- },
1117
- Action: [
1118
- 'kms:GenerateDataKey',
1119
- 'kms:Decrypt',
1120
- 'kms:DescribeKey',
1121
- ],
1122
- Resource: '*',
1123
- Condition: {
1124
- StringEquals: {
1125
- 'kms:ViaService': `lambda.${process.env.AWS_REGION ||
1126
- 'us-east-1'
1127
- }.amazonaws.com`,
1128
- },
1129
- },
1130
- },
1131
- ],
1132
- },
1133
- Tags: [
1134
- {
1135
- Key: 'Name',
1136
- Value: '${self:service}-${self:provider.stage}-frigg-kms-key',
1137
- },
1138
- {
1139
- Key: 'ManagedBy',
1140
- Value: 'Frigg',
676
+ console.log('No existing KMS key found, creating a new one...');
677
+ definition.resources.Resources.FriggKMSKey = {
678
+ Type: 'AWS::KMS::Key',
679
+ DeletionPolicy: 'Retain',
680
+ UpdateReplacePolicy: 'Retain',
681
+ Properties: {
682
+ EnableKeyRotation: true,
683
+ Description: 'Frigg KMS key for field-level encryption',
684
+ KeyPolicy: {
685
+ Version: '2012-10-17',
686
+ Statement: [
687
+ {
688
+ Sid: 'AllowRootAccountAdmin',
689
+ Effect: 'Allow',
690
+ Principal: {
691
+ AWS: { 'Fn::Sub': 'arn:aws:iam::${AWS::AccountId}:root' },
1141
692
  },
1142
- {
1143
- Key: 'Purpose',
1144
- Value: 'Field-level encryption for Frigg application',
693
+ Action: 'kms:*',
694
+ Resource: '*',
695
+ },
696
+ {
697
+ Sid: 'AllowLambdaService',
698
+ Effect: 'Allow',
699
+ Principal: { Service: 'lambda.amazonaws.com' },
700
+ Action: ['kms:GenerateDataKey', 'kms:Decrypt', 'kms:DescribeKey'],
701
+ Resource: '*',
702
+ Condition: {
703
+ StringEquals: {
704
+ 'kms:ViaService': `lambda.${process.env.AWS_REGION || 'us-east-1'}.amazonaws.com`,
705
+ },
1145
706
  },
1146
- ],
1147
- },
1148
- };
707
+ },
708
+ ],
709
+ },
710
+ Tags: [
711
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-frigg-kms-key' },
712
+ { Key: 'ManagedBy', Value: 'Frigg' },
713
+ { Key: 'Purpose', Value: 'Field-level encryption for Frigg application' },
714
+ ],
715
+ },
716
+ };
1149
717
 
1150
- // Create an alias for the new KMS key for consistent discovery
1151
- definition.resources.Resources.FriggKMSKeyAlias = {
1152
- Type: 'AWS::KMS::Alias',
1153
- DeletionPolicy: 'Retain',
1154
- Properties: {
1155
- AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
1156
- TargetKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] }
1157
- }
1158
- };
718
+ definition.resources.Resources.FriggKMSKeyAlias = {
719
+ Type: 'AWS::KMS::Alias',
720
+ DeletionPolicy: 'Retain',
721
+ Properties: {
722
+ AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
723
+ TargetKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
724
+ },
725
+ };
1159
726
 
1160
- definition.provider.iamRoleStatements.push({
1161
- Effect: 'Allow',
1162
- Action: ['kms:GenerateDataKey', 'kms:Decrypt'],
1163
- Resource: [{ 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] }],
1164
- });
727
+ definition.provider.iamRoleStatements.push({
728
+ Effect: 'Allow',
729
+ Action: ['kms:GenerateDataKey', 'kms:Decrypt'],
730
+ Resource: [{ 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] }],
731
+ });
1165
732
 
1166
- definition.provider.environment.KMS_KEY_ARN = {
1167
- 'Fn::GetAtt': ['FriggKMSKey', 'Arn'],
1168
- };
733
+ definition.provider.environment.KMS_KEY_ARN = { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] };
734
+ definition.custom.kmsGrants = {
735
+ kmsKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
736
+ };
737
+ }
1169
738
 
1170
- // Configure KMS grants to reference the created key
1171
- definition.custom.kmsGrants = {
1172
- kmsKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
1173
- };
1174
- } else {
1175
- // No key found and createIfNoneFound is not enabled - error
1176
- throw new Error(
1177
- 'KMS field-level encryption is enabled but no KMS key was found. ' +
1178
- 'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
1179
- );
1180
- }
1181
- }
739
+ definition.plugins.push('serverless-kms-grants');
740
+ if (!definition.custom.kmsGrants) {
741
+ definition.custom.kmsGrants = {
742
+ kmsKeyId: discoveredResources.defaultKmsKeyId || '${env:AWS_DISCOVERY_KMS_KEY_ID}',
743
+ };
744
+ }
745
+
746
+ if (!definition.provider.environment.KMS_KEY_ARN) {
747
+ definition.provider.environment.KMS_KEY_ARN =
748
+ discoveredResources.defaultKmsKeyId || '${env:AWS_DISCOVERY_KMS_KEY_ID}';
749
+ }
750
+ };
1182
751
 
1183
- definition.plugins.push('serverless-kms-grants');
752
+ const healVpcConfiguration = (discoveredResources, AppDefinition) => {
753
+ const healingReport = {
754
+ healed: [],
755
+ warnings: [],
756
+ errors: [],
757
+ recommendations: [],
758
+ criticalActions: [],
759
+ };
1184
760
 
1185
- // Configure KMS grants if not already set (when using existing key)
1186
- if (!definition.custom.kmsGrants) {
1187
- definition.custom.kmsGrants = {
1188
- kmsKeyId:
1189
- discoveredResources.defaultKmsKeyId ||
1190
- '${env:AWS_DISCOVERY_KMS_KEY_ID}',
1191
- };
1192
- }
761
+ if (!AppDefinition.vpc?.selfHeal) {
762
+ return healingReport;
763
+ }
1193
764
 
1194
- // Always set KMS_KEY_ARN from custom.kmsGrants for consistency
1195
- // This translates AWS_DISCOVERY_KMS_KEY_ID to the runtime variable KMS_KEY_ARN
1196
- if (!definition.provider.environment.KMS_KEY_ARN) {
1197
- // Use the discovered value directly when available (from in-process discovery)
1198
- // Otherwise fall back to environment variable (from separate discovery process)
1199
- definition.provider.environment.KMS_KEY_ARN =
1200
- discoveredResources.defaultKmsKeyId ||
1201
- '${env:AWS_DISCOVERY_KMS_KEY_ID}';
1202
- }
765
+ console.log('🔧 Self-healing mode enabled - checking for VPC misconfigurations...');
766
+
767
+ if (discoveredResources.natGatewayInPrivateSubnet) {
768
+ healingReport.warnings.push(
769
+ `NAT Gateway ${discoveredResources.natGatewayInPrivateSubnet} is in a private subnet`
770
+ );
771
+ healingReport.recommendations.push(
772
+ 'NAT Gateway should be recreated in a public subnet for proper internet connectivity'
773
+ );
774
+ discoveredResources.needsNewNatGateway = true;
775
+ healingReport.healed.push('Marked NAT Gateway for recreation in public subnet');
1203
776
  }
1204
777
 
1205
- /**
1206
- * Heals VPC configuration issues by fixing common misconfigurations
1207
- * @param {Object} discoveredResources - Resources discovered from AWS
1208
- * @param {Object} AppDefinition - Application definition with VPC settings
1209
- * @returns {Object} Healing report with actions taken and recommendations
1210
- */
1211
- const healVPCConfiguration = (discoveredResources, AppDefinition) => {
1212
- const healingReport = {
1213
- healed: [],
1214
- warnings: [],
1215
- errors: [],
1216
- recommendations: [],
1217
- criticalActions: []
1218
- };
778
+ if (discoveredResources.elasticIpAlreadyAssociated) {
779
+ healingReport.warnings.push(
780
+ `Elastic IP ${discoveredResources.existingElasticIp} is already associated`
781
+ );
1219
782
 
1220
- // Only heal if selfHeal is explicitly enabled
1221
- if (!AppDefinition.vpc?.selfHeal) {
1222
- return healingReport;
783
+ if (discoveredResources.existingNatGatewayId) {
784
+ healingReport.healed.push('Will reuse existing NAT Gateway instead of creating a new one');
785
+ discoveredResources.reuseExistingNatGateway = true;
786
+ } else {
787
+ healingReport.healed.push('Will allocate a new Elastic IP for NAT Gateway');
788
+ discoveredResources.allocateNewElasticIp = true;
1223
789
  }
790
+ }
1224
791
 
1225
- console.log('🔧 Self-healing mode enabled - checking for VPC misconfigurations...');
1226
-
1227
- // Check NAT Gateway placement
1228
- if (discoveredResources.natGatewayInPrivateSubnet) {
1229
- healingReport.warnings.push(
1230
- `NAT Gateway ${discoveredResources.natGatewayInPrivateSubnet} is in a private subnet`
1231
- );
1232
- healingReport.recommendations.push(
1233
- 'NAT Gateway should be recreated in a public subnet for proper internet connectivity'
1234
- );
792
+ if (
793
+ discoveredResources.privateSubnetsWithWrongRoutes &&
794
+ discoveredResources.privateSubnetsWithWrongRoutes.length > 0
795
+ ) {
796
+ healingReport.warnings.push(
797
+ `Found ${discoveredResources.privateSubnetsWithWrongRoutes.length} subnets that are PUBLIC but will be used for Lambda`
798
+ );
799
+ healingReport.healed.push(
800
+ 'Route tables will be corrected during deployment - converting public subnets to private'
801
+ );
802
+ healingReport.criticalActions.push(
803
+ 'SUBNET ISOLATION: Will create separate route tables to ensure Lambda subnets are private'
804
+ );
805
+ }
1235
806
 
1236
- // Mark that we need to create a new NAT Gateway
1237
- discoveredResources.needsNewNatGateway = true;
1238
- healingReport.healed.push('Marked NAT Gateway for recreation in public subnet');
1239
- }
807
+ if (discoveredResources.subnetConversionRequired) {
808
+ healingReport.warnings.push(
809
+ 'Subnet configuration mismatch detected - Lambda functions require private subnets'
810
+ );
811
+ healingReport.healed.push('Will create proper route table configuration for subnet isolation');
812
+ }
1240
813
 
1241
- // Check if EIP is already associated
1242
- if (discoveredResources.elasticIpAlreadyAssociated) {
1243
- healingReport.warnings.push(
1244
- `Elastic IP ${discoveredResources.existingElasticIp} is already associated`
1245
- );
814
+ if (discoveredResources.orphanedElasticIps?.length > 0) {
815
+ healingReport.warnings.push(
816
+ `Found ${discoveredResources.orphanedElasticIps.length} orphaned Elastic IPs`
817
+ );
818
+ healingReport.recommendations.push('Consider releasing orphaned Elastic IPs to avoid charges');
819
+ }
1246
820
 
1247
- // In self-heal mode, we'll try to reuse or create a new one
1248
- if (discoveredResources.existingNatGatewayId) {
1249
- healingReport.healed.push(
1250
- 'Will reuse existing NAT Gateway instead of creating a new one'
1251
- );
1252
- discoveredResources.reuseExistingNatGateway = true;
1253
- } else {
1254
- healingReport.healed.push(
1255
- 'Will allocate a new Elastic IP for NAT Gateway'
1256
- );
1257
- discoveredResources.allocateNewElasticIp = true;
1258
- }
1259
- }
821
+ if (healingReport.criticalActions.length > 0) {
822
+ console.log('🚨 CRITICAL ACTIONS:');
823
+ healingReport.criticalActions.forEach((action) => console.log(` - ${action}`));
824
+ }
1260
825
 
1261
- // Check route table associations and subnet conversion requirements
1262
- if (discoveredResources.privateSubnetsWithWrongRoutes &&
1263
- discoveredResources.privateSubnetsWithWrongRoutes.length > 0) {
1264
- healingReport.warnings.push(
1265
- `Found ${discoveredResources.privateSubnetsWithWrongRoutes.length} subnets that are PUBLIC but will be used for Lambda`
1266
- );
1267
- healingReport.healed.push(
1268
- 'Route tables will be corrected during deployment - converting public subnets to private'
1269
- );
1270
- healingReport.criticalActions.push(
1271
- 'SUBNET ISOLATION: Will create separate route tables to ensure Lambda subnets are private'
1272
- );
1273
- }
826
+ if (healingReport.healed.length > 0) {
827
+ console.log('✅ Self-healing actions:');
828
+ healingReport.healed.forEach((action) => console.log(` - ${action}`));
829
+ }
1274
830
 
1275
- // Check if subnet conversion is required
1276
- if (discoveredResources.subnetConversionRequired) {
1277
- healingReport.warnings.push(
1278
- 'Subnet configuration mismatch detected - Lambda functions require private subnets'
1279
- );
1280
- healingReport.healed.push(
1281
- 'Will create proper route table configuration for subnet isolation'
1282
- );
1283
- }
831
+ if (healingReport.warnings.length > 0) {
832
+ console.log('⚠️ Issues detected:');
833
+ healingReport.warnings.forEach((warning) => console.log(` - ${warning}`));
834
+ }
1284
835
 
1285
- // Check for orphaned resources
1286
- if (discoveredResources.orphanedElasticIps?.length > 0) {
1287
- healingReport.warnings.push(
1288
- `Found ${discoveredResources.orphanedElasticIps.length} orphaned Elastic IPs`
1289
- );
1290
- healingReport.recommendations.push(
1291
- 'Consider releasing orphaned Elastic IPs to avoid charges'
1292
- );
1293
- }
836
+ if (healingReport.recommendations.length > 0) {
837
+ console.log('💡 Recommendations:');
838
+ healingReport.recommendations.forEach((rec) => console.log(` - ${rec}`));
839
+ }
1294
840
 
1295
- // Log healing report
1296
- if (healingReport.criticalActions.length > 0) {
1297
- console.log('🚨 CRITICAL ACTIONS:');
1298
- healingReport.criticalActions.forEach(action => console.log(` - ${action}`));
1299
- }
841
+ return healingReport;
842
+ };
1300
843
 
1301
- if (healingReport.healed.length > 0) {
1302
- console.log('✅ Self-healing actions:');
1303
- healingReport.healed.forEach(action => console.log(` - ${action}`));
1304
- }
844
+ const configureVpc = (definition, AppDefinition, discoveredResources) => {
845
+ if (AppDefinition.vpc?.enable !== true) {
846
+ return;
847
+ }
1305
848
 
1306
- if (healingReport.warnings.length > 0) {
1307
- console.log('⚠️ Issues detected:');
1308
- healingReport.warnings.forEach(warning => console.log(` - ${warning}`));
1309
- }
849
+ definition.provider.iamRoleStatements.push({
850
+ Effect: 'Allow',
851
+ Action: [
852
+ 'ec2:CreateNetworkInterface',
853
+ 'ec2:DescribeNetworkInterfaces',
854
+ 'ec2:DeleteNetworkInterface',
855
+ 'ec2:AttachNetworkInterface',
856
+ 'ec2:DetachNetworkInterface',
857
+ ],
858
+ Resource: '*',
859
+ });
1310
860
 
1311
- if (healingReport.recommendations.length > 0) {
1312
- console.log('💡 Recommendations:');
1313
- healingReport.recommendations.forEach(rec => console.log(` - ${rec}`));
861
+ if (Object.keys(discoveredResources).length > 0) {
862
+ const healingReport = healVpcConfiguration(discoveredResources, AppDefinition);
863
+ if (healingReport.errors.length > 0 && !AppDefinition.vpc?.selfHeal) {
864
+ throw new Error(`VPC configuration errors detected: ${healingReport.errors.join(', ')}`);
1314
865
  }
866
+ }
1315
867
 
1316
- return healingReport;
868
+ const vpcManagement = AppDefinition.vpc.management || 'discover';
869
+ let vpcId = null;
870
+ const vpcConfig = {
871
+ securityGroupIds: [],
872
+ subnetIds: [],
1317
873
  };
1318
874
 
1319
- // VPC Configuration based on App Definition
1320
- if (AppDefinition.vpc?.enable === true) {
1321
- // Add VPC-related IAM permissions
1322
- definition.provider.iamRoleStatements.push({
1323
- Effect: 'Allow',
1324
- Action: [
1325
- 'ec2:CreateNetworkInterface',
1326
- 'ec2:DescribeNetworkInterfaces',
1327
- 'ec2:DeleteNetworkInterface',
1328
- 'ec2:AttachNetworkInterface',
1329
- 'ec2:DetachNetworkInterface',
1330
- ],
1331
- Resource: '*',
1332
- });
1333
-
1334
- // Run healing if enabled and we have discovered resources
1335
- if (discoveredResources && Object.keys(discoveredResources).length > 0) {
1336
- const healingReport = healVPCConfiguration(discoveredResources, AppDefinition);
875
+ console.log(`VPC Management Mode: ${vpcManagement}`);
1337
876
 
1338
- // If healing failed critically, throw an error unless selfHeal is true
1339
- if (healingReport.errors.length > 0 && !AppDefinition.vpc?.selfHeal) {
1340
- throw new Error(`VPC configuration errors detected: ${healingReport.errors.join(', ')}`);
1341
- }
877
+ if (vpcManagement === 'create-new') {
878
+ const vpcResources = createVPCInfrastructure(AppDefinition);
879
+ Object.assign(definition.resources.Resources, vpcResources);
880
+ vpcId = { Ref: 'FriggVPC' };
881
+ vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds || [{ Ref: 'FriggLambdaSecurityGroup' }];
882
+ } else if (vpcManagement === 'use-existing') {
883
+ if (!AppDefinition.vpc.vpcId) {
884
+ throw new Error('VPC management is set to "use-existing" but no vpcId was provided');
1342
885
  }
886
+ vpcId = AppDefinition.vpc.vpcId;
887
+ vpcConfig.securityGroupIds =
888
+ AppDefinition.vpc.securityGroupIds ||
889
+ (discoveredResources.defaultSecurityGroupId ? [discoveredResources.defaultSecurityGroupId] : []);
890
+ } else {
891
+ if (!discoveredResources.defaultVpcId) {
892
+ throw new Error(
893
+ 'VPC discovery failed: No VPC found. Either set vpc.management to "create-new" or provide vpc.vpcId with "use-existing".'
894
+ );
895
+ }
896
+ vpcId = discoveredResources.defaultVpcId;
897
+ vpcConfig.securityGroupIds =
898
+ AppDefinition.vpc.securityGroupIds ||
899
+ (discoveredResources.defaultSecurityGroupId ? [discoveredResources.defaultSecurityGroupId] : []);
900
+ }
1343
901
 
1344
- // STEP 1: Determine VPC (create, discover, or use existing)
1345
- const vpcManagement = AppDefinition.vpc.management || 'discover';
1346
- let vpcId = null;
1347
- let vpcConfig = {
1348
- securityGroupIds: [],
1349
- subnetIds: []
1350
- };
902
+ const defaultSubnetManagement = vpcManagement === 'create-new' ? 'create' : 'discover';
903
+ let subnetManagement = AppDefinition.vpc.subnets?.management || defaultSubnetManagement;
904
+ console.log(`Subnet Management Mode: ${subnetManagement}`);
905
+
906
+ const effectiveVpcId = vpcId || discoveredResources.defaultVpcId;
907
+ if (!effectiveVpcId) {
908
+ throw new Error('Cannot manage subnets without a VPC ID');
909
+ }
1351
910
 
1352
- console.log(`VPC Management Mode: ${vpcManagement}`);
911
+ if (subnetManagement === 'create') {
912
+ console.log('Creating new subnets...');
913
+ const subnetVpcId = vpcManagement === 'create-new' ? { Ref: 'FriggVPC' } : effectiveVpcId;
914
+ let subnet1Cidr;
915
+ let subnet2Cidr;
916
+ let publicSubnetCidr;
1353
917
 
1354
- // First, establish VPC context
1355
918
  if (vpcManagement === 'create-new') {
1356
- // Create new VPC infrastructure
1357
- const vpcResources = createVPCInfrastructure(AppDefinition);
1358
- Object.assign(definition.resources.Resources, vpcResources);
1359
- vpcId = { Ref: 'FriggVPC' }; // Reference to created VPC
1360
-
1361
- // Default security group for new VPC
1362
- vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds || [
1363
- { Ref: 'FriggLambdaSecurityGroup' }
1364
- ];
1365
- } else if (vpcManagement === 'use-existing') {
1366
- // Use explicitly provided VPC
1367
- if (!AppDefinition.vpc.vpcId) {
1368
- throw new Error('VPC management is set to "use-existing" but no vpcId was provided');
1369
- }
1370
- vpcId = AppDefinition.vpc.vpcId;
1371
- // Use provided security groups or try to discover default security group for the VPC
1372
- vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds ||
1373
- (discoveredResources.defaultSecurityGroupId ? [discoveredResources.defaultSecurityGroupId] : []);
919
+ const generatedCidrs = { 'Fn::Cidr': ['10.0.0.0/16', 3, 8] };
920
+ subnet1Cidr = { 'Fn::Select': [0, generatedCidrs] };
921
+ subnet2Cidr = { 'Fn::Select': [1, generatedCidrs] };
922
+ publicSubnetCidr = { 'Fn::Select': [2, generatedCidrs] };
1374
923
  } else {
1375
- // Discover VPC
1376
- if (!discoveredResources.defaultVpcId) {
1377
- throw new Error(
1378
- 'VPC discovery failed: No VPC found. ' +
1379
- 'Either set vpc.management to "create-new" or provide vpc.vpcId with "use-existing".'
1380
- );
1381
- }
1382
- vpcId = discoveredResources.defaultVpcId;
1383
- vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds ||
1384
- (discoveredResources.defaultSecurityGroupId ? [discoveredResources.defaultSecurityGroupId] : []);
924
+ subnet1Cidr = '172.31.240.0/24';
925
+ subnet2Cidr = '172.31.241.0/24';
926
+ publicSubnetCidr = '172.31.250.0/24';
1385
927
  }
1386
928
 
1387
- // STEP 2: Handle Subnet Management (independent of VPC management)
1388
- // When creating a new VPC, default to creating subnets unless explicitly specified
1389
- const defaultSubnetManagement = vpcManagement === 'create-new' ? 'create' : 'discover';
1390
- const subnetManagement = AppDefinition.vpc.subnets?.management || defaultSubnetManagement;
1391
- console.log(`Subnet Management Mode: ${subnetManagement}`);
929
+ definition.resources.Resources.FriggPrivateSubnet1 = {
930
+ Type: 'AWS::EC2::Subnet',
931
+ Properties: {
932
+ VpcId: subnetVpcId,
933
+ CidrBlock: subnet1Cidr,
934
+ AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
935
+ Tags: [
936
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-1' },
937
+ { Key: 'Type', Value: 'Private' },
938
+ { Key: 'ManagedBy', Value: 'Frigg' },
939
+ ],
940
+ },
941
+ };
1392
942
 
1393
- // Ensure we have a valid VPC ID for subnet operations
1394
- const effectiveVpcId = vpcId || discoveredResources.defaultVpcId;
1395
- if (!effectiveVpcId) {
1396
- throw new Error('Cannot manage subnets without a VPC ID');
1397
- }
943
+ definition.resources.Resources.FriggPrivateSubnet2 = {
944
+ Type: 'AWS::EC2::Subnet',
945
+ Properties: {
946
+ VpcId: subnetVpcId,
947
+ CidrBlock: subnet2Cidr,
948
+ AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
949
+ Tags: [
950
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-2' },
951
+ { Key: 'Type', Value: 'Private' },
952
+ { Key: 'ManagedBy', Value: 'Frigg' },
953
+ ],
954
+ },
955
+ };
1398
956
 
1399
- // Subnet decision tree
1400
- if (subnetManagement === 'create') {
1401
- // Create new subnets in the VPC (either new or existing)
1402
- console.log('Creating new subnets...');
957
+ definition.resources.Resources.FriggPublicSubnet = {
958
+ Type: 'AWS::EC2::Subnet',
959
+ Properties: {
960
+ VpcId: subnetVpcId,
961
+ CidrBlock: publicSubnetCidr,
962
+ MapPublicIpOnLaunch: true,
963
+ AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
964
+ Tags: [
965
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public' },
966
+ { Key: 'Type', Value: 'Public' },
967
+ { Key: 'ManagedBy', Value: 'Frigg' },
968
+ ],
969
+ },
970
+ };
1403
971
 
1404
- // Determine VpcId based on VPC management mode
1405
- const subnetVpcId = vpcManagement === 'create-new' ? { Ref: 'FriggVPC' } : effectiveVpcId;
972
+ vpcConfig.subnetIds = [{ Ref: 'FriggPrivateSubnet1' }, { Ref: 'FriggPrivateSubnet2' }];
1406
973
 
1407
- // Generate CIDR blocks based on VPC type
1408
- // For new VPC: use Fn::Cidr to generate from 10.0.0.0/16
1409
- // For existing VPC: use safer high-range /24 blocks less likely to conflict
1410
- let subnet1Cidr, subnet2Cidr, publicSubnetCidr;
974
+ if (!AppDefinition.vpc.natGateway || AppDefinition.vpc.natGateway.management === 'discover') {
975
+ if (vpcManagement === 'create-new' || !discoveredResources.internetGatewayId) {
976
+ if (!definition.resources.Resources.FriggInternetGateway) {
977
+ definition.resources.Resources.FriggInternetGateway = {
978
+ Type: 'AWS::EC2::InternetGateway',
979
+ Properties: {
980
+ Tags: [
981
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-igw' },
982
+ { Key: 'ManagedBy', Value: 'Frigg' },
983
+ ],
984
+ },
985
+ };
1411
986
 
1412
- if (vpcManagement === 'create-new') {
1413
- // Use Fn::Cidr to generate 3 /24 subnets from the VPC CIDR
1414
- // This creates [10.0.0.0/24, 10.0.1.0/24, 10.0.2.0/24]
1415
- const generatedCidrs = {
1416
- 'Fn::Cidr': ['10.0.0.0/16', 3, 8] // 3 subnets with /24 (256-8=248 bits)
1417
- };
1418
- subnet1Cidr = { 'Fn::Select': [0, generatedCidrs] }; // 10.0.0.0/24
1419
- subnet2Cidr = { 'Fn::Select': [1, generatedCidrs] }; // 10.0.1.0/24
1420
- publicSubnetCidr = { 'Fn::Select': [2, generatedCidrs] }; // 10.0.2.0/24
1421
- } else {
1422
- // For existing VPCs, use high-range /24 blocks less likely to conflict
1423
- // These are in the 172.31.x.x range for default VPC or high ranges for custom VPCs
1424
- subnet1Cidr = '172.31.240.0/24';
1425
- subnet2Cidr = '172.31.241.0/24';
1426
- publicSubnetCidr = '172.31.250.0/24';
987
+ definition.resources.Resources.FriggIGWAttachment = {
988
+ Type: 'AWS::EC2::VPCGatewayAttachment',
989
+ Properties: {
990
+ VpcId: subnetVpcId,
991
+ InternetGatewayId: { Ref: 'FriggInternetGateway' },
992
+ },
993
+ };
994
+ }
1427
995
  }
1428
996
 
1429
- // Create private subnets
1430
- definition.resources.Resources.FriggPrivateSubnet1 = {
1431
- Type: 'AWS::EC2::Subnet',
997
+ definition.resources.Resources.FriggPublicRouteTable = {
998
+ Type: 'AWS::EC2::RouteTable',
1432
999
  Properties: {
1433
1000
  VpcId: subnetVpcId,
1434
- CidrBlock: subnet1Cidr,
1435
- AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
1436
1001
  Tags: [
1437
- { Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-1' },
1438
- { Key: 'Type', Value: 'Private' },
1439
- { Key: 'ManagedBy', Value: 'Frigg' }
1440
- ]
1441
- }
1002
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-rt' },
1003
+ { Key: 'ManagedBy', Value: 'Frigg' },
1004
+ ],
1005
+ },
1442
1006
  };
1443
1007
 
1444
- definition.resources.Resources.FriggPrivateSubnet2 = {
1445
- Type: 'AWS::EC2::Subnet',
1008
+ definition.resources.Resources.FriggPublicRoute = {
1009
+ Type: 'AWS::EC2::Route',
1010
+ DependsOn: vpcManagement === 'create-new' ? 'FriggIGWAttachment' : undefined,
1446
1011
  Properties: {
1447
- VpcId: subnetVpcId,
1448
- CidrBlock: subnet2Cidr,
1449
- AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
1450
- Tags: [
1451
- { Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-2' },
1452
- { Key: 'Type', Value: 'Private' },
1453
- { Key: 'ManagedBy', Value: 'Frigg' }
1454
- ]
1455
- }
1012
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
1013
+ DestinationCidrBlock: '0.0.0.0/0',
1014
+ GatewayId: discoveredResources.internetGatewayId || { Ref: 'FriggInternetGateway' },
1015
+ },
1016
+ };
1017
+
1018
+ definition.resources.Resources.FriggPublicSubnetRouteTableAssociation = {
1019
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1020
+ Properties: {
1021
+ SubnetId: { Ref: 'FriggPublicSubnet' },
1022
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
1023
+ },
1456
1024
  };
1457
1025
 
1458
- // Create public subnet for NAT
1459
- definition.resources.Resources.FriggPublicSubnet = {
1460
- Type: 'AWS::EC2::Subnet',
1026
+ definition.resources.Resources.FriggLambdaRouteTable = {
1027
+ Type: 'AWS::EC2::RouteTable',
1461
1028
  Properties: {
1462
1029
  VpcId: subnetVpcId,
1463
- CidrBlock: publicSubnetCidr,
1464
- MapPublicIpOnLaunch: true,
1465
- AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
1466
1030
  Tags: [
1467
- { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public' },
1468
- { Key: 'Type', Value: 'Public' },
1469
- { Key: 'ManagedBy', Value: 'Frigg' }
1470
- ]
1471
- }
1031
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
1032
+ { Key: 'ManagedBy', Value: 'Frigg' },
1033
+ ],
1034
+ },
1472
1035
  };
1473
1036
 
1474
- vpcConfig.subnetIds = [
1475
- { Ref: 'FriggPrivateSubnet1' },
1476
- { Ref: 'FriggPrivateSubnet2' }
1477
- ];
1478
-
1479
- // IMPORTANT: Create route tables even without NAT Gateway management
1480
- // Otherwise subnets won't have proper routing
1481
- if (!AppDefinition.vpc.natGateway || AppDefinition.vpc.natGateway.management === 'discover') {
1482
- // Need to ensure public subnet has IGW route
1483
- if (vpcManagement === 'create-new' || !discoveredResources.internetGatewayId) {
1484
- // Create or reference IGW for public subnet
1485
- if (!definition.resources.Resources.FriggInternetGateway) {
1486
- definition.resources.Resources.FriggInternetGateway = {
1487
- Type: 'AWS::EC2::InternetGateway',
1488
- Properties: {
1489
- Tags: [
1490
- { Key: 'Name', Value: '${self:service}-${self:provider.stage}-igw' },
1491
- { Key: 'ManagedBy', Value: 'Frigg' }
1492
- ]
1493
- }
1494
- };
1495
-
1496
- definition.resources.Resources.FriggIGWAttachment = {
1497
- Type: 'AWS::EC2::VPCGatewayAttachment',
1498
- Properties: {
1499
- VpcId: subnetVpcId,
1500
- InternetGatewayId: { Ref: 'FriggInternetGateway' }
1501
- }
1502
- };
1503
- }
1504
- }
1505
-
1506
- // Create public route table with IGW route
1507
- definition.resources.Resources.FriggPublicRouteTable = {
1508
- Type: 'AWS::EC2::RouteTable',
1509
- Properties: {
1510
- VpcId: subnetVpcId,
1511
- Tags: [
1512
- { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-rt' },
1513
- { Key: 'ManagedBy', Value: 'Frigg' }
1514
- ]
1515
- }
1516
- };
1517
-
1518
- definition.resources.Resources.FriggPublicRoute = {
1519
- Type: 'AWS::EC2::Route',
1520
- DependsOn: vpcManagement === 'create-new' ? 'FriggIGWAttachment' : undefined,
1521
- Properties: {
1522
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
1523
- DestinationCidrBlock: '0.0.0.0/0',
1524
- GatewayId: discoveredResources.internetGatewayId || { Ref: 'FriggInternetGateway' }
1525
- }
1526
- };
1527
-
1528
- // Associate public subnet with public route table
1529
- definition.resources.Resources.FriggPublicSubnetRouteTableAssociation = {
1530
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1531
- Properties: {
1532
- SubnetId: { Ref: 'FriggPublicSubnet' },
1533
- RouteTableId: { Ref: 'FriggPublicRouteTable' }
1534
- }
1535
- };
1536
-
1537
- // Create private route table for Lambda subnets
1538
- definition.resources.Resources.FriggLambdaRouteTable = {
1539
- Type: 'AWS::EC2::RouteTable',
1540
- Properties: {
1541
- VpcId: subnetVpcId,
1542
- Tags: [
1543
- { Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
1544
- { Key: 'ManagedBy', Value: 'Frigg' }
1545
- ]
1546
- }
1547
- };
1548
-
1549
- // Associate private subnets with route table
1550
- definition.resources.Resources.FriggPrivateSubnet1RouteTableAssociation = {
1551
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1552
- Properties: {
1553
- SubnetId: { Ref: 'FriggPrivateSubnet1' },
1554
- RouteTableId: { Ref: 'FriggLambdaRouteTable' }
1555
- }
1556
- };
1037
+ definition.resources.Resources.FriggPrivateSubnet1RouteTableAssociation = {
1038
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1039
+ Properties: {
1040
+ SubnetId: { Ref: 'FriggPrivateSubnet1' },
1041
+ RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1042
+ },
1043
+ };
1557
1044
 
1558
- definition.resources.Resources.FriggPrivateSubnet2RouteTableAssociation = {
1559
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1560
- Properties: {
1561
- SubnetId: { Ref: 'FriggPrivateSubnet2' },
1562
- RouteTableId: { Ref: 'FriggLambdaRouteTable' }
1563
- }
1564
- };
1565
- }
1566
- } else if (subnetManagement === 'use-existing') {
1567
- // Use explicitly provided subnet IDs
1568
- if (!AppDefinition.vpc.subnets?.ids || AppDefinition.vpc.subnets.ids.length < 2) {
1045
+ definition.resources.Resources.FriggPrivateSubnet2RouteTableAssociation = {
1046
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1047
+ Properties: {
1048
+ SubnetId: { Ref: 'FriggPrivateSubnet2' },
1049
+ RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1050
+ },
1051
+ };
1052
+ }
1053
+ } else if (subnetManagement === 'use-existing') {
1054
+ if (!AppDefinition.vpc.subnets?.ids || AppDefinition.vpc.subnets.ids.length < 2) {
1055
+ throw new Error(
1056
+ 'Subnet management is "use-existing" but less than 2 subnet IDs provided. Provide at least 2 subnet IDs in vpc.subnets.ids.'
1057
+ );
1058
+ }
1059
+ vpcConfig.subnetIds = AppDefinition.vpc.subnets.ids;
1060
+ } else {
1061
+ vpcConfig.subnetIds =
1062
+ AppDefinition.vpc.subnets?.ids?.length > 0
1063
+ ? AppDefinition.vpc.subnets.ids
1064
+ : discoveredResources.privateSubnetId1 && discoveredResources.privateSubnetId2
1065
+ ? [discoveredResources.privateSubnetId1, discoveredResources.privateSubnetId2]
1066
+ : [];
1067
+
1068
+ if (vpcConfig.subnetIds.length < 2) {
1069
+ if (AppDefinition.vpc.selfHeal) {
1070
+ console.log('No subnets found but self-heal enabled - creating minimal subnet setup');
1071
+ subnetManagement = 'create';
1072
+ discoveredResources.createSubnets = true;
1073
+ } else {
1569
1074
  throw new Error(
1570
- 'Subnet management is "use-existing" but less than 2 subnet IDs provided. ' +
1571
- 'Provide at least 2 subnet IDs in vpc.subnets.ids.'
1075
+ 'No subnets discovered and subnets.management is "discover". Either enable vpc.selfHeal, set subnets.management to "create", or provide subnet IDs.'
1572
1076
  );
1573
1077
  }
1574
- vpcConfig.subnetIds = AppDefinition.vpc.subnets.ids;
1575
- } else {
1576
- // Discover mode (default)
1577
- vpcConfig.subnetIds =
1578
- AppDefinition.vpc.subnets?.ids?.length > 0
1579
- ? AppDefinition.vpc.subnets.ids
1580
- : (discoveredResources.privateSubnetId1 &&
1581
- discoveredResources.privateSubnetId2
1582
- ? [
1583
- discoveredResources.privateSubnetId1,
1584
- discoveredResources.privateSubnetId2,
1585
- ]
1586
- : []);
1587
-
1588
- if (vpcConfig.subnetIds.length < 2) {
1589
- if (AppDefinition.vpc.selfHeal) {
1590
- console.log('No subnets found but self-heal enabled - creating minimal subnet setup');
1591
- // Fall back to creating subnets
1592
- subnetManagement = 'create';
1593
- // Recursion would be complex here, so just set flag
1594
- discoveredResources.createSubnets = true;
1595
- } else {
1596
- throw new Error(
1597
- 'No subnets discovered and subnets.management is "discover". ' +
1598
- 'Either enable vpc.selfHeal, set subnets.management to "create", or provide subnet IDs.'
1599
- );
1600
- }
1601
- }
1602
1078
  }
1079
+ }
1603
1080
 
1604
- // Set VPC config for Lambda functions only if we have valid subnet IDs
1605
- if (
1606
- vpcConfig.subnetIds.length >= 2 &&
1607
- vpcConfig.securityGroupIds.length > 0
1608
- ) {
1609
- definition.provider.vpc = vpcConfig;
1610
-
1611
- // ALWAYS manage NAT Gateway through CloudFormation for self-healing
1612
- // This ensures NAT Gateway is always in the correct subnet with proper configuration
1613
-
1614
- // Use only the new 'management' property pattern from backend/index.js
1615
- const natGatewayManagement = AppDefinition.vpc.natGateway?.management || 'discover';
1616
- let needsNewNatGateway =
1617
- natGatewayManagement === 'createAndManage' ||
1618
- discoveredResources.needsNewNatGateway === true; // Use healing flag
1081
+ if (subnetManagement === 'create' && discoveredResources.createSubnets) {
1082
+ definition.resources.Resources.FriggLambdaRouteTable =
1083
+ definition.resources.Resources.FriggLambdaRouteTable || {
1084
+ Type: 'AWS::EC2::RouteTable',
1085
+ Properties: {
1086
+ VpcId: effectiveVpcId,
1087
+ Tags: [
1088
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
1089
+ { Key: 'ManagedBy', Value: 'Frigg' },
1090
+ { Key: 'Environment', Value: '${self:provider.stage}' },
1091
+ { Key: 'Service', Value: '${self:service}' },
1092
+ ],
1093
+ },
1094
+ };
1095
+ }
1619
1096
 
1620
- console.log('needsNewNatGateway', needsNewNatGateway);
1097
+ if (
1098
+ vpcConfig.subnetIds.length >= 2 &&
1099
+ vpcConfig.securityGroupIds.length > 0
1100
+ ) {
1101
+ definition.provider.vpc = vpcConfig;
1621
1102
 
1622
- // Remove unused helper function - validation is done in discovery
1103
+ const natGatewayManagement = AppDefinition.vpc.natGateway?.management || 'discover';
1104
+ let needsNewNatGateway =
1105
+ natGatewayManagement === 'createAndManage' ||
1106
+ discoveredResources.needsNewNatGateway === true;
1623
1107
 
1624
- // Variables to track NAT Gateway and EIP reuse
1625
- let reuseExistingNatGateway = false;
1626
- let useExistingEip = false;
1108
+ console.log('needsNewNatGateway', needsNewNatGateway);
1627
1109
 
1628
- if (needsNewNatGateway) {
1629
- // Always create new dedicated resources in create mode to avoid confusion with existing ones
1630
- console.log(
1631
- 'Create mode: Creating dedicated EIP, public subnet, and NAT Gateway...'
1632
- );
1110
+ let reuseExistingNatGateway = false;
1111
+ let useExistingEip = false;
1633
1112
 
1634
- // Check if we can reuse existing NAT Gateway and EIP to avoid conflicts
1113
+ if (needsNewNatGateway) {
1114
+ console.log('Create mode: Creating dedicated EIP, public subnet, and NAT Gateway...');
1635
1115
 
1636
- // Check if we have a Frigg-managed NAT Gateway that we can reuse
1637
- if (discoveredResources.existingNatGatewayId &&
1638
- discoveredResources.existingElasticIpAllocationId) {
1639
- // We have both NAT Gateway and EIP
1640
- console.log('Found existing Frigg-managed NAT Gateway and EIP');
1641
-
1642
- // CRITICAL: Check if NAT Gateway is in correct (public) subnet
1643
- if (!discoveredResources.natGatewayInPrivateSubnet) {
1644
- // NAT Gateway is properly configured, reuse it
1645
- console.log(' Existing NAT Gateway is in PUBLIC subnet, will reuse it');
1646
- reuseExistingNatGateway = true;
1116
+ if (
1117
+ discoveredResources.existingNatGatewayId &&
1118
+ discoveredResources.existingElasticIpAllocationId
1119
+ ) {
1120
+ console.log('Found existing Frigg-managed NAT Gateway and EIP');
1121
+ if (!discoveredResources.natGatewayInPrivateSubnet) {
1122
+ console.log('✅ Existing NAT Gateway is in PUBLIC subnet, will reuse it');
1123
+ reuseExistingNatGateway = true;
1124
+ } else {
1125
+ console.log(' NAT Gateway is in PRIVATE subnet - MUST create new one in PUBLIC subnet');
1126
+ if (AppDefinition.vpc.selfHeal) {
1127
+ console.log('Self-heal enabled: Creating new NAT Gateway in PUBLIC subnet');
1128
+ reuseExistingNatGateway = false;
1129
+ useExistingEip = false;
1130
+ discoveredResources.needsCleanup = true;
1647
1131
  } else {
1648
- // NAT Gateway is in PRIVATE subnet - NEVER reuse it
1649
- console.log(' NAT Gateway is in PRIVATE subnet - MUST create new one in PUBLIC subnet');
1650
-
1651
- if (AppDefinition.vpc.selfHeal) {
1652
- console.log('Self-heal enabled: Creating new NAT Gateway in PUBLIC subnet');
1653
- // Force creation of new NAT in public subnet
1654
- reuseExistingNatGateway = false;
1655
- // Cannot reuse the EIP since it's associated with wrong NAT
1656
- useExistingEip = false;
1657
- // Mark for cleanup recommendations
1658
- discoveredResources.needsCleanup = true;
1659
- } else {
1660
- throw new Error(
1661
- 'CRITICAL: NAT Gateway is in PRIVATE subnet (will not work!). ' +
1662
- 'Enable vpc.selfHeal to auto-fix or set natGateway.management to "createAndManage".'
1663
- );
1664
- }
1132
+ throw new Error(
1133
+ 'CRITICAL: NAT Gateway is in PRIVATE subnet (will not work!). Enable vpc.selfHeal to auto-fix or set natGateway.management to "createAndManage".'
1134
+ );
1665
1135
  }
1666
- } else if (discoveredResources.existingElasticIpAllocationId &&
1667
- !discoveredResources.existingNatGatewayId) {
1668
- // We have an EIP but no NAT Gateway - can reuse the EIP
1669
- console.log('Found orphaned EIP, will reuse it for new NAT Gateway in PUBLIC subnet');
1670
- useExistingEip = true;
1671
1136
  }
1137
+ } else if (
1138
+ discoveredResources.existingElasticIpAllocationId &&
1139
+ !discoveredResources.existingNatGatewayId
1140
+ ) {
1141
+ console.log('Found orphaned EIP, will reuse it for new NAT Gateway in PUBLIC subnet');
1142
+ useExistingEip = true;
1143
+ }
1672
1144
 
1673
- // Skip all resource creation if reusing existing NAT Gateway
1674
- if (reuseExistingNatGateway) {
1675
- console.log('Reusing existing NAT Gateway - skipping resource creation');
1676
- // The existing NAT Gateway will be used for routing
1677
- // No new resources need to be created
1678
- } else {
1679
- // Only create EIP if we're not reusing an existing one
1680
- if (!useExistingEip) {
1681
- definition.resources.Resources.FriggNATGatewayEIP = {
1682
- Type: 'AWS::EC2::EIP',
1683
- DeletionPolicy: 'Retain', // Prevent accidental deletion
1684
- UpdateReplacePolicy: 'Retain', // Prevent replacement during updates
1685
- Properties: {
1686
- Domain: 'vpc',
1687
- Tags: [
1688
- {
1689
- Key: 'Name',
1690
- Value: '${self:service}-${self:provider.stage}-nat-eip',
1691
- },
1692
- {
1693
- Key: 'ManagedBy',
1694
- Value: 'Frigg',
1695
- },
1696
- {
1697
- Key: 'Service',
1698
- Value: '${self:service}',
1699
- },
1700
- {
1701
- Key: 'Stage',
1702
- Value: '${self:provider.stage}',
1703
- },
1704
- ],
1705
- },
1706
- };
1707
- }
1708
-
1709
- // Create public subnet if needed (for NAT Gateway placement)
1710
- if (!discoveredResources.publicSubnetId || discoveredResources.createPublicSubnet) {
1711
- console.log(
1712
- 'No public subnet found, creating one for NAT Gateway placement...'
1713
- );
1145
+ if (reuseExistingNatGateway) {
1146
+ console.log('Reusing existing NAT Gateway - skipping resource creation');
1147
+ } else {
1148
+ if (!useExistingEip) {
1149
+ definition.resources.Resources.FriggNATGatewayEIP = {
1150
+ Type: 'AWS::EC2::EIP',
1151
+ DeletionPolicy: 'Retain',
1152
+ UpdateReplacePolicy: 'Retain',
1153
+ Properties: {
1154
+ Domain: 'vpc',
1155
+ Tags: [
1156
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-eip' },
1157
+ { Key: 'ManagedBy', Value: 'Frigg' },
1158
+ { Key: 'Service', Value: '${self:service}' },
1159
+ { Key: 'Stage', Value: '${self:provider.stage}' },
1160
+ ],
1161
+ },
1162
+ };
1163
+ }
1714
1164
 
1715
- // Check if Internet Gateway exists or create one
1716
- if (!discoveredResources.internetGatewayId) {
1717
- definition.resources.Resources.FriggInternetGateway =
1718
- {
1165
+ if (!discoveredResources.publicSubnetId) {
1166
+ if (discoveredResources.internetGatewayId) {
1167
+ console.log('Reusing existing Internet Gateway for NAT Gateway');
1168
+ } else {
1169
+ definition.resources.Resources.FriggInternetGateway =
1170
+ definition.resources.Resources.FriggInternetGateway || {
1719
1171
  Type: 'AWS::EC2::InternetGateway',
1720
1172
  Properties: {
1721
1173
  Tags: [
1722
- {
1723
- Key: 'Name',
1724
- Value: '${self:service}-${self:provider.stage}-igw',
1725
- },
1174
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-igw' },
1175
+ { Key: 'ManagedBy', Value: 'Frigg' },
1726
1176
  ],
1727
1177
  },
1728
1178
  };
1729
1179
 
1730
- definition.resources.Resources.FriggIGWAttachment =
1731
- {
1180
+ definition.resources.Resources.FriggIGWAttachment =
1181
+ definition.resources.Resources.FriggIGWAttachment || {
1732
1182
  Type: 'AWS::EC2::VPCGatewayAttachment',
1733
1183
  Properties: {
1734
1184
  VpcId: discoveredResources.defaultVpcId,
1735
- InternetGatewayId: {
1736
- Ref: 'FriggInternetGateway',
1737
- },
1185
+ InternetGatewayId: { Ref: 'FriggInternetGateway' },
1738
1186
  },
1739
1187
  };
1740
- }
1741
-
1742
- // Create a small public subnet for NAT Gateway
1743
- definition.resources.Resources.FriggPublicSubnet = {
1744
- Type: 'AWS::EC2::Subnet',
1745
- Properties: {
1746
- VpcId: discoveredResources.defaultVpcId,
1747
- CidrBlock:
1748
- AppDefinition.vpc.natGateway
1749
- ?.publicSubnetCidr || '172.31.250.0/24',
1750
- AvailabilityZone: {
1751
- 'Fn::Select': [0, { 'Fn::GetAZs': '' }],
1752
- },
1753
- MapPublicIpOnLaunch: true,
1754
- Tags: [
1755
- {
1756
- Key: 'Name',
1757
- Value: '${self:service}-${self:provider.stage}-public-subnet',
1758
- },
1759
- {
1760
- Key: 'Type',
1761
- Value: 'Public',
1762
- },
1763
- ],
1764
- },
1765
- };
1766
-
1767
- // Create route table for public subnet
1768
- definition.resources.Resources.FriggPublicRouteTable = {
1769
- Type: 'AWS::EC2::RouteTable',
1770
- Properties: {
1771
- VpcId: discoveredResources.defaultVpcId,
1772
- Tags: [
1773
- {
1774
- Key: 'Name',
1775
- Value: '${self:service}-${self:provider.stage}-public-rt',
1776
- },
1777
- ],
1778
- },
1779
- };
1780
-
1781
- // Add route to Internet Gateway
1782
- definition.resources.Resources.FriggPublicRoute = {
1783
- Type: 'AWS::EC2::Route',
1784
- DependsOn: discoveredResources.internetGatewayId
1785
- ? []
1786
- : 'FriggIGWAttachment',
1787
- Properties: {
1788
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
1789
- DestinationCidrBlock: '0.0.0.0/0',
1790
- GatewayId:
1791
- discoveredResources.internetGatewayId || {
1792
- Ref: 'FriggInternetGateway',
1793
- },
1794
- },
1795
- };
1796
-
1797
- // Associate public subnet with public route table
1798
- definition.resources.Resources.FriggPublicSubnetRouteTableAssociation =
1799
- {
1800
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1801
- Properties: {
1802
- SubnetId: { Ref: 'FriggPublicSubnet' },
1803
- RouteTableId: {
1804
- Ref: 'FriggPublicRouteTable',
1805
- },
1806
- },
1807
- };
1808
- }
1809
-
1810
- // Create NAT Gateway only if not reusing existing one
1811
- definition.resources.Resources.FriggNATGateway = {
1812
- Type: 'AWS::EC2::NatGateway',
1813
- DeletionPolicy: 'Retain', // Prevent accidental deletion
1814
- UpdateReplacePolicy: 'Retain', // Prevent replacement during updates
1815
- Properties: {
1816
- AllocationId: useExistingEip ?
1817
- discoveredResources.existingElasticIpAllocationId :
1818
- {
1819
- 'Fn::GetAtt': [
1820
- 'FriggNATGatewayEIP',
1821
- 'AllocationId',
1822
- ],
1823
- },
1824
- SubnetId: discoveredResources.publicSubnetId || {
1825
- Ref: 'FriggPublicSubnet',
1826
- },
1827
- Tags: [
1828
- {
1829
- Key: 'Name',
1830
- Value: '${self:service}-${self:provider.stage}-nat-gateway',
1831
- },
1832
- {
1833
- Key: 'ManagedBy',
1834
- Value: 'Frigg',
1835
- },
1836
- {
1837
- Key: 'Service',
1838
- Value: '${self:service}',
1839
- },
1840
- {
1841
- Key: 'Stage',
1842
- Value: '${self:provider.stage}',
1843
- },
1844
- ],
1845
- },
1846
- };
1847
- }
1848
- } else if (natGatewayManagement === 'discover' || natGatewayManagement === 'useExisting') {
1849
- // Discover or use existing NAT Gateway
1850
- if (natGatewayManagement === 'useExisting' && AppDefinition.vpc.natGateway?.id) {
1851
- // Use explicitly provided NAT Gateway ID
1852
- console.log(`Using explicitly provided NAT Gateway: ${AppDefinition.vpc.natGateway.id}`);
1853
- discoveredResources.existingNatGatewayId = AppDefinition.vpc.natGateway.id;
1854
- }
1855
-
1856
- if (discoveredResources.existingNatGatewayId) {
1857
- console.log('discoveredResources.existingNatGatewayId', discoveredResources.existingNatGatewayId);
1858
-
1859
- // CRITICAL: Verify NAT Gateway is in PUBLIC subnet
1860
- if (discoveredResources.natGatewayInPrivateSubnet) {
1861
- // NAT is in PRIVATE subnet - CANNOT use it
1862
- console.log('❌ CRITICAL: NAT Gateway is in PRIVATE subnet - Internet connectivity will NOT work!');
1863
-
1864
- if (AppDefinition.vpc.selfHeal === true) {
1865
- console.log('Self-heal enabled: Will create new NAT Gateway in PUBLIC subnet');
1866
- // Force creation of new NAT Gateway in public subnet
1867
- needsNewNatGateway = true;
1868
- discoveredResources.existingNatGatewayId = null; // Don't use the misconfigured NAT
1869
- // Ensure we have a public subnet for the NAT
1870
- if (!discoveredResources.publicSubnetId) {
1871
- console.log('No public subnet found - will create one for NAT Gateway');
1872
- discoveredResources.createPublicSubnet = true;
1873
- }
1874
- } else {
1875
- throw new Error(
1876
- 'CRITICAL: NAT Gateway is in PRIVATE subnet and will NOT provide internet connectivity! ' +
1877
- 'Options: 1) Enable vpc.selfHeal to auto-create proper NAT, ' +
1878
- '2) Set natGateway.management to "createAndManage", or ' +
1879
- '3) Manually fix the NAT Gateway placement.'
1880
- );
1881
- }
1882
- } else {
1883
- // NAT is correctly in public subnet
1884
- console.log('✅ NAT Gateway is correctly placed in PUBLIC subnet');
1885
- }
1886
- } else {
1887
- // No existing NAT Gateway found
1888
- if (natGatewayManagement === 'useExisting') {
1889
- throw new Error(
1890
- 'NAT Gateway management set to "useExisting" but no NAT Gateway found. ' +
1891
- 'Either provide natGateway.id or change management to "discover" or "createAndManage".'
1892
- );
1893
- } else if (AppDefinition.vpc.selfHeal === true) {
1894
- // Self-healing enabled, create a new NAT Gateway
1895
- console.log('No NAT Gateway found but self-healing enabled - creating new NAT Gateway in PUBLIC subnet');
1896
- needsNewNatGateway = true;
1897
- // Ensure we have a public subnet for the NAT
1898
- if (!discoveredResources.publicSubnetId) {
1899
- console.log('No public subnet found - will create one for NAT Gateway');
1900
- discoveredResources.createPublicSubnet = true;
1901
- }
1902
- } else {
1903
- throw new Error(
1904
- 'No existing NAT Gateway found in discovery mode. ' +
1905
- 'Set natGateway.management to "createAndManage" to create a new NAT Gateway.'
1906
- );
1907
- }
1908
1188
  }
1909
- }
1910
-
1911
- // ALWAYS create the route table resource in CloudFormation for consistency
1912
- // Use DeletionPolicy: Retain to prevent deletion when removed from template
1913
- // This ensures CloudFormation maintains consistent state management
1914
- console.log('Setting up route table for Lambda subnets');
1915
-
1916
- definition.resources.Resources.FriggLambdaRouteTable = {
1917
- Type: 'AWS::EC2::RouteTable',
1918
- DeletionPolicy: 'Retain', // Critical: Prevents deletion when resource is removed
1919
- UpdateReplacePolicy: 'Retain', // Prevents replacement during stack updates
1920
- Properties: {
1921
- VpcId: discoveredResources.defaultVpcId || {
1922
- Ref: 'FriggVPC',
1923
- },
1924
- Tags: [
1925
- {
1926
- Key: 'Name',
1927
- Value: '${self:service}-${self:provider.stage}-lambda-rt',
1928
- },
1929
- {
1930
- Key: 'ManagedBy',
1931
- Value: 'Frigg',
1932
- },
1933
- {
1934
- Key: 'Environment',
1935
- Value: '${self:provider.stage}',
1936
- },
1937
- {
1938
- Key: 'Service',
1939
- Value: '${self:service}',
1940
- },
1941
- ],
1942
- },
1943
- };
1944
-
1945
- // Always use CloudFormation reference for consistency
1946
- const routeTableId = { Ref: 'FriggLambdaRouteTable' };
1947
-
1948
- // Determine which NAT Gateway ID to use for routing
1949
- let natGatewayIdForRoute;
1950
-
1951
- if (reuseExistingNatGateway) {
1952
- // Use the existing NAT Gateway that we're reusing
1953
- natGatewayIdForRoute = discoveredResources.existingNatGatewayId;
1954
- console.log(`Using discovered NAT Gateway for routing: ${natGatewayIdForRoute}`);
1955
- } else if (needsNewNatGateway && !reuseExistingNatGateway) {
1956
- // Reference the new NAT Gateway being created
1957
- natGatewayIdForRoute = { Ref: 'FriggNATGateway' };
1958
- console.log('Using newly created NAT Gateway for routing');
1959
- } else if (discoveredResources.existingNatGatewayId) {
1960
- // Use the existing NAT Gateway ID
1961
- natGatewayIdForRoute = discoveredResources.existingNatGatewayId;
1962
- console.log(`Using discovered NAT Gateway for routing: ${natGatewayIdForRoute}`);
1963
- } else if (AppDefinition.vpc.natGateway?.id) {
1964
- // Use explicitly provided NAT Gateway ID
1965
- natGatewayIdForRoute = AppDefinition.vpc.natGateway.id;
1966
- console.log(`Using explicitly provided NAT Gateway for routing: ${natGatewayIdForRoute}`);
1967
- } else if (AppDefinition.vpc.selfHeal === true) {
1968
- // Self-healing enabled but no NAT Gateway - skip NAT route
1969
- natGatewayIdForRoute = null;
1970
- console.log('No NAT Gateway available - skipping NAT route creation');
1971
- } else {
1972
- throw new Error(
1973
- 'Unable to determine NAT Gateway ID for routing. ' +
1974
- 'Please check your configuration.'
1975
- );
1976
- }
1977
1189
 
1978
- // ALWAYS create/update NAT route if we have a NAT Gateway
1979
- // This ensures routes are always correct even if NAT Gateway changes
1980
- if (natGatewayIdForRoute) {
1981
- console.log(`Configuring NAT route: 0.0.0.0/0 → ${natGatewayIdForRoute}`);
1982
- definition.resources.Resources.FriggNATRoute = {
1983
- Type: 'AWS::EC2::Route',
1984
- DependsOn: 'FriggLambdaRouteTable',
1190
+ definition.resources.Resources.FriggPublicSubnet = {
1191
+ Type: 'AWS::EC2::Subnet',
1985
1192
  Properties: {
1986
- RouteTableId: routeTableId,
1987
- DestinationCidrBlock: '0.0.0.0/0',
1988
- NatGatewayId: natGatewayIdForRoute,
1989
- },
1990
- };
1991
- } else {
1992
- console.warn('⚠️ No NAT Gateway configured - Lambda functions will not have internet access');
1993
- }
1994
-
1995
- // Associate Lambda subnets with NAT Gateway route table
1996
- // CRITICAL: This fixes the "NAT Gateway in private subnet" issue by ensuring correct routing
1997
- if (AppDefinition.vpc.selfHeal === true) {
1998
- console.log('✅ Self-healing: Ensuring subnets have correct route table associations');
1999
- // In self-heal mode, we force the associations even if they might conflict
2000
- // CloudFormation will automatically disassociate from old route table first
2001
- }
2002
-
2003
- // ALWAYS create subnet associations to ensure correct routing
2004
- // CloudFormation will handle existing associations gracefully
2005
- // Only create associations for discovered subnets (not for Refs)
2006
- if (typeof vpcConfig.subnetIds[0] === 'string') {
2007
- definition.resources.Resources.FriggSubnet1RouteAssociation = {
2008
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
2009
- Properties: {
2010
- SubnetId: vpcConfig.subnetIds[0],
2011
- RouteTableId: routeTableId,
1193
+ VpcId: discoveredResources.defaultVpcId,
1194
+ CidrBlock:
1195
+ AppDefinition.vpc.natGateway?.publicSubnetCidr || '172.31.250.0/24',
1196
+ AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
1197
+ MapPublicIpOnLaunch: true,
1198
+ Tags: [
1199
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-subnet' },
1200
+ { Key: 'Type', Value: 'Public' },
1201
+ ],
2012
1202
  },
2013
- DependsOn: 'FriggLambdaRouteTable',
2014
1203
  };
2015
- }
2016
1204
 
2017
- if (typeof vpcConfig.subnetIds[1] === 'string') {
2018
- definition.resources.Resources.FriggSubnet2RouteAssociation = {
2019
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1205
+ definition.resources.Resources.FriggPublicRouteTable = {
1206
+ Type: 'AWS::EC2::RouteTable',
2020
1207
  Properties: {
2021
- SubnetId: vpcConfig.subnetIds[1],
2022
- RouteTableId: routeTableId,
1208
+ VpcId: discoveredResources.defaultVpcId,
1209
+ Tags: [
1210
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-rt' },
1211
+ ],
2023
1212
  },
2024
- DependsOn: 'FriggLambdaRouteTable',
2025
1213
  };
2026
- }
2027
1214
 
2028
- // If subnets are CloudFormation refs (newly created), associate them
2029
- if (typeof vpcConfig.subnetIds[0] === 'object' && vpcConfig.subnetIds[0].Ref) {
2030
- definition.resources.Resources.FriggNewSubnet1RouteAssociation = {
2031
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1215
+ definition.resources.Resources.FriggPublicRoute = {
1216
+ Type: 'AWS::EC2::Route',
1217
+ DependsOn: discoveredResources.internetGatewayId ? [] : 'FriggIGWAttachment',
2032
1218
  Properties: {
2033
- SubnetId: vpcConfig.subnetIds[0],
2034
- RouteTableId: routeTableId,
1219
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
1220
+ DestinationCidrBlock: '0.0.0.0/0',
1221
+ GatewayId: discoveredResources.internetGatewayId || { Ref: 'FriggInternetGateway' },
2035
1222
  },
2036
- DependsOn: ['FriggLambdaRouteTable', vpcConfig.subnetIds[0].Ref],
2037
1223
  };
2038
- }
2039
1224
 
2040
- if (typeof vpcConfig.subnetIds[1] === 'object' && vpcConfig.subnetIds[1].Ref) {
2041
- definition.resources.Resources.FriggNewSubnet2RouteAssociation = {
1225
+ definition.resources.Resources.FriggPublicSubnetRouteTableAssociation = {
2042
1226
  Type: 'AWS::EC2::SubnetRouteTableAssociation',
2043
1227
  Properties: {
2044
- SubnetId: vpcConfig.subnetIds[1],
2045
- RouteTableId: routeTableId,
2046
- },
2047
- DependsOn: ['FriggLambdaRouteTable', vpcConfig.subnetIds[1].Ref],
2048
- };
2049
- }
2050
-
2051
- // Add VPC endpoints for AWS service optimization
2052
- // ALWAYS create these to ensure Lambda functions have optimized access to AWS services
2053
- if (AppDefinition.vpc.enableVPCEndpoints !== false) {
2054
- definition.resources.Resources.VPCEndpointS3 = {
2055
- Type: 'AWS::EC2::VPCEndpoint',
2056
- Properties: {
2057
- VpcId: discoveredResources.defaultVpcId,
2058
- ServiceName:
2059
- 'com.amazonaws.${self:provider.region}.s3',
2060
- VpcEndpointType: 'Gateway',
2061
- RouteTableIds: [routeTableId],
2062
- },
2063
- };
2064
-
2065
- definition.resources.Resources.VPCEndpointDynamoDB = {
2066
- Type: 'AWS::EC2::VPCEndpoint',
2067
- Properties: {
2068
- VpcId: discoveredResources.defaultVpcId,
2069
- ServiceName:
2070
- 'com.amazonaws.${self:provider.region}.dynamodb',
2071
- VpcEndpointType: 'Gateway',
2072
- RouteTableIds: [routeTableId],
1228
+ SubnetId: { Ref: 'FriggPublicSubnet' },
1229
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
2073
1230
  },
2074
1231
  };
2075
1232
  }
2076
1233
 
2077
- // Add KMS VPC endpoint if using KMS encryption
2078
- if (
2079
- AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms'
2080
- ) {
2081
- // Validate we have VPC CIDR for security group configuration
2082
- if (!discoveredResources.vpcCidr) {
2083
- console.warn(
2084
- '⚠️ Warning: VPC CIDR not discovered. VPC endpoint security group may not work correctly.'
2085
- );
2086
- }
1234
+ definition.resources.Resources.FriggNATGateway = {
1235
+ Type: 'AWS::EC2::NatGateway',
1236
+ DeletionPolicy: 'Retain',
1237
+ UpdateReplacePolicy: 'Retain',
1238
+ Properties: {
1239
+ AllocationId: useExistingEip
1240
+ ? discoveredResources.existingElasticIpAllocationId
1241
+ : { 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'] },
1242
+ SubnetId:
1243
+ discoveredResources.publicSubnetId || { Ref: 'FriggPublicSubnet' },
1244
+ Tags: [
1245
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-gateway' },
1246
+ { Key: 'ManagedBy', Value: 'Frigg' },
1247
+ { Key: 'Service', Value: '${self:service}' },
1248
+ { Key: 'Stage', Value: '${self:provider.stage}' },
1249
+ ],
1250
+ },
1251
+ };
1252
+ }
1253
+ } else if (
1254
+ natGatewayManagement === 'discover' ||
1255
+ natGatewayManagement === 'useExisting'
1256
+ ) {
1257
+ if (natGatewayManagement === 'useExisting' && AppDefinition.vpc.natGateway?.id) {
1258
+ console.log(`Using explicitly provided NAT Gateway: ${AppDefinition.vpc.natGateway.id}`);
1259
+ discoveredResources.existingNatGatewayId = AppDefinition.vpc.natGateway.id;
1260
+ }
2087
1261
 
2088
- // Create security group for VPC endpoints if it doesn't exist
2089
- if (
2090
- !definition.resources.Resources
2091
- .VPCEndpointSecurityGroup
2092
- ) {
2093
- // Build ingress rules based on what we have
2094
- const vpcEndpointIngressRules = [];
2095
-
2096
- // CRITICAL: Allow from Lambda's security group (preferred method)
2097
- if (vpcConfig.securityGroupIds && vpcConfig.securityGroupIds.length > 0) {
2098
- // If we have the Lambda security group, reference it directly
2099
- const lambdaSgId = vpcConfig.securityGroupIds[0];
2100
- if (typeof lambdaSgId === 'string') {
2101
- // It's a discovered security group ID
2102
- vpcEndpointIngressRules.push({
2103
- IpProtocol: 'tcp',
2104
- FromPort: 443,
2105
- ToPort: 443,
2106
- SourceSecurityGroupId: lambdaSgId,
2107
- Description: 'HTTPS from Lambda security group',
2108
- });
2109
- } else if (lambdaSgId && lambdaSgId.Ref) {
2110
- // It's a CloudFormation reference
2111
- vpcEndpointIngressRules.push({
2112
- IpProtocol: 'tcp',
2113
- FromPort: 443,
2114
- ToPort: 443,
2115
- SourceSecurityGroupId: lambdaSgId,
2116
- Description: 'HTTPS from Lambda security group',
2117
- });
2118
- }
2119
- }
2120
-
2121
- // Fallback: If we don't have Lambda SG, use VPC CIDR
2122
- if (vpcEndpointIngressRules.length === 0 && discoveredResources.vpcCidr) {
2123
- vpcEndpointIngressRules.push({
2124
- IpProtocol: 'tcp',
2125
- FromPort: 443,
2126
- ToPort: 443,
2127
- CidrIp: discoveredResources.vpcCidr,
2128
- Description: 'HTTPS from VPC CIDR (fallback)',
2129
- });
2130
- }
2131
-
2132
- // Last resort: Allow from common private IP ranges
2133
- if (vpcEndpointIngressRules.length === 0) {
2134
- console.warn(
2135
- '⚠️ WARNING: No Lambda security group or VPC CIDR found. Using default private IP ranges.'
2136
- );
2137
- vpcEndpointIngressRules.push({
2138
- IpProtocol: 'tcp',
2139
- FromPort: 443,
2140
- ToPort: 443,
2141
- CidrIp: '172.31.0.0/16', // Default VPC CIDR
2142
- Description: 'HTTPS from default VPC range',
2143
- });
2144
- }
2145
-
2146
- definition.resources.Resources.VPCEndpointSecurityGroup =
2147
- {
2148
- Type: 'AWS::EC2::SecurityGroup',
2149
- Properties: {
2150
- GroupDescription:
2151
- 'Security group for VPC endpoints - allows HTTPS from Lambda functions',
2152
- VpcId: discoveredResources.defaultVpcId,
2153
- SecurityGroupIngress: vpcEndpointIngressRules,
2154
- Tags: [
2155
- {
2156
- Key: 'Name',
2157
- Value: '${self:service}-${self:provider.stage}-vpc-endpoints-sg',
2158
- },
2159
- {
2160
- Key: 'ManagedBy',
2161
- Value: 'Frigg',
2162
- },
2163
- {
2164
- Key: 'Purpose',
2165
- Value: 'Allow Lambda functions to access VPC endpoints',
2166
- },
2167
- ],
2168
- },
2169
- };
2170
- }
1262
+ if (discoveredResources.existingNatGatewayId) {
1263
+ console.log(
1264
+ 'discoveredResources.existingNatGatewayId',
1265
+ discoveredResources.existingNatGatewayId
1266
+ );
2171
1267
 
2172
- definition.resources.Resources.VPCEndpointKMS = {
2173
- Type: 'AWS::EC2::VPCEndpoint',
2174
- Properties: {
2175
- VpcId: discoveredResources.defaultVpcId,
2176
- ServiceName:
2177
- 'com.amazonaws.${self:provider.region}.kms',
2178
- VpcEndpointType: 'Interface',
2179
- SubnetIds: vpcConfig.subnetIds,
2180
- SecurityGroupIds: [
2181
- { Ref: 'VPCEndpointSecurityGroup' },
2182
- ],
2183
- PrivateDnsEnabled: true,
2184
- },
2185
- };
1268
+ if (discoveredResources.natGatewayInPrivateSubnet) {
1269
+ console.log('❌ CRITICAL: NAT Gateway is in PRIVATE subnet - Internet connectivity will NOT work!');
2186
1270
 
2187
- // Also add Secrets Manager endpoint if using Secrets Manager
2188
- if (AppDefinition.secretsManager?.enable === true) {
2189
- definition.resources.Resources.VPCEndpointSecretsManager =
2190
- {
2191
- Type: 'AWS::EC2::VPCEndpoint',
2192
- Properties: {
2193
- VpcId: discoveredResources.defaultVpcId,
2194
- ServiceName:
2195
- 'com.amazonaws.${self:provider.region}.secretsmanager',
2196
- VpcEndpointType: 'Interface',
2197
- SubnetIds: vpcConfig.subnetIds,
2198
- SecurityGroupIds: [
2199
- { Ref: 'VPCEndpointSecurityGroup' },
2200
- ],
2201
- PrivateDnsEnabled: true,
2202
- },
2203
- };
1271
+ if (AppDefinition.vpc.selfHeal === true) {
1272
+ console.log('Self-heal enabled: Will create new NAT Gateway in PUBLIC subnet');
1273
+ needsNewNatGateway = true;
1274
+ discoveredResources.existingNatGatewayId = null;
1275
+ if (!discoveredResources.publicSubnetId) {
1276
+ console.log('No public subnet found - will create one for NAT Gateway');
1277
+ discoveredResources.createPublicSubnet = true;
2204
1278
  }
1279
+ } else {
1280
+ throw new Error(
1281
+ 'CRITICAL: NAT Gateway is in PRIVATE subnet and will NOT provide internet connectivity! Options: 1) Enable vpc.selfHeal to auto-create proper NAT, 2) Set natGateway.management to "createAndManage", or 3) Manually fix the NAT Gateway placement.'
1282
+ );
2205
1283
  }
1284
+ } else {
1285
+ console.log(`Using discovered NAT Gateway for routing: ${discoveredResources.existingNatGatewayId}`);
2206
1286
  }
1287
+ } else if (!needsNewNatGateway && AppDefinition.vpc.natGateway?.id) {
1288
+ console.log(`Using explicitly provided NAT Gateway: ${AppDefinition.vpc.natGateway.id}`);
1289
+ discoveredResources.existingNatGatewayId = AppDefinition.vpc.natGateway.id;
2207
1290
  }
1291
+ }
2208
1292
 
2209
- // SSM Parameter Store Configuration based on App Definition
2210
- if (AppDefinition.ssm?.enable === true) {
2211
- // Add AWS Parameters and Secrets Lambda Extension layer
2212
- definition.provider.layers = [
2213
- 'arn:aws:lambda:${self:provider.region}:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11',
2214
- ];
1293
+ definition.resources.Resources.FriggLambdaRouteTable =
1294
+ definition.resources.Resources.FriggLambdaRouteTable || {
1295
+ Type: 'AWS::EC2::RouteTable',
1296
+ Properties: {
1297
+ VpcId: discoveredResources.defaultVpcId || vpcId,
1298
+ Tags: [
1299
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
1300
+ { Key: 'ManagedBy', Value: 'Frigg' },
1301
+ { Key: 'Environment', Value: '${self:provider.stage}' },
1302
+ { Key: 'Service', Value: '${self:service}' },
1303
+ ],
1304
+ },
1305
+ };
2215
1306
 
2216
- // Add SSM IAM permissions
2217
- definition.provider.iamRoleStatements.push({
2218
- Effect: 'Allow',
2219
- Action: [
2220
- 'ssm:GetParameter',
2221
- 'ssm:GetParameters',
2222
- 'ssm:GetParametersByPath',
2223
- ],
2224
- Resource: [
2225
- 'arn:aws:ssm:${self:provider.region}:*:parameter/${self:service}/${self:provider.stage}/*',
2226
- ],
2227
- });
1307
+ const routeTableId = { Ref: 'FriggLambdaRouteTable' };
1308
+ let natGatewayIdForRoute;
1309
+
1310
+ if (reuseExistingNatGateway) {
1311
+ natGatewayIdForRoute = discoveredResources.existingNatGatewayId;
1312
+ console.log(`Using discovered NAT Gateway for routing: ${natGatewayIdForRoute}`);
1313
+ } else if (needsNewNatGateway && !reuseExistingNatGateway) {
1314
+ natGatewayIdForRoute = { Ref: 'FriggNATGateway' };
1315
+ console.log('Using newly created NAT Gateway for routing');
1316
+ } else if (discoveredResources.existingNatGatewayId) {
1317
+ natGatewayIdForRoute = discoveredResources.existingNatGatewayId;
1318
+ console.log(`Using discovered NAT Gateway for routing: ${natGatewayIdForRoute}`);
1319
+ } else if (AppDefinition.vpc.natGateway?.id) {
1320
+ natGatewayIdForRoute = AppDefinition.vpc.natGateway.id;
1321
+ console.log(`Using explicitly provided NAT Gateway for routing: ${natGatewayIdForRoute}`);
1322
+ } else if (AppDefinition.vpc.selfHeal === true) {
1323
+ natGatewayIdForRoute = null;
1324
+ console.log('No NAT Gateway available - skipping NAT route creation');
1325
+ } else {
1326
+ throw new Error('No existing NAT Gateway found in discovery mode');
1327
+ }
2228
1328
 
2229
- // Add environment variable for SSM parameter prefix
2230
- definition.provider.environment.SSM_PARAMETER_PREFIX =
2231
- '/${self:service}/${self:provider.stage}';
2232
- }
1329
+ if (natGatewayIdForRoute) {
1330
+ console.log(`Configuring NAT route: 0.0.0.0/0 → ${natGatewayIdForRoute}`);
1331
+ definition.resources.Resources.FriggNATRoute = {
1332
+ Type: 'AWS::EC2::Route',
1333
+ DependsOn: 'FriggLambdaRouteTable',
1334
+ Properties: {
1335
+ RouteTableId: routeTableId,
1336
+ DestinationCidrBlock: '0.0.0.0/0',
1337
+ NatGatewayId: natGatewayIdForRoute,
1338
+ },
1339
+ };
1340
+ } else {
1341
+ console.warn('⚠️ No NAT Gateway configured - Lambda functions will not have internet access');
1342
+ }
2233
1343
 
2234
- // Add integration-specific functions and resources
2235
- if (
2236
- AppDefinition.integrations &&
2237
- Array.isArray(AppDefinition.integrations)
2238
- ) {
2239
- console.log(`Processing ${AppDefinition.integrations.length} integrations...`);
2240
- for (const integration of AppDefinition.integrations) {
2241
- if (
2242
- !integration ||
2243
- !integration.Definition ||
2244
- !integration.Definition.name
2245
- ) {
2246
- throw new Error(
2247
- 'Invalid integration: missing Definition or name'
2248
- );
2249
- }
2250
- const integrationName = integration.Definition.name;
1344
+ if (typeof vpcConfig.subnetIds[0] === 'string') {
1345
+ definition.resources.Resources.FriggSubnet1RouteAssociation = {
1346
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1347
+ Properties: {
1348
+ SubnetId: vpcConfig.subnetIds[0],
1349
+ RouteTableId: routeTableId,
1350
+ },
1351
+ DependsOn: 'FriggLambdaRouteTable',
1352
+ };
1353
+ }
2251
1354
 
2252
- // Add function for the integration
2253
- definition.functions[integrationName] = {
2254
- handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
2255
- events: [
2256
- {
2257
- httpApi: {
2258
- path: `/api/${integrationName}-integration/{proxy+}`,
2259
- method: 'ANY',
2260
- },
2261
- },
2262
- ],
1355
+ if (typeof vpcConfig.subnetIds[1] === 'string') {
1356
+ definition.resources.Resources.FriggSubnet2RouteAssociation = {
1357
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1358
+ Properties: {
1359
+ SubnetId: vpcConfig.subnetIds[1],
1360
+ RouteTableId: routeTableId,
1361
+ },
1362
+ DependsOn: 'FriggLambdaRouteTable',
2263
1363
  };
1364
+ }
2264
1365
 
2265
- // Add SQS Queue for the integration
2266
- const queueReference = `${integrationName.charAt(0).toUpperCase() +
2267
- integrationName.slice(1)
2268
- }Queue`;
2269
- const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
2270
- definition.resources.Resources[queueReference] = {
2271
- Type: 'AWS::SQS::Queue',
1366
+ if (typeof vpcConfig.subnetIds[0] === 'object' && vpcConfig.subnetIds[0].Ref) {
1367
+ definition.resources.Resources.FriggNewSubnet1RouteAssociation = {
1368
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
2272
1369
  Properties: {
2273
- QueueName: `\${self:custom.${queueReference}}`,
2274
- MessageRetentionPeriod: 60,
2275
- VisibilityTimeout: 1800, // 30 minutes
2276
- RedrivePolicy: {
2277
- maxReceiveCount: 1,
2278
- deadLetterTargetArn: {
2279
- 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
2280
- },
2281
- },
1370
+ SubnetId: vpcConfig.subnetIds[0],
1371
+ RouteTableId: routeTableId,
2282
1372
  },
1373
+ DependsOn: ['FriggLambdaRouteTable', vpcConfig.subnetIds[0].Ref],
2283
1374
  };
1375
+ }
2284
1376
 
2285
- // Add Queue Worker for the integration
2286
- const queueWorkerName = `${integrationName}QueueWorker`;
2287
- definition.functions[queueWorkerName] = {
2288
- handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
2289
- reservedConcurrency: 5,
2290
- events: [
2291
- {
2292
- sqs: {
2293
- arn: {
2294
- 'Fn::GetAtt': [queueReference, 'Arn'],
2295
- },
2296
- batchSize: 1,
2297
- },
2298
- },
2299
- ],
2300
- timeout: 600,
1377
+ if (typeof vpcConfig.subnetIds[1] === 'object' && vpcConfig.subnetIds[1].Ref) {
1378
+ definition.resources.Resources.FriggNewSubnet2RouteAssociation = {
1379
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1380
+ Properties: {
1381
+ SubnetId: vpcConfig.subnetIds[1],
1382
+ RouteTableId: routeTableId,
1383
+ },
1384
+ DependsOn: ['FriggLambdaRouteTable', vpcConfig.subnetIds[1].Ref],
2301
1385
  };
1386
+ }
2302
1387
 
2303
- // Add Queue URL for the integration to the ENVironment variables
2304
- definition.provider.environment = {
2305
- ...definition.provider.environment,
2306
- [`${integrationName.toUpperCase()}_QUEUE_URL`]: {
2307
- Ref: queueReference,
1388
+ if (AppDefinition.vpc.enableVPCEndpoints !== false) {
1389
+ definition.resources.Resources.VPCEndpointS3 = {
1390
+ Type: 'AWS::EC2::VPCEndpoint',
1391
+ Properties: {
1392
+ VpcId: discoveredResources.defaultVpcId,
1393
+ ServiceName: 'com.amazonaws.${self:provider.region}.s3',
1394
+ VpcEndpointType: 'Gateway',
1395
+ RouteTableIds: [routeTableId],
2308
1396
  },
2309
1397
  };
2310
1398
 
2311
- definition.custom[queueReference] = queueName;
1399
+ definition.resources.Resources.VPCEndpointDynamoDB = {
1400
+ Type: 'AWS::EC2::VPCEndpoint',
1401
+ Properties: {
1402
+ VpcId: discoveredResources.defaultVpcId,
1403
+ ServiceName: 'com.amazonaws.${self:provider.region}.dynamodb',
1404
+ VpcEndpointType: 'Gateway',
1405
+ RouteTableIds: [routeTableId],
1406
+ },
1407
+ };
2312
1408
  }
1409
+
1410
+ if (AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
1411
+ if (!discoveredResources.vpcCidr) {
1412
+ console.warn(
1413
+ '⚠️ Warning: VPC CIDR not discovered. VPC endpoint security group may not work correctly.'
1414
+ );
1415
+ }
1416
+
1417
+ if (!definition.resources.Resources.VPCEndpointSecurityGroup) {
1418
+ const vpcEndpointIngressRules = [];
1419
+
1420
+ if (vpcConfig.securityGroupIds && vpcConfig.securityGroupIds.length > 0) {
1421
+ for (const sg of vpcConfig.securityGroupIds) {
1422
+ if (typeof sg === 'string') {
1423
+ vpcEndpointIngressRules.push({
1424
+ IpProtocol: 'tcp',
1425
+ FromPort: 443,
1426
+ ToPort: 443,
1427
+ SourceSecurityGroupId: sg,
1428
+ Description: 'HTTPS from Lambda security group',
1429
+ });
1430
+ } else if (sg.Ref) {
1431
+ vpcEndpointIngressRules.push({
1432
+ IpProtocol: 'tcp',
1433
+ FromPort: 443,
1434
+ ToPort: 443,
1435
+ SourceSecurityGroupId: { Ref: sg.Ref },
1436
+ Description: 'HTTPS from Lambda security group',
1437
+ });
1438
+ }
1439
+ }
1440
+ }
1441
+
1442
+ if (vpcEndpointIngressRules.length === 0) {
1443
+ if (discoveredResources.vpcCidr) {
1444
+ vpcEndpointIngressRules.push({
1445
+ IpProtocol: 'tcp',
1446
+ FromPort: 443,
1447
+ ToPort: 443,
1448
+ CidrIp: discoveredResources.vpcCidr,
1449
+ Description: 'HTTPS from VPC CIDR (fallback)',
1450
+ });
1451
+ } else {
1452
+ console.warn(
1453
+ '⚠️ WARNING: No Lambda security group or VPC CIDR found. Using default private IP ranges.'
1454
+ );
1455
+ vpcEndpointIngressRules.push({
1456
+ IpProtocol: 'tcp',
1457
+ FromPort: 443,
1458
+ ToPort: 443,
1459
+ CidrIp: '172.31.0.0/16',
1460
+ Description: 'HTTPS from default VPC range',
1461
+ });
1462
+ }
1463
+ }
1464
+
1465
+ definition.resources.Resources.VPCEndpointSecurityGroup = {
1466
+ Type: 'AWS::EC2::SecurityGroup',
1467
+ Properties: {
1468
+ GroupDescription: 'Security group for VPC endpoints - allows HTTPS from Lambda functions',
1469
+ VpcId: discoveredResources.defaultVpcId,
1470
+ SecurityGroupIngress: vpcEndpointIngressRules,
1471
+ Tags: [
1472
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc-endpoints-sg' },
1473
+ { Key: 'ManagedBy', Value: 'Frigg' },
1474
+ { Key: 'Purpose', Value: 'Allow Lambda functions to access VPC endpoints' },
1475
+ ],
1476
+ },
1477
+ };
1478
+ }
1479
+
1480
+ definition.resources.Resources.VPCEndpointKMS = {
1481
+ Type: 'AWS::EC2::VPCEndpoint',
1482
+ Properties: {
1483
+ VpcId: discoveredResources.defaultVpcId,
1484
+ ServiceName: 'com.amazonaws.${self:provider.region}.kms',
1485
+ VpcEndpointType: 'Interface',
1486
+ SubnetIds: vpcConfig.subnetIds,
1487
+ SecurityGroupIds: [{ Ref: 'VPCEndpointSecurityGroup' }],
1488
+ PrivateDnsEnabled: true,
1489
+ },
1490
+ };
1491
+
1492
+ if (AppDefinition.secretsManager?.enable === true) {
1493
+ definition.resources.Resources.VPCEndpointSecretsManager = {
1494
+ Type: 'AWS::EC2::VPCEndpoint',
1495
+ Properties: {
1496
+ VpcId: discoveredResources.defaultVpcId,
1497
+ ServiceName: 'com.amazonaws.${self:provider.region}.secretsmanager',
1498
+ VpcEndpointType: 'Interface',
1499
+ SubnetIds: vpcConfig.subnetIds,
1500
+ SecurityGroupIds: [{ Ref: 'VPCEndpointSecurityGroup' }],
1501
+ PrivateDnsEnabled: true,
1502
+ },
1503
+ };
1504
+ }
1505
+ }
1506
+ }
1507
+ };
1508
+
1509
+ const configureSsm = (definition, AppDefinition) => {
1510
+ if (AppDefinition.ssm?.enable !== true) {
1511
+ return;
2313
1512
  }
2314
1513
 
2315
- // Discovery has already run successfully at this point if needed
2316
- // The discoveredResources object contains all the necessary AWS resources
1514
+ definition.provider.layers = [
1515
+ 'arn:aws:lambda:${self:provider.region}:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11',
1516
+ ];
2317
1517
 
2318
- // Add websocket function if enabled
2319
- if (AppDefinition.websockets?.enable === true) {
2320
- definition.functions.defaultWebsocket = {
2321
- handler:
2322
- 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
1518
+ definition.provider.iamRoleStatements.push({
1519
+ Effect: 'Allow',
1520
+ Action: ['ssm:GetParameter', 'ssm:GetParameters', 'ssm:GetParametersByPath'],
1521
+ Resource: ['arn:aws:ssm:${self:provider.region}:*:parameter/${self:service}/${self:provider.stage}/*'],
1522
+ });
1523
+
1524
+ definition.provider.environment.SSM_PARAMETER_PREFIX = '/${self:service}/${self:provider.stage}';
1525
+ };
1526
+
1527
+ const attachIntegrations = (definition, AppDefinition) => {
1528
+ if (!Array.isArray(AppDefinition.integrations) || AppDefinition.integrations.length === 0) {
1529
+ return;
1530
+ }
1531
+
1532
+ console.log(`Processing ${AppDefinition.integrations.length} integrations...`);
1533
+
1534
+ for (const integration of AppDefinition.integrations) {
1535
+ if (!integration?.Definition?.name) {
1536
+ throw new Error('Invalid integration: missing Definition or name');
1537
+ }
1538
+
1539
+ const integrationName = integration.Definition.name;
1540
+ const queueReference = `${integrationName.charAt(0).toUpperCase() + integrationName.slice(1)}Queue`;
1541
+ const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
1542
+
1543
+ definition.functions[integrationName] = {
1544
+ handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
2323
1545
  events: [
2324
1546
  {
2325
- websocket: {
2326
- route: '$connect',
1547
+ httpApi: {
1548
+ path: `/api/${integrationName}-integration/{proxy+}`,
1549
+ method: 'ANY',
2327
1550
  },
2328
1551
  },
2329
- {
2330
- websocket: {
2331
- route: '$default',
2332
- },
1552
+ ],
1553
+ };
1554
+
1555
+ definition.resources.Resources[queueReference] = {
1556
+ Type: 'AWS::SQS::Queue',
1557
+ Properties: {
1558
+ QueueName: `\${self:custom.${queueReference}}`,
1559
+ MessageRetentionPeriod: 60,
1560
+ VisibilityTimeout: 1800,
1561
+ RedrivePolicy: {
1562
+ maxReceiveCount: 1,
1563
+ deadLetterTargetArn: { 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
2333
1564
  },
1565
+ },
1566
+ };
1567
+
1568
+ const queueWorkerName = `${integrationName}QueueWorker`;
1569
+ definition.functions[queueWorkerName] = {
1570
+ handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
1571
+ reservedConcurrency: 5,
1572
+ events: [
2334
1573
  {
2335
- websocket: {
2336
- route: '$disconnect',
1574
+ sqs: {
1575
+ arn: { 'Fn::GetAtt': [queueReference, 'Arn'] },
1576
+ batchSize: 1,
2337
1577
  },
2338
1578
  },
2339
1579
  ],
1580
+ timeout: 600,
2340
1581
  };
1582
+
1583
+ definition.provider.environment = {
1584
+ ...definition.provider.environment,
1585
+ [`${integrationName.toUpperCase()}_QUEUE_URL`]: { Ref: queueReference },
1586
+ };
1587
+
1588
+ definition.custom[queueReference] = queueName;
1589
+ }
1590
+ };
1591
+
1592
+ const configureWebsockets = (definition, AppDefinition) => {
1593
+ if (AppDefinition.websockets?.enable !== true) {
1594
+ return;
2341
1595
  }
2342
1596
 
2343
- // Modify handler paths to point to the correct node_modules location
1597
+ definition.functions.defaultWebsocket = {
1598
+ handler: 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
1599
+ events: [
1600
+ { websocket: { route: '$connect' } },
1601
+ { websocket: { route: '$default' } },
1602
+ { websocket: { route: '$disconnect' } },
1603
+ ],
1604
+ };
1605
+ };
1606
+
1607
+ const composeServerlessDefinition = async (AppDefinition) => {
1608
+ console.log('composeServerlessDefinition', AppDefinition);
1609
+
1610
+ const discoveredResources = await gatherDiscoveredResources(AppDefinition);
1611
+ const appEnvironmentVars = getAppEnvironmentVars(AppDefinition);
1612
+ const definition = createBaseDefinition(AppDefinition, appEnvironmentVars, discoveredResources);
1613
+
1614
+ applyKmsConfiguration(definition, AppDefinition, discoveredResources);
1615
+ configureVpc(definition, AppDefinition, discoveredResources);
1616
+ configureSsm(definition, AppDefinition);
1617
+ attachIntegrations(definition, AppDefinition);
1618
+ configureWebsockets(definition, AppDefinition);
1619
+
2344
1620
  definition.functions = modifyHandlerPaths(definition.functions);
2345
1621
 
2346
1622
  return definition;