@friggframework/devtools 2.0.0-next.39 → 2.0.0-next.40
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/infrastructure/README.md +19 -8
- package/infrastructure/aws-discovery.js +951 -345
- package/infrastructure/aws-discovery.test.js +1031 -184
- package/infrastructure/build-time-discovery.test.js +3 -0
- package/infrastructure/iam-generator.js +46 -0
- package/infrastructure/iam-generator.test.js +7 -4
- package/infrastructure/serverless-template.js +1096 -813
- package/infrastructure/serverless-template.test.js +1036 -21
- package/package.json +8 -6
- package/infrastructure/AWS-DISCOVERY-TROUBLESHOOTING.md +0 -245
- package/infrastructure/AWS-IAM-CREDENTIAL-NEEDS.md +0 -627
- package/infrastructure/README-TESTING.md +0 -332
|
@@ -2,28 +2,13 @@ const path = require('path');
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const { AWSDiscovery } = require('./aws-discovery');
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*/
|
|
10
|
-
const shouldRunDiscovery = (AppDefinition) => {
|
|
11
|
-
return (
|
|
12
|
-
AppDefinition.vpc?.enable === true ||
|
|
13
|
-
AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
|
|
14
|
-
AppDefinition.ssm?.enable === true
|
|
15
|
-
);
|
|
16
|
-
};
|
|
5
|
+
const shouldRunDiscovery = (AppDefinition) =>
|
|
6
|
+
AppDefinition.vpc?.enable === true ||
|
|
7
|
+
AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
|
|
8
|
+
AppDefinition.ssm?.enable === true;
|
|
17
9
|
|
|
18
|
-
/**
|
|
19
|
-
* Extract environment variables from AppDefinition
|
|
20
|
-
* @param {Object} AppDefinition - Application definition
|
|
21
|
-
* @returns {Object} Environment variables to set in serverless
|
|
22
|
-
*/
|
|
23
10
|
const getAppEnvironmentVars = (AppDefinition) => {
|
|
24
11
|
const envVars = {};
|
|
25
|
-
|
|
26
|
-
// AWS Lambda reserved environment variables that cannot be set (from official AWS docs)
|
|
27
12
|
const reservedVars = new Set([
|
|
28
13
|
'_HANDLER',
|
|
29
14
|
'_X_AMZN_TRACE_ID',
|
|
@@ -42,117 +27,80 @@ const getAppEnvironmentVars = (AppDefinition) => {
|
|
|
42
27
|
'AWS_SESSION_TOKEN',
|
|
43
28
|
]);
|
|
44
29
|
|
|
45
|
-
if (AppDefinition.environment) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const skippedKeys = [];
|
|
30
|
+
if (!AppDefinition.environment) {
|
|
31
|
+
return envVars;
|
|
32
|
+
}
|
|
49
33
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
skippedKeys.push(key);
|
|
54
|
-
} else {
|
|
55
|
-
envVars[key] = `\${env:${key}, ''}`;
|
|
56
|
-
envKeys.push(key);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
34
|
+
console.log('📋 Loading environment variables from appDefinition...');
|
|
35
|
+
const envKeys = [];
|
|
36
|
+
const skippedKeys = [];
|
|
60
37
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
if (skippedKeys.length > 0) {
|
|
69
|
-
console.log(
|
|
70
|
-
` ⚠️ Skipped ${
|
|
71
|
-
skippedKeys.length
|
|
72
|
-
} reserved AWS Lambda variables: ${skippedKeys.join(', ')}`
|
|
73
|
-
);
|
|
38
|
+
for (const [key, value] of Object.entries(AppDefinition.environment)) {
|
|
39
|
+
if (value !== true) continue;
|
|
40
|
+
if (reservedVars.has(key)) {
|
|
41
|
+
skippedKeys.push(key);
|
|
42
|
+
continue;
|
|
74
43
|
}
|
|
44
|
+
envVars[key] = `\${env:${key}, ''}`;
|
|
45
|
+
envKeys.push(key);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (envKeys.length > 0) {
|
|
49
|
+
console.log(` Found ${envKeys.length} environment variables: ${envKeys.join(', ')}`);
|
|
50
|
+
}
|
|
51
|
+
if (skippedKeys.length > 0) {
|
|
52
|
+
console.log(
|
|
53
|
+
` ⚠️ Skipped ${skippedKeys.length} reserved AWS Lambda variables: ${skippedKeys.join(', ')}`
|
|
54
|
+
);
|
|
75
55
|
}
|
|
76
56
|
|
|
77
57
|
return envVars;
|
|
78
58
|
};
|
|
79
59
|
|
|
80
|
-
/**
|
|
81
|
-
* Find the actual path to node_modules directory
|
|
82
|
-
* Tries multiple methods to locate node_modules:
|
|
83
|
-
* 1. Traversing up from current directory
|
|
84
|
-
* 2. Using npm root command
|
|
85
|
-
* 3. Looking for package.json and adjacent node_modules
|
|
86
|
-
* @returns {string} Path to node_modules directory
|
|
87
|
-
*/
|
|
88
60
|
const findNodeModulesPath = () => {
|
|
89
61
|
try {
|
|
90
|
-
// Method 1: Try to find node_modules by traversing up from current directory
|
|
91
62
|
let currentDir = process.cwd();
|
|
92
63
|
let nodeModulesPath = null;
|
|
93
64
|
|
|
94
|
-
// Traverse up to 5 levels to find node_modules
|
|
95
65
|
for (let i = 0; i < 5; i++) {
|
|
96
66
|
const potentialPath = path.join(currentDir, 'node_modules');
|
|
97
67
|
if (fs.existsSync(potentialPath)) {
|
|
98
68
|
nodeModulesPath = potentialPath;
|
|
99
|
-
console.log(
|
|
100
|
-
`Found node_modules at: ${nodeModulesPath} (method 1)`
|
|
101
|
-
);
|
|
69
|
+
console.log(`Found node_modules at: ${nodeModulesPath} (method 1)`);
|
|
102
70
|
break;
|
|
103
71
|
}
|
|
104
|
-
// Move up one directory
|
|
105
72
|
const parentDir = path.dirname(currentDir);
|
|
106
|
-
if (parentDir === currentDir)
|
|
107
|
-
// We've reached the root
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
73
|
+
if (parentDir === currentDir) break;
|
|
110
74
|
currentDir = parentDir;
|
|
111
75
|
}
|
|
112
76
|
|
|
113
|
-
// Method 2: If method 1 fails, try using npm root command
|
|
114
77
|
if (!nodeModulesPath) {
|
|
115
78
|
try {
|
|
116
|
-
// This requires child_process, so let's require it here
|
|
117
79
|
const { execSync } = require('node:child_process');
|
|
118
|
-
const npmRoot = execSync('npm root', {
|
|
119
|
-
encoding: 'utf8',
|
|
120
|
-
}).trim();
|
|
80
|
+
const npmRoot = execSync('npm root', { encoding: 'utf8' }).trim();
|
|
121
81
|
if (fs.existsSync(npmRoot)) {
|
|
122
82
|
nodeModulesPath = npmRoot;
|
|
123
|
-
console.log(
|
|
124
|
-
`Found node_modules at: ${nodeModulesPath} (method 2)`
|
|
125
|
-
);
|
|
83
|
+
console.log(`Found node_modules at: ${nodeModulesPath} (method 2)`);
|
|
126
84
|
}
|
|
127
85
|
} catch (npmError) {
|
|
128
86
|
console.error('Error executing npm root:', npmError);
|
|
129
87
|
}
|
|
130
88
|
}
|
|
131
89
|
|
|
132
|
-
// Method 3: If all else fails, check for a package.json and assume node_modules is adjacent
|
|
133
90
|
if (!nodeModulesPath) {
|
|
134
91
|
currentDir = process.cwd();
|
|
135
92
|
for (let i = 0; i < 5; i++) {
|
|
136
93
|
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
137
94
|
if (fs.existsSync(packageJsonPath)) {
|
|
138
|
-
const potentialNodeModules = path.join(
|
|
139
|
-
currentDir,
|
|
140
|
-
'node_modules'
|
|
141
|
-
);
|
|
95
|
+
const potentialNodeModules = path.join(currentDir, 'node_modules');
|
|
142
96
|
if (fs.existsSync(potentialNodeModules)) {
|
|
143
97
|
nodeModulesPath = potentialNodeModules;
|
|
144
|
-
console.log(
|
|
145
|
-
`Found node_modules at: ${nodeModulesPath} (method 3)`
|
|
146
|
-
);
|
|
98
|
+
console.log(`Found node_modules at: ${nodeModulesPath} (method 3)`);
|
|
147
99
|
break;
|
|
148
100
|
}
|
|
149
101
|
}
|
|
150
|
-
// Move up one directory
|
|
151
102
|
const parentDir = path.dirname(currentDir);
|
|
152
|
-
if (parentDir === currentDir)
|
|
153
|
-
// We've reached the root
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
103
|
+
if (parentDir === currentDir) break;
|
|
156
104
|
currentDir = parentDir;
|
|
157
105
|
}
|
|
158
106
|
}
|
|
@@ -161,9 +109,7 @@ const findNodeModulesPath = () => {
|
|
|
161
109
|
return nodeModulesPath;
|
|
162
110
|
}
|
|
163
111
|
|
|
164
|
-
console.warn(
|
|
165
|
-
'Could not find node_modules path, falling back to default'
|
|
166
|
-
);
|
|
112
|
+
console.warn('Could not find node_modules path, falling back to default');
|
|
167
113
|
return path.resolve(process.cwd(), '../node_modules');
|
|
168
114
|
} catch (error) {
|
|
169
115
|
console.error('Error finding node_modules path:', error);
|
|
@@ -171,14 +117,7 @@ const findNodeModulesPath = () => {
|
|
|
171
117
|
}
|
|
172
118
|
};
|
|
173
119
|
|
|
174
|
-
/**
|
|
175
|
-
* Modify handler paths to point to the correct node_modules location
|
|
176
|
-
* Only modifies paths when running in offline mode
|
|
177
|
-
* @param {Object} functions - Serverless functions configuration object
|
|
178
|
-
* @returns {Object} Modified functions object with updated handler paths
|
|
179
|
-
*/
|
|
180
120
|
const modifyHandlerPaths = (functions) => {
|
|
181
|
-
// Check if we're running in offline mode
|
|
182
121
|
const isOffline = process.argv.includes('offline');
|
|
183
122
|
console.log('isOffline', isOffline);
|
|
184
123
|
|
|
@@ -194,32 +133,17 @@ const modifyHandlerPaths = (functions) => {
|
|
|
194
133
|
console.log('functionName', functionName);
|
|
195
134
|
const functionDef = modifiedFunctions[functionName];
|
|
196
135
|
if (functionDef?.handler?.includes('node_modules/')) {
|
|
197
|
-
// Replace node_modules/ with the actual path to node_modules/
|
|
198
136
|
const relativePath = path.relative(process.cwd(), nodeModulesPath);
|
|
199
|
-
functionDef.handler = functionDef.handler.replace(
|
|
200
|
-
|
|
201
|
-
`${relativePath}/`
|
|
202
|
-
);
|
|
203
|
-
console.log(
|
|
204
|
-
`Updated handler for ${functionName}: ${functionDef.handler}`
|
|
205
|
-
);
|
|
137
|
+
functionDef.handler = functionDef.handler.replace('node_modules/', `${relativePath}/`);
|
|
138
|
+
console.log(`Updated handler for ${functionName}: ${functionDef.handler}`);
|
|
206
139
|
}
|
|
207
140
|
}
|
|
208
141
|
|
|
209
142
|
return modifiedFunctions;
|
|
210
143
|
};
|
|
211
144
|
|
|
212
|
-
/**
|
|
213
|
-
* Create VPC infrastructure resources for CloudFormation
|
|
214
|
-
* Creates VPC, subnets, NAT gateway, route tables, and security groups
|
|
215
|
-
* @param {Object} AppDefinition - Application definition object
|
|
216
|
-
* @param {Object} AppDefinition.vpc - VPC configuration
|
|
217
|
-
* @param {string} [AppDefinition.vpc.cidrBlock='10.0.0.0/16'] - CIDR block for VPC
|
|
218
|
-
* @returns {Object} CloudFormation resources for VPC infrastructure
|
|
219
|
-
*/
|
|
220
145
|
const createVPCInfrastructure = (AppDefinition) => {
|
|
221
146
|
const vpcResources = {
|
|
222
|
-
// VPC
|
|
223
147
|
FriggVPC: {
|
|
224
148
|
Type: 'AWS::EC2::VPC',
|
|
225
149
|
Properties: {
|
|
@@ -227,28 +151,24 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
227
151
|
EnableDnsHostnames: true,
|
|
228
152
|
EnableDnsSupport: true,
|
|
229
153
|
Tags: [
|
|
230
|
-
{
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
},
|
|
154
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc' },
|
|
155
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
156
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
157
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
234
158
|
],
|
|
235
159
|
},
|
|
236
160
|
},
|
|
237
|
-
|
|
238
|
-
// Internet Gateway
|
|
239
161
|
FriggInternetGateway: {
|
|
240
162
|
Type: 'AWS::EC2::InternetGateway',
|
|
241
163
|
Properties: {
|
|
242
164
|
Tags: [
|
|
243
|
-
{
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
},
|
|
165
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-igw' },
|
|
166
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
167
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
168
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
247
169
|
],
|
|
248
170
|
},
|
|
249
171
|
},
|
|
250
|
-
|
|
251
|
-
// Attach Internet Gateway to VPC
|
|
252
172
|
FriggVPCGatewayAttachment: {
|
|
253
173
|
Type: 'AWS::EC2::VPCGatewayAttachment',
|
|
254
174
|
Properties: {
|
|
@@ -256,8 +176,6 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
256
176
|
InternetGatewayId: { Ref: 'FriggInternetGateway' },
|
|
257
177
|
},
|
|
258
178
|
},
|
|
259
|
-
|
|
260
|
-
// Public Subnet for NAT Gateway
|
|
261
179
|
FriggPublicSubnet: {
|
|
262
180
|
Type: 'AWS::EC2::Subnet',
|
|
263
181
|
Properties: {
|
|
@@ -266,15 +184,14 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
266
184
|
AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
|
|
267
185
|
MapPublicIpOnLaunch: true,
|
|
268
186
|
Tags: [
|
|
269
|
-
{
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
},
|
|
187
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-subnet' },
|
|
188
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
189
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
190
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
191
|
+
{ Key: 'Type', Value: 'Public' },
|
|
273
192
|
],
|
|
274
193
|
},
|
|
275
194
|
},
|
|
276
|
-
|
|
277
|
-
// Private Subnet 1 for Lambda
|
|
278
195
|
FriggPrivateSubnet1: {
|
|
279
196
|
Type: 'AWS::EC2::Subnet',
|
|
280
197
|
Properties: {
|
|
@@ -282,15 +199,14 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
282
199
|
CidrBlock: '10.0.2.0/24',
|
|
283
200
|
AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
|
|
284
201
|
Tags: [
|
|
285
|
-
{
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
},
|
|
202
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-subnet-1' },
|
|
203
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
204
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
205
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
206
|
+
{ Key: 'Type', Value: 'Private' },
|
|
289
207
|
],
|
|
290
208
|
},
|
|
291
209
|
},
|
|
292
|
-
|
|
293
|
-
// Private Subnet 2 for Lambda (different AZ for redundancy)
|
|
294
210
|
FriggPrivateSubnet2: {
|
|
295
211
|
Type: 'AWS::EC2::Subnet',
|
|
296
212
|
Properties: {
|
|
@@ -298,61 +214,53 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
298
214
|
CidrBlock: '10.0.3.0/24',
|
|
299
215
|
AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
|
|
300
216
|
Tags: [
|
|
301
|
-
{
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
},
|
|
217
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-subnet-2' },
|
|
218
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
219
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
220
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
221
|
+
{ Key: 'Type', Value: 'Private' },
|
|
305
222
|
],
|
|
306
223
|
},
|
|
307
224
|
},
|
|
308
|
-
|
|
309
|
-
// Elastic IP for NAT Gateway
|
|
310
225
|
FriggNATGatewayEIP: {
|
|
311
226
|
Type: 'AWS::EC2::EIP',
|
|
312
227
|
Properties: {
|
|
313
228
|
Domain: 'vpc',
|
|
314
229
|
Tags: [
|
|
315
|
-
{
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
},
|
|
230
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-eip' },
|
|
231
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
232
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
233
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
319
234
|
],
|
|
320
235
|
},
|
|
321
236
|
DependsOn: 'FriggVPCGatewayAttachment',
|
|
322
237
|
},
|
|
323
|
-
|
|
324
|
-
// NAT Gateway for private subnet internet access
|
|
325
238
|
FriggNATGateway: {
|
|
326
239
|
Type: 'AWS::EC2::NatGateway',
|
|
327
240
|
Properties: {
|
|
328
|
-
AllocationId: {
|
|
329
|
-
'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'],
|
|
330
|
-
},
|
|
241
|
+
AllocationId: { 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'] },
|
|
331
242
|
SubnetId: { Ref: 'FriggPublicSubnet' },
|
|
332
243
|
Tags: [
|
|
333
|
-
{
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
},
|
|
244
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-gateway' },
|
|
245
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
246
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
247
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
337
248
|
],
|
|
338
249
|
},
|
|
339
250
|
},
|
|
340
|
-
|
|
341
|
-
// Public Route Table
|
|
342
251
|
FriggPublicRouteTable: {
|
|
343
252
|
Type: 'AWS::EC2::RouteTable',
|
|
344
253
|
Properties: {
|
|
345
254
|
VpcId: { Ref: 'FriggVPC' },
|
|
346
255
|
Tags: [
|
|
347
|
-
{
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
},
|
|
256
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-rt' },
|
|
257
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
258
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
259
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
260
|
+
{ Key: 'Type', Value: 'Public' },
|
|
351
261
|
],
|
|
352
262
|
},
|
|
353
263
|
},
|
|
354
|
-
|
|
355
|
-
// Public Route to Internet Gateway
|
|
356
264
|
FriggPublicRoute: {
|
|
357
265
|
Type: 'AWS::EC2::Route',
|
|
358
266
|
Properties: {
|
|
@@ -362,8 +270,6 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
362
270
|
},
|
|
363
271
|
DependsOn: 'FriggVPCGatewayAttachment',
|
|
364
272
|
},
|
|
365
|
-
|
|
366
|
-
// Associate Public Subnet with Public Route Table
|
|
367
273
|
FriggPublicSubnetRouteTableAssociation: {
|
|
368
274
|
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
369
275
|
Properties: {
|
|
@@ -371,22 +277,19 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
371
277
|
RouteTableId: { Ref: 'FriggPublicRouteTable' },
|
|
372
278
|
},
|
|
373
279
|
},
|
|
374
|
-
|
|
375
|
-
// Private Route Table
|
|
376
280
|
FriggPrivateRouteTable: {
|
|
377
281
|
Type: 'AWS::EC2::RouteTable',
|
|
378
282
|
Properties: {
|
|
379
283
|
VpcId: { Ref: 'FriggVPC' },
|
|
380
284
|
Tags: [
|
|
381
|
-
{
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
},
|
|
285
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-rt' },
|
|
286
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
287
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
288
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
289
|
+
{ Key: 'Type', Value: 'Private' },
|
|
385
290
|
],
|
|
386
291
|
},
|
|
387
292
|
},
|
|
388
|
-
|
|
389
|
-
// Private Route to NAT Gateway
|
|
390
293
|
FriggPrivateRoute: {
|
|
391
294
|
Type: 'AWS::EC2::Route',
|
|
392
295
|
Properties: {
|
|
@@ -395,8 +298,6 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
395
298
|
NatGatewayId: { Ref: 'FriggNATGateway' },
|
|
396
299
|
},
|
|
397
300
|
},
|
|
398
|
-
|
|
399
|
-
// Associate Private Subnet 1 with Private Route Table
|
|
400
301
|
FriggPrivateSubnet1RouteTableAssociation: {
|
|
401
302
|
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
402
303
|
Properties: {
|
|
@@ -404,8 +305,6 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
404
305
|
RouteTableId: { Ref: 'FriggPrivateRouteTable' },
|
|
405
306
|
},
|
|
406
307
|
},
|
|
407
|
-
|
|
408
|
-
// Associate Private Subnet 2 with Private Route Table
|
|
409
308
|
FriggPrivateSubnet2RouteTableAssociation: {
|
|
410
309
|
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
411
310
|
Properties: {
|
|
@@ -413,63 +312,29 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
413
312
|
RouteTableId: { Ref: 'FriggPrivateRouteTable' },
|
|
414
313
|
},
|
|
415
314
|
},
|
|
416
|
-
|
|
417
|
-
// Security Group for Lambda functions
|
|
418
315
|
FriggLambdaSecurityGroup: {
|
|
419
316
|
Type: 'AWS::EC2::SecurityGroup',
|
|
420
317
|
Properties: {
|
|
421
318
|
GroupDescription: 'Security group for Frigg Lambda functions',
|
|
422
319
|
VpcId: { Ref: 'FriggVPC' },
|
|
423
320
|
SecurityGroupEgress: [
|
|
424
|
-
{
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
Description: 'HTTPS outbound',
|
|
430
|
-
},
|
|
431
|
-
{
|
|
432
|
-
IpProtocol: 'tcp',
|
|
433
|
-
FromPort: 80,
|
|
434
|
-
ToPort: 80,
|
|
435
|
-
CidrIp: '0.0.0.0/0',
|
|
436
|
-
Description: 'HTTP outbound',
|
|
437
|
-
},
|
|
438
|
-
{
|
|
439
|
-
IpProtocol: 'tcp',
|
|
440
|
-
FromPort: 53,
|
|
441
|
-
ToPort: 53,
|
|
442
|
-
CidrIp: '0.0.0.0/0',
|
|
443
|
-
Description: 'DNS TCP',
|
|
444
|
-
},
|
|
445
|
-
{
|
|
446
|
-
IpProtocol: 'udp',
|
|
447
|
-
FromPort: 53,
|
|
448
|
-
ToPort: 53,
|
|
449
|
-
CidrIp: '0.0.0.0/0',
|
|
450
|
-
Description: 'DNS UDP',
|
|
451
|
-
},
|
|
452
|
-
{
|
|
453
|
-
IpProtocol: 'tcp',
|
|
454
|
-
FromPort: 27017,
|
|
455
|
-
ToPort: 27017,
|
|
456
|
-
CidrIp: '0.0.0.0/0',
|
|
457
|
-
Description: 'MongoDB outbound',
|
|
458
|
-
},
|
|
321
|
+
{ IpProtocol: 'tcp', FromPort: 443, ToPort: 443, CidrIp: '0.0.0.0/0', Description: 'HTTPS outbound' },
|
|
322
|
+
{ IpProtocol: 'tcp', FromPort: 80, ToPort: 80, CidrIp: '0.0.0.0/0', Description: 'HTTP outbound' },
|
|
323
|
+
{ IpProtocol: 'tcp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS TCP' },
|
|
324
|
+
{ IpProtocol: 'udp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS UDP' },
|
|
325
|
+
{ IpProtocol: 'tcp', FromPort: 27017, ToPort: 27017, CidrIp: '0.0.0.0/0', Description: 'MongoDB outbound' },
|
|
459
326
|
],
|
|
460
327
|
Tags: [
|
|
461
|
-
{
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
},
|
|
328
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-sg' },
|
|
329
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
330
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
331
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
465
332
|
],
|
|
466
333
|
},
|
|
467
334
|
},
|
|
468
335
|
};
|
|
469
336
|
|
|
470
|
-
// Add VPC Endpoints for cost optimization
|
|
471
337
|
if (AppDefinition.vpc.enableVPCEndpoints !== false) {
|
|
472
|
-
// S3 Gateway Endpoint (free)
|
|
473
338
|
vpcResources.FriggS3VPCEndpoint = {
|
|
474
339
|
Type: 'AWS::EC2::VPCEndpoint',
|
|
475
340
|
Properties: {
|
|
@@ -480,7 +345,6 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
480
345
|
},
|
|
481
346
|
};
|
|
482
347
|
|
|
483
|
-
// DynamoDB Gateway Endpoint (free)
|
|
484
348
|
vpcResources.FriggDynamoDBVPCEndpoint = {
|
|
485
349
|
Type: 'AWS::EC2::VPCEndpoint',
|
|
486
350
|
Properties: {
|
|
@@ -491,7 +355,6 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
491
355
|
},
|
|
492
356
|
};
|
|
493
357
|
|
|
494
|
-
// KMS Interface Endpoint (paid, but useful if using KMS)
|
|
495
358
|
if (AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
|
|
496
359
|
vpcResources.FriggKMSVPCEndpoint = {
|
|
497
360
|
Type: 'AWS::EC2::VPCEndpoint',
|
|
@@ -499,58 +362,54 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
499
362
|
VpcId: { Ref: 'FriggVPC' },
|
|
500
363
|
ServiceName: 'com.amazonaws.${self:provider.region}.kms',
|
|
501
364
|
VpcEndpointType: 'Interface',
|
|
502
|
-
SubnetIds: [
|
|
503
|
-
|
|
504
|
-
{ Ref: 'FriggPrivateSubnet2' },
|
|
505
|
-
],
|
|
506
|
-
SecurityGroupIds: [
|
|
507
|
-
{ Ref: 'FriggVPCEndpointSecurityGroup' },
|
|
508
|
-
],
|
|
365
|
+
SubnetIds: [{ Ref: 'FriggPrivateSubnet1' }, { Ref: 'FriggPrivateSubnet2' }],
|
|
366
|
+
SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
|
|
509
367
|
PrivateDnsEnabled: true,
|
|
510
368
|
},
|
|
511
369
|
};
|
|
512
370
|
}
|
|
513
371
|
|
|
514
|
-
// Secrets Manager Interface Endpoint (paid, but useful for secrets)
|
|
515
372
|
vpcResources.FriggSecretsManagerVPCEndpoint = {
|
|
516
373
|
Type: 'AWS::EC2::VPCEndpoint',
|
|
517
374
|
Properties: {
|
|
518
375
|
VpcId: { Ref: 'FriggVPC' },
|
|
519
|
-
ServiceName:
|
|
520
|
-
'com.amazonaws.${self:provider.region}.secretsmanager',
|
|
376
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.secretsmanager',
|
|
521
377
|
VpcEndpointType: 'Interface',
|
|
522
|
-
SubnetIds: [
|
|
523
|
-
{ Ref: 'FriggPrivateSubnet1' },
|
|
524
|
-
{ Ref: 'FriggPrivateSubnet2' },
|
|
525
|
-
],
|
|
378
|
+
SubnetIds: [{ Ref: 'FriggPrivateSubnet1' }, { Ref: 'FriggPrivateSubnet2' }],
|
|
526
379
|
SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
|
|
527
380
|
PrivateDnsEnabled: true,
|
|
528
381
|
},
|
|
529
382
|
};
|
|
530
383
|
|
|
531
|
-
// Security Group for VPC Endpoints
|
|
532
384
|
vpcResources.FriggVPCEndpointSecurityGroup = {
|
|
533
385
|
Type: 'AWS::EC2::SecurityGroup',
|
|
534
386
|
Properties: {
|
|
535
|
-
GroupDescription: 'Security group for Frigg VPC Endpoints',
|
|
387
|
+
GroupDescription: 'Security group for Frigg VPC Endpoints - allows HTTPS from Lambda functions',
|
|
536
388
|
VpcId: { Ref: 'FriggVPC' },
|
|
537
389
|
SecurityGroupIngress: [
|
|
538
390
|
{
|
|
539
391
|
IpProtocol: 'tcp',
|
|
540
392
|
FromPort: 443,
|
|
541
393
|
ToPort: 443,
|
|
542
|
-
SourceSecurityGroupId: {
|
|
543
|
-
|
|
544
|
-
},
|
|
545
|
-
Description: 'HTTPS from Lambda',
|
|
394
|
+
SourceSecurityGroupId: { Ref: 'FriggLambdaSecurityGroup' },
|
|
395
|
+
Description: 'HTTPS from Lambda security group',
|
|
546
396
|
},
|
|
547
|
-
],
|
|
548
|
-
Tags: [
|
|
549
397
|
{
|
|
550
|
-
|
|
551
|
-
|
|
398
|
+
IpProtocol: 'tcp',
|
|
399
|
+
FromPort: 443,
|
|
400
|
+
ToPort: 443,
|
|
401
|
+
CidrIp: AppDefinition.vpc.cidrBlock || '10.0.0.0/16',
|
|
402
|
+
Description: 'HTTPS from VPC CIDR (fallback)',
|
|
552
403
|
},
|
|
553
404
|
],
|
|
405
|
+
Tags: [
|
|
406
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg' },
|
|
407
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
408
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
409
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
410
|
+
{ Key: 'Type', Value: 'VPCEndpoint' },
|
|
411
|
+
{ Key: 'Purpose', Value: 'Allow Lambda functions to access VPC endpoints' },
|
|
412
|
+
],
|
|
554
413
|
},
|
|
555
414
|
};
|
|
556
415
|
}
|
|
@@ -558,149 +417,105 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
558
417
|
return vpcResources;
|
|
559
418
|
};
|
|
560
419
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
* @param {string} [AppDefinition.provider='aws'] - Cloud provider
|
|
566
|
-
* @param {Array} AppDefinition.integrations - Array of integration definitions
|
|
567
|
-
* @param {Object} [AppDefinition.vpc] - VPC configuration
|
|
568
|
-
* @param {Object} [AppDefinition.encryption] - KMS encryption configuration
|
|
569
|
-
* @param {Object} [AppDefinition.ssm] - SSM parameter store configuration
|
|
570
|
-
* @param {Object} [AppDefinition.websockets] - WebSocket configuration
|
|
571
|
-
* @param {boolean} [AppDefinition.websockets.enable=false] - Enable WebSocket support for live update streaming
|
|
572
|
-
* @returns {Object} Complete serverless framework configuration
|
|
573
|
-
*/
|
|
574
|
-
const composeServerlessDefinition = async (AppDefinition) => {
|
|
575
|
-
// Store discovered resources
|
|
576
|
-
let discoveredResources = {};
|
|
420
|
+
const gatherDiscoveredResources = async (AppDefinition) => {
|
|
421
|
+
if (!shouldRunDiscovery(AppDefinition)) {
|
|
422
|
+
return {};
|
|
423
|
+
}
|
|
577
424
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const config = {
|
|
588
|
-
vpc: AppDefinition.vpc || {},
|
|
589
|
-
encryption: AppDefinition.encryption || {},
|
|
590
|
-
ssm: AppDefinition.ssm || {},
|
|
591
|
-
};
|
|
425
|
+
console.log('🔍 Running AWS resource discovery for serverless template...');
|
|
426
|
+
try {
|
|
427
|
+
const region = process.env.AWS_REGION || 'us-east-1';
|
|
428
|
+
const discovery = new AWSDiscovery(region);
|
|
429
|
+
const config = {
|
|
430
|
+
vpc: AppDefinition.vpc || {},
|
|
431
|
+
encryption: AppDefinition.encryption || {},
|
|
432
|
+
ssm: AppDefinition.ssm || {},
|
|
433
|
+
};
|
|
592
434
|
|
|
593
|
-
|
|
435
|
+
const discoveredResources = await discovery.discoverResources(config);
|
|
594
436
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
discoveredResources.privateSubnetId2
|
|
602
|
-
)
|
|
603
|
-
console.log(
|
|
604
|
-
` Subnets: ${discoveredResources.privateSubnetId1}, ${discoveredResources.privateSubnetId2}`
|
|
605
|
-
);
|
|
606
|
-
}
|
|
607
|
-
if (discoveredResources.defaultSecurityGroupId) {
|
|
608
|
-
console.log(
|
|
609
|
-
` Security Group: ${discoveredResources.defaultSecurityGroupId}`
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
if (discoveredResources.defaultKmsKeyId) {
|
|
613
|
-
console.log(
|
|
614
|
-
` KMS Key: ${discoveredResources.defaultKmsKeyId}`
|
|
615
|
-
);
|
|
616
|
-
}
|
|
617
|
-
} catch (error) {
|
|
618
|
-
console.error('❌ AWS discovery failed:', error.message);
|
|
619
|
-
throw new Error(`AWS discovery failed: ${error.message}`);
|
|
437
|
+
console.log('✅ AWS discovery completed successfully!');
|
|
438
|
+
if (discoveredResources.defaultVpcId) {
|
|
439
|
+
console.log(` VPC: ${discoveredResources.defaultVpcId}`);
|
|
440
|
+
}
|
|
441
|
+
if (discoveredResources.privateSubnetId1 && discoveredResources.privateSubnetId2) {
|
|
442
|
+
console.log(
|
|
443
|
+
` Subnets: ${discoveredResources.privateSubnetId1}, ${discoveredResources.privateSubnetId2}`
|
|
444
|
+
);
|
|
620
445
|
}
|
|
446
|
+
if (discoveredResources.defaultSecurityGroupId) {
|
|
447
|
+
console.log(` Security Group: ${discoveredResources.defaultSecurityGroupId}`);
|
|
448
|
+
}
|
|
449
|
+
if (discoveredResources.defaultKmsKeyId) {
|
|
450
|
+
console.log(` KMS Key: ${discoveredResources.defaultKmsKeyId}`);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return discoveredResources;
|
|
454
|
+
} catch (error) {
|
|
455
|
+
console.error('❌ AWS discovery failed:', error.message);
|
|
456
|
+
throw new Error(`AWS discovery failed: ${error.message}`);
|
|
621
457
|
}
|
|
458
|
+
};
|
|
622
459
|
|
|
623
|
-
|
|
624
|
-
const
|
|
460
|
+
const buildEnvironment = (appEnvironmentVars, discoveredResources) => {
|
|
461
|
+
const environment = {
|
|
462
|
+
STAGE: '${opt:stage, "dev"}',
|
|
463
|
+
AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1,
|
|
464
|
+
...appEnvironmentVars,
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const discoveryEnvMapping = {
|
|
468
|
+
defaultVpcId: 'AWS_DISCOVERY_VPC_ID',
|
|
469
|
+
defaultSecurityGroupId: 'AWS_DISCOVERY_SECURITY_GROUP_ID',
|
|
470
|
+
privateSubnetId1: 'AWS_DISCOVERY_SUBNET_ID_1',
|
|
471
|
+
privateSubnetId2: 'AWS_DISCOVERY_SUBNET_ID_2',
|
|
472
|
+
publicSubnetId: 'AWS_DISCOVERY_PUBLIC_SUBNET_ID',
|
|
473
|
+
defaultRouteTableId: 'AWS_DISCOVERY_ROUTE_TABLE_ID',
|
|
474
|
+
defaultKmsKeyId: 'AWS_DISCOVERY_KMS_KEY_ID',
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
for (const [key, envKey] of Object.entries(discoveryEnvMapping)) {
|
|
478
|
+
if (discoveredResources[key]) {
|
|
479
|
+
environment[envKey] = discoveredResources[key];
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return environment;
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const createBaseDefinition = (AppDefinition, appEnvironmentVars, discoveredResources) => {
|
|
487
|
+
const region = process.env.AWS_REGION || 'us-east-1';
|
|
625
488
|
|
|
626
|
-
|
|
489
|
+
return {
|
|
627
490
|
frameworkVersion: '>=3.17.0',
|
|
628
491
|
service: AppDefinition.name || 'create-frigg-app',
|
|
629
492
|
package: {
|
|
630
493
|
individually: true,
|
|
631
|
-
exclude: [
|
|
632
|
-
'!**/node_modules/aws-sdk/**',
|
|
633
|
-
'!**/node_modules/@aws-sdk/**',
|
|
634
|
-
'!package.json',
|
|
635
|
-
],
|
|
494
|
+
exclude: ['!**/node_modules/aws-sdk/**', '!**/node_modules/@aws-sdk/**', '!package.json'],
|
|
636
495
|
},
|
|
637
496
|
useDotenv: true,
|
|
638
497
|
provider: {
|
|
639
498
|
name: AppDefinition.provider || 'aws',
|
|
640
499
|
runtime: 'nodejs20.x',
|
|
641
500
|
timeout: 30,
|
|
642
|
-
region
|
|
501
|
+
region,
|
|
643
502
|
stage: '${opt:stage}',
|
|
644
|
-
environment:
|
|
645
|
-
STAGE: '${opt:stage, "dev"}',
|
|
646
|
-
AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1,
|
|
647
|
-
// Add environment variables from appDefinition
|
|
648
|
-
...appEnvironmentVars,
|
|
649
|
-
// Add discovered resources to environment if available
|
|
650
|
-
...(discoveredResources.defaultVpcId && {
|
|
651
|
-
AWS_DISCOVERY_VPC_ID: discoveredResources.defaultVpcId,
|
|
652
|
-
}),
|
|
653
|
-
...(discoveredResources.defaultSecurityGroupId && {
|
|
654
|
-
AWS_DISCOVERY_SECURITY_GROUP_ID:
|
|
655
|
-
discoveredResources.defaultSecurityGroupId,
|
|
656
|
-
}),
|
|
657
|
-
...(discoveredResources.privateSubnetId1 && {
|
|
658
|
-
AWS_DISCOVERY_SUBNET_ID_1:
|
|
659
|
-
discoveredResources.privateSubnetId1,
|
|
660
|
-
}),
|
|
661
|
-
...(discoveredResources.privateSubnetId2 && {
|
|
662
|
-
AWS_DISCOVERY_SUBNET_ID_2:
|
|
663
|
-
discoveredResources.privateSubnetId2,
|
|
664
|
-
}),
|
|
665
|
-
...(discoveredResources.publicSubnetId && {
|
|
666
|
-
AWS_DISCOVERY_PUBLIC_SUBNET_ID:
|
|
667
|
-
discoveredResources.publicSubnetId,
|
|
668
|
-
}),
|
|
669
|
-
...(discoveredResources.defaultRouteTableId && {
|
|
670
|
-
AWS_DISCOVERY_ROUTE_TABLE_ID:
|
|
671
|
-
discoveredResources.defaultRouteTableId,
|
|
672
|
-
}),
|
|
673
|
-
...(discoveredResources.defaultKmsKeyId && {
|
|
674
|
-
AWS_DISCOVERY_KMS_KEY_ID:
|
|
675
|
-
discoveredResources.defaultKmsKeyId,
|
|
676
|
-
}),
|
|
677
|
-
},
|
|
503
|
+
environment: buildEnvironment(appEnvironmentVars, discoveredResources),
|
|
678
504
|
iamRoleStatements: [
|
|
679
505
|
{
|
|
680
506
|
Effect: 'Allow',
|
|
681
507
|
Action: ['sns:Publish'],
|
|
682
|
-
Resource: {
|
|
683
|
-
Ref: 'InternalErrorBridgeTopic',
|
|
684
|
-
},
|
|
508
|
+
Resource: { Ref: 'InternalErrorBridgeTopic' },
|
|
685
509
|
},
|
|
686
510
|
{
|
|
687
511
|
Effect: 'Allow',
|
|
688
|
-
Action: [
|
|
689
|
-
'sqs:SendMessage',
|
|
690
|
-
'sqs:SendMessageBatch',
|
|
691
|
-
'sqs:GetQueueUrl',
|
|
692
|
-
'sqs:GetQueueAttributes',
|
|
693
|
-
],
|
|
512
|
+
Action: ['sqs:SendMessage', 'sqs:SendMessageBatch', 'sqs:GetQueueUrl', 'sqs:GetQueueAttributes'],
|
|
694
513
|
Resource: [
|
|
695
|
-
{
|
|
696
|
-
'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
|
|
697
|
-
},
|
|
514
|
+
{ 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
|
|
698
515
|
{
|
|
699
516
|
'Fn::Join': [
|
|
700
517
|
':',
|
|
701
|
-
[
|
|
702
|
-
'arn:aws:sqs:${self:provider.region}:*:${self:service}--${self:provider.stage}-*Queue',
|
|
703
|
-
],
|
|
518
|
+
['arn:aws:sqs:${self:provider.region}:*:${self:service}--${self:provider.stage}-*Queue'],
|
|
704
519
|
],
|
|
705
520
|
},
|
|
706
521
|
],
|
|
@@ -735,7 +550,7 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
735
550
|
autoCreate: false,
|
|
736
551
|
apiVersion: '2012-11-05',
|
|
737
552
|
endpoint: 'http://localhost:4566',
|
|
738
|
-
region
|
|
553
|
+
region,
|
|
739
554
|
accessKeyId: 'root',
|
|
740
555
|
secretAccessKey: 'root',
|
|
741
556
|
skipCacheInvalidation: false,
|
|
@@ -746,57 +561,22 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
746
561
|
},
|
|
747
562
|
functions: {
|
|
748
563
|
auth: {
|
|
749
|
-
handler:
|
|
750
|
-
'node_modules/@friggframework/core/handlers/routers/auth.handler',
|
|
564
|
+
handler: 'node_modules/@friggframework/core/handlers/routers/auth.handler',
|
|
751
565
|
events: [
|
|
752
|
-
{
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
method: 'ANY',
|
|
756
|
-
},
|
|
757
|
-
},
|
|
758
|
-
{
|
|
759
|
-
httpApi: {
|
|
760
|
-
path: '/api/integrations/{proxy+}',
|
|
761
|
-
method: 'ANY',
|
|
762
|
-
},
|
|
763
|
-
},
|
|
764
|
-
{
|
|
765
|
-
httpApi: {
|
|
766
|
-
path: '/api/authorize',
|
|
767
|
-
method: 'ANY',
|
|
768
|
-
},
|
|
769
|
-
},
|
|
566
|
+
{ httpApi: { path: '/api/integrations', method: 'ANY' } },
|
|
567
|
+
{ httpApi: { path: '/api/integrations/{proxy+}', method: 'ANY' } },
|
|
568
|
+
{ httpApi: { path: '/api/authorize', method: 'ANY' } },
|
|
770
569
|
],
|
|
771
570
|
},
|
|
772
571
|
user: {
|
|
773
|
-
handler:
|
|
774
|
-
|
|
775
|
-
events: [
|
|
776
|
-
{
|
|
777
|
-
httpApi: {
|
|
778
|
-
path: '/user/{proxy+}',
|
|
779
|
-
method: 'ANY',
|
|
780
|
-
},
|
|
781
|
-
},
|
|
782
|
-
],
|
|
572
|
+
handler: 'node_modules/@friggframework/core/handlers/routers/user.handler',
|
|
573
|
+
events: [{ httpApi: { path: '/user/{proxy+}', method: 'ANY' } }],
|
|
783
574
|
},
|
|
784
575
|
health: {
|
|
785
|
-
handler:
|
|
786
|
-
'node_modules/@friggframework/core/handlers/routers/health.handler',
|
|
576
|
+
handler: 'node_modules/@friggframework/core/handlers/routers/health.handler',
|
|
787
577
|
events: [
|
|
788
|
-
{
|
|
789
|
-
|
|
790
|
-
path: '/health',
|
|
791
|
-
method: 'GET',
|
|
792
|
-
},
|
|
793
|
-
},
|
|
794
|
-
{
|
|
795
|
-
httpApi: {
|
|
796
|
-
path: '/health/{proxy+}',
|
|
797
|
-
method: 'GET',
|
|
798
|
-
},
|
|
799
|
-
},
|
|
578
|
+
{ httpApi: { path: '/health', method: 'GET' } },
|
|
579
|
+
{ httpApi: { path: '/health/{proxy+}', method: 'GET' } },
|
|
800
580
|
],
|
|
801
581
|
},
|
|
802
582
|
},
|
|
@@ -805,8 +585,7 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
805
585
|
InternalErrorQueue: {
|
|
806
586
|
Type: 'AWS::SQS::Queue',
|
|
807
587
|
Properties: {
|
|
808
|
-
QueueName:
|
|
809
|
-
'${self:service}-internal-error-queue-${self:provider.stage}',
|
|
588
|
+
QueueName: '${self:service}-internal-error-queue-${self:provider.stage}',
|
|
810
589
|
MessageRetentionPeriod: 300,
|
|
811
590
|
},
|
|
812
591
|
},
|
|
@@ -816,9 +595,7 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
816
595
|
Subscription: [
|
|
817
596
|
{
|
|
818
597
|
Protocol: 'sqs',
|
|
819
|
-
Endpoint: {
|
|
820
|
-
'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
|
|
821
|
-
},
|
|
598
|
+
Endpoint: { 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
|
|
822
599
|
},
|
|
823
600
|
],
|
|
824
601
|
},
|
|
@@ -833,25 +610,11 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
833
610
|
{
|
|
834
611
|
Sid: 'Allow Dead Letter SNS to publish to SQS',
|
|
835
612
|
Effect: 'Allow',
|
|
836
|
-
Principal: {
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
Resource: {
|
|
840
|
-
'Fn::GetAtt': [
|
|
841
|
-
'InternalErrorQueue',
|
|
842
|
-
'Arn',
|
|
843
|
-
],
|
|
844
|
-
},
|
|
845
|
-
Action: [
|
|
846
|
-
'SQS:SendMessage',
|
|
847
|
-
'SQS:SendMessageBatch',
|
|
848
|
-
],
|
|
613
|
+
Principal: { Service: 'sns.amazonaws.com' },
|
|
614
|
+
Resource: { 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
|
|
615
|
+
Action: ['SQS:SendMessage', 'SQS:SendMessageBatch'],
|
|
849
616
|
Condition: {
|
|
850
|
-
ArnEquals: {
|
|
851
|
-
'aws:SourceArn': {
|
|
852
|
-
Ref: 'InternalErrorBridgeTopic',
|
|
853
|
-
},
|
|
854
|
-
},
|
|
617
|
+
ArnEquals: { 'aws:SourceArn': { Ref: 'InternalErrorBridgeTopic' } },
|
|
855
618
|
},
|
|
856
619
|
},
|
|
857
620
|
],
|
|
@@ -871,469 +634,989 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
871
634
|
Period: 60,
|
|
872
635
|
AlarmActions: [{ Ref: 'InternalErrorBridgeTopic' }],
|
|
873
636
|
Dimensions: [
|
|
874
|
-
{
|
|
875
|
-
|
|
876
|
-
Value: { Ref: 'HttpApi' },
|
|
877
|
-
},
|
|
878
|
-
{
|
|
879
|
-
Name: 'Stage',
|
|
880
|
-
Value: '${self:provider.stage}',
|
|
881
|
-
},
|
|
637
|
+
{ Name: 'ApiId', Value: { Ref: 'HttpApi' } },
|
|
638
|
+
{ Name: 'Stage', Value: '${self:provider.stage}' },
|
|
882
639
|
],
|
|
883
640
|
},
|
|
884
641
|
},
|
|
885
642
|
},
|
|
886
643
|
},
|
|
887
644
|
};
|
|
645
|
+
};
|
|
888
646
|
|
|
889
|
-
|
|
890
|
-
if (AppDefinition.encryption?.fieldLevelEncryptionMethod
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
// Use the existing discovered KMS key
|
|
894
|
-
console.log(
|
|
895
|
-
`Using existing KMS key: ${discoveredResources.defaultKmsKeyId}`
|
|
896
|
-
);
|
|
647
|
+
const applyKmsConfiguration = (definition, AppDefinition, discoveredResources) => {
|
|
648
|
+
if (AppDefinition.encryption?.fieldLevelEncryptionMethod !== 'kms') {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
897
651
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
652
|
+
if (discoveredResources.defaultKmsKeyId) {
|
|
653
|
+
console.log(`Using existing KMS key: ${discoveredResources.defaultKmsKeyId}`);
|
|
654
|
+
definition.resources.Resources.FriggKMSKeyAlias = {
|
|
655
|
+
Type: 'AWS::KMS::Alias',
|
|
656
|
+
DeletionPolicy: 'Retain',
|
|
657
|
+
Properties: {
|
|
658
|
+
AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
|
|
659
|
+
TargetKeyId: discoveredResources.defaultKmsKeyId,
|
|
660
|
+
},
|
|
661
|
+
};
|
|
903
662
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
663
|
+
definition.provider.iamRoleStatements.push({
|
|
664
|
+
Effect: 'Allow',
|
|
665
|
+
Action: ['kms:GenerateDataKey', 'kms:Decrypt'],
|
|
666
|
+
Resource: [discoveredResources.defaultKmsKeyId],
|
|
667
|
+
});
|
|
668
|
+
} else {
|
|
669
|
+
if (AppDefinition.encryption?.createResourceIfNoneFound !== true) {
|
|
670
|
+
throw new Error(
|
|
671
|
+
'KMS field-level encryption is enabled but no KMS key was found. ' +
|
|
672
|
+
'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
|
|
673
|
+
);
|
|
674
|
+
}
|
|
910
675
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
},
|
|
928
|
-
Action: 'kms:*',
|
|
929
|
-
Resource: '*',
|
|
930
|
-
},
|
|
931
|
-
{
|
|
932
|
-
Sid: 'AllowLambdaService',
|
|
933
|
-
Effect: 'Allow',
|
|
934
|
-
Principal: {
|
|
935
|
-
Service: 'lambda.amazonaws.com',
|
|
936
|
-
},
|
|
937
|
-
Action: [
|
|
938
|
-
'kms:GenerateDataKey',
|
|
939
|
-
'kms:Decrypt',
|
|
940
|
-
'kms:DescribeKey',
|
|
941
|
-
],
|
|
942
|
-
Resource: '*',
|
|
943
|
-
Condition: {
|
|
944
|
-
StringEquals: {
|
|
945
|
-
'kms:ViaService': `lambda.${
|
|
946
|
-
process.env.AWS_REGION ||
|
|
947
|
-
'us-east-1'
|
|
948
|
-
}.amazonaws.com`,
|
|
949
|
-
},
|
|
950
|
-
},
|
|
951
|
-
},
|
|
952
|
-
],
|
|
953
|
-
},
|
|
954
|
-
Tags: [
|
|
955
|
-
{
|
|
956
|
-
Key: 'Name',
|
|
957
|
-
Value: '${self:service}-${self:provider.stage}-frigg-kms-key',
|
|
676
|
+
console.log('No existing KMS key found, creating a new one...');
|
|
677
|
+
definition.resources.Resources.FriggKMSKey = {
|
|
678
|
+
Type: 'AWS::KMS::Key',
|
|
679
|
+
DeletionPolicy: 'Retain',
|
|
680
|
+
UpdateReplacePolicy: 'Retain',
|
|
681
|
+
Properties: {
|
|
682
|
+
EnableKeyRotation: true,
|
|
683
|
+
Description: 'Frigg KMS key for field-level encryption',
|
|
684
|
+
KeyPolicy: {
|
|
685
|
+
Version: '2012-10-17',
|
|
686
|
+
Statement: [
|
|
687
|
+
{
|
|
688
|
+
Sid: 'AllowRootAccountAdmin',
|
|
689
|
+
Effect: 'Allow',
|
|
690
|
+
Principal: {
|
|
691
|
+
AWS: { 'Fn::Sub': 'arn:aws:iam::${AWS::AccountId}:root' },
|
|
958
692
|
},
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
693
|
+
Action: 'kms:*',
|
|
694
|
+
Resource: '*',
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
Sid: 'AllowLambdaService',
|
|
698
|
+
Effect: 'Allow',
|
|
699
|
+
Principal: { Service: 'lambda.amazonaws.com' },
|
|
700
|
+
Action: ['kms:GenerateDataKey', 'kms:Decrypt', 'kms:DescribeKey'],
|
|
701
|
+
Resource: '*',
|
|
702
|
+
Condition: {
|
|
703
|
+
StringEquals: {
|
|
704
|
+
'kms:ViaService': `lambda.${process.env.AWS_REGION || 'us-east-1'}.amazonaws.com`,
|
|
705
|
+
},
|
|
962
706
|
},
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
707
|
+
},
|
|
708
|
+
],
|
|
709
|
+
},
|
|
710
|
+
Tags: [
|
|
711
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-frigg-kms-key' },
|
|
712
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
713
|
+
{ Key: 'Purpose', Value: 'Field-level encryption for Frigg application' },
|
|
714
|
+
],
|
|
715
|
+
},
|
|
716
|
+
};
|
|
972
717
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
718
|
+
definition.resources.Resources.FriggKMSKeyAlias = {
|
|
719
|
+
Type: 'AWS::KMS::Alias',
|
|
720
|
+
DeletionPolicy: 'Retain',
|
|
721
|
+
Properties: {
|
|
722
|
+
AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
|
|
723
|
+
TargetKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
|
|
724
|
+
},
|
|
725
|
+
};
|
|
976
726
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
// No key found and createIfNoneFound is not enabled - error
|
|
983
|
-
throw new Error(
|
|
984
|
-
'KMS field-level encryption is enabled but no KMS key was found. ' +
|
|
985
|
-
'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
|
|
986
|
-
);
|
|
987
|
-
}
|
|
988
|
-
}
|
|
727
|
+
definition.provider.iamRoleStatements.push({
|
|
728
|
+
Effect: 'Allow',
|
|
729
|
+
Action: ['kms:GenerateDataKey', 'kms:Decrypt'],
|
|
730
|
+
Resource: [{ 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] }],
|
|
731
|
+
});
|
|
989
732
|
|
|
990
|
-
definition.
|
|
733
|
+
definition.provider.environment.KMS_KEY_ARN = { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] };
|
|
734
|
+
definition.custom.kmsGrants = {
|
|
735
|
+
kmsKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
|
|
736
|
+
};
|
|
737
|
+
}
|
|
991
738
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
};
|
|
999
|
-
}
|
|
739
|
+
definition.plugins.push('serverless-kms-grants');
|
|
740
|
+
if (!definition.custom.kmsGrants) {
|
|
741
|
+
definition.custom.kmsGrants = {
|
|
742
|
+
kmsKeyId: discoveredResources.defaultKmsKeyId || '${env:AWS_DISCOVERY_KMS_KEY_ID}',
|
|
743
|
+
};
|
|
744
|
+
}
|
|
1000
745
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
// Use the discovered value directly when available (from in-process discovery)
|
|
1005
|
-
// Otherwise fall back to environment variable (from separate discovery process)
|
|
1006
|
-
definition.provider.environment.KMS_KEY_ARN =
|
|
1007
|
-
discoveredResources.defaultKmsKeyId ||
|
|
1008
|
-
'${env:AWS_DISCOVERY_KMS_KEY_ID}';
|
|
1009
|
-
}
|
|
746
|
+
if (!definition.provider.environment.KMS_KEY_ARN) {
|
|
747
|
+
definition.provider.environment.KMS_KEY_ARN =
|
|
748
|
+
discoveredResources.defaultKmsKeyId || '${env:AWS_DISCOVERY_KMS_KEY_ID}';
|
|
1010
749
|
}
|
|
750
|
+
};
|
|
1011
751
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
'ec2:DeleteNetworkInterface',
|
|
1021
|
-
'ec2:AttachNetworkInterface',
|
|
1022
|
-
'ec2:DetachNetworkInterface',
|
|
1023
|
-
],
|
|
1024
|
-
Resource: '*',
|
|
1025
|
-
});
|
|
752
|
+
const healVpcConfiguration = (discoveredResources, AppDefinition) => {
|
|
753
|
+
const healingReport = {
|
|
754
|
+
healed: [],
|
|
755
|
+
warnings: [],
|
|
756
|
+
errors: [],
|
|
757
|
+
recommendations: [],
|
|
758
|
+
criticalActions: [],
|
|
759
|
+
};
|
|
1026
760
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
const vpcConfig = {};
|
|
761
|
+
if (!AppDefinition.vpc?.selfHeal) {
|
|
762
|
+
return healingReport;
|
|
763
|
+
}
|
|
1031
764
|
|
|
1032
|
-
|
|
1033
|
-
// User provided custom security groups
|
|
1034
|
-
vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds;
|
|
1035
|
-
} else {
|
|
1036
|
-
// Use auto-created security group
|
|
1037
|
-
vpcConfig.securityGroupIds = [
|
|
1038
|
-
{ Ref: 'FriggLambdaSecurityGroup' },
|
|
1039
|
-
];
|
|
1040
|
-
}
|
|
765
|
+
console.log('🔧 Self-healing mode enabled - checking for VPC misconfigurations...');
|
|
1041
766
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
767
|
+
if (discoveredResources.natGatewayInPrivateSubnet) {
|
|
768
|
+
healingReport.warnings.push(
|
|
769
|
+
`NAT Gateway ${discoveredResources.natGatewayInPrivateSubnet} is in a private subnet`
|
|
770
|
+
);
|
|
771
|
+
healingReport.recommendations.push(
|
|
772
|
+
'NAT Gateway should be recreated in a public subnet for proper internet connectivity'
|
|
773
|
+
);
|
|
774
|
+
discoveredResources.needsNewNatGateway = true;
|
|
775
|
+
healingReport.healed.push('Marked NAT Gateway for recreation in public subnet');
|
|
776
|
+
}
|
|
1052
777
|
|
|
1053
|
-
|
|
1054
|
-
|
|
778
|
+
if (discoveredResources.elasticIpAlreadyAssociated) {
|
|
779
|
+
healingReport.warnings.push(
|
|
780
|
+
`Elastic IP ${discoveredResources.existingElasticIp} is already associated`
|
|
781
|
+
);
|
|
1055
782
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
783
|
+
if (discoveredResources.existingNatGatewayId) {
|
|
784
|
+
healingReport.healed.push('Will reuse existing NAT Gateway instead of creating a new one');
|
|
785
|
+
discoveredResources.reuseExistingNatGateway = true;
|
|
1059
786
|
} else {
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
AppDefinition.vpc.securityGroupIds ||
|
|
1065
|
-
(discoveredResources.defaultSecurityGroupId
|
|
1066
|
-
? [discoveredResources.defaultSecurityGroupId]
|
|
1067
|
-
: []),
|
|
1068
|
-
subnetIds:
|
|
1069
|
-
AppDefinition.vpc.subnetIds ||
|
|
1070
|
-
(discoveredResources.privateSubnetId1 &&
|
|
1071
|
-
discoveredResources.privateSubnetId2
|
|
1072
|
-
? [
|
|
1073
|
-
discoveredResources.privateSubnetId1,
|
|
1074
|
-
discoveredResources.privateSubnetId2,
|
|
1075
|
-
]
|
|
1076
|
-
: []),
|
|
1077
|
-
};
|
|
1078
|
-
|
|
1079
|
-
// Set VPC config for Lambda functions only if we have valid subnet IDs
|
|
1080
|
-
if (
|
|
1081
|
-
vpcConfig.subnetIds.length >= 2 &&
|
|
1082
|
-
vpcConfig.securityGroupIds.length > 0
|
|
1083
|
-
) {
|
|
1084
|
-
definition.provider.vpc = vpcConfig;
|
|
1085
|
-
|
|
1086
|
-
// Check if we have an existing NAT Gateway to use
|
|
1087
|
-
if (!discoveredResources.existingNatGatewayId) {
|
|
1088
|
-
// No existing NAT Gateway, create new resources
|
|
1089
|
-
|
|
1090
|
-
// Only create EIP if we don't have an existing one available
|
|
1091
|
-
if (!discoveredResources.existingElasticIpAllocationId) {
|
|
1092
|
-
definition.resources.Resources.FriggNATGatewayEIP = {
|
|
1093
|
-
Type: 'AWS::EC2::EIP',
|
|
1094
|
-
Properties: {
|
|
1095
|
-
Domain: 'vpc',
|
|
1096
|
-
Tags: [
|
|
1097
|
-
{
|
|
1098
|
-
Key: 'Name',
|
|
1099
|
-
Value: '${self:service}-${self:provider.stage}-nat-eip',
|
|
1100
|
-
},
|
|
1101
|
-
],
|
|
1102
|
-
},
|
|
1103
|
-
};
|
|
1104
|
-
}
|
|
787
|
+
healingReport.healed.push('Will allocate a new Elastic IP for NAT Gateway');
|
|
788
|
+
discoveredResources.allocateNewElasticIp = true;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
1105
791
|
|
|
1106
|
-
|
|
1107
|
-
|
|
792
|
+
if (
|
|
793
|
+
discoveredResources.privateSubnetsWithWrongRoutes &&
|
|
794
|
+
discoveredResources.privateSubnetsWithWrongRoutes.length > 0
|
|
795
|
+
) {
|
|
796
|
+
healingReport.warnings.push(
|
|
797
|
+
`Found ${discoveredResources.privateSubnetsWithWrongRoutes.length} subnets that are PUBLIC but will be used for Lambda`
|
|
798
|
+
);
|
|
799
|
+
healingReport.healed.push(
|
|
800
|
+
'Route tables will be corrected during deployment - converting public subnets to private'
|
|
801
|
+
);
|
|
802
|
+
healingReport.criticalActions.push(
|
|
803
|
+
'SUBNET ISOLATION: Will create separate route tables to ensure Lambda subnets are private'
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (discoveredResources.subnetConversionRequired) {
|
|
808
|
+
healingReport.warnings.push(
|
|
809
|
+
'Subnet configuration mismatch detected - Lambda functions require private subnets'
|
|
810
|
+
);
|
|
811
|
+
healingReport.healed.push('Will create proper route table configuration for subnet isolation');
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (discoveredResources.orphanedElasticIps?.length > 0) {
|
|
815
|
+
healingReport.warnings.push(
|
|
816
|
+
`Found ${discoveredResources.orphanedElasticIps.length} orphaned Elastic IPs`
|
|
817
|
+
);
|
|
818
|
+
healingReport.recommendations.push('Consider releasing orphaned Elastic IPs to avoid charges');
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (healingReport.criticalActions.length > 0) {
|
|
822
|
+
console.log('🚨 CRITICAL ACTIONS:');
|
|
823
|
+
healingReport.criticalActions.forEach((action) => console.log(` - ${action}`));
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (healingReport.healed.length > 0) {
|
|
827
|
+
console.log('✅ Self-healing actions:');
|
|
828
|
+
healingReport.healed.forEach((action) => console.log(` - ${action}`));
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (healingReport.warnings.length > 0) {
|
|
832
|
+
console.log('⚠️ Issues detected:');
|
|
833
|
+
healingReport.warnings.forEach((warning) => console.log(` - ${warning}`));
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (healingReport.recommendations.length > 0) {
|
|
837
|
+
console.log('💡 Recommendations:');
|
|
838
|
+
healingReport.recommendations.forEach((rec) => console.log(` - ${rec}`));
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
return healingReport;
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
845
|
+
if (AppDefinition.vpc?.enable !== true) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
definition.provider.iamRoleStatements.push({
|
|
850
|
+
Effect: 'Allow',
|
|
851
|
+
Action: [
|
|
852
|
+
'ec2:CreateNetworkInterface',
|
|
853
|
+
'ec2:DescribeNetworkInterfaces',
|
|
854
|
+
'ec2:DeleteNetworkInterface',
|
|
855
|
+
'ec2:AttachNetworkInterface',
|
|
856
|
+
'ec2:DetachNetworkInterface',
|
|
857
|
+
],
|
|
858
|
+
Resource: '*',
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
if (Object.keys(discoveredResources).length > 0) {
|
|
862
|
+
const healingReport = healVpcConfiguration(discoveredResources, AppDefinition);
|
|
863
|
+
if (healingReport.errors.length > 0 && !AppDefinition.vpc?.selfHeal) {
|
|
864
|
+
throw new Error(`VPC configuration errors detected: ${healingReport.errors.join(', ')}`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const vpcManagement = AppDefinition.vpc.management || 'discover';
|
|
869
|
+
let vpcId = null;
|
|
870
|
+
const vpcConfig = {
|
|
871
|
+
securityGroupIds: [],
|
|
872
|
+
subnetIds: [],
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
console.log(`VPC Management Mode: ${vpcManagement}`);
|
|
876
|
+
|
|
877
|
+
if (vpcManagement === 'create-new') {
|
|
878
|
+
const vpcResources = createVPCInfrastructure(AppDefinition);
|
|
879
|
+
Object.assign(definition.resources.Resources, vpcResources);
|
|
880
|
+
vpcId = { Ref: 'FriggVPC' };
|
|
881
|
+
vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds || [{ Ref: 'FriggLambdaSecurityGroup' }];
|
|
882
|
+
} else if (vpcManagement === 'use-existing') {
|
|
883
|
+
if (!AppDefinition.vpc.vpcId) {
|
|
884
|
+
throw new Error('VPC management is set to "use-existing" but no vpcId was provided');
|
|
885
|
+
}
|
|
886
|
+
vpcId = AppDefinition.vpc.vpcId;
|
|
887
|
+
vpcConfig.securityGroupIds =
|
|
888
|
+
AppDefinition.vpc.securityGroupIds ||
|
|
889
|
+
(discoveredResources.defaultSecurityGroupId ? [discoveredResources.defaultSecurityGroupId] : []);
|
|
890
|
+
} else {
|
|
891
|
+
if (!discoveredResources.defaultVpcId) {
|
|
892
|
+
throw new Error(
|
|
893
|
+
'VPC discovery failed: No VPC found. Either set vpc.management to "create-new" or provide vpc.vpcId with "use-existing".'
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
vpcId = discoveredResources.defaultVpcId;
|
|
897
|
+
vpcConfig.securityGroupIds =
|
|
898
|
+
AppDefinition.vpc.securityGroupIds ||
|
|
899
|
+
(discoveredResources.defaultSecurityGroupId ? [discoveredResources.defaultSecurityGroupId] : []);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const defaultSubnetManagement = vpcManagement === 'create-new' ? 'create' : 'discover';
|
|
903
|
+
let subnetManagement = AppDefinition.vpc.subnets?.management || defaultSubnetManagement;
|
|
904
|
+
console.log(`Subnet Management Mode: ${subnetManagement}`);
|
|
905
|
+
|
|
906
|
+
const effectiveVpcId = vpcId || discoveredResources.defaultVpcId;
|
|
907
|
+
if (!effectiveVpcId) {
|
|
908
|
+
throw new Error('Cannot manage subnets without a VPC ID');
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (subnetManagement === 'create') {
|
|
912
|
+
console.log('Creating new subnets...');
|
|
913
|
+
const subnetVpcId = vpcManagement === 'create-new' ? { Ref: 'FriggVPC' } : effectiveVpcId;
|
|
914
|
+
let subnet1Cidr;
|
|
915
|
+
let subnet2Cidr;
|
|
916
|
+
let publicSubnetCidr;
|
|
917
|
+
|
|
918
|
+
if (vpcManagement === 'create-new') {
|
|
919
|
+
const generatedCidrs = { 'Fn::Cidr': ['10.0.0.0/16', 3, 8] };
|
|
920
|
+
subnet1Cidr = { 'Fn::Select': [0, generatedCidrs] };
|
|
921
|
+
subnet2Cidr = { 'Fn::Select': [1, generatedCidrs] };
|
|
922
|
+
publicSubnetCidr = { 'Fn::Select': [2, generatedCidrs] };
|
|
923
|
+
} else {
|
|
924
|
+
subnet1Cidr = '172.31.240.0/24';
|
|
925
|
+
subnet2Cidr = '172.31.241.0/24';
|
|
926
|
+
publicSubnetCidr = '172.31.250.0/24';
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
definition.resources.Resources.FriggPrivateSubnet1 = {
|
|
930
|
+
Type: 'AWS::EC2::Subnet',
|
|
931
|
+
Properties: {
|
|
932
|
+
VpcId: subnetVpcId,
|
|
933
|
+
CidrBlock: subnet1Cidr,
|
|
934
|
+
AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
|
|
935
|
+
Tags: [
|
|
936
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-1' },
|
|
937
|
+
{ Key: 'Type', Value: 'Private' },
|
|
938
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
939
|
+
],
|
|
940
|
+
},
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
definition.resources.Resources.FriggPrivateSubnet2 = {
|
|
944
|
+
Type: 'AWS::EC2::Subnet',
|
|
945
|
+
Properties: {
|
|
946
|
+
VpcId: subnetVpcId,
|
|
947
|
+
CidrBlock: subnet2Cidr,
|
|
948
|
+
AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
|
|
949
|
+
Tags: [
|
|
950
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-2' },
|
|
951
|
+
{ Key: 'Type', Value: 'Private' },
|
|
952
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
953
|
+
],
|
|
954
|
+
},
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
definition.resources.Resources.FriggPublicSubnet = {
|
|
958
|
+
Type: 'AWS::EC2::Subnet',
|
|
959
|
+
Properties: {
|
|
960
|
+
VpcId: subnetVpcId,
|
|
961
|
+
CidrBlock: publicSubnetCidr,
|
|
962
|
+
MapPublicIpOnLaunch: true,
|
|
963
|
+
AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
|
|
964
|
+
Tags: [
|
|
965
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-public' },
|
|
966
|
+
{ Key: 'Type', Value: 'Public' },
|
|
967
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
968
|
+
],
|
|
969
|
+
},
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
vpcConfig.subnetIds = [{ Ref: 'FriggPrivateSubnet1' }, { Ref: 'FriggPrivateSubnet2' }];
|
|
973
|
+
|
|
974
|
+
if (!AppDefinition.vpc.natGateway || AppDefinition.vpc.natGateway.management === 'discover') {
|
|
975
|
+
if (vpcManagement === 'create-new' || !discoveredResources.internetGatewayId) {
|
|
976
|
+
if (!definition.resources.Resources.FriggInternetGateway) {
|
|
977
|
+
definition.resources.Resources.FriggInternetGateway = {
|
|
978
|
+
Type: 'AWS::EC2::InternetGateway',
|
|
1108
979
|
Properties: {
|
|
1109
|
-
AllocationId:
|
|
1110
|
-
discoveredResources.existingElasticIpAllocationId || {
|
|
1111
|
-
'Fn::GetAtt': [
|
|
1112
|
-
'FriggNATGatewayEIP',
|
|
1113
|
-
'AllocationId',
|
|
1114
|
-
],
|
|
1115
|
-
},
|
|
1116
|
-
SubnetId:
|
|
1117
|
-
discoveredResources.publicSubnetId ||
|
|
1118
|
-
discoveredResources.privateSubnetId1, // Use first discovered subnet if no public subnet found
|
|
1119
980
|
Tags: [
|
|
1120
|
-
{
|
|
1121
|
-
|
|
1122
|
-
Value: '${self:service}-${self:provider.stage}-nat-gateway',
|
|
1123
|
-
},
|
|
981
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-igw' },
|
|
982
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1124
983
|
],
|
|
1125
984
|
},
|
|
1126
985
|
};
|
|
1127
|
-
}
|
|
1128
986
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
Ref: 'FriggVPC',
|
|
987
|
+
definition.resources.Resources.FriggIGWAttachment = {
|
|
988
|
+
Type: 'AWS::EC2::VPCGatewayAttachment',
|
|
989
|
+
Properties: {
|
|
990
|
+
VpcId: subnetVpcId,
|
|
991
|
+
InternetGatewayId: { Ref: 'FriggInternetGateway' },
|
|
1135
992
|
},
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
Value: '${self:service}-${self:provider.stage}-lambda-rt',
|
|
1140
|
-
},
|
|
1141
|
-
],
|
|
1142
|
-
},
|
|
1143
|
-
};
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
}
|
|
1144
996
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
};
|
|
997
|
+
definition.resources.Resources.FriggPublicRouteTable = {
|
|
998
|
+
Type: 'AWS::EC2::RouteTable',
|
|
999
|
+
Properties: {
|
|
1000
|
+
VpcId: subnetVpcId,
|
|
1001
|
+
Tags: [
|
|
1002
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-rt' },
|
|
1003
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1004
|
+
],
|
|
1005
|
+
},
|
|
1006
|
+
};
|
|
1156
1007
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
},
|
|
1164
|
-
}
|
|
1008
|
+
definition.resources.Resources.FriggPublicRoute = {
|
|
1009
|
+
Type: 'AWS::EC2::Route',
|
|
1010
|
+
DependsOn: vpcManagement === 'create-new' ? 'FriggIGWAttachment' : undefined,
|
|
1011
|
+
Properties: {
|
|
1012
|
+
RouteTableId: { Ref: 'FriggPublicRouteTable' },
|
|
1013
|
+
DestinationCidrBlock: '0.0.0.0/0',
|
|
1014
|
+
GatewayId: discoveredResources.internetGatewayId || { Ref: 'FriggInternetGateway' },
|
|
1015
|
+
},
|
|
1016
|
+
};
|
|
1165
1017
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1018
|
+
definition.resources.Resources.FriggPublicSubnetRouteTableAssociation = {
|
|
1019
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1020
|
+
Properties: {
|
|
1021
|
+
SubnetId: { Ref: 'FriggPublicSubnet' },
|
|
1022
|
+
RouteTableId: { Ref: 'FriggPublicRouteTable' },
|
|
1023
|
+
},
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
definition.resources.Resources.FriggLambdaRouteTable = {
|
|
1027
|
+
Type: 'AWS::EC2::RouteTable',
|
|
1028
|
+
Properties: {
|
|
1029
|
+
VpcId: subnetVpcId,
|
|
1030
|
+
Tags: [
|
|
1031
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
|
|
1032
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1033
|
+
],
|
|
1034
|
+
},
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
definition.resources.Resources.FriggPrivateSubnet1RouteTableAssociation = {
|
|
1038
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1039
|
+
Properties: {
|
|
1040
|
+
SubnetId: { Ref: 'FriggPrivateSubnet1' },
|
|
1041
|
+
RouteTableId: { Ref: 'FriggLambdaRouteTable' },
|
|
1042
|
+
},
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
definition.resources.Resources.FriggPrivateSubnet2RouteTableAssociation = {
|
|
1046
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1047
|
+
Properties: {
|
|
1048
|
+
SubnetId: { Ref: 'FriggPrivateSubnet2' },
|
|
1049
|
+
RouteTableId: { Ref: 'FriggLambdaRouteTable' },
|
|
1050
|
+
},
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
} else if (subnetManagement === 'use-existing') {
|
|
1054
|
+
if (!AppDefinition.vpc.subnets?.ids || AppDefinition.vpc.subnets.ids.length < 2) {
|
|
1055
|
+
throw new Error(
|
|
1056
|
+
'Subnet management is "use-existing" but less than 2 subnet IDs provided. Provide at least 2 subnet IDs in vpc.subnets.ids.'
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
vpcConfig.subnetIds = AppDefinition.vpc.subnets.ids;
|
|
1060
|
+
} else {
|
|
1061
|
+
vpcConfig.subnetIds =
|
|
1062
|
+
AppDefinition.vpc.subnets?.ids?.length > 0
|
|
1063
|
+
? AppDefinition.vpc.subnets.ids
|
|
1064
|
+
: discoveredResources.privateSubnetId1 && discoveredResources.privateSubnetId2
|
|
1065
|
+
? [discoveredResources.privateSubnetId1, discoveredResources.privateSubnetId2]
|
|
1066
|
+
: [];
|
|
1067
|
+
|
|
1068
|
+
if (vpcConfig.subnetIds.length < 2) {
|
|
1069
|
+
if (AppDefinition.vpc.selfHeal) {
|
|
1070
|
+
console.log('No subnets found but self-heal enabled - creating minimal subnet setup');
|
|
1071
|
+
subnetManagement = 'create';
|
|
1072
|
+
discoveredResources.createSubnets = true;
|
|
1073
|
+
} else {
|
|
1074
|
+
throw new Error(
|
|
1075
|
+
'No subnets discovered and subnets.management is "discover". Either enable vpc.selfHeal, set subnets.management to "create", or provide subnet IDs.'
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (subnetManagement === 'create' && discoveredResources.createSubnets) {
|
|
1082
|
+
definition.resources.Resources.FriggLambdaRouteTable =
|
|
1083
|
+
definition.resources.Resources.FriggLambdaRouteTable || {
|
|
1084
|
+
Type: 'AWS::EC2::RouteTable',
|
|
1085
|
+
Properties: {
|
|
1086
|
+
VpcId: effectiveVpcId,
|
|
1087
|
+
Tags: [
|
|
1088
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
|
|
1089
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1090
|
+
{ Key: 'Environment', Value: '${self:provider.stage}' },
|
|
1091
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
1092
|
+
],
|
|
1093
|
+
},
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (
|
|
1098
|
+
vpcConfig.subnetIds.length >= 2 &&
|
|
1099
|
+
vpcConfig.securityGroupIds.length > 0
|
|
1100
|
+
) {
|
|
1101
|
+
definition.provider.vpc = vpcConfig;
|
|
1102
|
+
|
|
1103
|
+
const natGatewayManagement = AppDefinition.vpc.natGateway?.management || 'discover';
|
|
1104
|
+
let needsNewNatGateway =
|
|
1105
|
+
natGatewayManagement === 'createAndManage' ||
|
|
1106
|
+
discoveredResources.needsNewNatGateway === true;
|
|
1107
|
+
|
|
1108
|
+
console.log('needsNewNatGateway', needsNewNatGateway);
|
|
1109
|
+
|
|
1110
|
+
let reuseExistingNatGateway = false;
|
|
1111
|
+
let useExistingEip = false;
|
|
1112
|
+
|
|
1113
|
+
if (needsNewNatGateway) {
|
|
1114
|
+
console.log('Create mode: Creating dedicated EIP, public subnet, and NAT Gateway...');
|
|
1115
|
+
|
|
1116
|
+
if (
|
|
1117
|
+
discoveredResources.existingNatGatewayId &&
|
|
1118
|
+
discoveredResources.existingElasticIpAllocationId
|
|
1119
|
+
) {
|
|
1120
|
+
console.log('Found existing Frigg-managed NAT Gateway and EIP');
|
|
1121
|
+
if (!discoveredResources.natGatewayInPrivateSubnet) {
|
|
1122
|
+
console.log('✅ Existing NAT Gateway is in PUBLIC subnet, will reuse it');
|
|
1123
|
+
reuseExistingNatGateway = true;
|
|
1124
|
+
} else {
|
|
1125
|
+
console.log('❌ NAT Gateway is in PRIVATE subnet - MUST create new one in PUBLIC subnet');
|
|
1126
|
+
if (AppDefinition.vpc.selfHeal) {
|
|
1127
|
+
console.log('Self-heal enabled: Creating new NAT Gateway in PUBLIC subnet');
|
|
1128
|
+
reuseExistingNatGateway = false;
|
|
1129
|
+
useExistingEip = false;
|
|
1130
|
+
discoveredResources.needsCleanup = true;
|
|
1131
|
+
} else {
|
|
1132
|
+
throw new Error(
|
|
1133
|
+
'CRITICAL: NAT Gateway is in PRIVATE subnet (will not work!). Enable vpc.selfHeal to auto-fix or set natGateway.management to "createAndManage".'
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
} else if (
|
|
1138
|
+
discoveredResources.existingElasticIpAllocationId &&
|
|
1139
|
+
!discoveredResources.existingNatGatewayId
|
|
1140
|
+
) {
|
|
1141
|
+
console.log('Found orphaned EIP, will reuse it for new NAT Gateway in PUBLIC subnet');
|
|
1142
|
+
useExistingEip = true;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
if (reuseExistingNatGateway) {
|
|
1146
|
+
console.log('Reusing existing NAT Gateway - skipping resource creation');
|
|
1147
|
+
} else {
|
|
1148
|
+
if (!useExistingEip) {
|
|
1149
|
+
definition.resources.Resources.FriggNATGatewayEIP = {
|
|
1150
|
+
Type: 'AWS::EC2::EIP',
|
|
1151
|
+
DeletionPolicy: 'Retain',
|
|
1152
|
+
UpdateReplacePolicy: 'Retain',
|
|
1153
|
+
Properties: {
|
|
1154
|
+
Domain: 'vpc',
|
|
1155
|
+
Tags: [
|
|
1156
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-eip' },
|
|
1157
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1158
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
1159
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
1160
|
+
],
|
|
1161
|
+
},
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if (!discoveredResources.publicSubnetId) {
|
|
1166
|
+
if (discoveredResources.internetGatewayId) {
|
|
1167
|
+
console.log('Reusing existing Internet Gateway for NAT Gateway');
|
|
1168
|
+
} else {
|
|
1169
|
+
definition.resources.Resources.FriggInternetGateway =
|
|
1170
|
+
definition.resources.Resources.FriggInternetGateway || {
|
|
1171
|
+
Type: 'AWS::EC2::InternetGateway',
|
|
1172
|
+
Properties: {
|
|
1173
|
+
Tags: [
|
|
1174
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-igw' },
|
|
1175
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1176
|
+
],
|
|
1177
|
+
},
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
definition.resources.Resources.FriggIGWAttachment =
|
|
1181
|
+
definition.resources.Resources.FriggIGWAttachment || {
|
|
1182
|
+
Type: 'AWS::EC2::VPCGatewayAttachment',
|
|
1183
|
+
Properties: {
|
|
1184
|
+
VpcId: discoveredResources.defaultVpcId,
|
|
1185
|
+
InternetGatewayId: { Ref: 'FriggInternetGateway' },
|
|
1186
|
+
},
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1173
1189
|
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
definition.resources.Resources.VPCEndpointS3 = {
|
|
1177
|
-
Type: 'AWS::EC2::VPCEndpoint',
|
|
1190
|
+
definition.resources.Resources.FriggPublicSubnet = {
|
|
1191
|
+
Type: 'AWS::EC2::Subnet',
|
|
1178
1192
|
Properties: {
|
|
1179
1193
|
VpcId: discoveredResources.defaultVpcId,
|
|
1180
|
-
|
|
1181
|
-
'
|
|
1182
|
-
|
|
1183
|
-
|
|
1194
|
+
CidrBlock:
|
|
1195
|
+
AppDefinition.vpc.natGateway?.publicSubnetCidr || '172.31.250.0/24',
|
|
1196
|
+
AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
|
|
1197
|
+
MapPublicIpOnLaunch: true,
|
|
1198
|
+
Tags: [
|
|
1199
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-subnet' },
|
|
1200
|
+
{ Key: 'Type', Value: 'Public' },
|
|
1201
|
+
],
|
|
1184
1202
|
},
|
|
1185
1203
|
};
|
|
1186
1204
|
|
|
1187
|
-
definition.resources.Resources.
|
|
1188
|
-
Type: 'AWS::EC2::
|
|
1205
|
+
definition.resources.Resources.FriggPublicRouteTable = {
|
|
1206
|
+
Type: 'AWS::EC2::RouteTable',
|
|
1189
1207
|
Properties: {
|
|
1190
1208
|
VpcId: discoveredResources.defaultVpcId,
|
|
1191
|
-
|
|
1192
|
-
'
|
|
1193
|
-
|
|
1194
|
-
|
|
1209
|
+
Tags: [
|
|
1210
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-rt' },
|
|
1211
|
+
],
|
|
1212
|
+
},
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
definition.resources.Resources.FriggPublicRoute = {
|
|
1216
|
+
Type: 'AWS::EC2::Route',
|
|
1217
|
+
DependsOn: discoveredResources.internetGatewayId ? [] : 'FriggIGWAttachment',
|
|
1218
|
+
Properties: {
|
|
1219
|
+
RouteTableId: { Ref: 'FriggPublicRouteTable' },
|
|
1220
|
+
DestinationCidrBlock: '0.0.0.0/0',
|
|
1221
|
+
GatewayId: discoveredResources.internetGatewayId || { Ref: 'FriggInternetGateway' },
|
|
1222
|
+
},
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
definition.resources.Resources.FriggPublicSubnetRouteTableAssociation = {
|
|
1226
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1227
|
+
Properties: {
|
|
1228
|
+
SubnetId: { Ref: 'FriggPublicSubnet' },
|
|
1229
|
+
RouteTableId: { Ref: 'FriggPublicRouteTable' },
|
|
1195
1230
|
},
|
|
1196
1231
|
};
|
|
1197
1232
|
}
|
|
1233
|
+
|
|
1234
|
+
definition.resources.Resources.FriggNATGateway = {
|
|
1235
|
+
Type: 'AWS::EC2::NatGateway',
|
|
1236
|
+
DeletionPolicy: 'Retain',
|
|
1237
|
+
UpdateReplacePolicy: 'Retain',
|
|
1238
|
+
Properties: {
|
|
1239
|
+
AllocationId: useExistingEip
|
|
1240
|
+
? discoveredResources.existingElasticIpAllocationId
|
|
1241
|
+
: { 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'] },
|
|
1242
|
+
SubnetId:
|
|
1243
|
+
discoveredResources.publicSubnetId || { Ref: 'FriggPublicSubnet' },
|
|
1244
|
+
Tags: [
|
|
1245
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-gateway' },
|
|
1246
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1247
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
1248
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
1249
|
+
],
|
|
1250
|
+
},
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
} else if (
|
|
1254
|
+
natGatewayManagement === 'discover' ||
|
|
1255
|
+
natGatewayManagement === 'useExisting'
|
|
1256
|
+
) {
|
|
1257
|
+
if (natGatewayManagement === 'useExisting' && AppDefinition.vpc.natGateway?.id) {
|
|
1258
|
+
console.log(`Using explicitly provided NAT Gateway: ${AppDefinition.vpc.natGateway.id}`);
|
|
1259
|
+
discoveredResources.existingNatGatewayId = AppDefinition.vpc.natGateway.id;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (discoveredResources.existingNatGatewayId) {
|
|
1263
|
+
console.log(
|
|
1264
|
+
'discoveredResources.existingNatGatewayId',
|
|
1265
|
+
discoveredResources.existingNatGatewayId
|
|
1266
|
+
);
|
|
1267
|
+
|
|
1268
|
+
if (discoveredResources.natGatewayInPrivateSubnet) {
|
|
1269
|
+
console.log('❌ CRITICAL: NAT Gateway is in PRIVATE subnet - Internet connectivity will NOT work!');
|
|
1270
|
+
|
|
1271
|
+
if (AppDefinition.vpc.selfHeal === true) {
|
|
1272
|
+
console.log('Self-heal enabled: Will create new NAT Gateway in PUBLIC subnet');
|
|
1273
|
+
needsNewNatGateway = true;
|
|
1274
|
+
discoveredResources.existingNatGatewayId = null;
|
|
1275
|
+
if (!discoveredResources.publicSubnetId) {
|
|
1276
|
+
console.log('No public subnet found - will create one for NAT Gateway');
|
|
1277
|
+
discoveredResources.createPublicSubnet = true;
|
|
1278
|
+
}
|
|
1279
|
+
} else {
|
|
1280
|
+
throw new Error(
|
|
1281
|
+
'CRITICAL: NAT Gateway is in PRIVATE subnet and will NOT provide internet connectivity! Options: 1) Enable vpc.selfHeal to auto-create proper NAT, 2) Set natGateway.management to "createAndManage", or 3) Manually fix the NAT Gateway placement.'
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
} else {
|
|
1285
|
+
console.log(`Using discovered NAT Gateway for routing: ${discoveredResources.existingNatGatewayId}`);
|
|
1286
|
+
}
|
|
1287
|
+
} else if (!needsNewNatGateway && AppDefinition.vpc.natGateway?.id) {
|
|
1288
|
+
console.log(`Using explicitly provided NAT Gateway: ${AppDefinition.vpc.natGateway.id}`);
|
|
1289
|
+
discoveredResources.existingNatGatewayId = AppDefinition.vpc.natGateway.id;
|
|
1198
1290
|
}
|
|
1199
1291
|
}
|
|
1200
1292
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
'ssm:GetParametersByPath',
|
|
1215
|
-
],
|
|
1216
|
-
Resource: [
|
|
1217
|
-
'arn:aws:ssm:${self:provider.region}:*:parameter/${self:service}/${self:provider.stage}/*',
|
|
1218
|
-
],
|
|
1219
|
-
});
|
|
1293
|
+
definition.resources.Resources.FriggLambdaRouteTable =
|
|
1294
|
+
definition.resources.Resources.FriggLambdaRouteTable || {
|
|
1295
|
+
Type: 'AWS::EC2::RouteTable',
|
|
1296
|
+
Properties: {
|
|
1297
|
+
VpcId: discoveredResources.defaultVpcId || vpcId,
|
|
1298
|
+
Tags: [
|
|
1299
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
|
|
1300
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1301
|
+
{ Key: 'Environment', Value: '${self:provider.stage}' },
|
|
1302
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
1303
|
+
],
|
|
1304
|
+
},
|
|
1305
|
+
};
|
|
1220
1306
|
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1307
|
+
const routeTableId = { Ref: 'FriggLambdaRouteTable' };
|
|
1308
|
+
let natGatewayIdForRoute;
|
|
1309
|
+
|
|
1310
|
+
if (reuseExistingNatGateway) {
|
|
1311
|
+
natGatewayIdForRoute = discoveredResources.existingNatGatewayId;
|
|
1312
|
+
console.log(`Using discovered NAT Gateway for routing: ${natGatewayIdForRoute}`);
|
|
1313
|
+
} else if (needsNewNatGateway && !reuseExistingNatGateway) {
|
|
1314
|
+
natGatewayIdForRoute = { Ref: 'FriggNATGateway' };
|
|
1315
|
+
console.log('Using newly created NAT Gateway for routing');
|
|
1316
|
+
} else if (discoveredResources.existingNatGatewayId) {
|
|
1317
|
+
natGatewayIdForRoute = discoveredResources.existingNatGatewayId;
|
|
1318
|
+
console.log(`Using discovered NAT Gateway for routing: ${natGatewayIdForRoute}`);
|
|
1319
|
+
} else if (AppDefinition.vpc.natGateway?.id) {
|
|
1320
|
+
natGatewayIdForRoute = AppDefinition.vpc.natGateway.id;
|
|
1321
|
+
console.log(`Using explicitly provided NAT Gateway for routing: ${natGatewayIdForRoute}`);
|
|
1322
|
+
} else if (AppDefinition.vpc.selfHeal === true) {
|
|
1323
|
+
natGatewayIdForRoute = null;
|
|
1324
|
+
console.log('No NAT Gateway available - skipping NAT route creation');
|
|
1325
|
+
} else {
|
|
1326
|
+
throw new Error('No existing NAT Gateway found in discovery mode');
|
|
1224
1327
|
}
|
|
1225
1328
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1329
|
+
if (natGatewayIdForRoute) {
|
|
1330
|
+
console.log(`Configuring NAT route: 0.0.0.0/0 → ${natGatewayIdForRoute}`);
|
|
1331
|
+
definition.resources.Resources.FriggNATRoute = {
|
|
1332
|
+
Type: 'AWS::EC2::Route',
|
|
1333
|
+
DependsOn: 'FriggLambdaRouteTable',
|
|
1334
|
+
Properties: {
|
|
1335
|
+
RouteTableId: routeTableId,
|
|
1336
|
+
DestinationCidrBlock: '0.0.0.0/0',
|
|
1337
|
+
NatGatewayId: natGatewayIdForRoute,
|
|
1338
|
+
},
|
|
1339
|
+
};
|
|
1340
|
+
} else {
|
|
1341
|
+
console.warn('⚠️ No NAT Gateway configured - Lambda functions will not have internet access');
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
if (typeof vpcConfig.subnetIds[0] === 'string') {
|
|
1345
|
+
definition.resources.Resources.FriggSubnet1RouteAssociation = {
|
|
1346
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1347
|
+
Properties: {
|
|
1348
|
+
SubnetId: vpcConfig.subnetIds[0],
|
|
1349
|
+
RouteTableId: routeTableId,
|
|
1350
|
+
},
|
|
1351
|
+
DependsOn: 'FriggLambdaRouteTable',
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
if (typeof vpcConfig.subnetIds[1] === 'string') {
|
|
1356
|
+
definition.resources.Resources.FriggSubnet2RouteAssociation = {
|
|
1357
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1358
|
+
Properties: {
|
|
1359
|
+
SubnetId: vpcConfig.subnetIds[1],
|
|
1360
|
+
RouteTableId: routeTableId,
|
|
1361
|
+
},
|
|
1362
|
+
DependsOn: 'FriggLambdaRouteTable',
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
if (typeof vpcConfig.subnetIds[0] === 'object' && vpcConfig.subnetIds[0].Ref) {
|
|
1367
|
+
definition.resources.Resources.FriggNewSubnet1RouteAssociation = {
|
|
1368
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1369
|
+
Properties: {
|
|
1370
|
+
SubnetId: vpcConfig.subnetIds[0],
|
|
1371
|
+
RouteTableId: routeTableId,
|
|
1372
|
+
},
|
|
1373
|
+
DependsOn: ['FriggLambdaRouteTable', vpcConfig.subnetIds[0].Ref],
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
if (typeof vpcConfig.subnetIds[1] === 'object' && vpcConfig.subnetIds[1].Ref) {
|
|
1378
|
+
definition.resources.Resources.FriggNewSubnet2RouteAssociation = {
|
|
1379
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1380
|
+
Properties: {
|
|
1381
|
+
SubnetId: vpcConfig.subnetIds[1],
|
|
1382
|
+
RouteTableId: routeTableId,
|
|
1383
|
+
},
|
|
1384
|
+
DependsOn: ['FriggLambdaRouteTable', vpcConfig.subnetIds[1].Ref],
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
if (AppDefinition.vpc.enableVPCEndpoints !== false) {
|
|
1389
|
+
definition.resources.Resources.VPCEndpointS3 = {
|
|
1390
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
1391
|
+
Properties: {
|
|
1392
|
+
VpcId: discoveredResources.defaultVpcId,
|
|
1393
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.s3',
|
|
1394
|
+
VpcEndpointType: 'Gateway',
|
|
1395
|
+
RouteTableIds: [routeTableId],
|
|
1396
|
+
},
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1399
|
+
definition.resources.Resources.VPCEndpointDynamoDB = {
|
|
1400
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
1401
|
+
Properties: {
|
|
1402
|
+
VpcId: discoveredResources.defaultVpcId,
|
|
1403
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.dynamodb',
|
|
1404
|
+
VpcEndpointType: 'Gateway',
|
|
1405
|
+
RouteTableIds: [routeTableId],
|
|
1406
|
+
},
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
if (AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
|
|
1411
|
+
if (!discoveredResources.vpcCidr) {
|
|
1412
|
+
console.warn(
|
|
1413
|
+
'⚠️ Warning: VPC CIDR not discovered. VPC endpoint security group may not work correctly.'
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
if (!definition.resources.Resources.VPCEndpointSecurityGroup) {
|
|
1418
|
+
const vpcEndpointIngressRules = [];
|
|
1419
|
+
|
|
1420
|
+
if (vpcConfig.securityGroupIds && vpcConfig.securityGroupIds.length > 0) {
|
|
1421
|
+
for (const sg of vpcConfig.securityGroupIds) {
|
|
1422
|
+
if (typeof sg === 'string') {
|
|
1423
|
+
vpcEndpointIngressRules.push({
|
|
1424
|
+
IpProtocol: 'tcp',
|
|
1425
|
+
FromPort: 443,
|
|
1426
|
+
ToPort: 443,
|
|
1427
|
+
SourceSecurityGroupId: sg,
|
|
1428
|
+
Description: 'HTTPS from Lambda security group',
|
|
1429
|
+
});
|
|
1430
|
+
} else if (sg.Ref) {
|
|
1431
|
+
vpcEndpointIngressRules.push({
|
|
1432
|
+
IpProtocol: 'tcp',
|
|
1433
|
+
FromPort: 443,
|
|
1434
|
+
ToPort: 443,
|
|
1435
|
+
SourceSecurityGroupId: { Ref: sg.Ref },
|
|
1436
|
+
Description: 'HTTPS from Lambda security group',
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1240
1440
|
}
|
|
1241
|
-
const integrationName = integration.Definition.name;
|
|
1242
1441
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1442
|
+
if (vpcEndpointIngressRules.length === 0) {
|
|
1443
|
+
if (discoveredResources.vpcCidr) {
|
|
1444
|
+
vpcEndpointIngressRules.push({
|
|
1445
|
+
IpProtocol: 'tcp',
|
|
1446
|
+
FromPort: 443,
|
|
1447
|
+
ToPort: 443,
|
|
1448
|
+
CidrIp: discoveredResources.vpcCidr,
|
|
1449
|
+
Description: 'HTTPS from VPC CIDR (fallback)',
|
|
1450
|
+
});
|
|
1451
|
+
} else {
|
|
1452
|
+
console.warn(
|
|
1453
|
+
'⚠️ WARNING: No Lambda security group or VPC CIDR found. Using default private IP ranges.'
|
|
1454
|
+
);
|
|
1455
|
+
vpcEndpointIngressRules.push({
|
|
1456
|
+
IpProtocol: 'tcp',
|
|
1457
|
+
FromPort: 443,
|
|
1458
|
+
ToPort: 443,
|
|
1459
|
+
CidrIp: '172.31.0.0/16',
|
|
1460
|
+
Description: 'HTTPS from default VPC range',
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1255
1464
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
integrationName.charAt(0).toUpperCase() +
|
|
1259
|
-
integrationName.slice(1)
|
|
1260
|
-
}Queue`;
|
|
1261
|
-
const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
|
|
1262
|
-
definition.resources.Resources[queueReference] = {
|
|
1263
|
-
Type: 'AWS::SQS::Queue',
|
|
1465
|
+
definition.resources.Resources.VPCEndpointSecurityGroup = {
|
|
1466
|
+
Type: 'AWS::EC2::SecurityGroup',
|
|
1264
1467
|
Properties: {
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
},
|
|
1468
|
+
GroupDescription: 'Security group for VPC endpoints - allows HTTPS from Lambda functions',
|
|
1469
|
+
VpcId: discoveredResources.defaultVpcId,
|
|
1470
|
+
SecurityGroupIngress: vpcEndpointIngressRules,
|
|
1471
|
+
Tags: [
|
|
1472
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc-endpoints-sg' },
|
|
1473
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1474
|
+
{ Key: 'Purpose', Value: 'Allow Lambda functions to access VPC endpoints' },
|
|
1475
|
+
],
|
|
1274
1476
|
},
|
|
1275
1477
|
};
|
|
1478
|
+
}
|
|
1276
1479
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
batchSize: 1,
|
|
1289
|
-
},
|
|
1290
|
-
},
|
|
1291
|
-
],
|
|
1292
|
-
timeout: 600,
|
|
1293
|
-
};
|
|
1480
|
+
definition.resources.Resources.VPCEndpointKMS = {
|
|
1481
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
1482
|
+
Properties: {
|
|
1483
|
+
VpcId: discoveredResources.defaultVpcId,
|
|
1484
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.kms',
|
|
1485
|
+
VpcEndpointType: 'Interface',
|
|
1486
|
+
SubnetIds: vpcConfig.subnetIds,
|
|
1487
|
+
SecurityGroupIds: [{ Ref: 'VPCEndpointSecurityGroup' }],
|
|
1488
|
+
PrivateDnsEnabled: true,
|
|
1489
|
+
},
|
|
1490
|
+
};
|
|
1294
1491
|
|
|
1295
|
-
|
|
1296
|
-
definition.
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1492
|
+
if (AppDefinition.secretsManager?.enable === true) {
|
|
1493
|
+
definition.resources.Resources.VPCEndpointSecretsManager = {
|
|
1494
|
+
Type: 'AWS::EC2::VPCEndpoint',
|
|
1495
|
+
Properties: {
|
|
1496
|
+
VpcId: discoveredResources.defaultVpcId,
|
|
1497
|
+
ServiceName: 'com.amazonaws.${self:provider.region}.secretsmanager',
|
|
1498
|
+
VpcEndpointType: 'Interface',
|
|
1499
|
+
SubnetIds: vpcConfig.subnetIds,
|
|
1500
|
+
SecurityGroupIds: [{ Ref: 'VPCEndpointSecurityGroup' }],
|
|
1501
|
+
PrivateDnsEnabled: true,
|
|
1300
1502
|
},
|
|
1301
1503
|
};
|
|
1302
|
-
|
|
1303
|
-
definition.custom[queueReference] = queueName;
|
|
1304
1504
|
}
|
|
1305
1505
|
}
|
|
1306
1506
|
}
|
|
1507
|
+
};
|
|
1508
|
+
|
|
1509
|
+
const configureSsm = (definition, AppDefinition) => {
|
|
1510
|
+
if (AppDefinition.ssm?.enable !== true) {
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
definition.provider.layers = [
|
|
1515
|
+
'arn:aws:lambda:${self:provider.region}:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11',
|
|
1516
|
+
];
|
|
1517
|
+
|
|
1518
|
+
definition.provider.iamRoleStatements.push({
|
|
1519
|
+
Effect: 'Allow',
|
|
1520
|
+
Action: ['ssm:GetParameter', 'ssm:GetParameters', 'ssm:GetParametersByPath'],
|
|
1521
|
+
Resource: ['arn:aws:ssm:${self:provider.region}:*:parameter/${self:service}/${self:provider.stage}/*'],
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
definition.provider.environment.SSM_PARAMETER_PREFIX = '/${self:service}/${self:provider.stage}';
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
const attachIntegrations = (definition, AppDefinition) => {
|
|
1528
|
+
if (!Array.isArray(AppDefinition.integrations) || AppDefinition.integrations.length === 0) {
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1307
1531
|
|
|
1308
|
-
|
|
1309
|
-
// The discoveredResources object contains all the necessary AWS resources
|
|
1532
|
+
console.log(`Processing ${AppDefinition.integrations.length} integrations...`);
|
|
1310
1533
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1534
|
+
for (const integration of AppDefinition.integrations) {
|
|
1535
|
+
if (!integration?.Definition?.name) {
|
|
1536
|
+
throw new Error('Invalid integration: missing Definition or name');
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
const integrationName = integration.Definition.name;
|
|
1540
|
+
const queueReference = `${integrationName.charAt(0).toUpperCase() + integrationName.slice(1)}Queue`;
|
|
1541
|
+
const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
|
|
1542
|
+
|
|
1543
|
+
definition.functions[integrationName] = {
|
|
1544
|
+
handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
|
|
1316
1545
|
events: [
|
|
1317
1546
|
{
|
|
1318
|
-
|
|
1319
|
-
|
|
1547
|
+
httpApi: {
|
|
1548
|
+
path: `/api/${integrationName}-integration/{proxy+}`,
|
|
1549
|
+
method: 'ANY',
|
|
1320
1550
|
},
|
|
1321
1551
|
},
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1552
|
+
],
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1555
|
+
definition.resources.Resources[queueReference] = {
|
|
1556
|
+
Type: 'AWS::SQS::Queue',
|
|
1557
|
+
Properties: {
|
|
1558
|
+
QueueName: `\${self:custom.${queueReference}}`,
|
|
1559
|
+
MessageRetentionPeriod: 60,
|
|
1560
|
+
VisibilityTimeout: 1800,
|
|
1561
|
+
RedrivePolicy: {
|
|
1562
|
+
maxReceiveCount: 1,
|
|
1563
|
+
deadLetterTargetArn: { 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
|
|
1326
1564
|
},
|
|
1565
|
+
},
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
const queueWorkerName = `${integrationName}QueueWorker`;
|
|
1569
|
+
definition.functions[queueWorkerName] = {
|
|
1570
|
+
handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
|
|
1571
|
+
reservedConcurrency: 5,
|
|
1572
|
+
events: [
|
|
1327
1573
|
{
|
|
1328
|
-
|
|
1329
|
-
|
|
1574
|
+
sqs: {
|
|
1575
|
+
arn: { 'Fn::GetAtt': [queueReference, 'Arn'] },
|
|
1576
|
+
batchSize: 1,
|
|
1330
1577
|
},
|
|
1331
1578
|
},
|
|
1332
1579
|
],
|
|
1580
|
+
timeout: 600,
|
|
1581
|
+
};
|
|
1582
|
+
|
|
1583
|
+
definition.provider.environment = {
|
|
1584
|
+
...definition.provider.environment,
|
|
1585
|
+
[`${integrationName.toUpperCase()}_QUEUE_URL`]: { Ref: queueReference },
|
|
1333
1586
|
};
|
|
1587
|
+
|
|
1588
|
+
definition.custom[queueReference] = queueName;
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
|
|
1592
|
+
const configureWebsockets = (definition, AppDefinition) => {
|
|
1593
|
+
if (AppDefinition.websockets?.enable !== true) {
|
|
1594
|
+
return;
|
|
1334
1595
|
}
|
|
1335
1596
|
|
|
1336
|
-
|
|
1597
|
+
definition.functions.defaultWebsocket = {
|
|
1598
|
+
handler: 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
|
|
1599
|
+
events: [
|
|
1600
|
+
{ websocket: { route: '$connect' } },
|
|
1601
|
+
{ websocket: { route: '$default' } },
|
|
1602
|
+
{ websocket: { route: '$disconnect' } },
|
|
1603
|
+
],
|
|
1604
|
+
};
|
|
1605
|
+
};
|
|
1606
|
+
|
|
1607
|
+
const composeServerlessDefinition = async (AppDefinition) => {
|
|
1608
|
+
console.log('composeServerlessDefinition', AppDefinition);
|
|
1609
|
+
|
|
1610
|
+
const discoveredResources = await gatherDiscoveredResources(AppDefinition);
|
|
1611
|
+
const appEnvironmentVars = getAppEnvironmentVars(AppDefinition);
|
|
1612
|
+
const definition = createBaseDefinition(AppDefinition, appEnvironmentVars, discoveredResources);
|
|
1613
|
+
|
|
1614
|
+
applyKmsConfiguration(definition, AppDefinition, discoveredResources);
|
|
1615
|
+
configureVpc(definition, AppDefinition, discoveredResources);
|
|
1616
|
+
configureSsm(definition, AppDefinition);
|
|
1617
|
+
attachIntegrations(definition, AppDefinition);
|
|
1618
|
+
configureWebsockets(definition, AppDefinition);
|
|
1619
|
+
|
|
1337
1620
|
definition.functions = modifyHandlerPaths(definition.functions);
|
|
1338
1621
|
|
|
1339
1622
|
return definition;
|