@friggframework/devtools 2.0.0--canary.461.ec909cf.0 → 2.0.0--canary.461.7b36f0f.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.
Files changed (70) hide show
  1. package/frigg-cli/__tests__/unit/commands/build.test.js +6 -6
  2. package/frigg-cli/build-command/index.js +1 -1
  3. package/frigg-cli/deploy-command/index.js +6 -6
  4. package/frigg-cli/generate-command/index.js +2 -2
  5. package/frigg-cli/generate-iam-command.js +10 -10
  6. package/frigg-cli/start-command/index.js +1 -1
  7. package/frigg-cli/start-command/start-command.test.js +3 -3
  8. package/frigg-cli/utils/database-validator.js +14 -21
  9. package/infrastructure/REFACTOR.md +532 -0
  10. package/infrastructure/TRANSFORMATION-VISUAL.md +239 -0
  11. package/infrastructure/__tests__/postgres-config.test.js +1 -1
  12. package/infrastructure/create-frigg-infrastructure.js +1 -1
  13. package/infrastructure/{DEPLOYMENT-INSTRUCTIONS.md → docs/deployment-instructions.md} +3 -3
  14. package/infrastructure/{IAM-POLICY-TEMPLATES.md → docs/iam-policy-templates.md} +9 -10
  15. package/infrastructure/domains/database/aurora-discovery.js +81 -0
  16. package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
  17. package/infrastructure/domains/integration/integration-builder.js +178 -0
  18. package/infrastructure/domains/integration/integration-builder.test.js +362 -0
  19. package/infrastructure/domains/integration/websocket-builder.js +69 -0
  20. package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
  21. package/infrastructure/domains/networking/vpc-discovery.test.js +257 -0
  22. package/infrastructure/domains/parameters/ssm-builder.js +79 -0
  23. package/infrastructure/domains/parameters/ssm-builder.test.js +188 -0
  24. package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
  25. package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
  26. package/infrastructure/{iam-generator.js → domains/security/iam-generator.js} +2 -2
  27. package/infrastructure/domains/security/kms-builder.js +169 -0
  28. package/infrastructure/domains/security/kms-builder.test.js +354 -0
  29. package/infrastructure/domains/security/kms-discovery.js +80 -0
  30. package/infrastructure/domains/security/kms-discovery.test.js +176 -0
  31. package/infrastructure/domains/shared/base-builder.js +112 -0
  32. package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
  33. package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
  34. package/infrastructure/domains/shared/environment-builder.js +118 -0
  35. package/infrastructure/domains/shared/environment-builder.test.js +246 -0
  36. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +366 -0
  37. package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
  38. package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
  39. package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
  40. package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
  41. package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
  42. package/infrastructure/domains/shared/resource-discovery.js +132 -0
  43. package/infrastructure/domains/shared/resource-discovery.test.js +410 -0
  44. package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
  45. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +248 -0
  46. package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +259 -0
  47. package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
  48. package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +134 -0
  49. package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
  50. package/infrastructure/esbuild.config.js +53 -0
  51. package/infrastructure/infrastructure-composer.js +85 -0
  52. package/infrastructure/scripts/build-prisma-layer.js +60 -47
  53. package/infrastructure/{build-time-discovery.test.js → scripts/build-time-discovery.test.js} +5 -4
  54. package/layers/prisma/nodejs/package.json +8 -0
  55. package/management-ui/server/utils/environment/awsParameterStore.js +29 -18
  56. package/package.json +8 -8
  57. package/infrastructure/aws-discovery.js +0 -1704
  58. package/infrastructure/aws-discovery.test.js +0 -1666
  59. package/infrastructure/serverless-template.js +0 -2804
  60. package/infrastructure/serverless-template.test.js +0 -1897
  61. /package/infrastructure/{POSTGRES-CONFIGURATION.md → docs/POSTGRES-CONFIGURATION.md} +0 -0
  62. /package/infrastructure/{WEBSOCKET-CONFIGURATION.md → docs/WEBSOCKET-CONFIGURATION.md} +0 -0
  63. /package/infrastructure/{GENERATE-IAM-DOCS.md → docs/generate-iam-command.md} +0 -0
  64. /package/infrastructure/{iam-generator.test.js → domains/security/iam-generator.test.js} +0 -0
  65. /package/infrastructure/{frigg-deployment-iam-stack.yaml → domains/security/templates/frigg-deployment-iam-stack.yaml} +0 -0
  66. /package/infrastructure/{iam-policy-basic.json → domains/security/templates/iam-policy-basic.json} +0 -0
  67. /package/infrastructure/{iam-policy-full.json → domains/security/templates/iam-policy-full.json} +0 -0
  68. /package/infrastructure/{env-validator.js → domains/shared/validation/env-validator.js} +0 -0
  69. /package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +0 -0
  70. /package/infrastructure/{run-discovery.js → scripts/run-discovery.js} +0 -0
@@ -1,2804 +0,0 @@
1
- const path = require('path');
2
- const fs = require('fs');
3
- const { AWSDiscovery } = require('./aws-discovery');
4
- const { buildPrismaLayer } = require('./scripts/build-prisma-layer');
5
-
6
- const shouldRunDiscovery = (AppDefinition) => {
7
- console.log(
8
- '⚙️ Checking FRIGG_SKIP_AWS_DISCOVERY:',
9
- process.env.FRIGG_SKIP_AWS_DISCOVERY
10
- );
11
- if (process.env.FRIGG_SKIP_AWS_DISCOVERY === 'true') {
12
- console.log(
13
- '⚙️ Skipping AWS discovery because FRIGG_SKIP_AWS_DISCOVERY is set.'
14
- );
15
- return false;
16
- }
17
-
18
- return (
19
- AppDefinition.vpc?.enable === true ||
20
- AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
21
- AppDefinition.ssm?.enable === true ||
22
- AppDefinition.database?.postgres?.enable === true
23
- );
24
- };
25
-
26
- const getAppEnvironmentVars = (AppDefinition) => {
27
- const envVars = {};
28
- const reservedVars = new Set([
29
- '_HANDLER',
30
- '_X_AMZN_TRACE_ID',
31
- 'AWS_DEFAULT_REGION',
32
- 'AWS_EXECUTION_ENV',
33
- 'AWS_REGION',
34
- 'AWS_LAMBDA_FUNCTION_NAME',
35
- 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE',
36
- 'AWS_LAMBDA_FUNCTION_VERSION',
37
- 'AWS_LAMBDA_INITIALIZATION_TYPE',
38
- 'AWS_LAMBDA_LOG_GROUP_NAME',
39
- 'AWS_LAMBDA_LOG_STREAM_NAME',
40
- 'AWS_ACCESS_KEY',
41
- 'AWS_ACCESS_KEY_ID',
42
- 'AWS_SECRET_ACCESS_KEY',
43
- 'AWS_SESSION_TOKEN',
44
- ]);
45
-
46
- if (!AppDefinition.environment) {
47
- return envVars;
48
- }
49
-
50
- console.log('📋 Loading environment variables from appDefinition...');
51
- const envKeys = [];
52
- const skippedKeys = [];
53
-
54
- for (const [key, value] of Object.entries(AppDefinition.environment)) {
55
- if (value !== true) continue;
56
- if (reservedVars.has(key)) {
57
- skippedKeys.push(key);
58
- continue;
59
- }
60
- envVars[key] = `\${env:${key}, ''}`;
61
- envKeys.push(key);
62
- }
63
-
64
- if (envKeys.length > 0) {
65
- console.log(
66
- ` Found ${envKeys.length} environment variables: ${envKeys.join(
67
- ', '
68
- )}`
69
- );
70
- }
71
- if (skippedKeys.length > 0) {
72
- console.log(
73
- ` ⚠️ Skipped ${skippedKeys.length
74
- } reserved AWS Lambda variables: ${skippedKeys.join(', ')}`
75
- );
76
- }
77
-
78
- return envVars;
79
- };
80
-
81
- const findNodeModulesPath = () => {
82
- try {
83
- let currentDir = process.cwd();
84
- let nodeModulesPath = null;
85
-
86
- for (let i = 0; i < 5; i++) {
87
- const potentialPath = path.join(currentDir, 'node_modules');
88
- if (fs.existsSync(potentialPath)) {
89
- nodeModulesPath = potentialPath;
90
- console.log(
91
- `Found node_modules at: ${nodeModulesPath} (method 1)`
92
- );
93
- break;
94
- }
95
- const parentDir = path.dirname(currentDir);
96
- if (parentDir === currentDir) break;
97
- currentDir = parentDir;
98
- }
99
-
100
- if (!nodeModulesPath) {
101
- try {
102
- const { execSync } = require('node:child_process');
103
- const npmRoot = execSync('npm root', {
104
- encoding: 'utf8',
105
- }).trim();
106
- if (fs.existsSync(npmRoot)) {
107
- nodeModulesPath = npmRoot;
108
- console.log(
109
- `Found node_modules at: ${nodeModulesPath} (method 2)`
110
- );
111
- }
112
- } catch (npmError) {
113
- console.error('Error executing npm root:', npmError);
114
- }
115
- }
116
-
117
- if (!nodeModulesPath) {
118
- currentDir = process.cwd();
119
- for (let i = 0; i < 5; i++) {
120
- const packageJsonPath = path.join(currentDir, 'package.json');
121
- if (fs.existsSync(packageJsonPath)) {
122
- const potentialNodeModules = path.join(
123
- currentDir,
124
- 'node_modules'
125
- );
126
- if (fs.existsSync(potentialNodeModules)) {
127
- nodeModulesPath = potentialNodeModules;
128
- console.log(
129
- `Found node_modules at: ${nodeModulesPath} (method 3)`
130
- );
131
- break;
132
- }
133
- }
134
- const parentDir = path.dirname(currentDir);
135
- if (parentDir === currentDir) break;
136
- currentDir = parentDir;
137
- }
138
- }
139
-
140
- if (nodeModulesPath) {
141
- return nodeModulesPath;
142
- }
143
-
144
- console.warn(
145
- 'Could not find node_modules path, falling back to default'
146
- );
147
- return path.resolve(process.cwd(), '../node_modules');
148
- } catch (error) {
149
- console.error('Error finding node_modules path:', error);
150
- return path.resolve(process.cwd(), '../node_modules');
151
- }
152
- };
153
-
154
- const modifyHandlerPaths = (functions) => {
155
- const isOffline = process.argv.includes('offline');
156
- console.log('isOffline', isOffline);
157
-
158
- if (!isOffline) {
159
- console.log('Not in offline mode, skipping handler path modification');
160
- return functions;
161
- }
162
-
163
- const nodeModulesPath = findNodeModulesPath();
164
- const modifiedFunctions = { ...functions };
165
-
166
- for (const functionName of Object.keys(modifiedFunctions)) {
167
- console.log('functionName', functionName);
168
- const functionDef = modifiedFunctions[functionName];
169
- if (functionDef?.handler?.includes('node_modules/')) {
170
- const relativePath = path.relative(process.cwd(), nodeModulesPath);
171
- functionDef.handler = functionDef.handler.replace(
172
- 'node_modules/',
173
- `${relativePath}/`
174
- );
175
- console.log(
176
- `Updated handler for ${functionName}: ${functionDef.handler}`
177
- );
178
- }
179
- }
180
-
181
- return modifiedFunctions;
182
- };
183
-
184
- const createVPCInfrastructure = (AppDefinition) => {
185
- const vpcResources = {
186
- FriggVPC: {
187
- Type: 'AWS::EC2::VPC',
188
- Properties: {
189
- CidrBlock: AppDefinition.vpc.cidrBlock || '10.0.0.0/16',
190
- EnableDnsHostnames: true,
191
- EnableDnsSupport: true,
192
- Tags: [
193
- {
194
- Key: 'Name',
195
- Value: '${self:service}-${self:provider.stage}-vpc',
196
- },
197
- { Key: 'ManagedBy', Value: 'Frigg' },
198
- { Key: 'Service', Value: '${self:service}' },
199
- { Key: 'Stage', Value: '${self:provider.stage}' },
200
- ],
201
- },
202
- },
203
- FriggInternetGateway: {
204
- Type: 'AWS::EC2::InternetGateway',
205
- Properties: {
206
- Tags: [
207
- {
208
- Key: 'Name',
209
- Value: '${self:service}-${self:provider.stage}-igw',
210
- },
211
- { Key: 'ManagedBy', Value: 'Frigg' },
212
- { Key: 'Service', Value: '${self:service}' },
213
- { Key: 'Stage', Value: '${self:provider.stage}' },
214
- ],
215
- },
216
- },
217
- FriggVPCGatewayAttachment: {
218
- Type: 'AWS::EC2::VPCGatewayAttachment',
219
- Properties: {
220
- VpcId: { Ref: 'FriggVPC' },
221
- InternetGatewayId: { Ref: 'FriggInternetGateway' },
222
- },
223
- },
224
- FriggPublicSubnet: {
225
- Type: 'AWS::EC2::Subnet',
226
- Properties: {
227
- VpcId: { Ref: 'FriggVPC' },
228
- CidrBlock: '10.0.1.0/24',
229
- AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
230
- MapPublicIpOnLaunch: true,
231
- Tags: [
232
- {
233
- Key: 'Name',
234
- Value: '${self:service}-${self:provider.stage}-public-subnet-1',
235
- },
236
- { Key: 'ManagedBy', Value: 'Frigg' },
237
- { Key: 'Service', Value: '${self:service}' },
238
- { Key: 'Stage', Value: '${self:provider.stage}' },
239
- { Key: 'Type', Value: 'Public' },
240
- ],
241
- },
242
- },
243
- FriggPublicSubnet2: {
244
- Type: 'AWS::EC2::Subnet',
245
- Properties: {
246
- VpcId: { Ref: 'FriggVPC' },
247
- CidrBlock: '10.0.4.0/24',
248
- AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
249
- MapPublicIpOnLaunch: true,
250
- Tags: [
251
- {
252
- Key: 'Name',
253
- Value: '${self:service}-${self:provider.stage}-public-subnet-2',
254
- },
255
- { Key: 'ManagedBy', Value: 'Frigg' },
256
- { Key: 'Service', Value: '${self:service}' },
257
- { Key: 'Stage', Value: '${self:provider.stage}' },
258
- { Key: 'Type', Value: 'Public' },
259
- ],
260
- },
261
- },
262
- FriggPrivateSubnet1: {
263
- Type: 'AWS::EC2::Subnet',
264
- Properties: {
265
- VpcId: { Ref: 'FriggVPC' },
266
- CidrBlock: '10.0.2.0/24',
267
- AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
268
- Tags: [
269
- {
270
- Key: 'Name',
271
- Value: '${self:service}-${self:provider.stage}-private-subnet-1',
272
- },
273
- { Key: 'ManagedBy', Value: 'Frigg' },
274
- { Key: 'Service', Value: '${self:service}' },
275
- { Key: 'Stage', Value: '${self:provider.stage}' },
276
- { Key: 'Type', Value: 'Private' },
277
- ],
278
- },
279
- },
280
- FriggPrivateSubnet2: {
281
- Type: 'AWS::EC2::Subnet',
282
- Properties: {
283
- VpcId: { Ref: 'FriggVPC' },
284
- CidrBlock: '10.0.3.0/24',
285
- AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
286
- Tags: [
287
- {
288
- Key: 'Name',
289
- Value: '${self:service}-${self:provider.stage}-private-subnet-2',
290
- },
291
- { Key: 'ManagedBy', Value: 'Frigg' },
292
- { Key: 'Service', Value: '${self:service}' },
293
- { Key: 'Stage', Value: '${self:provider.stage}' },
294
- { Key: 'Type', Value: 'Private' },
295
- ],
296
- },
297
- },
298
- FriggNATGatewayEIP: {
299
- Type: 'AWS::EC2::EIP',
300
- Properties: {
301
- Domain: 'vpc',
302
- Tags: [
303
- {
304
- Key: 'Name',
305
- Value: '${self:service}-${self:provider.stage}-nat-eip',
306
- },
307
- { Key: 'ManagedBy', Value: 'Frigg' },
308
- { Key: 'Service', Value: '${self:service}' },
309
- { Key: 'Stage', Value: '${self:provider.stage}' },
310
- ],
311
- },
312
- DependsOn: 'FriggVPCGatewayAttachment',
313
- },
314
- FriggNATGateway: {
315
- Type: 'AWS::EC2::NatGateway',
316
- Properties: {
317
- AllocationId: {
318
- 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'],
319
- },
320
- SubnetId: { Ref: 'FriggPublicSubnet' },
321
- Tags: [
322
- {
323
- Key: 'Name',
324
- Value: '${self:service}-${self:provider.stage}-nat-gateway',
325
- },
326
- { Key: 'ManagedBy', Value: 'Frigg' },
327
- { Key: 'Service', Value: '${self:service}' },
328
- { Key: 'Stage', Value: '${self:provider.stage}' },
329
- ],
330
- },
331
- },
332
- FriggPublicRouteTable: {
333
- Type: 'AWS::EC2::RouteTable',
334
- Properties: {
335
- VpcId: { Ref: 'FriggVPC' },
336
- Tags: [
337
- {
338
- Key: 'Name',
339
- Value: '${self:service}-${self:provider.stage}-public-rt',
340
- },
341
- { Key: 'ManagedBy', Value: 'Frigg' },
342
- { Key: 'Service', Value: '${self:service}' },
343
- { Key: 'Stage', Value: '${self:provider.stage}' },
344
- { Key: 'Type', Value: 'Public' },
345
- ],
346
- },
347
- },
348
- FriggPublicRoute: {
349
- Type: 'AWS::EC2::Route',
350
- Properties: {
351
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
352
- DestinationCidrBlock: '0.0.0.0/0',
353
- GatewayId: { Ref: 'FriggInternetGateway' },
354
- },
355
- DependsOn: 'FriggVPCGatewayAttachment',
356
- },
357
- FriggPublicSubnetRouteTableAssociation: {
358
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
359
- Properties: {
360
- SubnetId: { Ref: 'FriggPublicSubnet' },
361
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
362
- },
363
- },
364
- FriggPublicSubnet2RouteTableAssociation: {
365
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
366
- Properties: {
367
- SubnetId: { Ref: 'FriggPublicSubnet2' },
368
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
369
- },
370
- },
371
- FriggPrivateRouteTable: {
372
- Type: 'AWS::EC2::RouteTable',
373
- Properties: {
374
- VpcId: { Ref: 'FriggVPC' },
375
- Tags: [
376
- {
377
- Key: 'Name',
378
- Value: '${self:service}-${self:provider.stage}-private-rt',
379
- },
380
- { Key: 'ManagedBy', Value: 'Frigg' },
381
- { Key: 'Service', Value: '${self:service}' },
382
- { Key: 'Stage', Value: '${self:provider.stage}' },
383
- { Key: 'Type', Value: 'Private' },
384
- ],
385
- },
386
- },
387
- FriggPrivateRoute: {
388
- Type: 'AWS::EC2::Route',
389
- Properties: {
390
- RouteTableId: { Ref: 'FriggPrivateRouteTable' },
391
- DestinationCidrBlock: '0.0.0.0/0',
392
- NatGatewayId: { Ref: 'FriggNATGateway' },
393
- },
394
- },
395
- FriggPrivateSubnet1RouteTableAssociation: {
396
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
397
- Properties: {
398
- SubnetId: { Ref: 'FriggPrivateSubnet1' },
399
- RouteTableId: { Ref: 'FriggPrivateRouteTable' },
400
- },
401
- },
402
- FriggPrivateSubnet2RouteTableAssociation: {
403
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
404
- Properties: {
405
- SubnetId: { Ref: 'FriggPrivateSubnet2' },
406
- RouteTableId: { Ref: 'FriggPrivateRouteTable' },
407
- },
408
- },
409
- FriggLambdaSecurityGroup: {
410
- Type: 'AWS::EC2::SecurityGroup',
411
- Properties: {
412
- GroupDescription: 'Security group for Frigg Lambda functions',
413
- VpcId: { Ref: 'FriggVPC' },
414
- SecurityGroupEgress: [
415
- {
416
- IpProtocol: 'tcp',
417
- FromPort: 443,
418
- ToPort: 443,
419
- CidrIp: '0.0.0.0/0',
420
- Description: 'HTTPS outbound',
421
- },
422
- {
423
- IpProtocol: 'tcp',
424
- FromPort: 80,
425
- ToPort: 80,
426
- CidrIp: '0.0.0.0/0',
427
- Description: 'HTTP outbound',
428
- },
429
- {
430
- IpProtocol: 'tcp',
431
- FromPort: 53,
432
- ToPort: 53,
433
- CidrIp: '0.0.0.0/0',
434
- Description: 'DNS TCP',
435
- },
436
- {
437
- IpProtocol: 'udp',
438
- FromPort: 53,
439
- ToPort: 53,
440
- CidrIp: '0.0.0.0/0',
441
- Description: 'DNS UDP',
442
- },
443
- {
444
- IpProtocol: 'tcp',
445
- FromPort: 27017,
446
- ToPort: 27017,
447
- CidrIp: '0.0.0.0/0',
448
- Description: 'MongoDB outbound',
449
- },
450
- ],
451
- Tags: [
452
- {
453
- Key: 'Name',
454
- Value: '${self:service}-${self:provider.stage}-lambda-sg',
455
- },
456
- { Key: 'ManagedBy', Value: 'Frigg' },
457
- { Key: 'Service', Value: '${self:service}' },
458
- { Key: 'Stage', Value: '${self:provider.stage}' },
459
- ],
460
- },
461
- },
462
- };
463
-
464
- if (AppDefinition.vpc.enableVPCEndpoints !== false) {
465
- vpcResources.FriggS3VPCEndpoint = {
466
- Type: 'AWS::EC2::VPCEndpoint',
467
- Properties: {
468
- VpcId: { Ref: 'FriggVPC' },
469
- ServiceName: 'com.amazonaws.${self:provider.region}.s3',
470
- VpcEndpointType: 'Gateway',
471
- RouteTableIds: [{ Ref: 'FriggPrivateRouteTable' }],
472
- },
473
- };
474
-
475
- vpcResources.FriggDynamoDBVPCEndpoint = {
476
- Type: 'AWS::EC2::VPCEndpoint',
477
- Properties: {
478
- VpcId: { Ref: 'FriggVPC' },
479
- ServiceName: 'com.amazonaws.${self:provider.region}.dynamodb',
480
- VpcEndpointType: 'Gateway',
481
- RouteTableIds: [{ Ref: 'FriggPrivateRouteTable' }],
482
- },
483
- };
484
-
485
- if (AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
486
- vpcResources.FriggKMSVPCEndpoint = {
487
- Type: 'AWS::EC2::VPCEndpoint',
488
- Properties: {
489
- VpcId: { Ref: 'FriggVPC' },
490
- ServiceName: 'com.amazonaws.${self:provider.region}.kms',
491
- VpcEndpointType: 'Interface',
492
- SubnetIds: [
493
- { Ref: 'FriggPrivateSubnet1' },
494
- { Ref: 'FriggPrivateSubnet2' },
495
- ],
496
- SecurityGroupIds: [
497
- { Ref: 'FriggVPCEndpointSecurityGroup' },
498
- ],
499
- PrivateDnsEnabled: true,
500
- },
501
- };
502
- }
503
-
504
- vpcResources.FriggSecretsManagerVPCEndpoint = {
505
- Type: 'AWS::EC2::VPCEndpoint',
506
- Properties: {
507
- VpcId: { Ref: 'FriggVPC' },
508
- ServiceName:
509
- 'com.amazonaws.${self:provider.region}.secretsmanager',
510
- VpcEndpointType: 'Interface',
511
- SubnetIds: [
512
- { Ref: 'FriggPrivateSubnet1' },
513
- { Ref: 'FriggPrivateSubnet2' },
514
- ],
515
- SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
516
- PrivateDnsEnabled: true,
517
- },
518
- };
519
-
520
- vpcResources.FriggVPCEndpointSecurityGroup = {
521
- Type: 'AWS::EC2::SecurityGroup',
522
- Properties: {
523
- GroupDescription:
524
- 'Security group for Frigg VPC Endpoints - allows HTTPS from Lambda functions',
525
- VpcId: { Ref: 'FriggVPC' },
526
- SecurityGroupIngress: [
527
- {
528
- IpProtocol: 'tcp',
529
- FromPort: 443,
530
- ToPort: 443,
531
- SourceSecurityGroupId: {
532
- Ref: 'FriggLambdaSecurityGroup',
533
- },
534
- Description: 'HTTPS from Lambda security group',
535
- },
536
- {
537
- IpProtocol: 'tcp',
538
- FromPort: 443,
539
- ToPort: 443,
540
- CidrIp: AppDefinition.vpc.cidrBlock || '10.0.0.0/16',
541
- Description: 'HTTPS from VPC CIDR (fallback)',
542
- },
543
- ],
544
- Tags: [
545
- {
546
- Key: 'Name',
547
- Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg',
548
- },
549
- { Key: 'ManagedBy', Value: 'Frigg' },
550
- { Key: 'Service', Value: '${self:service}' },
551
- { Key: 'Stage', Value: '${self:provider.stage}' },
552
- { Key: 'Type', Value: 'VPCEndpoint' },
553
- {
554
- Key: 'Purpose',
555
- Value: 'Allow Lambda functions to access VPC endpoints',
556
- },
557
- ],
558
- },
559
- };
560
- }
561
-
562
- return vpcResources;
563
- };
564
-
565
- const gatherDiscoveredResources = async (AppDefinition) => {
566
- if (!shouldRunDiscovery(AppDefinition)) {
567
- return {};
568
- }
569
-
570
- console.log('🔍 Running AWS resource discovery for serverless template...');
571
- try {
572
- const region = process.env.AWS_REGION || 'us-east-1';
573
- const discovery = new AWSDiscovery(region);
574
- // Use Serverless Framework's stage resolution (opt:stage with 'dev' as default)
575
- // This matches how serverless.yml resolves ${opt:stage, "dev"}
576
- // IMPORTANT: Use SLS_STAGE (not STAGE) to match actual deployment stage
577
- const stage = process.env.SLS_STAGE || 'dev';
578
-
579
- const config = {
580
- vpc: AppDefinition.vpc || {},
581
- encryption: AppDefinition.encryption || {},
582
- ssm: AppDefinition.ssm || {},
583
- database: AppDefinition.database || {},
584
- serviceName: AppDefinition.name || 'create-frigg-app',
585
- stage: stage,
586
- };
587
-
588
- const discoveredResources = await discovery.discoverResources(config);
589
-
590
- console.log('✅ AWS discovery completed successfully!');
591
- if (discoveredResources.defaultVpcId) {
592
- console.log(` VPC: ${discoveredResources.defaultVpcId}`);
593
- }
594
- if (
595
- discoveredResources.privateSubnetId1 &&
596
- discoveredResources.privateSubnetId2
597
- ) {
598
- console.log(
599
- ` Subnets: ${discoveredResources.privateSubnetId1}, ${discoveredResources.privateSubnetId2}`
600
- );
601
- }
602
- if (discoveredResources.defaultSecurityGroupId) {
603
- console.log(
604
- ` Security Group: ${discoveredResources.defaultSecurityGroupId}`
605
- );
606
- }
607
- if (discoveredResources.defaultKmsKeyId) {
608
- console.log(` KMS Key: ${discoveredResources.defaultKmsKeyId}`);
609
- }
610
-
611
- return discoveredResources;
612
- } catch (error) {
613
- console.error('❌ AWS discovery failed:', error.message);
614
- throw new Error(`AWS discovery failed: ${error.message}`);
615
- }
616
- };
617
-
618
- const buildEnvironment = (appEnvironmentVars, discoveredResources) => {
619
- const environment = {
620
- STAGE: '${opt:stage, "dev"}',
621
- AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1,
622
- ...appEnvironmentVars,
623
- };
624
-
625
- const discoveryEnvMapping = {
626
- defaultVpcId: 'AWS_DISCOVERY_VPC_ID',
627
- defaultSecurityGroupId: 'AWS_DISCOVERY_SECURITY_GROUP_ID',
628
- privateSubnetId1: 'AWS_DISCOVERY_SUBNET_ID_1',
629
- privateSubnetId2: 'AWS_DISCOVERY_SUBNET_ID_2',
630
- publicSubnetId: 'AWS_DISCOVERY_PUBLIC_SUBNET_ID', // Keep for backward compat
631
- publicSubnetId1: 'AWS_DISCOVERY_PUBLIC_SUBNET_ID_1',
632
- publicSubnetId2: 'AWS_DISCOVERY_PUBLIC_SUBNET_ID_2',
633
- defaultRouteTableId: 'AWS_DISCOVERY_ROUTE_TABLE_ID',
634
- defaultKmsKeyId: 'AWS_DISCOVERY_KMS_KEY_ID',
635
- };
636
-
637
- for (const [key, envKey] of Object.entries(discoveryEnvMapping)) {
638
- if (discoveredResources[key]) {
639
- environment[envKey] = discoveredResources[key];
640
- }
641
- }
642
-
643
- // Add Aurora discovery mappings
644
- if (discoveredResources.aurora) {
645
- if (discoveredResources.aurora.clusterIdentifier) {
646
- environment.AWS_DISCOVERY_AURORA_CLUSTER_ID = discoveredResources.aurora.clusterIdentifier;
647
- }
648
- if (discoveredResources.aurora.endpoint) {
649
- environment.AWS_DISCOVERY_AURORA_ENDPOINT = discoveredResources.aurora.endpoint;
650
- }
651
- if (discoveredResources.aurora.port) {
652
- environment.AWS_DISCOVERY_AURORA_PORT = discoveredResources.aurora.port.toString();
653
- }
654
- if (discoveredResources.aurora.secretArn) {
655
- environment.AWS_DISCOVERY_AURORA_SECRET_ARN = discoveredResources.aurora.secretArn;
656
- }
657
- }
658
-
659
- return environment;
660
- };
661
-
662
- const createBaseDefinition = (
663
- AppDefinition,
664
- appEnvironmentVars,
665
- discoveredResources
666
- ) => {
667
- const region = process.env.AWS_REGION || 'us-east-1';
668
-
669
- // Function-level package config to exclude Prisma and AWS SDK
670
- // Uses native Serverless package.exclude since jetpack function-level config isn't supported in v3
671
- const functionPackageConfig = {
672
- exclude: [
673
- // Exclude AWS SDK (already in Lambda runtime)
674
- 'node_modules/aws-sdk/**',
675
- 'node_modules/@aws-sdk/**',
676
-
677
- // Exclude Prisma (provided via Lambda Layer)
678
- 'node_modules/@prisma/**',
679
- 'node_modules/.prisma/**',
680
- 'node_modules/prisma/**',
681
- 'node_modules/@friggframework/core/generated/**',
682
-
683
- // Exclude nested node_modules from symlinked frigg packages (for npm link development)
684
- 'node_modules/@friggframework/core/node_modules/**',
685
- 'node_modules/@friggframework/devtools/node_modules/**',
686
- ],
687
- };
688
-
689
- return {
690
- frameworkVersion: '>=3.17.0',
691
- service: AppDefinition.name || 'create-frigg-app',
692
- package: {
693
- individually: true,
694
- // NOTE: These patterns are NOT used when serverless-jetpack is enabled with trace mode
695
- // Jetpack's trace mode completely overrides package.patterns during dependency resolution
696
- // These are kept commented out as a fallback if Jetpack needs to be disabled
697
- patterns: [
698
- // AWS SDK exclusions (already in Lambda runtime)
699
- // '!**/node_modules/aws-sdk/**',
700
- // '!**/node_modules/@aws-sdk/**',
701
-
702
- // Prisma exclusions (provided via Lambda Layer)
703
- // '!**/node_modules/@prisma/**',
704
- // '!**/node_modules/.prisma/**',
705
- // '!**/node_modules/@prisma-mongodb/**',
706
- // '!**/node_modules/@prisma-postgresql/**',
707
- // '!**/node_modules/prisma/**',
708
-
709
- // Exclude Prisma generated clients from @friggframework/core
710
- // '!**/node_modules/@friggframework/core/generated/**',
711
-
712
- // Exclude development and test files
713
- // '!**/test/**',
714
- // '!**/tests/**',
715
- // '!**/*.test.js',
716
- // '!**/*.spec.js',
717
- // '!**/*.map',
718
- // '!**/jest.config.js',
719
- // '!**/jest.unit.config.js',
720
- // '!**/.eslintrc.json',
721
- // '!**/.prettierrc',
722
- // '!**/.prettierignore',
723
- // '!**/.markdownlintignore',
724
- // '!**/docker-compose.yml',
725
- // '!**/package.json',
726
- // '!**/README.md',
727
- // '!**/*.md',
728
-
729
- // Exclude .DS_Store and other OS files
730
- // '!**/.DS_Store',
731
- // '!**/.git/**',
732
- // '!**/.claude-flow/**',
733
- ],
734
- },
735
- useDotenv: true,
736
- provider: {
737
- name: AppDefinition.provider || 'aws',
738
- ...(process.env.AWS_PROFILE && { profile: process.env.AWS_PROFILE }),
739
- runtime: 'nodejs20.x',
740
- timeout: 30,
741
- region,
742
- stage: '${opt:stage}',
743
- environment: buildEnvironment(
744
- appEnvironmentVars,
745
- discoveredResources
746
- ),
747
- iamRoleStatements: [
748
- {
749
- Effect: 'Allow',
750
- Action: ['sns:Publish'],
751
- Resource: { Ref: 'InternalErrorBridgeTopic' },
752
- },
753
- {
754
- Effect: 'Allow',
755
- Action: [
756
- 'sqs:SendMessage',
757
- 'sqs:SendMessageBatch',
758
- 'sqs:GetQueueUrl',
759
- 'sqs:GetQueueAttributes',
760
- ],
761
- Resource: [
762
- { 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
763
- {
764
- 'Fn::Join': [
765
- ':',
766
- [
767
- 'arn:aws:sqs:${self:provider.region}:*:${self:service}--${self:provider.stage}-*Queue',
768
- ],
769
- ],
770
- },
771
- ],
772
- },
773
- ],
774
- httpApi: {
775
- payload: '2.0',
776
- cors: {
777
- allowedOrigins: ['*'],
778
- allowedHeaders: ['*'],
779
- allowedMethods: ['*'],
780
- allowCredentials: false,
781
- },
782
- name: '${opt:stage, "dev"}-${self:service}',
783
- disableDefaultEndpoint: false,
784
- },
785
- },
786
- plugins: [
787
- 'serverless-jetpack',
788
- 'serverless-dotenv-plugin',
789
- 'serverless-offline-sqs',
790
- 'serverless-offline',
791
- '@friggframework/serverless-plugin',
792
- ],
793
- custom: {
794
- 'serverless-offline': {
795
- httpPort: 3001,
796
- lambdaPort: 4001,
797
- websocketPort: 3002,
798
- },
799
- 'serverless-offline-sqs': {
800
- autoCreate: false,
801
- apiVersion: '2012-11-05',
802
- endpoint: 'http://localhost:4566',
803
- region,
804
- accessKeyId: 'root',
805
- secretAccessKey: 'root',
806
- skipCacheInvalidation: false,
807
- },
808
- jetpack: {
809
- base: '..', // Essential for reaching handlers in node_modules/@friggframework
810
- // NOTE: Service-level preInclude applies to EVERYTHING (functions + layers)
811
- // We need to ONLY exclude from functions, not from the Prisma layer
812
- // Solution: Apply exclusions at function level instead
813
- },
814
- },
815
- functions: {
816
- auth: {
817
- handler: 'node_modules/@friggframework/core/handlers/routers/auth.handler',
818
- layers: [{ Ref: 'PrismaLambdaLayer' }],
819
- package: functionPackageConfig,
820
- events: [
821
- { httpApi: { path: '/api/integrations', method: 'ANY' } },
822
- {
823
- httpApi: {
824
- path: '/api/integrations/{proxy+}',
825
- method: 'ANY',
826
- },
827
- },
828
- { httpApi: { path: '/api/authorize', method: 'ANY' } },
829
- ],
830
- },
831
- user: {
832
- handler: 'node_modules/@friggframework/core/handlers/routers/user.handler',
833
- layers: [{ Ref: 'PrismaLambdaLayer' }],
834
- package: functionPackageConfig,
835
- events: [{ httpApi: { path: '/user/{proxy+}', method: 'ANY' } }],
836
- },
837
- health: {
838
- handler: 'node_modules/@friggframework/core/handlers/routers/health.handler',
839
- layers: [{ Ref: 'PrismaLambdaLayer' }],
840
- package: functionPackageConfig,
841
- events: [
842
- { httpApi: { path: '/health', method: 'GET' } },
843
- { httpApi: { path: '/health/{proxy+}', method: 'GET' } },
844
- ],
845
- },
846
- dbMigrate: {
847
- handler: 'node_modules/@friggframework/core/handlers/workers/db-migration.handler',
848
- // Uses Prisma Layer (includes CLI) - simpler than standalone packaging
849
- layers: [{ Ref: 'PrismaLambdaLayer' }],
850
- timeout: 300, // 5 minutes for long-running migrations
851
- memorySize: 512, // Extra memory for Prisma CLI operations
852
- reservedConcurrency: 1, // Prevent concurrent migrations
853
- description: 'Runs database migrations via Prisma (invoke manually from CI/CD). Uses Prisma layer with CLI.',
854
- package: functionPackageConfig, // Use same exclusions as other functions
855
- // No events - this function is invoked manually via AWS CLI
856
- maximumEventAge: 60, // Don't retry old migration requests (60 seconds)
857
- maximumRetryAttempts: 0, // Don't auto-retry failed migrations
858
- tags: {
859
- Purpose: 'DatabaseMigration',
860
- ManagedBy: 'Frigg',
861
- },
862
- // Environment variables for non-interactive Prisma CLI operation
863
- environment: {
864
- CI: '1', // Forces Prisma to non-interactive mode
865
- PRISMA_HIDE_UPDATE_MESSAGE: '1', // Suppress update messages
866
- PRISMA_MIGRATE_SKIP_SEED: '1', // Skip seeding during migrations
867
- },
868
- },
869
- },
870
- layers: {
871
- prisma: {
872
- path: 'layers/prisma',
873
- name: '${self:service}-prisma-${sls:stage}',
874
- description: 'Prisma ORM client with CLI and rhel-openssl-3.0.x binaries. Configured based on AppDefinition database settings. Used by all functions.',
875
- compatibleRuntimes: ['nodejs18.x', 'nodejs20.x'],
876
- retain: false, // Don't retain old layer versions
877
- },
878
- },
879
- resources: {
880
- Resources: {
881
- InternalErrorQueue: {
882
- Type: 'AWS::SQS::Queue',
883
- Properties: {
884
- QueueName:
885
- '${self:service}-internal-error-queue-${self:provider.stage}',
886
- MessageRetentionPeriod: 300,
887
- },
888
- },
889
- InternalErrorBridgeTopic: {
890
- Type: 'AWS::SNS::Topic',
891
- Properties: {
892
- Subscription: [
893
- {
894
- Protocol: 'sqs',
895
- Endpoint: {
896
- 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
897
- },
898
- },
899
- ],
900
- },
901
- },
902
- InternalErrorBridgePolicy: {
903
- Type: 'AWS::SQS::QueuePolicy',
904
- Properties: {
905
- Queues: [{ Ref: 'InternalErrorQueue' }],
906
- PolicyDocument: {
907
- Version: '2012-10-17',
908
- Statement: [
909
- {
910
- Sid: 'Allow Dead Letter SNS to publish to SQS',
911
- Effect: 'Allow',
912
- Principal: { Service: 'sns.amazonaws.com' },
913
- Resource: {
914
- 'Fn::GetAtt': [
915
- 'InternalErrorQueue',
916
- 'Arn',
917
- ],
918
- },
919
- Action: [
920
- 'SQS:SendMessage',
921
- 'SQS:SendMessageBatch',
922
- ],
923
- Condition: {
924
- ArnEquals: {
925
- 'aws:SourceArn': {
926
- Ref: 'InternalErrorBridgeTopic',
927
- },
928
- },
929
- },
930
- },
931
- ],
932
- },
933
- },
934
- },
935
- ApiGatewayAlarm5xx: {
936
- Type: 'AWS::CloudWatch::Alarm',
937
- Properties: {
938
- AlarmDescription: 'API Gateway 5xx Errors',
939
- Namespace: 'AWS/ApiGateway',
940
- MetricName: '5XXError',
941
- Statistic: 'Sum',
942
- Threshold: 0,
943
- ComparisonOperator: 'GreaterThanThreshold',
944
- EvaluationPeriods: 1,
945
- Period: 60,
946
- AlarmActions: [{ Ref: 'InternalErrorBridgeTopic' }],
947
- Dimensions: [
948
- { Name: 'ApiId', Value: { Ref: 'HttpApi' } },
949
- { Name: 'Stage', Value: '${self:provider.stage}' },
950
- ],
951
- },
952
- },
953
- },
954
- },
955
- };
956
- };
957
-
958
- const applyKmsConfiguration = (
959
- definition,
960
- AppDefinition,
961
- discoveredResources
962
- ) => {
963
- if (AppDefinition.encryption?.fieldLevelEncryptionMethod !== 'kms') {
964
- return;
965
- }
966
-
967
- // Skip KMS configuration for local development when AWS discovery is disabled
968
- if (process.env.FRIGG_SKIP_AWS_DISCOVERY === 'true') {
969
- console.log(
970
- '⚙️ Skipping KMS configuration for local development (FRIGG_SKIP_AWS_DISCOVERY is set)'
971
- );
972
- return;
973
- }
974
-
975
- if (discoveredResources.defaultKmsKeyId) {
976
- console.log(`Using existing KMS key: ${discoveredResources.defaultKmsKeyId}`);
977
-
978
- // Only create alias if it doesn't already exist
979
- if (!discoveredResources.kmsAliasExists) {
980
- console.log('Creating KMS alias for discovered key...');
981
- definition.resources.Resources.FriggKMSKeyAlias = {
982
- Type: 'AWS::KMS::Alias',
983
- DeletionPolicy: 'Retain',
984
- Properties: {
985
- AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
986
- TargetKeyId: discoveredResources.defaultKmsKeyId,
987
- },
988
- };
989
- } else {
990
- console.log('KMS alias already exists, skipping alias creation');
991
- }
992
-
993
- definition.provider.iamRoleStatements.push({
994
- Effect: 'Allow',
995
- Action: ['kms:GenerateDataKey', 'kms:Decrypt'],
996
- Resource: [discoveredResources.defaultKmsKeyId],
997
- });
998
- } else {
999
- if (AppDefinition.encryption?.createResourceIfNoneFound !== true) {
1000
- throw new Error(
1001
- 'KMS field-level encryption is enabled but no KMS key was found. ' +
1002
- 'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
1003
- );
1004
- }
1005
-
1006
- console.log('No existing KMS key found, creating a new one...');
1007
- definition.resources.Resources.FriggKMSKey = {
1008
- Type: 'AWS::KMS::Key',
1009
- DeletionPolicy: 'Retain',
1010
- UpdateReplacePolicy: 'Retain',
1011
- Properties: {
1012
- EnableKeyRotation: true,
1013
- Description: 'Frigg KMS key for field-level encryption',
1014
- KeyPolicy: {
1015
- Version: '2012-10-17',
1016
- Statement: [
1017
- {
1018
- Sid: 'AllowRootAccountAdmin',
1019
- Effect: 'Allow',
1020
- Principal: {
1021
- AWS: {
1022
- 'Fn::Sub':
1023
- 'arn:aws:iam::${AWS::AccountId}:root',
1024
- },
1025
- },
1026
- Action: 'kms:*',
1027
- Resource: '*',
1028
- },
1029
- {
1030
- Sid: 'AllowLambdaService',
1031
- Effect: 'Allow',
1032
- Principal: { Service: 'lambda.amazonaws.com' },
1033
- Action: [
1034
- 'kms:GenerateDataKey',
1035
- 'kms:Decrypt',
1036
- 'kms:DescribeKey',
1037
- ],
1038
- Resource: '*',
1039
- Condition: {
1040
- StringEquals: {
1041
- 'kms:ViaService': `lambda.${process.env.AWS_REGION || 'us-east-1'
1042
- }.amazonaws.com`,
1043
- },
1044
- },
1045
- },
1046
- ],
1047
- },
1048
- Tags: [
1049
- {
1050
- Key: 'Name',
1051
- Value: '${self:service}-${self:provider.stage}-frigg-kms-key',
1052
- },
1053
- { Key: 'ManagedBy', Value: 'Frigg' },
1054
- {
1055
- Key: 'Purpose',
1056
- Value: 'Field-level encryption for Frigg application',
1057
- },
1058
- ],
1059
- },
1060
- };
1061
-
1062
- definition.resources.Resources.FriggKMSKeyAlias = {
1063
- Type: 'AWS::KMS::Alias',
1064
- DeletionPolicy: 'Retain',
1065
- Properties: {
1066
- AliasName:
1067
- 'alias/${self:service}-${self:provider.stage}-frigg-kms',
1068
- TargetKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
1069
- },
1070
- };
1071
-
1072
- definition.provider.iamRoleStatements.push({
1073
- Effect: 'Allow',
1074
- Action: ['kms:GenerateDataKey', 'kms:Decrypt'],
1075
- Resource: [{ 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] }],
1076
- });
1077
-
1078
- definition.provider.environment.KMS_KEY_ARN = {
1079
- 'Fn::GetAtt': ['FriggKMSKey', 'Arn'],
1080
- };
1081
- definition.custom.kmsGrants = {
1082
- kmsKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
1083
- };
1084
- }
1085
-
1086
- definition.plugins.push('serverless-kms-grants');
1087
- if (!definition.custom.kmsGrants) {
1088
- definition.custom.kmsGrants = {
1089
- kmsKeyId:
1090
- discoveredResources.defaultKmsKeyId ||
1091
- '${env:AWS_DISCOVERY_KMS_KEY_ID}',
1092
- };
1093
- }
1094
-
1095
- if (!definition.provider.environment.KMS_KEY_ARN) {
1096
- definition.provider.environment.KMS_KEY_ARN =
1097
- discoveredResources.defaultKmsKeyId ||
1098
- '${env:AWS_DISCOVERY_KMS_KEY_ID}';
1099
- }
1100
- };
1101
-
1102
- const healVpcConfiguration = (discoveredResources, AppDefinition) => {
1103
- const healingReport = {
1104
- healed: [],
1105
- warnings: [],
1106
- errors: [],
1107
- recommendations: [],
1108
- criticalActions: [],
1109
- };
1110
-
1111
- if (!AppDefinition.vpc?.selfHeal) {
1112
- return healingReport;
1113
- }
1114
-
1115
- console.log(
1116
- '🔧 Self-healing mode enabled - checking for VPC misconfigurations...'
1117
- );
1118
-
1119
- if (discoveredResources.natGatewayInPrivateSubnet) {
1120
- healingReport.warnings.push(
1121
- `NAT Gateway ${discoveredResources.natGatewayInPrivateSubnet} is in a private subnet`
1122
- );
1123
- healingReport.recommendations.push(
1124
- 'NAT Gateway should be recreated in a public subnet for proper internet connectivity'
1125
- );
1126
- discoveredResources.needsNewNatGateway = true;
1127
- healingReport.healed.push(
1128
- 'Marked NAT Gateway for recreation in public subnet'
1129
- );
1130
- }
1131
-
1132
- if (discoveredResources.elasticIpAlreadyAssociated) {
1133
- healingReport.warnings.push(
1134
- `Elastic IP ${discoveredResources.existingElasticIp} is already associated`
1135
- );
1136
-
1137
- if (discoveredResources.existingNatGatewayId) {
1138
- healingReport.healed.push(
1139
- 'Will reuse existing NAT Gateway instead of creating a new one'
1140
- );
1141
- discoveredResources.reuseExistingNatGateway = true;
1142
- } else {
1143
- healingReport.healed.push(
1144
- 'Will allocate a new Elastic IP for NAT Gateway'
1145
- );
1146
- discoveredResources.allocateNewElasticIp = true;
1147
- }
1148
- }
1149
-
1150
- if (
1151
- discoveredResources.privateSubnetsWithWrongRoutes &&
1152
- discoveredResources.privateSubnetsWithWrongRoutes.length > 0
1153
- ) {
1154
- healingReport.warnings.push(
1155
- `Found ${discoveredResources.privateSubnetsWithWrongRoutes.length} subnets that are PUBLIC but will be used for Lambda`
1156
- );
1157
- healingReport.healed.push(
1158
- 'Route tables will be corrected during deployment - converting public subnets to private'
1159
- );
1160
- healingReport.criticalActions.push(
1161
- 'SUBNET ISOLATION: Will create separate route tables to ensure Lambda subnets are private'
1162
- );
1163
- }
1164
-
1165
- if (discoveredResources.subnetConversionRequired) {
1166
- healingReport.warnings.push(
1167
- 'Subnet configuration mismatch detected - Lambda functions require private subnets'
1168
- );
1169
- healingReport.healed.push(
1170
- 'Will create proper route table configuration for subnet isolation'
1171
- );
1172
- }
1173
-
1174
- if (discoveredResources.orphanedElasticIps?.length > 0) {
1175
- healingReport.warnings.push(
1176
- `Found ${discoveredResources.orphanedElasticIps.length} orphaned Elastic IPs`
1177
- );
1178
- healingReport.recommendations.push(
1179
- 'Consider releasing orphaned Elastic IPs to avoid charges'
1180
- );
1181
- }
1182
-
1183
- if (healingReport.criticalActions.length > 0) {
1184
- console.log('🚨 CRITICAL ACTIONS:');
1185
- healingReport.criticalActions.forEach((action) =>
1186
- console.log(` - ${action}`)
1187
- );
1188
- }
1189
-
1190
- if (healingReport.healed.length > 0) {
1191
- console.log('✅ Self-healing actions:');
1192
- healingReport.healed.forEach((action) => console.log(` - ${action}`));
1193
- }
1194
-
1195
- if (healingReport.warnings.length > 0) {
1196
- console.log('⚠️ Issues detected:');
1197
- healingReport.warnings.forEach((warning) =>
1198
- console.log(` - ${warning}`)
1199
- );
1200
- }
1201
-
1202
- if (healingReport.recommendations.length > 0) {
1203
- console.log('💡 Recommendations:');
1204
- healingReport.recommendations.forEach((rec) =>
1205
- console.log(` - ${rec}`)
1206
- );
1207
- }
1208
-
1209
- return healingReport;
1210
- };
1211
-
1212
- const configureVpc = (definition, AppDefinition, discoveredResources) => {
1213
- if (AppDefinition.vpc?.enable !== true) {
1214
- return;
1215
- }
1216
-
1217
- // Skip VPC configuration for local development when AWS discovery is disabled
1218
- if (process.env.FRIGG_SKIP_AWS_DISCOVERY === 'true') {
1219
- console.log(
1220
- '⚙️ Skipping VPC configuration for local development (FRIGG_SKIP_AWS_DISCOVERY is set)'
1221
- );
1222
- return;
1223
- }
1224
-
1225
- definition.provider.iamRoleStatements.push({
1226
- Effect: 'Allow',
1227
- Action: [
1228
- 'ec2:CreateNetworkInterface',
1229
- 'ec2:DescribeNetworkInterfaces',
1230
- 'ec2:DeleteNetworkInterface',
1231
- 'ec2:AttachNetworkInterface',
1232
- 'ec2:DetachNetworkInterface',
1233
- ],
1234
- Resource: '*',
1235
- });
1236
-
1237
- if (Object.keys(discoveredResources).length > 0) {
1238
- const healingReport = healVpcConfiguration(
1239
- discoveredResources,
1240
- AppDefinition
1241
- );
1242
- if (healingReport.errors.length > 0 && !AppDefinition.vpc?.selfHeal) {
1243
- throw new Error(
1244
- `VPC configuration errors detected: ${healingReport.errors.join(
1245
- ', '
1246
- )}`
1247
- );
1248
- }
1249
- }
1250
-
1251
- const vpcManagement = AppDefinition.vpc.management || 'discover';
1252
- let vpcId = null;
1253
- const vpcConfig = {
1254
- securityGroupIds: [],
1255
- subnetIds: [],
1256
- };
1257
-
1258
- console.log(`VPC Management Mode: ${vpcManagement}`);
1259
-
1260
- if (vpcManagement === 'create-new') {
1261
- const vpcResources = createVPCInfrastructure(AppDefinition);
1262
- Object.assign(definition.resources.Resources, vpcResources);
1263
- vpcId = { Ref: 'FriggVPC' };
1264
- vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds || [
1265
- { Ref: 'FriggLambdaSecurityGroup' },
1266
- ];
1267
- } else if (vpcManagement === 'use-existing') {
1268
- if (!AppDefinition.vpc.vpcId) {
1269
- throw new Error(
1270
- 'VPC management is set to "use-existing" but no vpcId was provided'
1271
- );
1272
- }
1273
- vpcId = AppDefinition.vpc.vpcId;
1274
- vpcConfig.securityGroupIds =
1275
- AppDefinition.vpc.securityGroupIds ||
1276
- (discoveredResources.defaultSecurityGroupId
1277
- ? [discoveredResources.defaultSecurityGroupId]
1278
- : []);
1279
- } else {
1280
- if (!discoveredResources.defaultVpcId) {
1281
- throw new Error(
1282
- 'VPC discovery failed: No VPC found. Either set vpc.management to "create-new" or provide vpc.vpcId with "use-existing".'
1283
- );
1284
- }
1285
- vpcId = discoveredResources.defaultVpcId;
1286
- vpcConfig.securityGroupIds =
1287
- AppDefinition.vpc.securityGroupIds ||
1288
- (discoveredResources.defaultSecurityGroupId
1289
- ? [discoveredResources.defaultSecurityGroupId]
1290
- : []);
1291
- }
1292
-
1293
- const defaultSubnetManagement =
1294
- vpcManagement === 'create-new' ? 'create' : 'discover';
1295
- let subnetManagement =
1296
- AppDefinition.vpc.subnets?.management || defaultSubnetManagement;
1297
- console.log(`Subnet Management Mode: ${subnetManagement}`);
1298
-
1299
- const effectiveVpcId = vpcId || discoveredResources.defaultVpcId;
1300
- if (!effectiveVpcId) {
1301
- throw new Error('Cannot manage subnets without a VPC ID');
1302
- }
1303
-
1304
- if (subnetManagement === 'create') {
1305
- console.log('Creating new subnets...');
1306
- const subnetVpcId =
1307
- vpcManagement === 'create-new'
1308
- ? { Ref: 'FriggVPC' }
1309
- : effectiveVpcId;
1310
- let subnet1Cidr;
1311
- let subnet2Cidr;
1312
- let publicSubnetCidr;
1313
-
1314
- if (vpcManagement === 'create-new') {
1315
- const generatedCidrs = { 'Fn::Cidr': ['10.0.0.0/16', 3, 8] };
1316
- subnet1Cidr = { 'Fn::Select': [0, generatedCidrs] };
1317
- subnet2Cidr = { 'Fn::Select': [1, generatedCidrs] };
1318
- publicSubnetCidr = { 'Fn::Select': [2, generatedCidrs] };
1319
- } else {
1320
- subnet1Cidr = '172.31.240.0/24';
1321
- subnet2Cidr = '172.31.241.0/24';
1322
- publicSubnetCidr = '172.31.250.0/24';
1323
- }
1324
-
1325
- definition.resources.Resources.FriggPrivateSubnet1 = {
1326
- Type: 'AWS::EC2::Subnet',
1327
- Properties: {
1328
- VpcId: subnetVpcId,
1329
- CidrBlock: subnet1Cidr,
1330
- AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
1331
- Tags: [
1332
- {
1333
- Key: 'Name',
1334
- Value: '${self:service}-${self:provider.stage}-private-1',
1335
- },
1336
- { Key: 'Type', Value: 'Private' },
1337
- { Key: 'ManagedBy', Value: 'Frigg' },
1338
- ],
1339
- },
1340
- };
1341
-
1342
- definition.resources.Resources.FriggPrivateSubnet2 = {
1343
- Type: 'AWS::EC2::Subnet',
1344
- Properties: {
1345
- VpcId: subnetVpcId,
1346
- CidrBlock: subnet2Cidr,
1347
- AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
1348
- Tags: [
1349
- {
1350
- Key: 'Name',
1351
- Value: '${self:service}-${self:provider.stage}-private-2',
1352
- },
1353
- { Key: 'Type', Value: 'Private' },
1354
- { Key: 'ManagedBy', Value: 'Frigg' },
1355
- ],
1356
- },
1357
- };
1358
-
1359
- definition.resources.Resources.FriggPublicSubnet = {
1360
- Type: 'AWS::EC2::Subnet',
1361
- Properties: {
1362
- VpcId: subnetVpcId,
1363
- CidrBlock: publicSubnetCidr,
1364
- MapPublicIpOnLaunch: true,
1365
- AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
1366
- Tags: [
1367
- {
1368
- Key: 'Name',
1369
- Value: '${self:service}-${self:provider.stage}-public-1',
1370
- },
1371
- { Key: 'Type', Value: 'Public' },
1372
- { Key: 'ManagedBy', Value: 'Frigg' },
1373
- ],
1374
- },
1375
- };
1376
-
1377
- // Create second public subnet in different AZ for Aurora
1378
- let publicSubnet2Cidr;
1379
- if (vpcManagement === 'create-new') {
1380
- const generatedCidrs = { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] };
1381
- publicSubnet2Cidr = { 'Fn::Select': [3, generatedCidrs] };
1382
- } else {
1383
- publicSubnet2Cidr = '172.31.251.0/24';
1384
- }
1385
-
1386
- definition.resources.Resources.FriggPublicSubnet2 = {
1387
- Type: 'AWS::EC2::Subnet',
1388
- Properties: {
1389
- VpcId: subnetVpcId,
1390
- CidrBlock: publicSubnet2Cidr,
1391
- MapPublicIpOnLaunch: true,
1392
- AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
1393
- Tags: [
1394
- {
1395
- Key: 'Name',
1396
- Value: '${self:service}-${self:provider.stage}-public-2',
1397
- },
1398
- { Key: 'Type', Value: 'Public' },
1399
- { Key: 'ManagedBy', Value: 'Frigg' },
1400
- ],
1401
- },
1402
- };
1403
-
1404
- vpcConfig.subnetIds = [
1405
- { Ref: 'FriggPrivateSubnet1' },
1406
- { Ref: 'FriggPrivateSubnet2' },
1407
- ];
1408
-
1409
- // Map created subnets to discoveredResources for Aurora to use
1410
- discoveredResources.publicSubnetId1 = { Ref: 'FriggPublicSubnet' };
1411
- discoveredResources.publicSubnetId2 = { Ref: 'FriggPublicSubnet2' };
1412
- discoveredResources.privateSubnetId1 = { Ref: 'FriggPrivateSubnet1' };
1413
- discoveredResources.privateSubnetId2 = { Ref: 'FriggPrivateSubnet2' };
1414
-
1415
- if (
1416
- !AppDefinition.vpc.natGateway ||
1417
- AppDefinition.vpc.natGateway.management === 'discover'
1418
- ) {
1419
- if (
1420
- vpcManagement === 'create-new' ||
1421
- !discoveredResources.internetGatewayId
1422
- ) {
1423
- if (!definition.resources.Resources.FriggInternetGateway) {
1424
- definition.resources.Resources.FriggInternetGateway = {
1425
- Type: 'AWS::EC2::InternetGateway',
1426
- Properties: {
1427
- Tags: [
1428
- {
1429
- Key: 'Name',
1430
- Value: '${self:service}-${self:provider.stage}-igw',
1431
- },
1432
- { Key: 'ManagedBy', Value: 'Frigg' },
1433
- ],
1434
- },
1435
- };
1436
-
1437
- definition.resources.Resources.FriggIGWAttachment = {
1438
- Type: 'AWS::EC2::VPCGatewayAttachment',
1439
- Properties: {
1440
- VpcId: subnetVpcId,
1441
- InternetGatewayId: { Ref: 'FriggInternetGateway' },
1442
- },
1443
- };
1444
- }
1445
- }
1446
-
1447
- definition.resources.Resources.FriggPublicRouteTable = {
1448
- Type: 'AWS::EC2::RouteTable',
1449
- Properties: {
1450
- VpcId: subnetVpcId,
1451
- Tags: [
1452
- {
1453
- Key: 'Name',
1454
- Value: '${self:service}-${self:provider.stage}-public-rt',
1455
- },
1456
- { Key: 'ManagedBy', Value: 'Frigg' },
1457
- ],
1458
- },
1459
- };
1460
-
1461
- definition.resources.Resources.FriggPublicRoute = {
1462
- Type: 'AWS::EC2::Route',
1463
- DependsOn:
1464
- vpcManagement === 'create-new'
1465
- ? 'FriggIGWAttachment'
1466
- : undefined,
1467
- Properties: {
1468
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
1469
- DestinationCidrBlock: '0.0.0.0/0',
1470
- GatewayId: discoveredResources.internetGatewayId || {
1471
- Ref: 'FriggInternetGateway',
1472
- },
1473
- },
1474
- };
1475
-
1476
- definition.resources.Resources.FriggPublicSubnetRouteTableAssociation =
1477
- {
1478
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1479
- Properties: {
1480
- SubnetId: { Ref: 'FriggPublicSubnet' },
1481
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
1482
- },
1483
- };
1484
-
1485
- definition.resources.Resources.FriggPublicSubnet2RouteTableAssociation =
1486
- {
1487
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1488
- Properties: {
1489
- SubnetId: { Ref: 'FriggPublicSubnet2' },
1490
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
1491
- },
1492
- };
1493
-
1494
- definition.resources.Resources.FriggLambdaRouteTable = {
1495
- Type: 'AWS::EC2::RouteTable',
1496
- Properties: {
1497
- VpcId: subnetVpcId,
1498
- Tags: [
1499
- {
1500
- Key: 'Name',
1501
- Value: '${self:service}-${self:provider.stage}-lambda-rt',
1502
- },
1503
- { Key: 'ManagedBy', Value: 'Frigg' },
1504
- ],
1505
- },
1506
- };
1507
-
1508
- definition.resources.Resources.FriggPrivateSubnet1RouteTableAssociation =
1509
- {
1510
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1511
- Properties: {
1512
- SubnetId: { Ref: 'FriggPrivateSubnet1' },
1513
- RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1514
- },
1515
- };
1516
-
1517
- definition.resources.Resources.FriggPrivateSubnet2RouteTableAssociation =
1518
- {
1519
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1520
- Properties: {
1521
- SubnetId: { Ref: 'FriggPrivateSubnet2' },
1522
- RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1523
- },
1524
- };
1525
- }
1526
- } else if (subnetManagement === 'use-existing') {
1527
- if (
1528
- !AppDefinition.vpc.subnets?.ids ||
1529
- AppDefinition.vpc.subnets.ids.length < 2
1530
- ) {
1531
- throw new Error(
1532
- 'Subnet management is "use-existing" but less than 2 subnet IDs provided. Provide at least 2 subnet IDs in vpc.subnets.ids.'
1533
- );
1534
- }
1535
- vpcConfig.subnetIds = AppDefinition.vpc.subnets.ids;
1536
- } else {
1537
- vpcConfig.subnetIds =
1538
- AppDefinition.vpc.subnets?.ids?.length > 0
1539
- ? AppDefinition.vpc.subnets.ids
1540
- : discoveredResources.privateSubnetId1 &&
1541
- discoveredResources.privateSubnetId2
1542
- ? [
1543
- discoveredResources.privateSubnetId1,
1544
- discoveredResources.privateSubnetId2,
1545
- ]
1546
- : [];
1547
-
1548
- if (vpcConfig.subnetIds.length < 2) {
1549
- if (AppDefinition.vpc.selfHeal) {
1550
- console.log(
1551
- 'No subnets found but self-heal enabled - creating minimal subnet setup'
1552
- );
1553
- subnetManagement = 'create';
1554
- discoveredResources.createSubnets = true;
1555
- } else {
1556
- throw new Error(
1557
- 'No subnets discovered and subnets.management is "discover". Either enable vpc.selfHeal, set subnets.management to "create", or provide subnet IDs.'
1558
- );
1559
- }
1560
- }
1561
- }
1562
-
1563
- if (subnetManagement === 'create' && discoveredResources.createSubnets) {
1564
- definition.resources.Resources.FriggLambdaRouteTable = definition
1565
- .resources.Resources.FriggLambdaRouteTable || {
1566
- Type: 'AWS::EC2::RouteTable',
1567
- Properties: {
1568
- VpcId: effectiveVpcId,
1569
- Tags: [
1570
- {
1571
- Key: 'Name',
1572
- Value: '${self:service}-${self:provider.stage}-lambda-rt',
1573
- },
1574
- { Key: 'ManagedBy', Value: 'Frigg' },
1575
- { Key: 'Environment', Value: '${self:provider.stage}' },
1576
- { Key: 'Service', Value: '${self:service}' },
1577
- ],
1578
- },
1579
- };
1580
- }
1581
-
1582
- if (
1583
- vpcConfig.subnetIds.length >= 2 &&
1584
- vpcConfig.securityGroupIds.length > 0
1585
- ) {
1586
- definition.provider.vpc = vpcConfig;
1587
-
1588
- const natGatewayManagement =
1589
- AppDefinition.vpc.natGateway?.management || 'discover';
1590
- let needsNewNatGateway =
1591
- natGatewayManagement === 'createAndManage' ||
1592
- discoveredResources.needsNewNatGateway === true;
1593
-
1594
- console.log('needsNewNatGateway', needsNewNatGateway);
1595
-
1596
- let reuseExistingNatGateway = false;
1597
- let useExistingEip = false;
1598
-
1599
- if (needsNewNatGateway) {
1600
- console.log(
1601
- 'Create mode: Creating dedicated EIP, public subnet, and NAT Gateway...'
1602
- );
1603
-
1604
- if (
1605
- discoveredResources.existingNatGatewayId &&
1606
- discoveredResources.existingElasticIpAllocationId
1607
- ) {
1608
- console.log('Found existing Frigg-managed NAT Gateway and EIP');
1609
- if (!discoveredResources.natGatewayInPrivateSubnet) {
1610
- console.log(
1611
- '✅ Existing NAT Gateway is in PUBLIC subnet, will reuse it'
1612
- );
1613
- reuseExistingNatGateway = true;
1614
- } else {
1615
- console.log(
1616
- '❌ NAT Gateway is in PRIVATE subnet - MUST create new one in PUBLIC subnet'
1617
- );
1618
- if (AppDefinition.vpc.selfHeal) {
1619
- console.log(
1620
- 'Self-heal enabled: Creating new NAT Gateway in PUBLIC subnet'
1621
- );
1622
- reuseExistingNatGateway = false;
1623
- useExistingEip = false;
1624
- discoveredResources.needsCleanup = true;
1625
- } else {
1626
- throw new Error(
1627
- 'CRITICAL: NAT Gateway is in PRIVATE subnet (will not work!). Enable vpc.selfHeal to auto-fix or set natGateway.management to "createAndManage".'
1628
- );
1629
- }
1630
- }
1631
- } else if (
1632
- discoveredResources.existingElasticIpAllocationId &&
1633
- !discoveredResources.existingNatGatewayId
1634
- ) {
1635
- console.log(
1636
- 'Found orphaned EIP, will reuse it for new NAT Gateway in PUBLIC subnet'
1637
- );
1638
- useExistingEip = true;
1639
- }
1640
-
1641
- if (reuseExistingNatGateway) {
1642
- console.log(
1643
- 'Reusing existing NAT Gateway - skipping resource creation'
1644
- );
1645
- } else {
1646
- if (!useExistingEip) {
1647
- definition.resources.Resources.FriggNATGatewayEIP = {
1648
- Type: 'AWS::EC2::EIP',
1649
- DeletionPolicy: 'Retain',
1650
- UpdateReplacePolicy: 'Retain',
1651
- Properties: {
1652
- Domain: 'vpc',
1653
- Tags: [
1654
- {
1655
- Key: 'Name',
1656
- Value: '${self:service}-${self:provider.stage}-nat-eip',
1657
- },
1658
- { Key: 'ManagedBy', Value: 'Frigg' },
1659
- { Key: 'Service', Value: '${self:service}' },
1660
- {
1661
- Key: 'Stage',
1662
- Value: '${self:provider.stage}',
1663
- },
1664
- ],
1665
- },
1666
- };
1667
- }
1668
-
1669
- if (!discoveredResources.publicSubnetId) {
1670
- if (discoveredResources.internetGatewayId) {
1671
- console.log(
1672
- 'Reusing existing Internet Gateway for NAT Gateway'
1673
- );
1674
- } else {
1675
- definition.resources.Resources.FriggInternetGateway =
1676
- definition.resources.Resources
1677
- .FriggInternetGateway || {
1678
- Type: 'AWS::EC2::InternetGateway',
1679
- Properties: {
1680
- Tags: [
1681
- {
1682
- Key: 'Name',
1683
- Value: '${self:service}-${self:provider.stage}-igw',
1684
- },
1685
- { Key: 'ManagedBy', Value: 'Frigg' },
1686
- ],
1687
- },
1688
- };
1689
-
1690
- definition.resources.Resources.FriggIGWAttachment =
1691
- definition.resources.Resources
1692
- .FriggIGWAttachment || {
1693
- Type: 'AWS::EC2::VPCGatewayAttachment',
1694
- Properties: {
1695
- VpcId: discoveredResources.defaultVpcId,
1696
- InternetGatewayId: {
1697
- Ref: 'FriggInternetGateway',
1698
- },
1699
- },
1700
- };
1701
- }
1702
-
1703
- definition.resources.Resources.FriggPublicSubnet = {
1704
- Type: 'AWS::EC2::Subnet',
1705
- Properties: {
1706
- VpcId: discoveredResources.defaultVpcId,
1707
- CidrBlock:
1708
- AppDefinition.vpc.natGateway
1709
- ?.publicSubnetCidr || '172.31.250.0/24',
1710
- AvailabilityZone: {
1711
- 'Fn::Select': [0, { 'Fn::GetAZs': '' }],
1712
- },
1713
- MapPublicIpOnLaunch: true,
1714
- Tags: [
1715
- {
1716
- Key: 'Name',
1717
- Value: '${self:service}-${self:provider.stage}-public-subnet-1',
1718
- },
1719
- { Key: 'Type', Value: 'Public' },
1720
- ],
1721
- },
1722
- };
1723
-
1724
- definition.resources.Resources.FriggPublicSubnet2 = {
1725
- Type: 'AWS::EC2::Subnet',
1726
- Properties: {
1727
- VpcId: discoveredResources.defaultVpcId,
1728
- CidrBlock:
1729
- AppDefinition.vpc.natGateway
1730
- ?.publicSubnetCidr2 || '172.31.251.0/24',
1731
- AvailabilityZone: {
1732
- 'Fn::Select': [1, { 'Fn::GetAZs': '' }],
1733
- },
1734
- MapPublicIpOnLaunch: true,
1735
- Tags: [
1736
- {
1737
- Key: 'Name',
1738
- Value: '${self:service}-${self:provider.stage}-public-subnet-2',
1739
- },
1740
- { Key: 'Type', Value: 'Public' },
1741
- ],
1742
- },
1743
- };
1744
-
1745
- definition.resources.Resources.FriggPublicRouteTable = {
1746
- Type: 'AWS::EC2::RouteTable',
1747
- Properties: {
1748
- VpcId: discoveredResources.defaultVpcId,
1749
- Tags: [
1750
- {
1751
- Key: 'Name',
1752
- Value: '${self:service}-${self:provider.stage}-public-rt',
1753
- },
1754
- ],
1755
- },
1756
- };
1757
-
1758
- definition.resources.Resources.FriggPublicRoute = {
1759
- Type: 'AWS::EC2::Route',
1760
- DependsOn: discoveredResources.internetGatewayId
1761
- ? []
1762
- : 'FriggIGWAttachment',
1763
- Properties: {
1764
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
1765
- DestinationCidrBlock: '0.0.0.0/0',
1766
- GatewayId:
1767
- discoveredResources.internetGatewayId || {
1768
- Ref: 'FriggInternetGateway',
1769
- },
1770
- },
1771
- };
1772
-
1773
- definition.resources.Resources.FriggPublicSubnetRouteTableAssociation =
1774
- {
1775
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1776
- Properties: {
1777
- SubnetId: { Ref: 'FriggPublicSubnet' },
1778
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
1779
- },
1780
- };
1781
-
1782
- definition.resources.Resources.FriggPublicSubnet2RouteTableAssociation =
1783
- {
1784
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1785
- Properties: {
1786
- SubnetId: { Ref: 'FriggPublicSubnet2' },
1787
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
1788
- },
1789
- };
1790
-
1791
- // Map created public subnets to discoveredResources for Aurora
1792
- discoveredResources.publicSubnetId1 = { Ref: 'FriggPublicSubnet' };
1793
- discoveredResources.publicSubnetId2 = { Ref: 'FriggPublicSubnet2' };
1794
- }
1795
-
1796
- definition.resources.Resources.FriggNATGateway = {
1797
- Type: 'AWS::EC2::NatGateway',
1798
- DeletionPolicy: 'Retain',
1799
- UpdateReplacePolicy: 'Retain',
1800
- Properties: {
1801
- AllocationId: useExistingEip
1802
- ? discoveredResources.existingElasticIpAllocationId
1803
- : {
1804
- 'Fn::GetAtt': [
1805
- 'FriggNATGatewayEIP',
1806
- 'AllocationId',
1807
- ],
1808
- },
1809
- SubnetId: discoveredResources.publicSubnetId || {
1810
- Ref: 'FriggPublicSubnet',
1811
- },
1812
- Tags: [
1813
- {
1814
- Key: 'Name',
1815
- Value: '${self:service}-${self:provider.stage}-nat-gateway',
1816
- },
1817
- { Key: 'ManagedBy', Value: 'Frigg' },
1818
- { Key: 'Service', Value: '${self:service}' },
1819
- { Key: 'Stage', Value: '${self:provider.stage}' },
1820
- ],
1821
- },
1822
- };
1823
- }
1824
- } else if (
1825
- natGatewayManagement === 'discover' ||
1826
- natGatewayManagement === 'useExisting'
1827
- ) {
1828
- if (
1829
- natGatewayManagement === 'useExisting' &&
1830
- AppDefinition.vpc.natGateway?.id
1831
- ) {
1832
- console.log(
1833
- `Using explicitly provided NAT Gateway: ${AppDefinition.vpc.natGateway.id}`
1834
- );
1835
- discoveredResources.existingNatGatewayId =
1836
- AppDefinition.vpc.natGateway.id;
1837
- }
1838
-
1839
- if (discoveredResources.existingNatGatewayId) {
1840
- console.log(
1841
- 'discoveredResources.existingNatGatewayId',
1842
- discoveredResources.existingNatGatewayId
1843
- );
1844
-
1845
- if (discoveredResources.natGatewayInPrivateSubnet) {
1846
- console.log(
1847
- '❌ CRITICAL: NAT Gateway is in PRIVATE subnet - Internet connectivity will NOT work!'
1848
- );
1849
-
1850
- if (AppDefinition.vpc.selfHeal === true) {
1851
- console.log(
1852
- 'Self-heal enabled: Will create new NAT Gateway in PUBLIC subnet'
1853
- );
1854
- needsNewNatGateway = true;
1855
- discoveredResources.existingNatGatewayId = null;
1856
- if (!discoveredResources.publicSubnetId) {
1857
- console.log(
1858
- 'No public subnet found - will create one for NAT Gateway'
1859
- );
1860
- discoveredResources.createPublicSubnet = true;
1861
- }
1862
- } else {
1863
- throw new Error(
1864
- '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.'
1865
- );
1866
- }
1867
- } else {
1868
- console.log(
1869
- `Using discovered NAT Gateway for routing: ${discoveredResources.existingNatGatewayId}`
1870
- );
1871
- }
1872
- } else if (
1873
- !needsNewNatGateway &&
1874
- AppDefinition.vpc.natGateway?.id
1875
- ) {
1876
- console.log(
1877
- `Using explicitly provided NAT Gateway: ${AppDefinition.vpc.natGateway.id}`
1878
- );
1879
- discoveredResources.existingNatGatewayId =
1880
- AppDefinition.vpc.natGateway.id;
1881
- }
1882
- }
1883
-
1884
- definition.resources.Resources.FriggLambdaRouteTable = definition
1885
- .resources.Resources.FriggLambdaRouteTable || {
1886
- Type: 'AWS::EC2::RouteTable',
1887
- Properties: {
1888
- VpcId: discoveredResources.defaultVpcId || vpcId,
1889
- Tags: [
1890
- {
1891
- Key: 'Name',
1892
- Value: '${self:service}-${self:provider.stage}-lambda-rt',
1893
- },
1894
- { Key: 'ManagedBy', Value: 'Frigg' },
1895
- { Key: 'Environment', Value: '${self:provider.stage}' },
1896
- { Key: 'Service', Value: '${self:service}' },
1897
- ],
1898
- },
1899
- };
1900
-
1901
- const routeTableId = { Ref: 'FriggLambdaRouteTable' };
1902
- let natGatewayIdForRoute;
1903
-
1904
- if (reuseExistingNatGateway) {
1905
- natGatewayIdForRoute = discoveredResources.existingNatGatewayId;
1906
- console.log(
1907
- `Using discovered NAT Gateway for routing: ${natGatewayIdForRoute}`
1908
- );
1909
- } else if (needsNewNatGateway && !reuseExistingNatGateway) {
1910
- natGatewayIdForRoute = { Ref: 'FriggNATGateway' };
1911
- console.log('Using newly created NAT Gateway for routing');
1912
- } else if (discoveredResources.existingNatGatewayId) {
1913
- natGatewayIdForRoute = discoveredResources.existingNatGatewayId;
1914
- console.log(
1915
- `Using discovered NAT Gateway for routing: ${natGatewayIdForRoute}`
1916
- );
1917
- } else if (AppDefinition.vpc.natGateway?.id) {
1918
- natGatewayIdForRoute = AppDefinition.vpc.natGateway.id;
1919
- console.log(
1920
- `Using explicitly provided NAT Gateway for routing: ${natGatewayIdForRoute}`
1921
- );
1922
- } else if (AppDefinition.vpc.selfHeal === true) {
1923
- natGatewayIdForRoute = null;
1924
- console.log(
1925
- 'No NAT Gateway available - skipping NAT route creation'
1926
- );
1927
- } else {
1928
- throw new Error('No existing NAT Gateway found in discovery mode');
1929
- }
1930
-
1931
- if (natGatewayIdForRoute) {
1932
- console.log(
1933
- `Configuring NAT route: 0.0.0.0/0 → ${natGatewayIdForRoute}`
1934
- );
1935
- definition.resources.Resources.FriggNATRoute = {
1936
- Type: 'AWS::EC2::Route',
1937
- DependsOn: 'FriggLambdaRouteTable',
1938
- Properties: {
1939
- RouteTableId: routeTableId,
1940
- DestinationCidrBlock: '0.0.0.0/0',
1941
- NatGatewayId: natGatewayIdForRoute,
1942
- },
1943
- };
1944
- } else {
1945
- console.warn(
1946
- '⚠️ No NAT Gateway configured - Lambda functions will not have internet access'
1947
- );
1948
- }
1949
-
1950
- if (typeof vpcConfig.subnetIds[0] === 'string') {
1951
- definition.resources.Resources.FriggSubnet1RouteAssociation = {
1952
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1953
- Properties: {
1954
- SubnetId: vpcConfig.subnetIds[0],
1955
- RouteTableId: routeTableId,
1956
- },
1957
- DependsOn: 'FriggLambdaRouteTable',
1958
- };
1959
- }
1960
-
1961
- if (typeof vpcConfig.subnetIds[1] === 'string') {
1962
- definition.resources.Resources.FriggSubnet2RouteAssociation = {
1963
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1964
- Properties: {
1965
- SubnetId: vpcConfig.subnetIds[1],
1966
- RouteTableId: routeTableId,
1967
- },
1968
- DependsOn: 'FriggLambdaRouteTable',
1969
- };
1970
- }
1971
-
1972
- if (
1973
- typeof vpcConfig.subnetIds[0] === 'object' &&
1974
- vpcConfig.subnetIds[0].Ref
1975
- ) {
1976
- definition.resources.Resources.FriggNewSubnet1RouteAssociation = {
1977
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1978
- Properties: {
1979
- SubnetId: vpcConfig.subnetIds[0],
1980
- RouteTableId: routeTableId,
1981
- },
1982
- DependsOn: [
1983
- 'FriggLambdaRouteTable',
1984
- vpcConfig.subnetIds[0].Ref,
1985
- ],
1986
- };
1987
- }
1988
-
1989
- if (
1990
- typeof vpcConfig.subnetIds[1] === 'object' &&
1991
- vpcConfig.subnetIds[1].Ref
1992
- ) {
1993
- definition.resources.Resources.FriggNewSubnet2RouteAssociation = {
1994
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1995
- Properties: {
1996
- SubnetId: vpcConfig.subnetIds[1],
1997
- RouteTableId: routeTableId,
1998
- },
1999
- DependsOn: [
2000
- 'FriggLambdaRouteTable',
2001
- vpcConfig.subnetIds[1].Ref,
2002
- ],
2003
- };
2004
- }
2005
-
2006
- if (AppDefinition.vpc.enableVPCEndpoints !== false) {
2007
- definition.resources.Resources.VPCEndpointS3 = {
2008
- Type: 'AWS::EC2::VPCEndpoint',
2009
- Properties: {
2010
- VpcId: discoveredResources.defaultVpcId,
2011
- ServiceName: 'com.amazonaws.${self:provider.region}.s3',
2012
- VpcEndpointType: 'Gateway',
2013
- RouteTableIds: [routeTableId],
2014
- },
2015
- };
2016
-
2017
- definition.resources.Resources.VPCEndpointDynamoDB = {
2018
- Type: 'AWS::EC2::VPCEndpoint',
2019
- Properties: {
2020
- VpcId: discoveredResources.defaultVpcId,
2021
- ServiceName:
2022
- 'com.amazonaws.${self:provider.region}.dynamodb',
2023
- VpcEndpointType: 'Gateway',
2024
- RouteTableIds: [routeTableId],
2025
- },
2026
- };
2027
- }
2028
-
2029
- if (AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
2030
- if (!discoveredResources.vpcCidr) {
2031
- console.warn(
2032
- '⚠️ Warning: VPC CIDR not discovered. VPC endpoint security group may not work correctly.'
2033
- );
2034
- }
2035
-
2036
- if (!definition.resources.Resources.VPCEndpointSecurityGroup) {
2037
- const vpcEndpointIngressRules = [];
2038
-
2039
- if (
2040
- vpcConfig.securityGroupIds &&
2041
- vpcConfig.securityGroupIds.length > 0
2042
- ) {
2043
- for (const sg of vpcConfig.securityGroupIds) {
2044
- if (typeof sg === 'string') {
2045
- vpcEndpointIngressRules.push({
2046
- IpProtocol: 'tcp',
2047
- FromPort: 443,
2048
- ToPort: 443,
2049
- SourceSecurityGroupId: sg,
2050
- Description: 'HTTPS from Lambda security group',
2051
- });
2052
- } else if (sg.Ref) {
2053
- vpcEndpointIngressRules.push({
2054
- IpProtocol: 'tcp',
2055
- FromPort: 443,
2056
- ToPort: 443,
2057
- SourceSecurityGroupId: { Ref: sg.Ref },
2058
- Description: 'HTTPS from Lambda security group',
2059
- });
2060
- }
2061
- }
2062
- }
2063
-
2064
- if (vpcEndpointIngressRules.length === 0) {
2065
- if (discoveredResources.vpcCidr) {
2066
- vpcEndpointIngressRules.push({
2067
- IpProtocol: 'tcp',
2068
- FromPort: 443,
2069
- ToPort: 443,
2070
- CidrIp: discoveredResources.vpcCidr,
2071
- Description: 'HTTPS from VPC CIDR (fallback)',
2072
- });
2073
- } else {
2074
- console.warn(
2075
- '⚠️ WARNING: No Lambda security group or VPC CIDR found. Using default private IP ranges.'
2076
- );
2077
- vpcEndpointIngressRules.push({
2078
- IpProtocol: 'tcp',
2079
- FromPort: 443,
2080
- ToPort: 443,
2081
- CidrIp: '172.31.0.0/16',
2082
- Description: 'HTTPS from default VPC range',
2083
- });
2084
- }
2085
- }
2086
-
2087
- definition.resources.Resources.VPCEndpointSecurityGroup = {
2088
- Type: 'AWS::EC2::SecurityGroup',
2089
- Properties: {
2090
- GroupDescription:
2091
- 'Security group for VPC endpoints - allows HTTPS from Lambda functions',
2092
- VpcId: discoveredResources.defaultVpcId,
2093
- SecurityGroupIngress: vpcEndpointIngressRules,
2094
- Tags: [
2095
- {
2096
- Key: 'Name',
2097
- Value: '${self:service}-${self:provider.stage}-vpc-endpoints-sg',
2098
- },
2099
- { Key: 'ManagedBy', Value: 'Frigg' },
2100
- {
2101
- Key: 'Purpose',
2102
- Value: 'Allow Lambda functions to access VPC endpoints',
2103
- },
2104
- ],
2105
- },
2106
- };
2107
- }
2108
-
2109
- definition.resources.Resources.VPCEndpointKMS = {
2110
- Type: 'AWS::EC2::VPCEndpoint',
2111
- Properties: {
2112
- VpcId: discoveredResources.defaultVpcId,
2113
- ServiceName: 'com.amazonaws.${self:provider.region}.kms',
2114
- VpcEndpointType: 'Interface',
2115
- SubnetIds: vpcConfig.subnetIds,
2116
- SecurityGroupIds: [{ Ref: 'VPCEndpointSecurityGroup' }],
2117
- PrivateDnsEnabled: true,
2118
- },
2119
- };
2120
-
2121
- // Create Secrets Manager VPC Endpoint if explicitly enabled OR if Aurora is enabled
2122
- // (Aurora requires Secrets Manager access for credential retrieval)
2123
- if (AppDefinition.secretsManager?.enable === true || AppDefinition.database?.postgres?.enable === true) {
2124
- definition.resources.Resources.VPCEndpointSecretsManager = {
2125
- Type: 'AWS::EC2::VPCEndpoint',
2126
- Properties: {
2127
- VpcId: discoveredResources.defaultVpcId,
2128
- ServiceName:
2129
- 'com.amazonaws.${self:provider.region}.secretsmanager',
2130
- VpcEndpointType: 'Interface',
2131
- SubnetIds: vpcConfig.subnetIds,
2132
- SecurityGroupIds: [{ Ref: 'VPCEndpointSecurityGroup' }],
2133
- PrivateDnsEnabled: true,
2134
- },
2135
- };
2136
- }
2137
- }
2138
- }
2139
- };
2140
-
2141
- const createAuroraInfrastructure = (definition, AppDefinition, discoveredResources) => {
2142
- const dbConfig = AppDefinition.database.postgres;
2143
- const publiclyAccessible = dbConfig.publiclyAccessible === true;
2144
-
2145
- console.log('🔧 Creating Aurora Serverless v2 infrastructure...');
2146
- console.log(` Publicly Accessible: ${publiclyAccessible}`);
2147
-
2148
- // 1. DB Subnet Group
2149
- // Use public subnets if publicly accessible, private subnets otherwise
2150
- let subnetIds;
2151
- if (publiclyAccessible) {
2152
- subnetIds = [discoveredResources.publicSubnetId1, discoveredResources.publicSubnetId2];
2153
- console.log(` Using public subnets: ${subnetIds.join(', ')}`);
2154
-
2155
- // Safety check - this should have been caught earlier, but double-check
2156
- if (!subnetIds[0] || !subnetIds[1]) {
2157
- throw new Error(
2158
- 'Public subnets are required for publicly accessible Aurora deployment but were not found. ' +
2159
- 'This should have been caught earlier in validation.'
2160
- );
2161
- }
2162
- } else {
2163
- subnetIds = [discoveredResources.privateSubnetId1, discoveredResources.privateSubnetId2];
2164
- console.log(` Using private subnets: ${subnetIds.join(', ')}`);
2165
-
2166
- // Safety check - this should have been caught earlier, but double-check
2167
- if (!subnetIds[0] || !subnetIds[1]) {
2168
- throw new Error(
2169
- 'Private subnets are required for private Aurora deployment but were not found. ' +
2170
- 'This should have been caught earlier in validation.'
2171
- );
2172
- }
2173
- }
2174
-
2175
- definition.resources.Resources.FriggDBSubnetGroup = {
2176
- Type: 'AWS::RDS::DBSubnetGroup',
2177
- Properties: {
2178
- DBSubnetGroupDescription: `Subnet group for Frigg Aurora cluster (${publiclyAccessible ? 'public' : 'private'})`,
2179
- SubnetIds: subnetIds,
2180
- Tags: [
2181
- { Key: 'Name', Value: '${self:service}-${self:provider.stage}-db-subnet-group' },
2182
- { Key: 'ManagedBy', Value: 'Frigg' },
2183
- { Key: 'Service', Value: '${self:service}' },
2184
- { Key: 'Stage', Value: '${self:provider.stage}' },
2185
- ]
2186
- }
2187
- };
2188
-
2189
- // 2. Security Group
2190
- // Build security group ingress rules based on configuration
2191
- const securityGroupIngress = [];
2192
-
2193
- // Always allow Lambda functions to access the database (if VPC is configured)
2194
- if (AppDefinition.vpc?.enable) {
2195
- const lambdaSecurityGroupId = AppDefinition.vpc?.management === 'create-new'
2196
- ? { Ref: 'FriggLambdaSecurityGroup' }
2197
- : discoveredResources.defaultSecurityGroupId;
2198
-
2199
- securityGroupIngress.push({
2200
- IpProtocol: 'tcp',
2201
- FromPort: 5432,
2202
- ToPort: 5432,
2203
- SourceSecurityGroupId: lambdaSecurityGroupId,
2204
- Description: 'PostgreSQL access from Lambda functions'
2205
- });
2206
- }
2207
-
2208
- // Add IP whitelist rules for public access
2209
- if (publiclyAccessible && dbConfig.allowedIpAddresses) {
2210
- const allowedIps = Array.isArray(dbConfig.allowedIpAddresses)
2211
- ? dbConfig.allowedIpAddresses
2212
- : [dbConfig.allowedIpAddresses];
2213
-
2214
- console.log(` Adding ${allowedIps.length} whitelisted IP address(es)`);
2215
-
2216
- allowedIps.forEach((ip, index) => {
2217
- // Ensure IP has CIDR notation
2218
- const cidrIp = ip.includes('/') ? ip : `${ip}/32`;
2219
- securityGroupIngress.push({
2220
- IpProtocol: 'tcp',
2221
- FromPort: 5432,
2222
- ToPort: 5432,
2223
- CidrIp: cidrIp,
2224
- Description: `PostgreSQL access from whitelisted IP ${index + 1}`
2225
- });
2226
- });
2227
- }
2228
-
2229
- // If publicly accessible but no IPs specified, warn the user
2230
- if (publiclyAccessible && !dbConfig.allowedIpAddresses) {
2231
- console.log(' ⚠️ WARNING: Database is publicly accessible but no IP whitelist configured!');
2232
- console.log(' ⚠️ Add allowedIpAddresses to your database.postgres config for security.');
2233
- }
2234
-
2235
- definition.resources.Resources.FriggAuroraSecurityGroup = {
2236
- Type: 'AWS::EC2::SecurityGroup',
2237
- Properties: {
2238
- GroupDescription: `Security group for Frigg Aurora PostgreSQL (${publiclyAccessible ? 'public' : 'private'})`,
2239
- VpcId: discoveredResources.defaultVpcId,
2240
- SecurityGroupIngress: securityGroupIngress,
2241
- Tags: [
2242
- { Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-sg' },
2243
- { Key: 'ManagedBy', Value: 'Frigg' },
2244
- { Key: 'Service', Value: '${self:service}' },
2245
- { Key: 'Stage', Value: '${self:provider.stage}' },
2246
- ]
2247
- }
2248
- };
2249
-
2250
- // 3. Secrets Manager Secret (database credentials)
2251
- definition.resources.Resources.FriggDatabaseSecret = {
2252
- Type: 'AWS::SecretsManager::Secret',
2253
- Properties: {
2254
- Name: '${self:service}-${self:provider.stage}-aurora-credentials',
2255
- Description: 'Aurora PostgreSQL credentials for Frigg application',
2256
- GenerateSecretString: {
2257
- SecretStringTemplate: JSON.stringify({
2258
- username: dbConfig.masterUsername || 'frigg_admin'
2259
- }),
2260
- GenerateStringKey: 'password',
2261
- PasswordLength: 32,
2262
- ExcludeCharacters: '"@/\\'
2263
- },
2264
- Tags: [
2265
- { Key: 'ManagedBy', Value: 'Frigg' },
2266
- { Key: 'Service', Value: '${self:service}' },
2267
- { Key: 'Stage', Value: '${self:provider.stage}' },
2268
- ]
2269
- }
2270
- };
2271
-
2272
- // 4. Aurora Serverless v2 Cluster
2273
- definition.resources.Resources.FriggAuroraCluster = {
2274
- Type: 'AWS::RDS::DBCluster',
2275
- DeletionPolicy: 'Snapshot',
2276
- UpdateReplacePolicy: 'Snapshot',
2277
- Properties: {
2278
- Engine: 'aurora-postgresql',
2279
- EngineVersion: dbConfig.engineVersion || '15.3',
2280
- EngineMode: 'provisioned', // Required for Serverless v2
2281
- DatabaseName: dbConfig.databaseName || 'frigg_db',
2282
- MasterUsername: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:username}}' },
2283
- MasterUserPassword: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:password}}' },
2284
- DBSubnetGroupName: { Ref: 'FriggDBSubnetGroup' },
2285
- VpcSecurityGroupIds: [{ Ref: 'FriggAuroraSecurityGroup' }],
2286
- ServerlessV2ScalingConfiguration: {
2287
- MinCapacity: dbConfig.scaling?.minCapacity || 0.5,
2288
- MaxCapacity: dbConfig.scaling?.maxCapacity || 1.0
2289
- },
2290
- BackupRetentionPeriod: dbConfig.backupRetentionDays || 7,
2291
- PreferredBackupWindow: dbConfig.preferredBackupWindow || '03:00-04:00',
2292
- DeletionProtection: dbConfig.deletionProtection !== false,
2293
- EnableCloudwatchLogsExports: ['postgresql'],
2294
- Tags: [
2295
- { Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-cluster' },
2296
- { Key: 'ManagedBy', Value: 'Frigg' },
2297
- { Key: 'Service', Value: '${self:service}' },
2298
- { Key: 'Stage', Value: '${self:provider.stage}' },
2299
- ]
2300
- }
2301
- };
2302
-
2303
- // 5. Aurora Serverless v2 Instance
2304
- definition.resources.Resources.FriggAuroraInstance = {
2305
- Type: 'AWS::RDS::DBInstance',
2306
- Properties: {
2307
- Engine: 'aurora-postgresql',
2308
- DBInstanceClass: 'db.serverless',
2309
- DBClusterIdentifier: { Ref: 'FriggAuroraCluster' },
2310
- PubliclyAccessible: publiclyAccessible,
2311
- EnablePerformanceInsights: dbConfig.enablePerformanceInsights || false,
2312
- Tags: [
2313
- { Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-instance' },
2314
- { Key: 'ManagedBy', Value: 'Frigg' },
2315
- { Key: 'Service', Value: '${self:service}' },
2316
- { Key: 'Stage', Value: '${self:provider.stage}' },
2317
- ]
2318
- }
2319
- };
2320
-
2321
- // 6. Secret Attachment (links cluster to secret)
2322
- definition.resources.Resources.FriggSecretAttachment = {
2323
- Type: 'AWS::SecretsManager::SecretTargetAttachment',
2324
- Properties: {
2325
- SecretId: { Ref: 'FriggDatabaseSecret' },
2326
- TargetId: { Ref: 'FriggAuroraCluster' },
2327
- TargetType: 'AWS::RDS::DBCluster'
2328
- }
2329
- };
2330
-
2331
- // 7. Add IAM permissions for Secrets Manager
2332
- definition.provider.iamRoleStatements.push({
2333
- Effect: 'Allow',
2334
- Action: [
2335
- 'secretsmanager:GetSecretValue',
2336
- 'secretsmanager:DescribeSecret'
2337
- ],
2338
- Resource: { Ref: 'FriggDatabaseSecret' }
2339
- });
2340
-
2341
- // 8. Set DATABASE_URL environment variable
2342
- definition.provider.environment.DATABASE_URL = {
2343
- 'Fn::Sub': [
2344
- 'postgresql://${Username}:${Password}@${Endpoint}:5432/${DatabaseName}',
2345
- {
2346
- Username: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:username}}' },
2347
- Password: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:password}}' },
2348
- Endpoint: { 'Fn::GetAtt': ['FriggAuroraCluster', 'Endpoint'] },
2349
- DatabaseName: dbConfig.databaseName || 'frigg_db'
2350
- }
2351
- ]
2352
- };
2353
-
2354
- // 9. Set DB_TYPE for Prisma client selection
2355
- definition.provider.environment.DB_TYPE = 'postgresql';
2356
-
2357
- console.log('✅ Aurora infrastructure resources created');
2358
- };
2359
-
2360
- const useExistingAurora = (definition, AppDefinition, discoveredResources) => {
2361
- const dbConfig = AppDefinition.database.postgres;
2362
- const selfHeal = AppDefinition.database?.postgres?.selfHeal !== false; // Default to true
2363
-
2364
- console.log(`🔗 Using existing Aurora cluster: ${discoveredResources.aurora.clusterIdentifier}`);
2365
- console.log(`[DEBUG] discoveredResources.aurora.isFriggManaged: ${discoveredResources.aurora.isFriggManaged}`);
2366
- console.log(`[DEBUG] selfHeal: ${selfHeal}`);
2367
- console.log(`[DEBUG] discoveredResources.aurora.secretArn: ${discoveredResources.aurora.secretArn}`);
2368
- console.log(`[DEBUG] dbConfig.secretArn: ${dbConfig.secretArn}`);
2369
-
2370
- // Add IAM permissions for Secrets Manager if secret exists
2371
- if (discoveredResources.aurora.secretArn) {
2372
- definition.provider.iamRoleStatements.push({
2373
- Effect: 'Allow',
2374
- Action: [
2375
- 'secretsmanager:GetSecretValue',
2376
- 'secretsmanager:DescribeSecret'
2377
- ],
2378
- Resource: discoveredResources.aurora.secretArn
2379
- });
2380
-
2381
- // Set DATABASE_URL from discovered secret
2382
- definition.provider.environment.DATABASE_URL = {
2383
- 'Fn::Sub': [
2384
- 'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
2385
- {
2386
- Username: { 'Fn::Sub': `{{resolve:secretsmanager:${discoveredResources.aurora.secretArn}:SecretString:username}}` },
2387
- Password: { 'Fn::Sub': `{{resolve:secretsmanager:${discoveredResources.aurora.secretArn}:SecretString:password}}` },
2388
- Endpoint: discoveredResources.aurora.endpoint,
2389
- Port: discoveredResources.aurora.port,
2390
- DatabaseName: dbConfig.databaseName || 'frigg_db'
2391
- }
2392
- ]
2393
- };
2394
- } else if (dbConfig.secretArn) {
2395
- // Use user-provided secret ARN
2396
- definition.provider.iamRoleStatements.push({
2397
- Effect: 'Allow',
2398
- Action: [
2399
- 'secretsmanager:GetSecretValue',
2400
- 'secretsmanager:DescribeSecret'
2401
- ],
2402
- Resource: dbConfig.secretArn
2403
- });
2404
-
2405
- definition.provider.environment.DATABASE_URL = {
2406
- 'Fn::Sub': [
2407
- 'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
2408
- {
2409
- Username: { 'Fn::Sub': `{{resolve:secretsmanager:${dbConfig.secretArn}:SecretString:username}}` },
2410
- Password: { 'Fn::Sub': `{{resolve:secretsmanager:${dbConfig.secretArn}:SecretString:password}}` },
2411
- Endpoint: discoveredResources.aurora.endpoint,
2412
- Port: discoveredResources.aurora.port,
2413
- DatabaseName: dbConfig.databaseName || 'frigg_db'
2414
- }
2415
- ]
2416
- };
2417
- } else if (selfHeal && discoveredResources.aurora?.isFriggManaged) {
2418
- // Self-healing mode: recreate missing secret for Frigg-managed cluster
2419
- console.log('⚠️ No database secret found for Frigg-managed cluster');
2420
- console.log('🔧 Self-healing enabled: Creating new database secret with automatic password rotation');
2421
-
2422
- // Get the current master username from the cluster
2423
- const currentUsername = discoveredResources.aurora.masterUsername || dbConfig.masterUsername || 'frigg_admin';
2424
-
2425
- // Create Secrets Manager Secret (database credentials)
2426
- // Note: We generate a NEW password, which will be synced to the cluster via SecretTargetAttachment
2427
- definition.resources.Resources.FriggDatabaseSecret = {
2428
- Type: 'AWS::SecretsManager::Secret',
2429
- Properties: {
2430
- Name: '${self:service}-${self:provider.stage}-aurora-credentials',
2431
- Description: 'Aurora PostgreSQL credentials for Frigg application (auto-healed)',
2432
- GenerateSecretString: {
2433
- SecretStringTemplate: JSON.stringify({
2434
- username: currentUsername
2435
- }),
2436
- GenerateStringKey: 'password',
2437
- PasswordLength: 32,
2438
- ExcludeCharacters: '"@/\\`\''
2439
- },
2440
- Tags: [
2441
- { Key: 'ManagedBy', Value: 'Frigg' },
2442
- { Key: 'Service', Value: '${self:service}' },
2443
- { Key: 'Stage', Value: '${self:provider.stage}' },
2444
- { Key: 'AutoHealed', Value: 'true' }
2445
- ]
2446
- }
2447
- };
2448
-
2449
- // Create SecretTargetAttachment to link secret to existing cluster
2450
- // This will automatically rotate the cluster password to match the secret!
2451
- definition.resources.Resources.FriggSecretAttachment = {
2452
- Type: 'AWS::SecretsManager::SecretTargetAttachment',
2453
- Properties: {
2454
- SecretId: { Ref: 'FriggDatabaseSecret' },
2455
- TargetId: discoveredResources.aurora.clusterIdentifier,
2456
- TargetType: 'AWS::RDS::DBCluster'
2457
- }
2458
- };
2459
-
2460
- // Add IAM permissions for the new secret
2461
- definition.provider.iamRoleStatements.push({
2462
- Effect: 'Allow',
2463
- Action: [
2464
- 'secretsmanager:GetSecretValue',
2465
- 'secretsmanager:DescribeSecret'
2466
- ],
2467
- Resource: { Ref: 'FriggDatabaseSecret' }
2468
- });
2469
-
2470
- // Set DATABASE_URL from new secret
2471
- definition.provider.environment.DATABASE_URL = {
2472
- 'Fn::Sub': [
2473
- 'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
2474
- {
2475
- Username: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:username}}' },
2476
- Password: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:password}}' },
2477
- Endpoint: discoveredResources.aurora.endpoint,
2478
- Port: discoveredResources.aurora.port,
2479
- DatabaseName: dbConfig.databaseName || 'frigg_db'
2480
- }
2481
- ]
2482
- };
2483
-
2484
- console.log('✅ Self-healing configuration complete:');
2485
- console.log(' - New secret will be created with auto-generated password');
2486
- console.log(' - SecretTargetAttachment will automatically update cluster password');
2487
- console.log(' - No manual password sync required!');
2488
- } else {
2489
- throw new Error(
2490
- 'No database secret found. Options:\n' +
2491
- ' 1. Provide secretArn in database.postgres configuration\n' +
2492
- ' 2. Ensure Secrets Manager secret exists\n' +
2493
- ' 3. Enable self-healing: set database.postgres.selfHeal to true (for Frigg-managed clusters only)'
2494
- );
2495
- }
2496
-
2497
- // Set DB_TYPE for Prisma client selection
2498
- definition.provider.environment.DB_TYPE = 'postgresql';
2499
-
2500
- console.log('✅ Existing Aurora cluster configured');
2501
- };
2502
-
2503
- const useDiscoveredAurora = (definition, AppDefinition, discoveredResources) => {
2504
- console.log(`🔍 Using discovered Aurora cluster: ${discoveredResources.aurora.clusterIdentifier}`);
2505
- useExistingAurora(definition, AppDefinition, discoveredResources);
2506
- };
2507
-
2508
- const configurePostgres = (definition, AppDefinition, discoveredResources) => {
2509
- if (!AppDefinition.database?.postgres?.enable) {
2510
- return;
2511
- }
2512
-
2513
- const dbConfig = AppDefinition.database.postgres;
2514
- const publiclyAccessible = dbConfig.publiclyAccessible === true;
2515
-
2516
- // Validate VPC is enabled for private deployments
2517
- // Public deployments can work without VPC if using default VPC
2518
- if (!publiclyAccessible && !AppDefinition.vpc?.enable) {
2519
- throw new Error(
2520
- 'Aurora PostgreSQL requires VPC deployment for private access. ' +
2521
- 'Either set vpc.enable to true, or set database.postgres.publiclyAccessible to true for public access.'
2522
- );
2523
- }
2524
-
2525
- // Validate subnets based on deployment type
2526
- const vpcManagement = AppDefinition.vpc?.management || 'discover';
2527
-
2528
- if (publiclyAccessible) {
2529
- // For public deployments, validate public subnets exist
2530
- if (vpcManagement !== 'create-new' && (!discoveredResources.publicSubnetId1 || !discoveredResources.publicSubnetId2)) {
2531
- throw new Error(
2532
- 'Aurora PostgreSQL with publiclyAccessible requires at least 2 public subnets in different availability zones. ' +
2533
- 'No public subnets were discovered in your VPC. ' +
2534
- 'Options:\n' +
2535
- ' 1. Create public subnets in your VPC with Internet Gateway attached\n' +
2536
- ' 2. Use VPC management mode "create-new" (will create public subnets automatically)\n' +
2537
- ' 3. Set publiclyAccessible to false and use private subnets instead'
2538
- );
2539
- }
2540
- } else {
2541
- // For private deployments, validate private subnets exist
2542
- if (vpcManagement !== 'create-new' && (!discoveredResources.privateSubnetId1 || !discoveredResources.privateSubnetId2)) {
2543
- throw new Error(
2544
- 'Aurora PostgreSQL requires at least 2 private subnets in different availability zones for private deployment. ' +
2545
- 'No private subnets were discovered in your VPC. ' +
2546
- 'Options:\n' +
2547
- ' 1. Create private subnets in your VPC\n' +
2548
- ' 2. Use VPC management mode "create-new" (will create private subnets automatically)\n' +
2549
- ' 3. Set publiclyAccessible to true and use public subnets instead'
2550
- );
2551
- }
2552
- }
2553
-
2554
- const management = dbConfig.management || 'discover';
2555
-
2556
- console.log(`\n🐘 PostgreSQL Management Mode: ${management}`);
2557
-
2558
- if (management === 'create-new' || discoveredResources.aurora?.needsCreation) {
2559
- createAuroraInfrastructure(definition, AppDefinition, discoveredResources);
2560
- } else if (management === 'use-existing') {
2561
- if (!discoveredResources.aurora?.clusterIdentifier && !dbConfig.clusterIdentifier) {
2562
- throw new Error('PostgreSQL management is set to "use-existing" but no clusterIdentifier was found or provided');
2563
- }
2564
- useExistingAurora(definition, AppDefinition, discoveredResources);
2565
- } else {
2566
- // discover mode
2567
- if (discoveredResources.aurora?.clusterIdentifier) {
2568
- useDiscoveredAurora(definition, AppDefinition, discoveredResources);
2569
- } else {
2570
- throw new Error('No Aurora cluster found in discovery mode. Set management to "create-new" or provide clusterIdentifier with "use-existing".');
2571
- }
2572
- }
2573
- };
2574
-
2575
- const configureSsm = (definition, AppDefinition) => {
2576
- if (AppDefinition.ssm?.enable !== true) {
2577
- return;
2578
- }
2579
-
2580
- definition.provider.layers = [
2581
- 'arn:aws:lambda:${self:provider.region}:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11',
2582
- ];
2583
-
2584
- definition.provider.iamRoleStatements.push({
2585
- Effect: 'Allow',
2586
- Action: [
2587
- 'ssm:GetParameter',
2588
- 'ssm:GetParameters',
2589
- 'ssm:GetParametersByPath',
2590
- ],
2591
- Resource: [
2592
- 'arn:aws:ssm:${self:provider.region}:*:parameter/${self:service}/${self:provider.stage}/*',
2593
- ],
2594
- });
2595
-
2596
- definition.provider.environment.SSM_PARAMETER_PREFIX =
2597
- '/${self:service}/${self:provider.stage}';
2598
- };
2599
-
2600
- const attachIntegrations = (definition, AppDefinition) => {
2601
- if (
2602
- !Array.isArray(AppDefinition.integrations) ||
2603
- AppDefinition.integrations.length === 0
2604
- ) {
2605
- return;
2606
- }
2607
-
2608
- console.log(
2609
- `Processing ${AppDefinition.integrations.length} integrations...`
2610
- );
2611
-
2612
- // Get the functionPackageConfig from the definition (defined in createBaseDefinition)
2613
- const functionPackageConfig = {
2614
- exclude: [
2615
- 'node_modules/aws-sdk/**',
2616
- 'node_modules/@aws-sdk/**',
2617
- 'node_modules/@prisma/**',
2618
- 'node_modules/.prisma/**',
2619
- 'node_modules/prisma/**',
2620
- 'node_modules/@friggframework/core/generated/**',
2621
- ],
2622
- };
2623
-
2624
- for (const integration of AppDefinition.integrations) {
2625
- if (!integration?.Definition?.name) {
2626
- throw new Error('Invalid integration: missing Definition or name');
2627
- }
2628
-
2629
- const integrationName = integration.Definition.name;
2630
- const queueReference = `${integrationName.charAt(0).toUpperCase() + integrationName.slice(1)
2631
- }Queue`;
2632
- const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
2633
-
2634
- definition.functions[integrationName] = {
2635
- handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
2636
- package: functionPackageConfig,
2637
- events: [
2638
- {
2639
- httpApi: {
2640
- path: `/api/${integrationName}-integration/{proxy+}`,
2641
- method: 'ANY',
2642
- },
2643
- },
2644
- ],
2645
- };
2646
-
2647
- definition.resources.Resources[queueReference] = {
2648
- Type: 'AWS::SQS::Queue',
2649
- Properties: {
2650
- QueueName: `\${self:custom.${queueReference}}`,
2651
- MessageRetentionPeriod: 60,
2652
- VisibilityTimeout: 1800,
2653
- RedrivePolicy: {
2654
- maxReceiveCount: 1,
2655
- deadLetterTargetArn: {
2656
- 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
2657
- },
2658
- },
2659
- },
2660
- };
2661
-
2662
- const queueWorkerName = `${integrationName}QueueWorker`;
2663
- definition.functions[queueWorkerName] = {
2664
- handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
2665
- package: functionPackageConfig,
2666
- reservedConcurrency: 5,
2667
- events: [
2668
- {
2669
- sqs: {
2670
- arn: { 'Fn::GetAtt': [queueReference, 'Arn'] },
2671
- batchSize: 1,
2672
- },
2673
- },
2674
- ],
2675
- timeout: 600,
2676
- };
2677
-
2678
- definition.provider.environment = {
2679
- ...definition.provider.environment,
2680
- [`${integrationName.toUpperCase()}_QUEUE_URL`]: {
2681
- Ref: queueReference,
2682
- },
2683
- };
2684
-
2685
- definition.custom[queueReference] = queueName;
2686
-
2687
- // Add webhook handler if enabled
2688
- const webhookConfig = integration.Definition.webhooks;
2689
- if (webhookConfig && (webhookConfig === true || webhookConfig.enabled === true)) {
2690
- const webhookFunctionName = `${integrationName}Webhook`;
2691
-
2692
- definition.functions[webhookFunctionName] = {
2693
- handler: `node_modules/@friggframework/core/handlers/routers/integration-webhook-routers.handlers.${integrationName}Webhook.handler`,
2694
- events: [
2695
- {
2696
- httpApi: {
2697
- path: `/api/${integrationName}-integration/webhooks`,
2698
- method: 'POST',
2699
- },
2700
- },
2701
- {
2702
- httpApi: {
2703
- path: `/api/${integrationName}-integration/webhooks/{integrationId}`,
2704
- method: 'POST',
2705
- },
2706
- },
2707
- ],
2708
- };
2709
- }
2710
- }
2711
- };
2712
-
2713
- const configureWebsockets = (definition, AppDefinition) => {
2714
- if (AppDefinition.websockets?.enable !== true) {
2715
- return;
2716
- }
2717
-
2718
- definition.functions.defaultWebsocket = {
2719
- handler:
2720
- 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
2721
- events: [
2722
- { websocket: { route: '$connect' } },
2723
- { websocket: { route: '$default' } },
2724
- { websocket: { route: '$disconnect' } },
2725
- ],
2726
- };
2727
- };
2728
-
2729
- /**
2730
- * Ensure Prisma Lambda Layer exists
2731
- * Automatically builds the layer if it doesn't exist in the project root
2732
- * @param {Object} databaseConfig - Database configuration from AppDefinition.database
2733
- */
2734
- async function ensurePrismaLayerExists(databaseConfig = {}) {
2735
- const projectRoot = process.cwd();
2736
- const layerPath = path.join(projectRoot, 'layers/prisma');
2737
-
2738
- // Check if layer already exists
2739
- if (fs.existsSync(layerPath)) {
2740
- console.log('✓ Prisma Lambda Layer already exists at', layerPath);
2741
- return;
2742
- }
2743
-
2744
- // Layer doesn't exist - build it automatically
2745
- console.log('📦 Prisma Lambda Layer not found - building automatically...');
2746
- console.log(' Building layer with CLI (used by all functions including dbMigrate)');
2747
- console.log(' This may take a minute on first deployment.\n');
2748
-
2749
- try {
2750
- // Build layer WITH CLI (includeCLI = true) - all functions use same layer
2751
- await buildPrismaLayer(databaseConfig, true);
2752
- console.log('✓ Prisma Lambda Layer built successfully\n');
2753
- } catch (error) {
2754
- console.error('✗ Failed to build Prisma Lambda Layer:', error.message);
2755
- console.error(' You may need to run: npm install @friggframework/core\n');
2756
- throw error;
2757
- }
2758
- }
2759
-
2760
- const composeServerlessDefinition = async (AppDefinition) => {
2761
- console.log('composeServerlessDefinition', AppDefinition);
2762
-
2763
- // Ensure Prisma layer exists before generating serverless config
2764
- // Pass database config so layer only includes needed database clients
2765
- await ensurePrismaLayerExists(AppDefinition.database || {});
2766
-
2767
- const discoveredResources = await gatherDiscoveredResources(AppDefinition);
2768
- const appEnvironmentVars = getAppEnvironmentVars(AppDefinition);
2769
- const definition = createBaseDefinition(
2770
- AppDefinition,
2771
- appEnvironmentVars,
2772
- discoveredResources
2773
- );
2774
-
2775
- // Check if we're in local build mode (AWS discovery was skipped)
2776
- const isLocalBuild = !shouldRunDiscovery(AppDefinition);
2777
-
2778
- if (isLocalBuild) {
2779
- console.log(
2780
- '🏠 Local build mode detected - skipping AWS-dependent configurations'
2781
- );
2782
- }
2783
-
2784
- // Apply configurations (skip AWS-dependent ones in local build mode)
2785
- if (!isLocalBuild) {
2786
- applyKmsConfiguration(definition, AppDefinition, discoveredResources);
2787
- configureVpc(definition, AppDefinition, discoveredResources);
2788
- configurePostgres(definition, AppDefinition, discoveredResources);
2789
- configureSsm(definition, AppDefinition);
2790
- } else {
2791
- console.log(
2792
- ' ⏭️ Skipping: KMS, VPC, PostgreSQL, SSM configurations'
2793
- );
2794
- }
2795
-
2796
- attachIntegrations(definition, AppDefinition);
2797
- configureWebsockets(definition, AppDefinition);
2798
-
2799
- definition.functions = modifyHandlerPaths(definition.functions);
2800
-
2801
- return definition;
2802
- };
2803
-
2804
- module.exports = { composeServerlessDefinition };