@friggframework/devtools 2.0.0--canary.454.e2a280d.0 → 2.0.0--canary.459.51231dd.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frigg-cli/__tests__/unit/commands/db-setup.test.js +1 -1
- package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +486 -0
- package/frigg-cli/db-setup-command/index.js +1 -1
- package/frigg-cli/utils/prisma-runner.js +280 -0
- package/infrastructure/README.md +0 -51
- package/infrastructure/aws-discovery.js +2 -504
- package/infrastructure/aws-discovery.test.js +1 -447
- package/infrastructure/serverless-template.js +636 -646
- package/infrastructure/serverless-template.test.js +0 -91
- package/management-ui/src/App.jsx +1 -85
- package/management-ui/src/hooks/useFrigg.jsx +1 -215
- package/package.json +6 -8
- package/infrastructure/POSTGRES-CONFIGURATION.md +0 -645
- package/infrastructure/__tests__/postgres-config.test.js +0 -914
- package/infrastructure/scripts/build-prisma-layer.js +0 -394
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const { AWSDiscovery } = require('./aws-discovery');
|
|
4
|
-
const { buildPrismaLayer } = require('./scripts/build-prisma-layer');
|
|
5
4
|
|
|
6
5
|
const shouldRunDiscovery = (AppDefinition) => {
|
|
7
|
-
console.log(
|
|
6
|
+
console.log(
|
|
7
|
+
'⚙️ Checking FRIGG_SKIP_AWS_DISCOVERY:',
|
|
8
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY
|
|
9
|
+
);
|
|
8
10
|
if (process.env.FRIGG_SKIP_AWS_DISCOVERY === 'true') {
|
|
9
|
-
console.log(
|
|
11
|
+
console.log(
|
|
12
|
+
'⚙️ Skipping AWS discovery because FRIGG_SKIP_AWS_DISCOVERY is set.'
|
|
13
|
+
);
|
|
10
14
|
return false;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
return (
|
|
14
18
|
AppDefinition.vpc?.enable === true ||
|
|
15
19
|
AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
|
|
16
|
-
AppDefinition.ssm?.enable === true
|
|
17
|
-
AppDefinition.database?.postgres?.enable === true
|
|
20
|
+
AppDefinition.ssm?.enable === true
|
|
18
21
|
);
|
|
19
22
|
};
|
|
20
23
|
|
|
@@ -57,11 +60,17 @@ const getAppEnvironmentVars = (AppDefinition) => {
|
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
if (envKeys.length > 0) {
|
|
60
|
-
console.log(
|
|
63
|
+
console.log(
|
|
64
|
+
` Found ${envKeys.length} environment variables: ${envKeys.join(
|
|
65
|
+
', '
|
|
66
|
+
)}`
|
|
67
|
+
);
|
|
61
68
|
}
|
|
62
69
|
if (skippedKeys.length > 0) {
|
|
63
70
|
console.log(
|
|
64
|
-
` ⚠️ Skipped ${
|
|
71
|
+
` ⚠️ Skipped ${
|
|
72
|
+
skippedKeys.length
|
|
73
|
+
} reserved AWS Lambda variables: ${skippedKeys.join(', ')}`
|
|
65
74
|
);
|
|
66
75
|
}
|
|
67
76
|
|
|
@@ -77,7 +86,9 @@ const findNodeModulesPath = () => {
|
|
|
77
86
|
const potentialPath = path.join(currentDir, 'node_modules');
|
|
78
87
|
if (fs.existsSync(potentialPath)) {
|
|
79
88
|
nodeModulesPath = potentialPath;
|
|
80
|
-
console.log(
|
|
89
|
+
console.log(
|
|
90
|
+
`Found node_modules at: ${nodeModulesPath} (method 1)`
|
|
91
|
+
);
|
|
81
92
|
break;
|
|
82
93
|
}
|
|
83
94
|
const parentDir = path.dirname(currentDir);
|
|
@@ -88,10 +99,14 @@ const findNodeModulesPath = () => {
|
|
|
88
99
|
if (!nodeModulesPath) {
|
|
89
100
|
try {
|
|
90
101
|
const { execSync } = require('node:child_process');
|
|
91
|
-
const npmRoot = execSync('npm root', {
|
|
102
|
+
const npmRoot = execSync('npm root', {
|
|
103
|
+
encoding: 'utf8',
|
|
104
|
+
}).trim();
|
|
92
105
|
if (fs.existsSync(npmRoot)) {
|
|
93
106
|
nodeModulesPath = npmRoot;
|
|
94
|
-
console.log(
|
|
107
|
+
console.log(
|
|
108
|
+
`Found node_modules at: ${nodeModulesPath} (method 2)`
|
|
109
|
+
);
|
|
95
110
|
}
|
|
96
111
|
} catch (npmError) {
|
|
97
112
|
console.error('Error executing npm root:', npmError);
|
|
@@ -103,10 +118,15 @@ const findNodeModulesPath = () => {
|
|
|
103
118
|
for (let i = 0; i < 5; i++) {
|
|
104
119
|
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
105
120
|
if (fs.existsSync(packageJsonPath)) {
|
|
106
|
-
const potentialNodeModules = path.join(
|
|
121
|
+
const potentialNodeModules = path.join(
|
|
122
|
+
currentDir,
|
|
123
|
+
'node_modules'
|
|
124
|
+
);
|
|
107
125
|
if (fs.existsSync(potentialNodeModules)) {
|
|
108
126
|
nodeModulesPath = potentialNodeModules;
|
|
109
|
-
console.log(
|
|
127
|
+
console.log(
|
|
128
|
+
`Found node_modules at: ${nodeModulesPath} (method 3)`
|
|
129
|
+
);
|
|
110
130
|
break;
|
|
111
131
|
}
|
|
112
132
|
}
|
|
@@ -120,7 +140,9 @@ const findNodeModulesPath = () => {
|
|
|
120
140
|
return nodeModulesPath;
|
|
121
141
|
}
|
|
122
142
|
|
|
123
|
-
console.warn(
|
|
143
|
+
console.warn(
|
|
144
|
+
'Could not find node_modules path, falling back to default'
|
|
145
|
+
);
|
|
124
146
|
return path.resolve(process.cwd(), '../node_modules');
|
|
125
147
|
} catch (error) {
|
|
126
148
|
console.error('Error finding node_modules path:', error);
|
|
@@ -145,8 +167,13 @@ const modifyHandlerPaths = (functions) => {
|
|
|
145
167
|
const functionDef = modifiedFunctions[functionName];
|
|
146
168
|
if (functionDef?.handler?.includes('node_modules/')) {
|
|
147
169
|
const relativePath = path.relative(process.cwd(), nodeModulesPath);
|
|
148
|
-
functionDef.handler = functionDef.handler.replace(
|
|
149
|
-
|
|
170
|
+
functionDef.handler = functionDef.handler.replace(
|
|
171
|
+
'node_modules/',
|
|
172
|
+
`${relativePath}/`
|
|
173
|
+
);
|
|
174
|
+
console.log(
|
|
175
|
+
`Updated handler for ${functionName}: ${functionDef.handler}`
|
|
176
|
+
);
|
|
150
177
|
}
|
|
151
178
|
}
|
|
152
179
|
|
|
@@ -162,7 +189,10 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
162
189
|
EnableDnsHostnames: true,
|
|
163
190
|
EnableDnsSupport: true,
|
|
164
191
|
Tags: [
|
|
165
|
-
{
|
|
192
|
+
{
|
|
193
|
+
Key: 'Name',
|
|
194
|
+
Value: '${self:service}-${self:provider.stage}-vpc',
|
|
195
|
+
},
|
|
166
196
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
167
197
|
{ Key: 'Service', Value: '${self:service}' },
|
|
168
198
|
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
@@ -173,7 +203,10 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
173
203
|
Type: 'AWS::EC2::InternetGateway',
|
|
174
204
|
Properties: {
|
|
175
205
|
Tags: [
|
|
176
|
-
{
|
|
206
|
+
{
|
|
207
|
+
Key: 'Name',
|
|
208
|
+
Value: '${self:service}-${self:provider.stage}-igw',
|
|
209
|
+
},
|
|
177
210
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
178
211
|
{ Key: 'Service', Value: '${self:service}' },
|
|
179
212
|
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
@@ -195,7 +228,10 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
195
228
|
AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
|
|
196
229
|
MapPublicIpOnLaunch: true,
|
|
197
230
|
Tags: [
|
|
198
|
-
{
|
|
231
|
+
{
|
|
232
|
+
Key: 'Name',
|
|
233
|
+
Value: '${self:service}-${self:provider.stage}-public-subnet',
|
|
234
|
+
},
|
|
199
235
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
200
236
|
{ Key: 'Service', Value: '${self:service}' },
|
|
201
237
|
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
@@ -210,7 +246,10 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
210
246
|
CidrBlock: '10.0.2.0/24',
|
|
211
247
|
AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
|
|
212
248
|
Tags: [
|
|
213
|
-
{
|
|
249
|
+
{
|
|
250
|
+
Key: 'Name',
|
|
251
|
+
Value: '${self:service}-${self:provider.stage}-private-subnet-1',
|
|
252
|
+
},
|
|
214
253
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
215
254
|
{ Key: 'Service', Value: '${self:service}' },
|
|
216
255
|
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
@@ -225,7 +264,10 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
225
264
|
CidrBlock: '10.0.3.0/24',
|
|
226
265
|
AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
|
|
227
266
|
Tags: [
|
|
228
|
-
{
|
|
267
|
+
{
|
|
268
|
+
Key: 'Name',
|
|
269
|
+
Value: '${self:service}-${self:provider.stage}-private-subnet-2',
|
|
270
|
+
},
|
|
229
271
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
230
272
|
{ Key: 'Service', Value: '${self:service}' },
|
|
231
273
|
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
@@ -238,7 +280,10 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
238
280
|
Properties: {
|
|
239
281
|
Domain: 'vpc',
|
|
240
282
|
Tags: [
|
|
241
|
-
{
|
|
283
|
+
{
|
|
284
|
+
Key: 'Name',
|
|
285
|
+
Value: '${self:service}-${self:provider.stage}-nat-eip',
|
|
286
|
+
},
|
|
242
287
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
243
288
|
{ Key: 'Service', Value: '${self:service}' },
|
|
244
289
|
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
@@ -249,10 +294,15 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
249
294
|
FriggNATGateway: {
|
|
250
295
|
Type: 'AWS::EC2::NatGateway',
|
|
251
296
|
Properties: {
|
|
252
|
-
AllocationId: {
|
|
297
|
+
AllocationId: {
|
|
298
|
+
'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'],
|
|
299
|
+
},
|
|
253
300
|
SubnetId: { Ref: 'FriggPublicSubnet' },
|
|
254
301
|
Tags: [
|
|
255
|
-
{
|
|
302
|
+
{
|
|
303
|
+
Key: 'Name',
|
|
304
|
+
Value: '${self:service}-${self:provider.stage}-nat-gateway',
|
|
305
|
+
},
|
|
256
306
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
257
307
|
{ Key: 'Service', Value: '${self:service}' },
|
|
258
308
|
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
@@ -264,7 +314,10 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
264
314
|
Properties: {
|
|
265
315
|
VpcId: { Ref: 'FriggVPC' },
|
|
266
316
|
Tags: [
|
|
267
|
-
{
|
|
317
|
+
{
|
|
318
|
+
Key: 'Name',
|
|
319
|
+
Value: '${self:service}-${self:provider.stage}-public-rt',
|
|
320
|
+
},
|
|
268
321
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
269
322
|
{ Key: 'Service', Value: '${self:service}' },
|
|
270
323
|
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
@@ -293,7 +346,10 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
293
346
|
Properties: {
|
|
294
347
|
VpcId: { Ref: 'FriggVPC' },
|
|
295
348
|
Tags: [
|
|
296
|
-
{
|
|
349
|
+
{
|
|
350
|
+
Key: 'Name',
|
|
351
|
+
Value: '${self:service}-${self:provider.stage}-private-rt',
|
|
352
|
+
},
|
|
297
353
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
298
354
|
{ Key: 'Service', Value: '${self:service}' },
|
|
299
355
|
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
@@ -329,14 +385,47 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
329
385
|
GroupDescription: 'Security group for Frigg Lambda functions',
|
|
330
386
|
VpcId: { Ref: 'FriggVPC' },
|
|
331
387
|
SecurityGroupEgress: [
|
|
332
|
-
{
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
388
|
+
{
|
|
389
|
+
IpProtocol: 'tcp',
|
|
390
|
+
FromPort: 443,
|
|
391
|
+
ToPort: 443,
|
|
392
|
+
CidrIp: '0.0.0.0/0',
|
|
393
|
+
Description: 'HTTPS outbound',
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
IpProtocol: 'tcp',
|
|
397
|
+
FromPort: 80,
|
|
398
|
+
ToPort: 80,
|
|
399
|
+
CidrIp: '0.0.0.0/0',
|
|
400
|
+
Description: 'HTTP outbound',
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
IpProtocol: 'tcp',
|
|
404
|
+
FromPort: 53,
|
|
405
|
+
ToPort: 53,
|
|
406
|
+
CidrIp: '0.0.0.0/0',
|
|
407
|
+
Description: 'DNS TCP',
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
IpProtocol: 'udp',
|
|
411
|
+
FromPort: 53,
|
|
412
|
+
ToPort: 53,
|
|
413
|
+
CidrIp: '0.0.0.0/0',
|
|
414
|
+
Description: 'DNS UDP',
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
IpProtocol: 'tcp',
|
|
418
|
+
FromPort: 27017,
|
|
419
|
+
ToPort: 27017,
|
|
420
|
+
CidrIp: '0.0.0.0/0',
|
|
421
|
+
Description: 'MongoDB outbound',
|
|
422
|
+
},
|
|
337
423
|
],
|
|
338
424
|
Tags: [
|
|
339
|
-
{
|
|
425
|
+
{
|
|
426
|
+
Key: 'Name',
|
|
427
|
+
Value: '${self:service}-${self:provider.stage}-lambda-sg',
|
|
428
|
+
},
|
|
340
429
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
341
430
|
{ Key: 'Service', Value: '${self:service}' },
|
|
342
431
|
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
@@ -373,8 +462,13 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
373
462
|
VpcId: { Ref: 'FriggVPC' },
|
|
374
463
|
ServiceName: 'com.amazonaws.${self:provider.region}.kms',
|
|
375
464
|
VpcEndpointType: 'Interface',
|
|
376
|
-
SubnetIds: [
|
|
377
|
-
|
|
465
|
+
SubnetIds: [
|
|
466
|
+
{ Ref: 'FriggPrivateSubnet1' },
|
|
467
|
+
{ Ref: 'FriggPrivateSubnet2' },
|
|
468
|
+
],
|
|
469
|
+
SecurityGroupIds: [
|
|
470
|
+
{ Ref: 'FriggVPCEndpointSecurityGroup' },
|
|
471
|
+
],
|
|
378
472
|
PrivateDnsEnabled: true,
|
|
379
473
|
},
|
|
380
474
|
};
|
|
@@ -384,9 +478,13 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
384
478
|
Type: 'AWS::EC2::VPCEndpoint',
|
|
385
479
|
Properties: {
|
|
386
480
|
VpcId: { Ref: 'FriggVPC' },
|
|
387
|
-
ServiceName:
|
|
481
|
+
ServiceName:
|
|
482
|
+
'com.amazonaws.${self:provider.region}.secretsmanager',
|
|
388
483
|
VpcEndpointType: 'Interface',
|
|
389
|
-
SubnetIds: [
|
|
484
|
+
SubnetIds: [
|
|
485
|
+
{ Ref: 'FriggPrivateSubnet1' },
|
|
486
|
+
{ Ref: 'FriggPrivateSubnet2' },
|
|
487
|
+
],
|
|
390
488
|
SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
|
|
391
489
|
PrivateDnsEnabled: true,
|
|
392
490
|
},
|
|
@@ -395,14 +493,17 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
395
493
|
vpcResources.FriggVPCEndpointSecurityGroup = {
|
|
396
494
|
Type: 'AWS::EC2::SecurityGroup',
|
|
397
495
|
Properties: {
|
|
398
|
-
GroupDescription:
|
|
496
|
+
GroupDescription:
|
|
497
|
+
'Security group for Frigg VPC Endpoints - allows HTTPS from Lambda functions',
|
|
399
498
|
VpcId: { Ref: 'FriggVPC' },
|
|
400
499
|
SecurityGroupIngress: [
|
|
401
500
|
{
|
|
402
501
|
IpProtocol: 'tcp',
|
|
403
502
|
FromPort: 443,
|
|
404
503
|
ToPort: 443,
|
|
405
|
-
SourceSecurityGroupId: {
|
|
504
|
+
SourceSecurityGroupId: {
|
|
505
|
+
Ref: 'FriggLambdaSecurityGroup',
|
|
506
|
+
},
|
|
406
507
|
Description: 'HTTPS from Lambda security group',
|
|
407
508
|
},
|
|
408
509
|
{
|
|
@@ -414,12 +515,18 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
414
515
|
},
|
|
415
516
|
],
|
|
416
517
|
Tags: [
|
|
417
|
-
{
|
|
518
|
+
{
|
|
519
|
+
Key: 'Name',
|
|
520
|
+
Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg',
|
|
521
|
+
},
|
|
418
522
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
419
523
|
{ Key: 'Service', Value: '${self:service}' },
|
|
420
524
|
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
421
525
|
{ Key: 'Type', Value: 'VPCEndpoint' },
|
|
422
|
-
{
|
|
526
|
+
{
|
|
527
|
+
Key: 'Purpose',
|
|
528
|
+
Value: 'Allow Lambda functions to access VPC endpoints',
|
|
529
|
+
},
|
|
423
530
|
],
|
|
424
531
|
},
|
|
425
532
|
};
|
|
@@ -437,17 +544,10 @@ const gatherDiscoveredResources = async (AppDefinition) => {
|
|
|
437
544
|
try {
|
|
438
545
|
const region = process.env.AWS_REGION || 'us-east-1';
|
|
439
546
|
const discovery = new AWSDiscovery(region);
|
|
440
|
-
// Use Serverless Framework's stage resolution (opt:stage with 'dev' as default)
|
|
441
|
-
// This matches how serverless.yml resolves ${opt:stage, "dev"}
|
|
442
|
-
// IMPORTANT: Use SLS_STAGE (not STAGE) to match actual deployment stage
|
|
443
|
-
const stage = process.env.SLS_STAGE || 'dev';
|
|
444
|
-
|
|
445
547
|
const config = {
|
|
446
548
|
vpc: AppDefinition.vpc || {},
|
|
447
549
|
encryption: AppDefinition.encryption || {},
|
|
448
550
|
ssm: AppDefinition.ssm || {},
|
|
449
|
-
serviceName: AppDefinition.name || 'create-frigg-app',
|
|
450
|
-
stage: stage,
|
|
451
551
|
};
|
|
452
552
|
|
|
453
553
|
const discoveredResources = await discovery.discoverResources(config);
|
|
@@ -456,13 +556,18 @@ const gatherDiscoveredResources = async (AppDefinition) => {
|
|
|
456
556
|
if (discoveredResources.defaultVpcId) {
|
|
457
557
|
console.log(` VPC: ${discoveredResources.defaultVpcId}`);
|
|
458
558
|
}
|
|
459
|
-
if (
|
|
559
|
+
if (
|
|
560
|
+
discoveredResources.privateSubnetId1 &&
|
|
561
|
+
discoveredResources.privateSubnetId2
|
|
562
|
+
) {
|
|
460
563
|
console.log(
|
|
461
564
|
` Subnets: ${discoveredResources.privateSubnetId1}, ${discoveredResources.privateSubnetId2}`
|
|
462
565
|
);
|
|
463
566
|
}
|
|
464
567
|
if (discoveredResources.defaultSecurityGroupId) {
|
|
465
|
-
console.log(
|
|
568
|
+
console.log(
|
|
569
|
+
` Security Group: ${discoveredResources.defaultSecurityGroupId}`
|
|
570
|
+
);
|
|
466
571
|
}
|
|
467
572
|
if (discoveredResources.defaultKmsKeyId) {
|
|
468
573
|
console.log(` KMS Key: ${discoveredResources.defaultKmsKeyId}`);
|
|
@@ -498,26 +603,14 @@ const buildEnvironment = (appEnvironmentVars, discoveredResources) => {
|
|
|
498
603
|
}
|
|
499
604
|
}
|
|
500
605
|
|
|
501
|
-
// Add Aurora discovery mappings
|
|
502
|
-
if (discoveredResources.aurora) {
|
|
503
|
-
if (discoveredResources.aurora.clusterIdentifier) {
|
|
504
|
-
environment.AWS_DISCOVERY_AURORA_CLUSTER_ID = discoveredResources.aurora.clusterIdentifier;
|
|
505
|
-
}
|
|
506
|
-
if (discoveredResources.aurora.endpoint) {
|
|
507
|
-
environment.AWS_DISCOVERY_AURORA_ENDPOINT = discoveredResources.aurora.endpoint;
|
|
508
|
-
}
|
|
509
|
-
if (discoveredResources.aurora.port) {
|
|
510
|
-
environment.AWS_DISCOVERY_AURORA_PORT = discoveredResources.aurora.port.toString();
|
|
511
|
-
}
|
|
512
|
-
if (discoveredResources.aurora.secretArn) {
|
|
513
|
-
environment.AWS_DISCOVERY_AURORA_SECRET_ARN = discoveredResources.aurora.secretArn;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
606
|
return environment;
|
|
518
607
|
};
|
|
519
608
|
|
|
520
|
-
const createBaseDefinition = (
|
|
609
|
+
const createBaseDefinition = (
|
|
610
|
+
AppDefinition,
|
|
611
|
+
appEnvironmentVars,
|
|
612
|
+
discoveredResources
|
|
613
|
+
) => {
|
|
521
614
|
const region = process.env.AWS_REGION || 'us-east-1';
|
|
522
615
|
|
|
523
616
|
return {
|
|
@@ -525,54 +618,23 @@ const createBaseDefinition = (AppDefinition, appEnvironmentVars, discoveredResou
|
|
|
525
618
|
service: AppDefinition.name || 'create-frigg-app',
|
|
526
619
|
package: {
|
|
527
620
|
individually: true,
|
|
528
|
-
|
|
529
|
-
// AWS SDK exclusions (already in Lambda runtime)
|
|
621
|
+
exclude: [
|
|
530
622
|
'!**/node_modules/aws-sdk/**',
|
|
531
623
|
'!**/node_modules/@aws-sdk/**',
|
|
532
|
-
|
|
533
|
-
// Prisma exclusions (provided via Lambda Layer)
|
|
534
|
-
'!**/node_modules/@prisma/**',
|
|
535
|
-
'!**/node_modules/.prisma/**',
|
|
536
|
-
'!**/node_modules/@prisma-mongodb/**',
|
|
537
|
-
'!**/node_modules/@prisma-postgresql/**',
|
|
538
|
-
'!**/node_modules/prisma/**',
|
|
539
|
-
|
|
540
|
-
// Exclude Prisma generated clients from @friggframework/core
|
|
541
|
-
// These are 81MB and provided via Lambda Layer instead
|
|
542
|
-
'!**/node_modules/@friggframework/core/generated/**',
|
|
543
|
-
|
|
544
|
-
// Exclude development and test files
|
|
545
|
-
'!**/test/**',
|
|
546
|
-
'!**/tests/**',
|
|
547
|
-
'!**/*.test.js',
|
|
548
|
-
'!**/*.spec.js',
|
|
549
|
-
'!**/*.map',
|
|
550
|
-
'!**/jest.config.js',
|
|
551
|
-
'!**/jest.unit.config.js',
|
|
552
|
-
'!**/.eslintrc.json',
|
|
553
|
-
'!**/.prettierrc',
|
|
554
|
-
'!**/.prettierignore',
|
|
555
|
-
'!**/.markdownlintignore',
|
|
556
|
-
'!**/docker-compose.yml',
|
|
557
|
-
'!**/package.json',
|
|
558
|
-
'!**/README.md',
|
|
559
|
-
'!**/*.md',
|
|
560
|
-
|
|
561
|
-
// Exclude .DS_Store and other OS files
|
|
562
|
-
'!**/.DS_Store',
|
|
563
|
-
'!**/.git/**',
|
|
564
|
-
'!**/.claude-flow/**',
|
|
624
|
+
'!package.json',
|
|
565
625
|
],
|
|
566
626
|
},
|
|
567
627
|
useDotenv: true,
|
|
568
628
|
provider: {
|
|
569
629
|
name: AppDefinition.provider || 'aws',
|
|
570
|
-
...(process.env.AWS_PROFILE && { profile: process.env.AWS_PROFILE }),
|
|
571
630
|
runtime: 'nodejs20.x',
|
|
572
631
|
timeout: 30,
|
|
573
632
|
region,
|
|
574
633
|
stage: '${opt:stage}',
|
|
575
|
-
environment: buildEnvironment(
|
|
634
|
+
environment: buildEnvironment(
|
|
635
|
+
appEnvironmentVars,
|
|
636
|
+
discoveredResources
|
|
637
|
+
),
|
|
576
638
|
iamRoleStatements: [
|
|
577
639
|
{
|
|
578
640
|
Effect: 'Allow',
|
|
@@ -581,13 +643,20 @@ const createBaseDefinition = (AppDefinition, appEnvironmentVars, discoveredResou
|
|
|
581
643
|
},
|
|
582
644
|
{
|
|
583
645
|
Effect: 'Allow',
|
|
584
|
-
Action: [
|
|
646
|
+
Action: [
|
|
647
|
+
'sqs:SendMessage',
|
|
648
|
+
'sqs:SendMessageBatch',
|
|
649
|
+
'sqs:GetQueueUrl',
|
|
650
|
+
'sqs:GetQueueAttributes',
|
|
651
|
+
],
|
|
585
652
|
Resource: [
|
|
586
653
|
{ 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
|
|
587
654
|
{
|
|
588
655
|
'Fn::Join': [
|
|
589
656
|
':',
|
|
590
|
-
[
|
|
657
|
+
[
|
|
658
|
+
'arn:aws:sqs:${self:provider.region}:*:${self:service}--${self:provider.stage}-*Queue',
|
|
659
|
+
],
|
|
591
660
|
],
|
|
592
661
|
},
|
|
593
662
|
],
|
|
@@ -635,73 +704,42 @@ const createBaseDefinition = (AppDefinition, appEnvironmentVars, discoveredResou
|
|
|
635
704
|
},
|
|
636
705
|
functions: {
|
|
637
706
|
auth: {
|
|
638
|
-
handler:
|
|
639
|
-
|
|
707
|
+
handler:
|
|
708
|
+
'node_modules/@friggframework/core/handlers/routers/auth.handler',
|
|
640
709
|
events: [
|
|
641
710
|
{ httpApi: { path: '/api/integrations', method: 'ANY' } },
|
|
642
|
-
{
|
|
711
|
+
{
|
|
712
|
+
httpApi: {
|
|
713
|
+
path: '/api/integrations/{proxy+}',
|
|
714
|
+
method: 'ANY',
|
|
715
|
+
},
|
|
716
|
+
},
|
|
643
717
|
{ httpApi: { path: '/api/authorize', method: 'ANY' } },
|
|
644
718
|
],
|
|
645
719
|
},
|
|
646
720
|
user: {
|
|
647
|
-
handler:
|
|
648
|
-
|
|
649
|
-
events: [
|
|
721
|
+
handler:
|
|
722
|
+
'node_modules/@friggframework/core/handlers/routers/user.handler',
|
|
723
|
+
events: [
|
|
724
|
+
{ httpApi: { path: '/user/{proxy+}', method: 'ANY' } },
|
|
725
|
+
],
|
|
650
726
|
},
|
|
651
727
|
health: {
|
|
652
|
-
handler:
|
|
653
|
-
|
|
728
|
+
handler:
|
|
729
|
+
'node_modules/@friggframework/core/handlers/routers/health.handler',
|
|
654
730
|
events: [
|
|
655
731
|
{ httpApi: { path: '/health', method: 'GET' } },
|
|
656
732
|
{ httpApi: { path: '/health/{proxy+}', method: 'GET' } },
|
|
657
733
|
],
|
|
658
734
|
},
|
|
659
|
-
dbMigrate: {
|
|
660
|
-
handler: 'node_modules/@friggframework/core/handlers/workers/db-migration.handler',
|
|
661
|
-
layers: [{ Ref: 'PrismaLambdaLayer' }],
|
|
662
|
-
timeout: 300, // 5 minutes for long-running migrations
|
|
663
|
-
memorySize: 512, // Extra memory for Prisma CLI operations
|
|
664
|
-
reservedConcurrency: 1, // Prevent concurrent migrations
|
|
665
|
-
description: 'Runs database migrations via Prisma (invoke manually from CI/CD)',
|
|
666
|
-
// No events - this function is invoked manually via AWS CLI
|
|
667
|
-
maximumEventAge: 60, // Don't retry old migration requests (60 seconds)
|
|
668
|
-
maximumRetryAttempts: 0, // Don't auto-retry failed migrations
|
|
669
|
-
tags: {
|
|
670
|
-
Purpose: 'DatabaseMigration',
|
|
671
|
-
ManagedBy: 'Frigg',
|
|
672
|
-
},
|
|
673
|
-
// Environment variables for non-interactive Prisma CLI operation
|
|
674
|
-
environment: {
|
|
675
|
-
CI: '1', // Forces Prisma to non-interactive mode
|
|
676
|
-
PRISMA_HIDE_UPDATE_MESSAGE: '1', // Suppress update messages
|
|
677
|
-
PRISMA_MIGRATE_SKIP_SEED: '1', // Skip seeding during migrations
|
|
678
|
-
},
|
|
679
|
-
// Function-specific packaging: Include Prisma schemas (CLI from layer)
|
|
680
|
-
package: {
|
|
681
|
-
patterns: [
|
|
682
|
-
// Include Prisma schemas from @friggframework/core
|
|
683
|
-
// Note: Prisma CLI and clients come from Lambda Layer
|
|
684
|
-
'node_modules/@friggframework/core/prisma-mongodb/**',
|
|
685
|
-
'node_modules/@friggframework/core/prisma-postgresql/**',
|
|
686
|
-
],
|
|
687
|
-
},
|
|
688
|
-
},
|
|
689
|
-
},
|
|
690
|
-
layers: {
|
|
691
|
-
prisma: {
|
|
692
|
-
path: 'layers/prisma',
|
|
693
|
-
name: '${self:service}-prisma-${sls:stage}',
|
|
694
|
-
description: 'Prisma ORM clients for MongoDB and PostgreSQL with rhel-openssl-3.0.x binaries. Reduces function sizes by ~60% (120MB → 45MB). See LAMBDA-LAYER-PRISMA.md for details.',
|
|
695
|
-
compatibleRuntimes: ['nodejs18.x', 'nodejs20.x'],
|
|
696
|
-
retain: false, // Don't retain old layer versions
|
|
697
|
-
},
|
|
698
735
|
},
|
|
699
736
|
resources: {
|
|
700
737
|
Resources: {
|
|
701
738
|
InternalErrorQueue: {
|
|
702
739
|
Type: 'AWS::SQS::Queue',
|
|
703
740
|
Properties: {
|
|
704
|
-
QueueName:
|
|
741
|
+
QueueName:
|
|
742
|
+
'${self:service}-internal-error-queue-${self:provider.stage}',
|
|
705
743
|
MessageRetentionPeriod: 300,
|
|
706
744
|
},
|
|
707
745
|
},
|
|
@@ -711,7 +749,9 @@ const createBaseDefinition = (AppDefinition, appEnvironmentVars, discoveredResou
|
|
|
711
749
|
Subscription: [
|
|
712
750
|
{
|
|
713
751
|
Protocol: 'sqs',
|
|
714
|
-
Endpoint: {
|
|
752
|
+
Endpoint: {
|
|
753
|
+
'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
|
|
754
|
+
},
|
|
715
755
|
},
|
|
716
756
|
],
|
|
717
757
|
},
|
|
@@ -727,10 +767,22 @@ const createBaseDefinition = (AppDefinition, appEnvironmentVars, discoveredResou
|
|
|
727
767
|
Sid: 'Allow Dead Letter SNS to publish to SQS',
|
|
728
768
|
Effect: 'Allow',
|
|
729
769
|
Principal: { Service: 'sns.amazonaws.com' },
|
|
730
|
-
Resource: {
|
|
731
|
-
|
|
770
|
+
Resource: {
|
|
771
|
+
'Fn::GetAtt': [
|
|
772
|
+
'InternalErrorQueue',
|
|
773
|
+
'Arn',
|
|
774
|
+
],
|
|
775
|
+
},
|
|
776
|
+
Action: [
|
|
777
|
+
'SQS:SendMessage',
|
|
778
|
+
'SQS:SendMessageBatch',
|
|
779
|
+
],
|
|
732
780
|
Condition: {
|
|
733
|
-
ArnEquals: {
|
|
781
|
+
ArnEquals: {
|
|
782
|
+
'aws:SourceArn': {
|
|
783
|
+
Ref: 'InternalErrorBridgeTopic',
|
|
784
|
+
},
|
|
785
|
+
},
|
|
734
786
|
},
|
|
735
787
|
},
|
|
736
788
|
],
|
|
@@ -760,34 +812,36 @@ const createBaseDefinition = (AppDefinition, appEnvironmentVars, discoveredResou
|
|
|
760
812
|
};
|
|
761
813
|
};
|
|
762
814
|
|
|
763
|
-
const applyKmsConfiguration = (
|
|
815
|
+
const applyKmsConfiguration = (
|
|
816
|
+
definition,
|
|
817
|
+
AppDefinition,
|
|
818
|
+
discoveredResources
|
|
819
|
+
) => {
|
|
764
820
|
if (AppDefinition.encryption?.fieldLevelEncryptionMethod !== 'kms') {
|
|
765
821
|
return;
|
|
766
822
|
}
|
|
767
823
|
|
|
768
824
|
// Skip KMS configuration for local development when AWS discovery is disabled
|
|
769
825
|
if (process.env.FRIGG_SKIP_AWS_DISCOVERY === 'true') {
|
|
770
|
-
console.log(
|
|
826
|
+
console.log(
|
|
827
|
+
'⚙️ Skipping KMS configuration for local development (FRIGG_SKIP_AWS_DISCOVERY is set)'
|
|
828
|
+
);
|
|
771
829
|
return;
|
|
772
830
|
}
|
|
773
831
|
|
|
774
832
|
if (discoveredResources.defaultKmsKeyId) {
|
|
775
|
-
console.log(
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
};
|
|
788
|
-
} else {
|
|
789
|
-
console.log('KMS alias already exists, skipping alias creation');
|
|
790
|
-
}
|
|
833
|
+
console.log(
|
|
834
|
+
`Using existing KMS key: ${discoveredResources.defaultKmsKeyId}`
|
|
835
|
+
);
|
|
836
|
+
definition.resources.Resources.FriggKMSKeyAlias = {
|
|
837
|
+
Type: 'AWS::KMS::Alias',
|
|
838
|
+
DeletionPolicy: 'Retain',
|
|
839
|
+
Properties: {
|
|
840
|
+
AliasName:
|
|
841
|
+
'alias/${self:service}-${self:provider.stage}-frigg-kms',
|
|
842
|
+
TargetKeyId: discoveredResources.defaultKmsKeyId,
|
|
843
|
+
},
|
|
844
|
+
};
|
|
791
845
|
|
|
792
846
|
definition.provider.iamRoleStatements.push({
|
|
793
847
|
Effect: 'Allow',
|
|
@@ -798,7 +852,7 @@ const applyKmsConfiguration = (definition, AppDefinition, discoveredResources) =
|
|
|
798
852
|
if (AppDefinition.encryption?.createResourceIfNoneFound !== true) {
|
|
799
853
|
throw new Error(
|
|
800
854
|
'KMS field-level encryption is enabled but no KMS key was found. ' +
|
|
801
|
-
|
|
855
|
+
'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
|
|
802
856
|
);
|
|
803
857
|
}
|
|
804
858
|
|
|
@@ -817,7 +871,10 @@ const applyKmsConfiguration = (definition, AppDefinition, discoveredResources) =
|
|
|
817
871
|
Sid: 'AllowRootAccountAdmin',
|
|
818
872
|
Effect: 'Allow',
|
|
819
873
|
Principal: {
|
|
820
|
-
AWS: {
|
|
874
|
+
AWS: {
|
|
875
|
+
'Fn::Sub':
|
|
876
|
+
'arn:aws:iam::${AWS::AccountId}:root',
|
|
877
|
+
},
|
|
821
878
|
},
|
|
822
879
|
Action: 'kms:*',
|
|
823
880
|
Resource: '*',
|
|
@@ -826,20 +883,32 @@ const applyKmsConfiguration = (definition, AppDefinition, discoveredResources) =
|
|
|
826
883
|
Sid: 'AllowLambdaService',
|
|
827
884
|
Effect: 'Allow',
|
|
828
885
|
Principal: { Service: 'lambda.amazonaws.com' },
|
|
829
|
-
Action: [
|
|
886
|
+
Action: [
|
|
887
|
+
'kms:GenerateDataKey',
|
|
888
|
+
'kms:Decrypt',
|
|
889
|
+
'kms:DescribeKey',
|
|
890
|
+
],
|
|
830
891
|
Resource: '*',
|
|
831
892
|
Condition: {
|
|
832
893
|
StringEquals: {
|
|
833
|
-
'kms:ViaService': `lambda.${
|
|
894
|
+
'kms:ViaService': `lambda.${
|
|
895
|
+
process.env.AWS_REGION || 'us-east-1'
|
|
896
|
+
}.amazonaws.com`,
|
|
834
897
|
},
|
|
835
898
|
},
|
|
836
899
|
},
|
|
837
900
|
],
|
|
838
901
|
},
|
|
839
902
|
Tags: [
|
|
840
|
-
{
|
|
903
|
+
{
|
|
904
|
+
Key: 'Name',
|
|
905
|
+
Value: '${self:service}-${self:provider.stage}-frigg-kms-key',
|
|
906
|
+
},
|
|
841
907
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
842
|
-
{
|
|
908
|
+
{
|
|
909
|
+
Key: 'Purpose',
|
|
910
|
+
Value: 'Field-level encryption for Frigg application',
|
|
911
|
+
},
|
|
843
912
|
],
|
|
844
913
|
},
|
|
845
914
|
};
|
|
@@ -848,7 +917,8 @@ const applyKmsConfiguration = (definition, AppDefinition, discoveredResources) =
|
|
|
848
917
|
Type: 'AWS::KMS::Alias',
|
|
849
918
|
DeletionPolicy: 'Retain',
|
|
850
919
|
Properties: {
|
|
851
|
-
AliasName:
|
|
920
|
+
AliasName:
|
|
921
|
+
'alias/${self:service}-${self:provider.stage}-frigg-kms',
|
|
852
922
|
TargetKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
|
|
853
923
|
},
|
|
854
924
|
};
|
|
@@ -859,7 +929,9 @@ const applyKmsConfiguration = (definition, AppDefinition, discoveredResources) =
|
|
|
859
929
|
Resource: [{ 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] }],
|
|
860
930
|
});
|
|
861
931
|
|
|
862
|
-
definition.provider.environment.KMS_KEY_ARN = {
|
|
932
|
+
definition.provider.environment.KMS_KEY_ARN = {
|
|
933
|
+
'Fn::GetAtt': ['FriggKMSKey', 'Arn'],
|
|
934
|
+
};
|
|
863
935
|
definition.custom.kmsGrants = {
|
|
864
936
|
kmsKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
|
|
865
937
|
};
|
|
@@ -868,13 +940,16 @@ const applyKmsConfiguration = (definition, AppDefinition, discoveredResources) =
|
|
|
868
940
|
definition.plugins.push('serverless-kms-grants');
|
|
869
941
|
if (!definition.custom.kmsGrants) {
|
|
870
942
|
definition.custom.kmsGrants = {
|
|
871
|
-
kmsKeyId:
|
|
943
|
+
kmsKeyId:
|
|
944
|
+
discoveredResources.defaultKmsKeyId ||
|
|
945
|
+
'${env:AWS_DISCOVERY_KMS_KEY_ID}',
|
|
872
946
|
};
|
|
873
947
|
}
|
|
874
948
|
|
|
875
949
|
if (!definition.provider.environment.KMS_KEY_ARN) {
|
|
876
950
|
definition.provider.environment.KMS_KEY_ARN =
|
|
877
|
-
discoveredResources.defaultKmsKeyId ||
|
|
951
|
+
discoveredResources.defaultKmsKeyId ||
|
|
952
|
+
'${env:AWS_DISCOVERY_KMS_KEY_ID}';
|
|
878
953
|
}
|
|
879
954
|
};
|
|
880
955
|
|
|
@@ -891,7 +966,9 @@ const healVpcConfiguration = (discoveredResources, AppDefinition) => {
|
|
|
891
966
|
return healingReport;
|
|
892
967
|
}
|
|
893
968
|
|
|
894
|
-
console.log(
|
|
969
|
+
console.log(
|
|
970
|
+
'🔧 Self-healing mode enabled - checking for VPC misconfigurations...'
|
|
971
|
+
);
|
|
895
972
|
|
|
896
973
|
if (discoveredResources.natGatewayInPrivateSubnet) {
|
|
897
974
|
healingReport.warnings.push(
|
|
@@ -901,7 +978,9 @@ const healVpcConfiguration = (discoveredResources, AppDefinition) => {
|
|
|
901
978
|
'NAT Gateway should be recreated in a public subnet for proper internet connectivity'
|
|
902
979
|
);
|
|
903
980
|
discoveredResources.needsNewNatGateway = true;
|
|
904
|
-
healingReport.healed.push(
|
|
981
|
+
healingReport.healed.push(
|
|
982
|
+
'Marked NAT Gateway for recreation in public subnet'
|
|
983
|
+
);
|
|
905
984
|
}
|
|
906
985
|
|
|
907
986
|
if (discoveredResources.elasticIpAlreadyAssociated) {
|
|
@@ -910,10 +989,14 @@ const healVpcConfiguration = (discoveredResources, AppDefinition) => {
|
|
|
910
989
|
);
|
|
911
990
|
|
|
912
991
|
if (discoveredResources.existingNatGatewayId) {
|
|
913
|
-
healingReport.healed.push(
|
|
992
|
+
healingReport.healed.push(
|
|
993
|
+
'Will reuse existing NAT Gateway instead of creating a new one'
|
|
994
|
+
);
|
|
914
995
|
discoveredResources.reuseExistingNatGateway = true;
|
|
915
996
|
} else {
|
|
916
|
-
healingReport.healed.push(
|
|
997
|
+
healingReport.healed.push(
|
|
998
|
+
'Will allocate a new Elastic IP for NAT Gateway'
|
|
999
|
+
);
|
|
917
1000
|
discoveredResources.allocateNewElasticIp = true;
|
|
918
1001
|
}
|
|
919
1002
|
}
|
|
@@ -937,19 +1020,25 @@ const healVpcConfiguration = (discoveredResources, AppDefinition) => {
|
|
|
937
1020
|
healingReport.warnings.push(
|
|
938
1021
|
'Subnet configuration mismatch detected - Lambda functions require private subnets'
|
|
939
1022
|
);
|
|
940
|
-
healingReport.healed.push(
|
|
1023
|
+
healingReport.healed.push(
|
|
1024
|
+
'Will create proper route table configuration for subnet isolation'
|
|
1025
|
+
);
|
|
941
1026
|
}
|
|
942
1027
|
|
|
943
1028
|
if (discoveredResources.orphanedElasticIps?.length > 0) {
|
|
944
1029
|
healingReport.warnings.push(
|
|
945
1030
|
`Found ${discoveredResources.orphanedElasticIps.length} orphaned Elastic IPs`
|
|
946
1031
|
);
|
|
947
|
-
healingReport.recommendations.push(
|
|
1032
|
+
healingReport.recommendations.push(
|
|
1033
|
+
'Consider releasing orphaned Elastic IPs to avoid charges'
|
|
1034
|
+
);
|
|
948
1035
|
}
|
|
949
1036
|
|
|
950
1037
|
if (healingReport.criticalActions.length > 0) {
|
|
951
1038
|
console.log('🚨 CRITICAL ACTIONS:');
|
|
952
|
-
healingReport.criticalActions.forEach((action) =>
|
|
1039
|
+
healingReport.criticalActions.forEach((action) =>
|
|
1040
|
+
console.log(` - ${action}`)
|
|
1041
|
+
);
|
|
953
1042
|
}
|
|
954
1043
|
|
|
955
1044
|
if (healingReport.healed.length > 0) {
|
|
@@ -959,12 +1048,16 @@ const healVpcConfiguration = (discoveredResources, AppDefinition) => {
|
|
|
959
1048
|
|
|
960
1049
|
if (healingReport.warnings.length > 0) {
|
|
961
1050
|
console.log('⚠️ Issues detected:');
|
|
962
|
-
healingReport.warnings.forEach((warning) =>
|
|
1051
|
+
healingReport.warnings.forEach((warning) =>
|
|
1052
|
+
console.log(` - ${warning}`)
|
|
1053
|
+
);
|
|
963
1054
|
}
|
|
964
1055
|
|
|
965
1056
|
if (healingReport.recommendations.length > 0) {
|
|
966
1057
|
console.log('💡 Recommendations:');
|
|
967
|
-
healingReport.recommendations.forEach((rec) =>
|
|
1058
|
+
healingReport.recommendations.forEach((rec) =>
|
|
1059
|
+
console.log(` - ${rec}`)
|
|
1060
|
+
);
|
|
968
1061
|
}
|
|
969
1062
|
|
|
970
1063
|
return healingReport;
|
|
@@ -977,7 +1070,9 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
977
1070
|
|
|
978
1071
|
// Skip VPC configuration for local development when AWS discovery is disabled
|
|
979
1072
|
if (process.env.FRIGG_SKIP_AWS_DISCOVERY === 'true') {
|
|
980
|
-
console.log(
|
|
1073
|
+
console.log(
|
|
1074
|
+
'⚙️ Skipping VPC configuration for local development (FRIGG_SKIP_AWS_DISCOVERY is set)'
|
|
1075
|
+
);
|
|
981
1076
|
return;
|
|
982
1077
|
}
|
|
983
1078
|
|
|
@@ -994,9 +1089,16 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
994
1089
|
});
|
|
995
1090
|
|
|
996
1091
|
if (Object.keys(discoveredResources).length > 0) {
|
|
997
|
-
const healingReport = healVpcConfiguration(
|
|
1092
|
+
const healingReport = healVpcConfiguration(
|
|
1093
|
+
discoveredResources,
|
|
1094
|
+
AppDefinition
|
|
1095
|
+
);
|
|
998
1096
|
if (healingReport.errors.length > 0 && !AppDefinition.vpc?.selfHeal) {
|
|
999
|
-
throw new Error(
|
|
1097
|
+
throw new Error(
|
|
1098
|
+
`VPC configuration errors detected: ${healingReport.errors.join(
|
|
1099
|
+
', '
|
|
1100
|
+
)}`
|
|
1101
|
+
);
|
|
1000
1102
|
}
|
|
1001
1103
|
}
|
|
1002
1104
|
|
|
@@ -1013,15 +1115,21 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1013
1115
|
const vpcResources = createVPCInfrastructure(AppDefinition);
|
|
1014
1116
|
Object.assign(definition.resources.Resources, vpcResources);
|
|
1015
1117
|
vpcId = { Ref: 'FriggVPC' };
|
|
1016
|
-
vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds || [
|
|
1118
|
+
vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds || [
|
|
1119
|
+
{ Ref: 'FriggLambdaSecurityGroup' },
|
|
1120
|
+
];
|
|
1017
1121
|
} else if (vpcManagement === 'use-existing') {
|
|
1018
1122
|
if (!AppDefinition.vpc.vpcId) {
|
|
1019
|
-
throw new Error(
|
|
1123
|
+
throw new Error(
|
|
1124
|
+
'VPC management is set to "use-existing" but no vpcId was provided'
|
|
1125
|
+
);
|
|
1020
1126
|
}
|
|
1021
1127
|
vpcId = AppDefinition.vpc.vpcId;
|
|
1022
1128
|
vpcConfig.securityGroupIds =
|
|
1023
1129
|
AppDefinition.vpc.securityGroupIds ||
|
|
1024
|
-
(discoveredResources.defaultSecurityGroupId
|
|
1130
|
+
(discoveredResources.defaultSecurityGroupId
|
|
1131
|
+
? [discoveredResources.defaultSecurityGroupId]
|
|
1132
|
+
: []);
|
|
1025
1133
|
} else {
|
|
1026
1134
|
if (!discoveredResources.defaultVpcId) {
|
|
1027
1135
|
throw new Error(
|
|
@@ -1031,11 +1139,15 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1031
1139
|
vpcId = discoveredResources.defaultVpcId;
|
|
1032
1140
|
vpcConfig.securityGroupIds =
|
|
1033
1141
|
AppDefinition.vpc.securityGroupIds ||
|
|
1034
|
-
(discoveredResources.defaultSecurityGroupId
|
|
1142
|
+
(discoveredResources.defaultSecurityGroupId
|
|
1143
|
+
? [discoveredResources.defaultSecurityGroupId]
|
|
1144
|
+
: []);
|
|
1035
1145
|
}
|
|
1036
1146
|
|
|
1037
|
-
const defaultSubnetManagement =
|
|
1038
|
-
|
|
1147
|
+
const defaultSubnetManagement =
|
|
1148
|
+
vpcManagement === 'create-new' ? 'create' : 'discover';
|
|
1149
|
+
let subnetManagement =
|
|
1150
|
+
AppDefinition.vpc.subnets?.management || defaultSubnetManagement;
|
|
1039
1151
|
console.log(`Subnet Management Mode: ${subnetManagement}`);
|
|
1040
1152
|
|
|
1041
1153
|
const effectiveVpcId = vpcId || discoveredResources.defaultVpcId;
|
|
@@ -1045,7 +1157,10 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1045
1157
|
|
|
1046
1158
|
if (subnetManagement === 'create') {
|
|
1047
1159
|
console.log('Creating new subnets...');
|
|
1048
|
-
const subnetVpcId =
|
|
1160
|
+
const subnetVpcId =
|
|
1161
|
+
vpcManagement === 'create-new'
|
|
1162
|
+
? { Ref: 'FriggVPC' }
|
|
1163
|
+
: effectiveVpcId;
|
|
1049
1164
|
let subnet1Cidr;
|
|
1050
1165
|
let subnet2Cidr;
|
|
1051
1166
|
let publicSubnetCidr;
|
|
@@ -1068,7 +1183,10 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1068
1183
|
CidrBlock: subnet1Cidr,
|
|
1069
1184
|
AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
|
|
1070
1185
|
Tags: [
|
|
1071
|
-
{
|
|
1186
|
+
{
|
|
1187
|
+
Key: 'Name',
|
|
1188
|
+
Value: '${self:service}-${self:provider.stage}-private-1',
|
|
1189
|
+
},
|
|
1072
1190
|
{ Key: 'Type', Value: 'Private' },
|
|
1073
1191
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1074
1192
|
],
|
|
@@ -1082,7 +1200,10 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1082
1200
|
CidrBlock: subnet2Cidr,
|
|
1083
1201
|
AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
|
|
1084
1202
|
Tags: [
|
|
1085
|
-
{
|
|
1203
|
+
{
|
|
1204
|
+
Key: 'Name',
|
|
1205
|
+
Value: '${self:service}-${self:provider.stage}-private-2',
|
|
1206
|
+
},
|
|
1086
1207
|
{ Key: 'Type', Value: 'Private' },
|
|
1087
1208
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1088
1209
|
],
|
|
@@ -1097,23 +1218,38 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1097
1218
|
MapPublicIpOnLaunch: true,
|
|
1098
1219
|
AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
|
|
1099
1220
|
Tags: [
|
|
1100
|
-
{
|
|
1221
|
+
{
|
|
1222
|
+
Key: 'Name',
|
|
1223
|
+
Value: '${self:service}-${self:provider.stage}-public',
|
|
1224
|
+
},
|
|
1101
1225
|
{ Key: 'Type', Value: 'Public' },
|
|
1102
1226
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1103
1227
|
],
|
|
1104
1228
|
},
|
|
1105
1229
|
};
|
|
1106
1230
|
|
|
1107
|
-
vpcConfig.subnetIds = [
|
|
1231
|
+
vpcConfig.subnetIds = [
|
|
1232
|
+
{ Ref: 'FriggPrivateSubnet1' },
|
|
1233
|
+
{ Ref: 'FriggPrivateSubnet2' },
|
|
1234
|
+
];
|
|
1108
1235
|
|
|
1109
|
-
if (
|
|
1110
|
-
|
|
1236
|
+
if (
|
|
1237
|
+
!AppDefinition.vpc.natGateway ||
|
|
1238
|
+
AppDefinition.vpc.natGateway.management === 'discover'
|
|
1239
|
+
) {
|
|
1240
|
+
if (
|
|
1241
|
+
vpcManagement === 'create-new' ||
|
|
1242
|
+
!discoveredResources.internetGatewayId
|
|
1243
|
+
) {
|
|
1111
1244
|
if (!definition.resources.Resources.FriggInternetGateway) {
|
|
1112
1245
|
definition.resources.Resources.FriggInternetGateway = {
|
|
1113
1246
|
Type: 'AWS::EC2::InternetGateway',
|
|
1114
1247
|
Properties: {
|
|
1115
1248
|
Tags: [
|
|
1116
|
-
{
|
|
1249
|
+
{
|
|
1250
|
+
Key: 'Name',
|
|
1251
|
+
Value: '${self:service}-${self:provider.stage}-igw',
|
|
1252
|
+
},
|
|
1117
1253
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1118
1254
|
],
|
|
1119
1255
|
},
|
|
@@ -1134,7 +1270,10 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1134
1270
|
Properties: {
|
|
1135
1271
|
VpcId: subnetVpcId,
|
|
1136
1272
|
Tags: [
|
|
1137
|
-
{
|
|
1273
|
+
{
|
|
1274
|
+
Key: 'Name',
|
|
1275
|
+
Value: '${self:service}-${self:provider.stage}-public-rt',
|
|
1276
|
+
},
|
|
1138
1277
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1139
1278
|
],
|
|
1140
1279
|
},
|
|
@@ -1142,51 +1281,65 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1142
1281
|
|
|
1143
1282
|
definition.resources.Resources.FriggPublicRoute = {
|
|
1144
1283
|
Type: 'AWS::EC2::Route',
|
|
1145
|
-
DependsOn:
|
|
1284
|
+
DependsOn:
|
|
1285
|
+
vpcManagement === 'create-new'
|
|
1286
|
+
? 'FriggIGWAttachment'
|
|
1287
|
+
: undefined,
|
|
1146
1288
|
Properties: {
|
|
1147
1289
|
RouteTableId: { Ref: 'FriggPublicRouteTable' },
|
|
1148
1290
|
DestinationCidrBlock: '0.0.0.0/0',
|
|
1149
|
-
GatewayId: discoveredResources.internetGatewayId || {
|
|
1291
|
+
GatewayId: discoveredResources.internetGatewayId || {
|
|
1292
|
+
Ref: 'FriggInternetGateway',
|
|
1293
|
+
},
|
|
1150
1294
|
},
|
|
1151
1295
|
};
|
|
1152
1296
|
|
|
1153
|
-
definition.resources.Resources.FriggPublicSubnetRouteTableAssociation =
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1297
|
+
definition.resources.Resources.FriggPublicSubnetRouteTableAssociation =
|
|
1298
|
+
{
|
|
1299
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1300
|
+
Properties: {
|
|
1301
|
+
SubnetId: { Ref: 'FriggPublicSubnet' },
|
|
1302
|
+
RouteTableId: { Ref: 'FriggPublicRouteTable' },
|
|
1303
|
+
},
|
|
1304
|
+
};
|
|
1160
1305
|
|
|
1161
1306
|
definition.resources.Resources.FriggLambdaRouteTable = {
|
|
1162
1307
|
Type: 'AWS::EC2::RouteTable',
|
|
1163
1308
|
Properties: {
|
|
1164
1309
|
VpcId: subnetVpcId,
|
|
1165
1310
|
Tags: [
|
|
1166
|
-
{
|
|
1311
|
+
{
|
|
1312
|
+
Key: 'Name',
|
|
1313
|
+
Value: '${self:service}-${self:provider.stage}-lambda-rt',
|
|
1314
|
+
},
|
|
1167
1315
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1168
1316
|
],
|
|
1169
1317
|
},
|
|
1170
1318
|
};
|
|
1171
1319
|
|
|
1172
|
-
definition.resources.Resources.FriggPrivateSubnet1RouteTableAssociation =
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1320
|
+
definition.resources.Resources.FriggPrivateSubnet1RouteTableAssociation =
|
|
1321
|
+
{
|
|
1322
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1323
|
+
Properties: {
|
|
1324
|
+
SubnetId: { Ref: 'FriggPrivateSubnet1' },
|
|
1325
|
+
RouteTableId: { Ref: 'FriggLambdaRouteTable' },
|
|
1326
|
+
},
|
|
1327
|
+
};
|
|
1179
1328
|
|
|
1180
|
-
definition.resources.Resources.FriggPrivateSubnet2RouteTableAssociation =
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1329
|
+
definition.resources.Resources.FriggPrivateSubnet2RouteTableAssociation =
|
|
1330
|
+
{
|
|
1331
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1332
|
+
Properties: {
|
|
1333
|
+
SubnetId: { Ref: 'FriggPrivateSubnet2' },
|
|
1334
|
+
RouteTableId: { Ref: 'FriggLambdaRouteTable' },
|
|
1335
|
+
},
|
|
1336
|
+
};
|
|
1187
1337
|
}
|
|
1188
1338
|
} else if (subnetManagement === 'use-existing') {
|
|
1189
|
-
if (
|
|
1339
|
+
if (
|
|
1340
|
+
!AppDefinition.vpc.subnets?.ids ||
|
|
1341
|
+
AppDefinition.vpc.subnets.ids.length < 2
|
|
1342
|
+
) {
|
|
1190
1343
|
throw new Error(
|
|
1191
1344
|
'Subnet management is "use-existing" but less than 2 subnet IDs provided. Provide at least 2 subnet IDs in vpc.subnets.ids.'
|
|
1192
1345
|
);
|
|
@@ -1196,13 +1349,19 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1196
1349
|
vpcConfig.subnetIds =
|
|
1197
1350
|
AppDefinition.vpc.subnets?.ids?.length > 0
|
|
1198
1351
|
? AppDefinition.vpc.subnets.ids
|
|
1199
|
-
: discoveredResources.privateSubnetId1 &&
|
|
1200
|
-
|
|
1201
|
-
|
|
1352
|
+
: discoveredResources.privateSubnetId1 &&
|
|
1353
|
+
discoveredResources.privateSubnetId2
|
|
1354
|
+
? [
|
|
1355
|
+
discoveredResources.privateSubnetId1,
|
|
1356
|
+
discoveredResources.privateSubnetId2,
|
|
1357
|
+
]
|
|
1358
|
+
: [];
|
|
1202
1359
|
|
|
1203
1360
|
if (vpcConfig.subnetIds.length < 2) {
|
|
1204
1361
|
if (AppDefinition.vpc.selfHeal) {
|
|
1205
|
-
console.log(
|
|
1362
|
+
console.log(
|
|
1363
|
+
'No subnets found but self-heal enabled - creating minimal subnet setup'
|
|
1364
|
+
);
|
|
1206
1365
|
subnetManagement = 'create';
|
|
1207
1366
|
discoveredResources.createSubnets = true;
|
|
1208
1367
|
} else {
|
|
@@ -1214,19 +1373,22 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1214
1373
|
}
|
|
1215
1374
|
|
|
1216
1375
|
if (subnetManagement === 'create' && discoveredResources.createSubnets) {
|
|
1217
|
-
definition.resources.Resources.FriggLambdaRouteTable =
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1376
|
+
definition.resources.Resources.FriggLambdaRouteTable = definition
|
|
1377
|
+
.resources.Resources.FriggLambdaRouteTable || {
|
|
1378
|
+
Type: 'AWS::EC2::RouteTable',
|
|
1379
|
+
Properties: {
|
|
1380
|
+
VpcId: effectiveVpcId,
|
|
1381
|
+
Tags: [
|
|
1382
|
+
{
|
|
1383
|
+
Key: 'Name',
|
|
1384
|
+
Value: '${self:service}-${self:provider.stage}-lambda-rt',
|
|
1385
|
+
},
|
|
1386
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1387
|
+
{ Key: 'Environment', Value: '${self:provider.stage}' },
|
|
1388
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
1389
|
+
],
|
|
1390
|
+
},
|
|
1391
|
+
};
|
|
1230
1392
|
}
|
|
1231
1393
|
|
|
1232
1394
|
if (
|
|
@@ -1235,7 +1397,8 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1235
1397
|
) {
|
|
1236
1398
|
definition.provider.vpc = vpcConfig;
|
|
1237
1399
|
|
|
1238
|
-
const natGatewayManagement =
|
|
1400
|
+
const natGatewayManagement =
|
|
1401
|
+
AppDefinition.vpc.natGateway?.management || 'discover';
|
|
1239
1402
|
let needsNewNatGateway =
|
|
1240
1403
|
natGatewayManagement === 'createAndManage' ||
|
|
1241
1404
|
discoveredResources.needsNewNatGateway === true;
|
|
@@ -1246,7 +1409,9 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1246
1409
|
let useExistingEip = false;
|
|
1247
1410
|
|
|
1248
1411
|
if (needsNewNatGateway) {
|
|
1249
|
-
console.log(
|
|
1412
|
+
console.log(
|
|
1413
|
+
'Create mode: Creating dedicated EIP, public subnet, and NAT Gateway...'
|
|
1414
|
+
);
|
|
1250
1415
|
|
|
1251
1416
|
if (
|
|
1252
1417
|
discoveredResources.existingNatGatewayId &&
|
|
@@ -1254,12 +1419,18 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1254
1419
|
) {
|
|
1255
1420
|
console.log('Found existing Frigg-managed NAT Gateway and EIP');
|
|
1256
1421
|
if (!discoveredResources.natGatewayInPrivateSubnet) {
|
|
1257
|
-
console.log(
|
|
1422
|
+
console.log(
|
|
1423
|
+
'✅ Existing NAT Gateway is in PUBLIC subnet, will reuse it'
|
|
1424
|
+
);
|
|
1258
1425
|
reuseExistingNatGateway = true;
|
|
1259
1426
|
} else {
|
|
1260
|
-
console.log(
|
|
1427
|
+
console.log(
|
|
1428
|
+
'❌ NAT Gateway is in PRIVATE subnet - MUST create new one in PUBLIC subnet'
|
|
1429
|
+
);
|
|
1261
1430
|
if (AppDefinition.vpc.selfHeal) {
|
|
1262
|
-
console.log(
|
|
1431
|
+
console.log(
|
|
1432
|
+
'Self-heal enabled: Creating new NAT Gateway in PUBLIC subnet'
|
|
1433
|
+
);
|
|
1263
1434
|
reuseExistingNatGateway = false;
|
|
1264
1435
|
useExistingEip = false;
|
|
1265
1436
|
discoveredResources.needsCleanup = true;
|
|
@@ -1273,12 +1444,16 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1273
1444
|
discoveredResources.existingElasticIpAllocationId &&
|
|
1274
1445
|
!discoveredResources.existingNatGatewayId
|
|
1275
1446
|
) {
|
|
1276
|
-
console.log(
|
|
1447
|
+
console.log(
|
|
1448
|
+
'Found orphaned EIP, will reuse it for new NAT Gateway in PUBLIC subnet'
|
|
1449
|
+
);
|
|
1277
1450
|
useExistingEip = true;
|
|
1278
1451
|
}
|
|
1279
1452
|
|
|
1280
1453
|
if (reuseExistingNatGateway) {
|
|
1281
|
-
console.log(
|
|
1454
|
+
console.log(
|
|
1455
|
+
'Reusing existing NAT Gateway - skipping resource creation'
|
|
1456
|
+
);
|
|
1282
1457
|
} else {
|
|
1283
1458
|
if (!useExistingEip) {
|
|
1284
1459
|
definition.resources.Resources.FriggNATGatewayEIP = {
|
|
@@ -1288,10 +1463,16 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1288
1463
|
Properties: {
|
|
1289
1464
|
Domain: 'vpc',
|
|
1290
1465
|
Tags: [
|
|
1291
|
-
{
|
|
1466
|
+
{
|
|
1467
|
+
Key: 'Name',
|
|
1468
|
+
Value: '${self:service}-${self:provider.stage}-nat-eip',
|
|
1469
|
+
},
|
|
1292
1470
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1293
1471
|
{ Key: 'Service', Value: '${self:service}' },
|
|
1294
|
-
{
|
|
1472
|
+
{
|
|
1473
|
+
Key: 'Stage',
|
|
1474
|
+
Value: '${self:provider.stage}',
|
|
1475
|
+
},
|
|
1295
1476
|
],
|
|
1296
1477
|
},
|
|
1297
1478
|
};
|
|
@@ -1299,25 +1480,34 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1299
1480
|
|
|
1300
1481
|
if (!discoveredResources.publicSubnetId) {
|
|
1301
1482
|
if (discoveredResources.internetGatewayId) {
|
|
1302
|
-
console.log(
|
|
1483
|
+
console.log(
|
|
1484
|
+
'Reusing existing Internet Gateway for NAT Gateway'
|
|
1485
|
+
);
|
|
1303
1486
|
} else {
|
|
1304
1487
|
definition.resources.Resources.FriggInternetGateway =
|
|
1305
|
-
definition.resources.Resources
|
|
1488
|
+
definition.resources.Resources
|
|
1489
|
+
.FriggInternetGateway || {
|
|
1306
1490
|
Type: 'AWS::EC2::InternetGateway',
|
|
1307
1491
|
Properties: {
|
|
1308
1492
|
Tags: [
|
|
1309
|
-
{
|
|
1493
|
+
{
|
|
1494
|
+
Key: 'Name',
|
|
1495
|
+
Value: '${self:service}-${self:provider.stage}-igw',
|
|
1496
|
+
},
|
|
1310
1497
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1311
1498
|
],
|
|
1312
1499
|
},
|
|
1313
1500
|
};
|
|
1314
1501
|
|
|
1315
1502
|
definition.resources.Resources.FriggIGWAttachment =
|
|
1316
|
-
definition.resources.Resources
|
|
1503
|
+
definition.resources.Resources
|
|
1504
|
+
.FriggIGWAttachment || {
|
|
1317
1505
|
Type: 'AWS::EC2::VPCGatewayAttachment',
|
|
1318
1506
|
Properties: {
|
|
1319
1507
|
VpcId: discoveredResources.defaultVpcId,
|
|
1320
|
-
InternetGatewayId: {
|
|
1508
|
+
InternetGatewayId: {
|
|
1509
|
+
Ref: 'FriggInternetGateway',
|
|
1510
|
+
},
|
|
1321
1511
|
},
|
|
1322
1512
|
};
|
|
1323
1513
|
}
|
|
@@ -1327,11 +1517,17 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1327
1517
|
Properties: {
|
|
1328
1518
|
VpcId: discoveredResources.defaultVpcId,
|
|
1329
1519
|
CidrBlock:
|
|
1330
|
-
AppDefinition.vpc.natGateway
|
|
1331
|
-
|
|
1520
|
+
AppDefinition.vpc.natGateway
|
|
1521
|
+
?.publicSubnetCidr || '172.31.250.0/24',
|
|
1522
|
+
AvailabilityZone: {
|
|
1523
|
+
'Fn::Select': [0, { 'Fn::GetAZs': '' }],
|
|
1524
|
+
},
|
|
1332
1525
|
MapPublicIpOnLaunch: true,
|
|
1333
1526
|
Tags: [
|
|
1334
|
-
{
|
|
1527
|
+
{
|
|
1528
|
+
Key: 'Name',
|
|
1529
|
+
Value: '${self:service}-${self:provider.stage}-public-subnet',
|
|
1530
|
+
},
|
|
1335
1531
|
{ Key: 'Type', Value: 'Public' },
|
|
1336
1532
|
],
|
|
1337
1533
|
},
|
|
@@ -1342,28 +1538,37 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1342
1538
|
Properties: {
|
|
1343
1539
|
VpcId: discoveredResources.defaultVpcId,
|
|
1344
1540
|
Tags: [
|
|
1345
|
-
{
|
|
1541
|
+
{
|
|
1542
|
+
Key: 'Name',
|
|
1543
|
+
Value: '${self:service}-${self:provider.stage}-public-rt',
|
|
1544
|
+
},
|
|
1346
1545
|
],
|
|
1347
1546
|
},
|
|
1348
1547
|
};
|
|
1349
1548
|
|
|
1350
1549
|
definition.resources.Resources.FriggPublicRoute = {
|
|
1351
1550
|
Type: 'AWS::EC2::Route',
|
|
1352
|
-
DependsOn: discoveredResources.internetGatewayId
|
|
1551
|
+
DependsOn: discoveredResources.internetGatewayId
|
|
1552
|
+
? []
|
|
1553
|
+
: 'FriggIGWAttachment',
|
|
1353
1554
|
Properties: {
|
|
1354
1555
|
RouteTableId: { Ref: 'FriggPublicRouteTable' },
|
|
1355
1556
|
DestinationCidrBlock: '0.0.0.0/0',
|
|
1356
|
-
GatewayId:
|
|
1557
|
+
GatewayId:
|
|
1558
|
+
discoveredResources.internetGatewayId || {
|
|
1559
|
+
Ref: 'FriggInternetGateway',
|
|
1560
|
+
},
|
|
1357
1561
|
},
|
|
1358
1562
|
};
|
|
1359
1563
|
|
|
1360
|
-
definition.resources.Resources.FriggPublicSubnetRouteTableAssociation =
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1564
|
+
definition.resources.Resources.FriggPublicSubnetRouteTableAssociation =
|
|
1565
|
+
{
|
|
1566
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1567
|
+
Properties: {
|
|
1568
|
+
SubnetId: { Ref: 'FriggPublicSubnet' },
|
|
1569
|
+
RouteTableId: { Ref: 'FriggPublicRouteTable' },
|
|
1570
|
+
},
|
|
1571
|
+
};
|
|
1367
1572
|
}
|
|
1368
1573
|
|
|
1369
1574
|
definition.resources.Resources.FriggNATGateway = {
|
|
@@ -1373,11 +1578,20 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1373
1578
|
Properties: {
|
|
1374
1579
|
AllocationId: useExistingEip
|
|
1375
1580
|
? discoveredResources.existingElasticIpAllocationId
|
|
1376
|
-
: {
|
|
1377
|
-
|
|
1378
|
-
|
|
1581
|
+
: {
|
|
1582
|
+
'Fn::GetAtt': [
|
|
1583
|
+
'FriggNATGatewayEIP',
|
|
1584
|
+
'AllocationId',
|
|
1585
|
+
],
|
|
1586
|
+
},
|
|
1587
|
+
SubnetId: discoveredResources.publicSubnetId || {
|
|
1588
|
+
Ref: 'FriggPublicSubnet',
|
|
1589
|
+
},
|
|
1379
1590
|
Tags: [
|
|
1380
|
-
{
|
|
1591
|
+
{
|
|
1592
|
+
Key: 'Name',
|
|
1593
|
+
Value: '${self:service}-${self:provider.stage}-nat-gateway',
|
|
1594
|
+
},
|
|
1381
1595
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1382
1596
|
{ Key: 'Service', Value: '${self:service}' },
|
|
1383
1597
|
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
@@ -1389,9 +1603,15 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1389
1603
|
natGatewayManagement === 'discover' ||
|
|
1390
1604
|
natGatewayManagement === 'useExisting'
|
|
1391
1605
|
) {
|
|
1392
|
-
if (
|
|
1393
|
-
|
|
1394
|
-
|
|
1606
|
+
if (
|
|
1607
|
+
natGatewayManagement === 'useExisting' &&
|
|
1608
|
+
AppDefinition.vpc.natGateway?.id
|
|
1609
|
+
) {
|
|
1610
|
+
console.log(
|
|
1611
|
+
`Using explicitly provided NAT Gateway: ${AppDefinition.vpc.natGateway.id}`
|
|
1612
|
+
);
|
|
1613
|
+
discoveredResources.existingNatGatewayId =
|
|
1614
|
+
AppDefinition.vpc.natGateway.id;
|
|
1395
1615
|
}
|
|
1396
1616
|
|
|
1397
1617
|
if (discoveredResources.existingNatGatewayId) {
|
|
@@ -1401,14 +1621,20 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1401
1621
|
);
|
|
1402
1622
|
|
|
1403
1623
|
if (discoveredResources.natGatewayInPrivateSubnet) {
|
|
1404
|
-
console.log(
|
|
1624
|
+
console.log(
|
|
1625
|
+
'❌ CRITICAL: NAT Gateway is in PRIVATE subnet - Internet connectivity will NOT work!'
|
|
1626
|
+
);
|
|
1405
1627
|
|
|
1406
1628
|
if (AppDefinition.vpc.selfHeal === true) {
|
|
1407
|
-
console.log(
|
|
1629
|
+
console.log(
|
|
1630
|
+
'Self-heal enabled: Will create new NAT Gateway in PUBLIC subnet'
|
|
1631
|
+
);
|
|
1408
1632
|
needsNewNatGateway = true;
|
|
1409
1633
|
discoveredResources.existingNatGatewayId = null;
|
|
1410
1634
|
if (!discoveredResources.publicSubnetId) {
|
|
1411
|
-
console.log(
|
|
1635
|
+
console.log(
|
|
1636
|
+
'No public subnet found - will create one for NAT Gateway'
|
|
1637
|
+
);
|
|
1412
1638
|
discoveredResources.createPublicSubnet = true;
|
|
1413
1639
|
}
|
|
1414
1640
|
} else {
|
|
@@ -1417,52 +1643,73 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1417
1643
|
);
|
|
1418
1644
|
}
|
|
1419
1645
|
} else {
|
|
1420
|
-
console.log(
|
|
1646
|
+
console.log(
|
|
1647
|
+
`Using discovered NAT Gateway for routing: ${discoveredResources.existingNatGatewayId}`
|
|
1648
|
+
);
|
|
1421
1649
|
}
|
|
1422
|
-
} else if (
|
|
1423
|
-
|
|
1424
|
-
|
|
1650
|
+
} else if (
|
|
1651
|
+
!needsNewNatGateway &&
|
|
1652
|
+
AppDefinition.vpc.natGateway?.id
|
|
1653
|
+
) {
|
|
1654
|
+
console.log(
|
|
1655
|
+
`Using explicitly provided NAT Gateway: ${AppDefinition.vpc.natGateway.id}`
|
|
1656
|
+
);
|
|
1657
|
+
discoveredResources.existingNatGatewayId =
|
|
1658
|
+
AppDefinition.vpc.natGateway.id;
|
|
1425
1659
|
}
|
|
1426
1660
|
}
|
|
1427
1661
|
|
|
1428
|
-
definition.resources.Resources.FriggLambdaRouteTable =
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1662
|
+
definition.resources.Resources.FriggLambdaRouteTable = definition
|
|
1663
|
+
.resources.Resources.FriggLambdaRouteTable || {
|
|
1664
|
+
Type: 'AWS::EC2::RouteTable',
|
|
1665
|
+
Properties: {
|
|
1666
|
+
VpcId: discoveredResources.defaultVpcId || vpcId,
|
|
1667
|
+
Tags: [
|
|
1668
|
+
{
|
|
1669
|
+
Key: 'Name',
|
|
1670
|
+
Value: '${self:service}-${self:provider.stage}-lambda-rt',
|
|
1671
|
+
},
|
|
1672
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1673
|
+
{ Key: 'Environment', Value: '${self:provider.stage}' },
|
|
1674
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
1675
|
+
],
|
|
1676
|
+
},
|
|
1677
|
+
};
|
|
1441
1678
|
|
|
1442
1679
|
const routeTableId = { Ref: 'FriggLambdaRouteTable' };
|
|
1443
1680
|
let natGatewayIdForRoute;
|
|
1444
1681
|
|
|
1445
1682
|
if (reuseExistingNatGateway) {
|
|
1446
1683
|
natGatewayIdForRoute = discoveredResources.existingNatGatewayId;
|
|
1447
|
-
console.log(
|
|
1684
|
+
console.log(
|
|
1685
|
+
`Using discovered NAT Gateway for routing: ${natGatewayIdForRoute}`
|
|
1686
|
+
);
|
|
1448
1687
|
} else if (needsNewNatGateway && !reuseExistingNatGateway) {
|
|
1449
1688
|
natGatewayIdForRoute = { Ref: 'FriggNATGateway' };
|
|
1450
1689
|
console.log('Using newly created NAT Gateway for routing');
|
|
1451
1690
|
} else if (discoveredResources.existingNatGatewayId) {
|
|
1452
1691
|
natGatewayIdForRoute = discoveredResources.existingNatGatewayId;
|
|
1453
|
-
console.log(
|
|
1692
|
+
console.log(
|
|
1693
|
+
`Using discovered NAT Gateway for routing: ${natGatewayIdForRoute}`
|
|
1694
|
+
);
|
|
1454
1695
|
} else if (AppDefinition.vpc.natGateway?.id) {
|
|
1455
1696
|
natGatewayIdForRoute = AppDefinition.vpc.natGateway.id;
|
|
1456
|
-
console.log(
|
|
1697
|
+
console.log(
|
|
1698
|
+
`Using explicitly provided NAT Gateway for routing: ${natGatewayIdForRoute}`
|
|
1699
|
+
);
|
|
1457
1700
|
} else if (AppDefinition.vpc.selfHeal === true) {
|
|
1458
1701
|
natGatewayIdForRoute = null;
|
|
1459
|
-
console.log(
|
|
1702
|
+
console.log(
|
|
1703
|
+
'No NAT Gateway available - skipping NAT route creation'
|
|
1704
|
+
);
|
|
1460
1705
|
} else {
|
|
1461
1706
|
throw new Error('No existing NAT Gateway found in discovery mode');
|
|
1462
1707
|
}
|
|
1463
1708
|
|
|
1464
1709
|
if (natGatewayIdForRoute) {
|
|
1465
|
-
console.log(
|
|
1710
|
+
console.log(
|
|
1711
|
+
`Configuring NAT route: 0.0.0.0/0 → ${natGatewayIdForRoute}`
|
|
1712
|
+
);
|
|
1466
1713
|
definition.resources.Resources.FriggNATRoute = {
|
|
1467
1714
|
Type: 'AWS::EC2::Route',
|
|
1468
1715
|
DependsOn: 'FriggLambdaRouteTable',
|
|
@@ -1473,7 +1720,9 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1473
1720
|
},
|
|
1474
1721
|
};
|
|
1475
1722
|
} else {
|
|
1476
|
-
console.warn(
|
|
1723
|
+
console.warn(
|
|
1724
|
+
'⚠️ No NAT Gateway configured - Lambda functions will not have internet access'
|
|
1725
|
+
);
|
|
1477
1726
|
}
|
|
1478
1727
|
|
|
1479
1728
|
if (typeof vpcConfig.subnetIds[0] === 'string') {
|
|
@@ -1498,25 +1747,37 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1498
1747
|
};
|
|
1499
1748
|
}
|
|
1500
1749
|
|
|
1501
|
-
if (
|
|
1750
|
+
if (
|
|
1751
|
+
typeof vpcConfig.subnetIds[0] === 'object' &&
|
|
1752
|
+
vpcConfig.subnetIds[0].Ref
|
|
1753
|
+
) {
|
|
1502
1754
|
definition.resources.Resources.FriggNewSubnet1RouteAssociation = {
|
|
1503
1755
|
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1504
1756
|
Properties: {
|
|
1505
1757
|
SubnetId: vpcConfig.subnetIds[0],
|
|
1506
1758
|
RouteTableId: routeTableId,
|
|
1507
1759
|
},
|
|
1508
|
-
DependsOn: [
|
|
1760
|
+
DependsOn: [
|
|
1761
|
+
'FriggLambdaRouteTable',
|
|
1762
|
+
vpcConfig.subnetIds[0].Ref,
|
|
1763
|
+
],
|
|
1509
1764
|
};
|
|
1510
1765
|
}
|
|
1511
1766
|
|
|
1512
|
-
if (
|
|
1767
|
+
if (
|
|
1768
|
+
typeof vpcConfig.subnetIds[1] === 'object' &&
|
|
1769
|
+
vpcConfig.subnetIds[1].Ref
|
|
1770
|
+
) {
|
|
1513
1771
|
definition.resources.Resources.FriggNewSubnet2RouteAssociation = {
|
|
1514
1772
|
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1515
1773
|
Properties: {
|
|
1516
1774
|
SubnetId: vpcConfig.subnetIds[1],
|
|
1517
1775
|
RouteTableId: routeTableId,
|
|
1518
1776
|
},
|
|
1519
|
-
DependsOn: [
|
|
1777
|
+
DependsOn: [
|
|
1778
|
+
'FriggLambdaRouteTable',
|
|
1779
|
+
vpcConfig.subnetIds[1].Ref,
|
|
1780
|
+
],
|
|
1520
1781
|
};
|
|
1521
1782
|
}
|
|
1522
1783
|
|
|
@@ -1535,7 +1796,8 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1535
1796
|
Type: 'AWS::EC2::VPCEndpoint',
|
|
1536
1797
|
Properties: {
|
|
1537
1798
|
VpcId: discoveredResources.defaultVpcId,
|
|
1538
|
-
ServiceName:
|
|
1799
|
+
ServiceName:
|
|
1800
|
+
'com.amazonaws.${self:provider.region}.dynamodb',
|
|
1539
1801
|
VpcEndpointType: 'Gateway',
|
|
1540
1802
|
RouteTableIds: [routeTableId],
|
|
1541
1803
|
},
|
|
@@ -1552,7 +1814,10 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1552
1814
|
if (!definition.resources.Resources.VPCEndpointSecurityGroup) {
|
|
1553
1815
|
const vpcEndpointIngressRules = [];
|
|
1554
1816
|
|
|
1555
|
-
if (
|
|
1817
|
+
if (
|
|
1818
|
+
vpcConfig.securityGroupIds &&
|
|
1819
|
+
vpcConfig.securityGroupIds.length > 0
|
|
1820
|
+
) {
|
|
1556
1821
|
for (const sg of vpcConfig.securityGroupIds) {
|
|
1557
1822
|
if (typeof sg === 'string') {
|
|
1558
1823
|
vpcEndpointIngressRules.push({
|
|
@@ -1600,13 +1865,20 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1600
1865
|
definition.resources.Resources.VPCEndpointSecurityGroup = {
|
|
1601
1866
|
Type: 'AWS::EC2::SecurityGroup',
|
|
1602
1867
|
Properties: {
|
|
1603
|
-
GroupDescription:
|
|
1868
|
+
GroupDescription:
|
|
1869
|
+
'Security group for VPC endpoints - allows HTTPS from Lambda functions',
|
|
1604
1870
|
VpcId: discoveredResources.defaultVpcId,
|
|
1605
1871
|
SecurityGroupIngress: vpcEndpointIngressRules,
|
|
1606
1872
|
Tags: [
|
|
1607
|
-
{
|
|
1873
|
+
{
|
|
1874
|
+
Key: 'Name',
|
|
1875
|
+
Value: '${self:service}-${self:provider.stage}-vpc-endpoints-sg',
|
|
1876
|
+
},
|
|
1608
1877
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1609
|
-
{
|
|
1878
|
+
{
|
|
1879
|
+
Key: 'Purpose',
|
|
1880
|
+
Value: 'Allow Lambda functions to access VPC endpoints',
|
|
1881
|
+
},
|
|
1610
1882
|
],
|
|
1611
1883
|
},
|
|
1612
1884
|
};
|
|
@@ -1624,14 +1896,13 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1624
1896
|
},
|
|
1625
1897
|
};
|
|
1626
1898
|
|
|
1627
|
-
|
|
1628
|
-
// (Aurora requires Secrets Manager access for credential retrieval)
|
|
1629
|
-
if (AppDefinition.secretsManager?.enable === true || AppDefinition.database?.postgres?.enable === true) {
|
|
1899
|
+
if (AppDefinition.secretsManager?.enable === true) {
|
|
1630
1900
|
definition.resources.Resources.VPCEndpointSecretsManager = {
|
|
1631
1901
|
Type: 'AWS::EC2::VPCEndpoint',
|
|
1632
1902
|
Properties: {
|
|
1633
1903
|
VpcId: discoveredResources.defaultVpcId,
|
|
1634
|
-
ServiceName:
|
|
1904
|
+
ServiceName:
|
|
1905
|
+
'com.amazonaws.${self:provider.region}.secretsmanager',
|
|
1635
1906
|
VpcEndpointType: 'Interface',
|
|
1636
1907
|
SubnetIds: vpcConfig.subnetIds,
|
|
1637
1908
|
SecurityGroupIds: [{ Ref: 'VPCEndpointSecurityGroup' }],
|
|
@@ -1643,283 +1914,6 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1643
1914
|
}
|
|
1644
1915
|
};
|
|
1645
1916
|
|
|
1646
|
-
const createAuroraInfrastructure = (definition, AppDefinition, discoveredResources) => {
|
|
1647
|
-
const dbConfig = AppDefinition.database.postgres;
|
|
1648
|
-
|
|
1649
|
-
console.log('🔧 Creating Aurora Serverless v2 infrastructure...');
|
|
1650
|
-
|
|
1651
|
-
// 1. DB Subnet Group (using Lambda private subnets)
|
|
1652
|
-
definition.resources.Resources.FriggDBSubnetGroup = {
|
|
1653
|
-
Type: 'AWS::RDS::DBSubnetGroup',
|
|
1654
|
-
Properties: {
|
|
1655
|
-
DBSubnetGroupDescription: 'Subnet group for Frigg Aurora cluster',
|
|
1656
|
-
SubnetIds: [
|
|
1657
|
-
discoveredResources.privateSubnetId1,
|
|
1658
|
-
discoveredResources.privateSubnetId2
|
|
1659
|
-
],
|
|
1660
|
-
Tags: [
|
|
1661
|
-
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-db-subnet-group' },
|
|
1662
|
-
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1663
|
-
{ Key: 'Service', Value: '${self:service}' },
|
|
1664
|
-
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
1665
|
-
]
|
|
1666
|
-
}
|
|
1667
|
-
};
|
|
1668
|
-
|
|
1669
|
-
// 2. Security Group (allow Lambda SG to access 5432)
|
|
1670
|
-
// In create-new VPC mode, Lambda uses FriggLambdaSecurityGroup
|
|
1671
|
-
// In other modes, use discovered default security group
|
|
1672
|
-
const lambdaSecurityGroupId = AppDefinition.vpc?.management === 'create-new'
|
|
1673
|
-
? { Ref: 'FriggLambdaSecurityGroup' }
|
|
1674
|
-
: discoveredResources.defaultSecurityGroupId;
|
|
1675
|
-
|
|
1676
|
-
definition.resources.Resources.FriggAuroraSecurityGroup = {
|
|
1677
|
-
Type: 'AWS::EC2::SecurityGroup',
|
|
1678
|
-
Properties: {
|
|
1679
|
-
GroupDescription: 'Security group for Frigg Aurora PostgreSQL',
|
|
1680
|
-
VpcId: discoveredResources.defaultVpcId,
|
|
1681
|
-
SecurityGroupIngress: [
|
|
1682
|
-
{
|
|
1683
|
-
IpProtocol: 'tcp',
|
|
1684
|
-
FromPort: 5432,
|
|
1685
|
-
ToPort: 5432,
|
|
1686
|
-
SourceSecurityGroupId: lambdaSecurityGroupId,
|
|
1687
|
-
Description: 'PostgreSQL access from Lambda functions'
|
|
1688
|
-
}
|
|
1689
|
-
],
|
|
1690
|
-
Tags: [
|
|
1691
|
-
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-sg' },
|
|
1692
|
-
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1693
|
-
{ Key: 'Service', Value: '${self:service}' },
|
|
1694
|
-
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
1695
|
-
]
|
|
1696
|
-
}
|
|
1697
|
-
};
|
|
1698
|
-
|
|
1699
|
-
// 3. Secrets Manager Secret (database credentials)
|
|
1700
|
-
definition.resources.Resources.FriggDatabaseSecret = {
|
|
1701
|
-
Type: 'AWS::SecretsManager::Secret',
|
|
1702
|
-
Properties: {
|
|
1703
|
-
Name: '${self:service}-${self:provider.stage}-aurora-credentials',
|
|
1704
|
-
Description: 'Aurora PostgreSQL credentials for Frigg application',
|
|
1705
|
-
GenerateSecretString: {
|
|
1706
|
-
SecretStringTemplate: JSON.stringify({
|
|
1707
|
-
username: dbConfig.masterUsername || 'frigg_admin'
|
|
1708
|
-
}),
|
|
1709
|
-
GenerateStringKey: 'password',
|
|
1710
|
-
PasswordLength: 32,
|
|
1711
|
-
ExcludeCharacters: '"@/\\'
|
|
1712
|
-
},
|
|
1713
|
-
Tags: [
|
|
1714
|
-
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1715
|
-
{ Key: 'Service', Value: '${self:service}' },
|
|
1716
|
-
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
1717
|
-
]
|
|
1718
|
-
}
|
|
1719
|
-
};
|
|
1720
|
-
|
|
1721
|
-
// 4. Aurora Serverless v2 Cluster
|
|
1722
|
-
definition.resources.Resources.FriggAuroraCluster = {
|
|
1723
|
-
Type: 'AWS::RDS::DBCluster',
|
|
1724
|
-
DeletionPolicy: 'Snapshot',
|
|
1725
|
-
UpdateReplacePolicy: 'Snapshot',
|
|
1726
|
-
Properties: {
|
|
1727
|
-
Engine: 'aurora-postgresql',
|
|
1728
|
-
EngineVersion: dbConfig.engineVersion || '15.3',
|
|
1729
|
-
EngineMode: 'provisioned', // Required for Serverless v2
|
|
1730
|
-
DatabaseName: dbConfig.databaseName || 'frigg_db',
|
|
1731
|
-
MasterUsername: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:username}}' },
|
|
1732
|
-
MasterUserPassword: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:password}}' },
|
|
1733
|
-
DBSubnetGroupName: { Ref: 'FriggDBSubnetGroup' },
|
|
1734
|
-
VpcSecurityGroupIds: [{ Ref: 'FriggAuroraSecurityGroup' }],
|
|
1735
|
-
ServerlessV2ScalingConfiguration: {
|
|
1736
|
-
MinCapacity: dbConfig.scaling?.minCapacity || 0.5,
|
|
1737
|
-
MaxCapacity: dbConfig.scaling?.maxCapacity || 1.0
|
|
1738
|
-
},
|
|
1739
|
-
BackupRetentionPeriod: dbConfig.backupRetentionDays || 7,
|
|
1740
|
-
PreferredBackupWindow: dbConfig.preferredBackupWindow || '03:00-04:00',
|
|
1741
|
-
DeletionProtection: dbConfig.deletionProtection !== false,
|
|
1742
|
-
EnableCloudwatchLogsExports: ['postgresql'],
|
|
1743
|
-
Tags: [
|
|
1744
|
-
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-cluster' },
|
|
1745
|
-
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1746
|
-
{ Key: 'Service', Value: '${self:service}' },
|
|
1747
|
-
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
1748
|
-
]
|
|
1749
|
-
}
|
|
1750
|
-
};
|
|
1751
|
-
|
|
1752
|
-
// 5. Aurora Serverless v2 Instance
|
|
1753
|
-
definition.resources.Resources.FriggAuroraInstance = {
|
|
1754
|
-
Type: 'AWS::RDS::DBInstance',
|
|
1755
|
-
Properties: {
|
|
1756
|
-
Engine: 'aurora-postgresql',
|
|
1757
|
-
DBInstanceClass: 'db.serverless',
|
|
1758
|
-
DBClusterIdentifier: { Ref: 'FriggAuroraCluster' },
|
|
1759
|
-
PubliclyAccessible: false,
|
|
1760
|
-
EnablePerformanceInsights: dbConfig.enablePerformanceInsights || false,
|
|
1761
|
-
Tags: [
|
|
1762
|
-
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-instance' },
|
|
1763
|
-
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1764
|
-
{ Key: 'Service', Value: '${self:service}' },
|
|
1765
|
-
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
1766
|
-
]
|
|
1767
|
-
}
|
|
1768
|
-
};
|
|
1769
|
-
|
|
1770
|
-
// 6. Secret Attachment (links cluster to secret)
|
|
1771
|
-
definition.resources.Resources.FriggSecretAttachment = {
|
|
1772
|
-
Type: 'AWS::SecretsManager::SecretTargetAttachment',
|
|
1773
|
-
Properties: {
|
|
1774
|
-
SecretId: { Ref: 'FriggDatabaseSecret' },
|
|
1775
|
-
TargetId: { Ref: 'FriggAuroraCluster' },
|
|
1776
|
-
TargetType: 'AWS::RDS::DBCluster'
|
|
1777
|
-
}
|
|
1778
|
-
};
|
|
1779
|
-
|
|
1780
|
-
// 7. Add IAM permissions for Secrets Manager
|
|
1781
|
-
definition.provider.iamRoleStatements.push({
|
|
1782
|
-
Effect: 'Allow',
|
|
1783
|
-
Action: [
|
|
1784
|
-
'secretsmanager:GetSecretValue',
|
|
1785
|
-
'secretsmanager:DescribeSecret'
|
|
1786
|
-
],
|
|
1787
|
-
Resource: { Ref: 'FriggDatabaseSecret' }
|
|
1788
|
-
});
|
|
1789
|
-
|
|
1790
|
-
// 8. Set DATABASE_URL environment variable
|
|
1791
|
-
definition.provider.environment.DATABASE_URL = {
|
|
1792
|
-
'Fn::Sub': [
|
|
1793
|
-
'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
|
|
1794
|
-
{
|
|
1795
|
-
Username: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:username}}' },
|
|
1796
|
-
Password: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:password}}' },
|
|
1797
|
-
Endpoint: { 'Fn::GetAtt': ['FriggAuroraCluster', 'Endpoint'] },
|
|
1798
|
-
Port: { 'Fn::GetAtt': ['FriggAuroraCluster', 'Port'] },
|
|
1799
|
-
DatabaseName: dbConfig.databaseName || 'frigg_db'
|
|
1800
|
-
}
|
|
1801
|
-
]
|
|
1802
|
-
};
|
|
1803
|
-
|
|
1804
|
-
// 9. Set DB_TYPE for Prisma client selection
|
|
1805
|
-
definition.provider.environment.DB_TYPE = 'postgresql';
|
|
1806
|
-
|
|
1807
|
-
console.log('✅ Aurora infrastructure resources created');
|
|
1808
|
-
};
|
|
1809
|
-
|
|
1810
|
-
const useExistingAurora = (definition, AppDefinition, discoveredResources) => {
|
|
1811
|
-
const dbConfig = AppDefinition.database.postgres;
|
|
1812
|
-
|
|
1813
|
-
console.log(`🔗 Using existing Aurora cluster: ${discoveredResources.aurora.clusterIdentifier}`);
|
|
1814
|
-
|
|
1815
|
-
// Add IAM permissions for Secrets Manager if secret exists
|
|
1816
|
-
if (discoveredResources.aurora.secretArn) {
|
|
1817
|
-
definition.provider.iamRoleStatements.push({
|
|
1818
|
-
Effect: 'Allow',
|
|
1819
|
-
Action: [
|
|
1820
|
-
'secretsmanager:GetSecretValue',
|
|
1821
|
-
'secretsmanager:DescribeSecret'
|
|
1822
|
-
],
|
|
1823
|
-
Resource: discoveredResources.aurora.secretArn
|
|
1824
|
-
});
|
|
1825
|
-
|
|
1826
|
-
// Set DATABASE_URL from discovered secret
|
|
1827
|
-
definition.provider.environment.DATABASE_URL = {
|
|
1828
|
-
'Fn::Sub': [
|
|
1829
|
-
'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
|
|
1830
|
-
{
|
|
1831
|
-
Username: { 'Fn::Sub': `{{resolve:secretsmanager:${discoveredResources.aurora.secretArn}:SecretString:username}}` },
|
|
1832
|
-
Password: { 'Fn::Sub': `{{resolve:secretsmanager:${discoveredResources.aurora.secretArn}:SecretString:password}}` },
|
|
1833
|
-
Endpoint: discoveredResources.aurora.endpoint,
|
|
1834
|
-
Port: discoveredResources.aurora.port,
|
|
1835
|
-
DatabaseName: dbConfig.databaseName || 'frigg_db'
|
|
1836
|
-
}
|
|
1837
|
-
]
|
|
1838
|
-
};
|
|
1839
|
-
} else if (dbConfig.secretArn) {
|
|
1840
|
-
// Use user-provided secret ARN
|
|
1841
|
-
definition.provider.iamRoleStatements.push({
|
|
1842
|
-
Effect: 'Allow',
|
|
1843
|
-
Action: [
|
|
1844
|
-
'secretsmanager:GetSecretValue',
|
|
1845
|
-
'secretsmanager:DescribeSecret'
|
|
1846
|
-
],
|
|
1847
|
-
Resource: dbConfig.secretArn
|
|
1848
|
-
});
|
|
1849
|
-
|
|
1850
|
-
definition.provider.environment.DATABASE_URL = {
|
|
1851
|
-
'Fn::Sub': [
|
|
1852
|
-
'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
|
|
1853
|
-
{
|
|
1854
|
-
Username: { 'Fn::Sub': `{{resolve:secretsmanager:${dbConfig.secretArn}:SecretString:username}}` },
|
|
1855
|
-
Password: { 'Fn::Sub': `{{resolve:secretsmanager:${dbConfig.secretArn}:SecretString:password}}` },
|
|
1856
|
-
Endpoint: discoveredResources.aurora.endpoint,
|
|
1857
|
-
Port: discoveredResources.aurora.port,
|
|
1858
|
-
DatabaseName: dbConfig.databaseName || 'frigg_db'
|
|
1859
|
-
}
|
|
1860
|
-
]
|
|
1861
|
-
};
|
|
1862
|
-
} else {
|
|
1863
|
-
throw new Error('No database secret found. Provide secretArn in database.postgres configuration or ensure Secrets Manager secret exists.');
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
// Set DB_TYPE for Prisma client selection
|
|
1867
|
-
definition.provider.environment.DB_TYPE = 'postgresql';
|
|
1868
|
-
|
|
1869
|
-
console.log('✅ Existing Aurora cluster configured');
|
|
1870
|
-
};
|
|
1871
|
-
|
|
1872
|
-
const useDiscoveredAurora = (definition, AppDefinition, discoveredResources) => {
|
|
1873
|
-
console.log(`🔍 Using discovered Aurora cluster: ${discoveredResources.aurora.clusterIdentifier}`);
|
|
1874
|
-
useExistingAurora(definition, AppDefinition, discoveredResources);
|
|
1875
|
-
};
|
|
1876
|
-
|
|
1877
|
-
const configurePostgres = (definition, AppDefinition, discoveredResources) => {
|
|
1878
|
-
if (!AppDefinition.database?.postgres?.enable) {
|
|
1879
|
-
return;
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
// Validate VPC is enabled (required for Aurora deployment)
|
|
1883
|
-
if (!AppDefinition.vpc?.enable) {
|
|
1884
|
-
throw new Error(
|
|
1885
|
-
'Aurora PostgreSQL requires VPC deployment. ' +
|
|
1886
|
-
'Set vpc.enable to true in your app definition.'
|
|
1887
|
-
);
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
// Validate private subnets exist (Aurora requires at least 2 subnets in different AZs)
|
|
1891
|
-
// Skip validation if VPC management is 'create-new' (subnets will be created)
|
|
1892
|
-
const vpcManagement = AppDefinition.vpc?.management || 'discover';
|
|
1893
|
-
if (vpcManagement !== 'create-new' && (!discoveredResources.privateSubnetId1 || !discoveredResources.privateSubnetId2)) {
|
|
1894
|
-
throw new Error(
|
|
1895
|
-
'Aurora PostgreSQL requires at least 2 private subnets in different availability zones. ' +
|
|
1896
|
-
'No private subnets were discovered in your VPC. ' +
|
|
1897
|
-
'Please create private subnets or use VPC management mode "create-new".'
|
|
1898
|
-
);
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
const dbConfig = AppDefinition.database.postgres;
|
|
1902
|
-
const management = dbConfig.management || 'discover';
|
|
1903
|
-
|
|
1904
|
-
console.log(`\n🐘 PostgreSQL Management Mode: ${management}`);
|
|
1905
|
-
|
|
1906
|
-
if (management === 'create-new' || discoveredResources.aurora?.needsCreation) {
|
|
1907
|
-
createAuroraInfrastructure(definition, AppDefinition, discoveredResources);
|
|
1908
|
-
} else if (management === 'use-existing') {
|
|
1909
|
-
if (!discoveredResources.aurora?.clusterIdentifier && !dbConfig.clusterIdentifier) {
|
|
1910
|
-
throw new Error('PostgreSQL management is set to "use-existing" but no clusterIdentifier was found or provided');
|
|
1911
|
-
}
|
|
1912
|
-
useExistingAurora(definition, AppDefinition, discoveredResources);
|
|
1913
|
-
} else {
|
|
1914
|
-
// discover mode
|
|
1915
|
-
if (discoveredResources.aurora?.clusterIdentifier) {
|
|
1916
|
-
useDiscoveredAurora(definition, AppDefinition, discoveredResources);
|
|
1917
|
-
} else {
|
|
1918
|
-
throw new Error('No Aurora cluster found in discovery mode. Set management to "create-new" or provide clusterIdentifier with "use-existing".');
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
};
|
|
1922
|
-
|
|
1923
1917
|
const configureSsm = (definition, AppDefinition) => {
|
|
1924
1918
|
if (AppDefinition.ssm?.enable !== true) {
|
|
1925
1919
|
return;
|
|
@@ -1931,19 +1925,31 @@ const configureSsm = (definition, AppDefinition) => {
|
|
|
1931
1925
|
|
|
1932
1926
|
definition.provider.iamRoleStatements.push({
|
|
1933
1927
|
Effect: 'Allow',
|
|
1934
|
-
Action: [
|
|
1935
|
-
|
|
1928
|
+
Action: [
|
|
1929
|
+
'ssm:GetParameter',
|
|
1930
|
+
'ssm:GetParameters',
|
|
1931
|
+
'ssm:GetParametersByPath',
|
|
1932
|
+
],
|
|
1933
|
+
Resource: [
|
|
1934
|
+
'arn:aws:ssm:${self:provider.region}:*:parameter/${self:service}/${self:provider.stage}/*',
|
|
1935
|
+
],
|
|
1936
1936
|
});
|
|
1937
1937
|
|
|
1938
|
-
definition.provider.environment.SSM_PARAMETER_PREFIX =
|
|
1938
|
+
definition.provider.environment.SSM_PARAMETER_PREFIX =
|
|
1939
|
+
'/${self:service}/${self:provider.stage}';
|
|
1939
1940
|
};
|
|
1940
1941
|
|
|
1941
1942
|
const attachIntegrations = (definition, AppDefinition) => {
|
|
1942
|
-
if (
|
|
1943
|
+
if (
|
|
1944
|
+
!Array.isArray(AppDefinition.integrations) ||
|
|
1945
|
+
AppDefinition.integrations.length === 0
|
|
1946
|
+
) {
|
|
1943
1947
|
return;
|
|
1944
1948
|
}
|
|
1945
1949
|
|
|
1946
|
-
console.log(
|
|
1950
|
+
console.log(
|
|
1951
|
+
`Processing ${AppDefinition.integrations.length} integrations...`
|
|
1952
|
+
);
|
|
1947
1953
|
|
|
1948
1954
|
for (const integration of AppDefinition.integrations) {
|
|
1949
1955
|
if (!integration?.Definition?.name) {
|
|
@@ -1951,7 +1957,9 @@ const attachIntegrations = (definition, AppDefinition) => {
|
|
|
1951
1957
|
}
|
|
1952
1958
|
|
|
1953
1959
|
const integrationName = integration.Definition.name;
|
|
1954
|
-
const queueReference = `${
|
|
1960
|
+
const queueReference = `${
|
|
1961
|
+
integrationName.charAt(0).toUpperCase() + integrationName.slice(1)
|
|
1962
|
+
}Queue`;
|
|
1955
1963
|
const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
|
|
1956
1964
|
|
|
1957
1965
|
definition.functions[integrationName] = {
|
|
@@ -1974,7 +1982,9 @@ const attachIntegrations = (definition, AppDefinition) => {
|
|
|
1974
1982
|
VisibilityTimeout: 1800,
|
|
1975
1983
|
RedrivePolicy: {
|
|
1976
1984
|
maxReceiveCount: 1,
|
|
1977
|
-
deadLetterTargetArn: {
|
|
1985
|
+
deadLetterTargetArn: {
|
|
1986
|
+
'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
|
|
1987
|
+
},
|
|
1978
1988
|
},
|
|
1979
1989
|
},
|
|
1980
1990
|
};
|
|
@@ -1996,7 +2006,9 @@ const attachIntegrations = (definition, AppDefinition) => {
|
|
|
1996
2006
|
|
|
1997
2007
|
definition.provider.environment = {
|
|
1998
2008
|
...definition.provider.environment,
|
|
1999
|
-
[`${integrationName.toUpperCase()}_QUEUE_URL`]: {
|
|
2009
|
+
[`${integrationName.toUpperCase()}_QUEUE_URL`]: {
|
|
2010
|
+
Ref: queueReference,
|
|
2011
|
+
},
|
|
2000
2012
|
};
|
|
2001
2013
|
|
|
2002
2014
|
definition.custom[queueReference] = queueName;
|
|
@@ -2009,7 +2021,8 @@ const configureWebsockets = (definition, AppDefinition) => {
|
|
|
2009
2021
|
}
|
|
2010
2022
|
|
|
2011
2023
|
definition.functions.defaultWebsocket = {
|
|
2012
|
-
handler:
|
|
2024
|
+
handler:
|
|
2025
|
+
'node_modules/@friggframework/core/handlers/routers/websocket.handler',
|
|
2013
2026
|
events: [
|
|
2014
2027
|
{ websocket: { route: '$connect' } },
|
|
2015
2028
|
{ websocket: { route: '$default' } },
|
|
@@ -2018,49 +2031,24 @@ const configureWebsockets = (definition, AppDefinition) => {
|
|
|
2018
2031
|
};
|
|
2019
2032
|
};
|
|
2020
2033
|
|
|
2021
|
-
/**
|
|
2022
|
-
* Ensure Prisma Lambda Layer exists
|
|
2023
|
-
* Automatically builds the layer if it doesn't exist in the project root
|
|
2024
|
-
*/
|
|
2025
|
-
async function ensurePrismaLayerExists() {
|
|
2026
|
-
const projectRoot = process.cwd();
|
|
2027
|
-
const layerPath = path.join(projectRoot, 'layers/prisma');
|
|
2028
|
-
|
|
2029
|
-
// Check if layer already exists
|
|
2030
|
-
if (fs.existsSync(layerPath)) {
|
|
2031
|
-
console.log('✓ Prisma Lambda Layer already exists at', layerPath);
|
|
2032
|
-
return;
|
|
2033
|
-
}
|
|
2034
|
-
|
|
2035
|
-
// Layer doesn't exist - build it automatically
|
|
2036
|
-
console.log('📦 Prisma Lambda Layer not found - building automatically...');
|
|
2037
|
-
console.log(' This may take a minute on first deployment.\n');
|
|
2038
|
-
|
|
2039
|
-
try {
|
|
2040
|
-
await buildPrismaLayer();
|
|
2041
|
-
console.log('✓ Prisma Lambda Layer built successfully\n');
|
|
2042
|
-
} catch (error) {
|
|
2043
|
-
console.error('✗ Failed to build Prisma Lambda Layer:', error.message);
|
|
2044
|
-
console.error(' You may need to run: npm install @friggframework/core\n');
|
|
2045
|
-
throw error;
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
2034
|
const composeServerlessDefinition = async (AppDefinition) => {
|
|
2050
2035
|
console.log('composeServerlessDefinition', AppDefinition);
|
|
2051
2036
|
|
|
2052
|
-
// Ensure Prisma layer exists before generating serverless config
|
|
2053
|
-
await ensurePrismaLayerExists();
|
|
2054
|
-
|
|
2055
2037
|
const discoveredResources = await gatherDiscoveredResources(AppDefinition);
|
|
2056
2038
|
const appEnvironmentVars = getAppEnvironmentVars(AppDefinition);
|
|
2057
|
-
const definition = createBaseDefinition(
|
|
2039
|
+
const definition = createBaseDefinition(
|
|
2040
|
+
AppDefinition,
|
|
2041
|
+
appEnvironmentVars,
|
|
2042
|
+
discoveredResources
|
|
2043
|
+
);
|
|
2058
2044
|
|
|
2059
2045
|
// Check if we're in local build mode (AWS discovery was skipped)
|
|
2060
2046
|
const isLocalBuild = !shouldRunDiscovery(AppDefinition);
|
|
2061
2047
|
|
|
2062
2048
|
if (isLocalBuild) {
|
|
2063
|
-
console.log(
|
|
2049
|
+
console.log(
|
|
2050
|
+
'🏠 Local build mode detected - skipping AWS-dependent configurations'
|
|
2051
|
+
);
|
|
2064
2052
|
}
|
|
2065
2053
|
|
|
2066
2054
|
// Apply configurations (skip AWS-dependent ones in local build mode)
|
|
@@ -2070,7 +2058,9 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
2070
2058
|
configurePostgres(definition, AppDefinition, discoveredResources);
|
|
2071
2059
|
configureSsm(definition, AppDefinition);
|
|
2072
2060
|
} else {
|
|
2073
|
-
console.log(
|
|
2061
|
+
console.log(
|
|
2062
|
+
' ⏭️ Skipping: KMS, VPC, PostgreSQL, SSM configurations'
|
|
2063
|
+
);
|
|
2074
2064
|
}
|
|
2075
2065
|
|
|
2076
2066
|
attachIntegrations(definition, AppDefinition);
|