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