@friggframework/devtools 2.0.0--canary.461.ec909cf.0 → 2.0.0--canary.461.9483dbe.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/build.test.js +6 -6
- package/frigg-cli/build-command/index.js +1 -1
- package/frigg-cli/deploy-command/index.js +6 -6
- package/frigg-cli/generate-command/index.js +2 -2
- package/frigg-cli/generate-iam-command.js +10 -10
- package/frigg-cli/start-command/index.js +1 -1
- package/frigg-cli/start-command/start-command.test.js +3 -3
- package/frigg-cli/utils/database-validator.js +14 -21
- package/infrastructure/REFACTOR.md +532 -0
- package/infrastructure/TRANSFORMATION-VISUAL.md +239 -0
- package/infrastructure/__tests__/postgres-config.test.js +1 -1
- package/infrastructure/create-frigg-infrastructure.js +1 -1
- package/infrastructure/{DEPLOYMENT-INSTRUCTIONS.md → docs/deployment-instructions.md} +3 -3
- package/infrastructure/{IAM-POLICY-TEMPLATES.md → docs/iam-policy-templates.md} +9 -10
- package/infrastructure/domains/database/aurora-discovery.js +81 -0
- package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
- package/infrastructure/domains/integration/integration-builder.js +178 -0
- package/infrastructure/domains/integration/integration-builder.test.js +362 -0
- package/infrastructure/domains/integration/websocket-builder.js +69 -0
- package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
- package/infrastructure/domains/networking/vpc-discovery.test.js +257 -0
- package/infrastructure/domains/parameters/ssm-builder.js +79 -0
- package/infrastructure/domains/parameters/ssm-builder.test.js +188 -0
- package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
- package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
- package/infrastructure/{iam-generator.js → domains/security/iam-generator.js} +2 -2
- package/infrastructure/domains/security/kms-builder.js +169 -0
- package/infrastructure/domains/security/kms-builder.test.js +354 -0
- package/infrastructure/domains/security/kms-discovery.js +80 -0
- package/infrastructure/domains/security/kms-discovery.test.js +176 -0
- package/infrastructure/domains/shared/base-builder.js +112 -0
- package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
- package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
- package/infrastructure/domains/shared/environment-builder.js +118 -0
- package/infrastructure/domains/shared/environment-builder.test.js +246 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +366 -0
- package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
- package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
- package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
- package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
- package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
- package/infrastructure/domains/shared/resource-discovery.js +132 -0
- package/infrastructure/domains/shared/resource-discovery.test.js +410 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +2 -3
- package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +248 -0
- package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +259 -0
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +134 -0
- package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
- package/infrastructure/esbuild.config.js +53 -0
- package/infrastructure/infrastructure-composer.js +85 -0
- package/infrastructure/scripts/build-prisma-layer.js +60 -47
- package/infrastructure/{build-time-discovery.test.js → scripts/build-time-discovery.test.js} +5 -4
- package/layers/prisma/nodejs/package.json +8 -0
- package/management-ui/server/utils/environment/awsParameterStore.js +29 -18
- package/package.json +8 -8
- package/infrastructure/aws-discovery.js +0 -1704
- package/infrastructure/aws-discovery.test.js +0 -1666
- package/infrastructure/serverless-template.js +0 -2804
- package/infrastructure/serverless-template.test.js +0 -1897
- /package/infrastructure/{POSTGRES-CONFIGURATION.md → docs/POSTGRES-CONFIGURATION.md} +0 -0
- /package/infrastructure/{WEBSOCKET-CONFIGURATION.md → docs/WEBSOCKET-CONFIGURATION.md} +0 -0
- /package/infrastructure/{GENERATE-IAM-DOCS.md → docs/generate-iam-command.md} +0 -0
- /package/infrastructure/{iam-generator.test.js → domains/security/iam-generator.test.js} +0 -0
- /package/infrastructure/{frigg-deployment-iam-stack.yaml → domains/security/templates/frigg-deployment-iam-stack.yaml} +0 -0
- /package/infrastructure/{iam-policy-basic.json → domains/security/templates/iam-policy-basic.json} +0 -0
- /package/infrastructure/{iam-policy-full.json → domains/security/templates/iam-policy-full.json} +0 -0
- /package/infrastructure/{env-validator.js → domains/shared/validation/env-validator.js} +0 -0
- /package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +0 -0
- /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 };
|