@venturekit/infra 0.0.0-dev.20260312012510 → 0.0.0-dev.20260329102450
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/dist/cdk/index.js +1 -5
- package/dist/cdk/index.js.map +1 -1
- package/dist/cdk/stack.d.ts +133 -1
- package/dist/cdk/stack.d.ts.map +1 -1
- package/dist/cdk/stack.js +889 -115
- package/dist/cdk/stack.js.map +1 -1
- package/dist/index.js +1 -6
- package/dist/index.js.map +1 -1
- package/dist/venture.d.ts +11 -3
- package/dist/venture.d.ts.map +1 -1
- package/dist/venture.js +12 -14
- package/dist/venture.js.map +1 -1
- package/package.json +3 -3
package/dist/cdk/stack.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* VentureStack — CDK Stack for VentureKit applications
|
|
4
3
|
*
|
|
@@ -7,72 +6,122 @@
|
|
|
7
6
|
*
|
|
8
7
|
* Consumers never see this file — it's invoked by the generated CDK app in .vk/cdk/.
|
|
9
8
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (mod && mod.__esModule) return mod;
|
|
37
|
-
var result = {};
|
|
38
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
-
__setModuleDefault(result, mod);
|
|
40
|
-
return result;
|
|
41
|
-
};
|
|
42
|
-
})();
|
|
43
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
-
exports.VentureStack = void 0;
|
|
45
|
-
const cdk = __importStar(require("aws-cdk-lib"));
|
|
46
|
-
const ec2 = __importStar(require("aws-cdk-lib/aws-ec2"));
|
|
47
|
-
const rds = __importStar(require("aws-cdk-lib/aws-rds"));
|
|
48
|
-
const s3 = __importStar(require("aws-cdk-lib/aws-s3"));
|
|
49
|
-
const sqs = __importStar(require("aws-cdk-lib/aws-sqs"));
|
|
50
|
-
const cognito = __importStar(require("aws-cdk-lib/aws-cognito"));
|
|
51
|
-
const elasticache = __importStar(require("aws-cdk-lib/aws-elasticache"));
|
|
52
|
-
const apigatewayv2 = __importStar(require("aws-cdk-lib/aws-apigatewayv2"));
|
|
53
|
-
class VentureStack extends cdk.Stack {
|
|
9
|
+
import * as cdk from 'aws-cdk-lib';
|
|
10
|
+
import * as ec2 from 'aws-cdk-lib/aws-ec2';
|
|
11
|
+
import * as rds from 'aws-cdk-lib/aws-rds';
|
|
12
|
+
import * as s3 from 'aws-cdk-lib/aws-s3';
|
|
13
|
+
import * as sqs from 'aws-cdk-lib/aws-sqs';
|
|
14
|
+
import * as cognito from 'aws-cdk-lib/aws-cognito';
|
|
15
|
+
import * as elasticache from 'aws-cdk-lib/aws-elasticache';
|
|
16
|
+
import * as events from 'aws-cdk-lib/aws-events';
|
|
17
|
+
import * as targets from 'aws-cdk-lib/aws-events-targets';
|
|
18
|
+
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
19
|
+
import * as lambdaEventSources from 'aws-cdk-lib/aws-lambda-event-sources';
|
|
20
|
+
import * as apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2';
|
|
21
|
+
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
|
|
22
|
+
import * as logs from 'aws-cdk-lib/aws-logs';
|
|
23
|
+
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
|
|
24
|
+
import * as cloudwatchActions from 'aws-cdk-lib/aws-cloudwatch-actions';
|
|
25
|
+
import * as sns from 'aws-cdk-lib/aws-sns';
|
|
26
|
+
import * as ssm from 'aws-cdk-lib/aws-ssm';
|
|
27
|
+
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
|
|
28
|
+
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
|
|
29
|
+
import * as route53 from 'aws-cdk-lib/aws-route53';
|
|
30
|
+
import * as route53Targets from 'aws-cdk-lib/aws-route53-targets';
|
|
31
|
+
import { resolveEnvConfig, DATA_SAFETY_CONFIG } from '@venturekit/core';
|
|
32
|
+
import * as path from 'path';
|
|
33
|
+
import * as fs from 'fs';
|
|
34
|
+
export class VentureStack extends cdk.Stack {
|
|
54
35
|
outputs = {};
|
|
36
|
+
/** Resolved environment configuration driving all resource sizing */
|
|
37
|
+
envConfig;
|
|
38
|
+
/** Data safety configuration for removal policies */
|
|
39
|
+
dataSafetyConfig;
|
|
40
|
+
/** Whether this is a free-tier deployment */
|
|
41
|
+
isFreeTier;
|
|
42
|
+
/** Standard tags applied to all taggable resources */
|
|
43
|
+
standardTags;
|
|
55
44
|
constructor(scope, id, props) {
|
|
56
45
|
super(scope, id, props);
|
|
57
|
-
const { projectName, stage, routesDir, infrastructure, projectDir } = props;
|
|
58
|
-
//
|
|
46
|
+
const { projectName, stage, routesDir, functionsDir, queuesDir, cronsDir, infrastructure, projectDir, preset } = props;
|
|
47
|
+
// --- Resolve environment configuration ---
|
|
48
|
+
// Priority: explicit envConfig > resolve from preset > fallback to 'micro'
|
|
49
|
+
const env = (stage === 'prod' ? 'prod' : stage === 'stage' ? 'stage' : 'dev');
|
|
50
|
+
this.envConfig = props.envConfig ?? resolveEnvConfig(env, { preset: preset ?? 'micro' });
|
|
51
|
+
this.isFreeTier = preset === 'free' || this.envConfig.dataSafety === 'relaxed' && preset === 'free';
|
|
52
|
+
this.dataSafetyConfig = DATA_SAFETY_CONFIG[this.envConfig.dataSafety];
|
|
53
|
+
// --- Well-Architected: Operational Excellence — consistent tagging ---
|
|
54
|
+
this.standardTags = {
|
|
55
|
+
Project: projectName,
|
|
56
|
+
Stage: stage,
|
|
57
|
+
ManagedBy: 'venturekit',
|
|
58
|
+
Preset: preset ?? 'micro',
|
|
59
|
+
};
|
|
60
|
+
Object.entries(this.standardTags).forEach(([key, value]) => {
|
|
61
|
+
cdk.Tags.of(this).add(key, value);
|
|
62
|
+
});
|
|
63
|
+
const lambdaConfig = this.envConfig.lambda;
|
|
64
|
+
const vpcConfig = this.envConfig.vpc;
|
|
65
|
+
const apiConfig = this.envConfig.api;
|
|
66
|
+
const obsConfig = this.envConfig.observability;
|
|
67
|
+
// --- Shared VPC ---
|
|
68
|
+
// Well-Architected: Security — network isolation for databases and caches
|
|
69
|
+
// Free tier: skip VPC entirely (NAT gateways are not free)
|
|
70
|
+
// Stable logical ID 'Vpc' ensures preset changes don't replace the VPC
|
|
59
71
|
let vpc;
|
|
60
|
-
const needsVpc =
|
|
61
|
-
(infrastructure?.
|
|
72
|
+
const needsVpc = !this.isFreeTier && (lambdaConfig.vpcEnabled ||
|
|
73
|
+
(infrastructure?.databases && infrastructure.databases.length > 0) ||
|
|
74
|
+
(infrastructure?.caches && infrastructure.caches.length > 0));
|
|
62
75
|
if (needsVpc) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
const subnetConfig = [
|
|
77
|
+
{ name: 'public', subnetType: ec2.SubnetType.PUBLIC, cidrMask: vpcConfig.publicSubnets.cidrMask },
|
|
78
|
+
{ name: 'private', subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, cidrMask: vpcConfig.privateSubnets.cidrMask },
|
|
79
|
+
];
|
|
80
|
+
if (vpcConfig.isolatedSubnets) {
|
|
81
|
+
subnetConfig.push({
|
|
82
|
+
name: 'isolated',
|
|
83
|
+
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
|
|
84
|
+
cidrMask: vpcConfig.isolatedSubnets.cidrMask,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
vpc = new ec2.Vpc(this, 'Vpc', {
|
|
88
|
+
maxAzs: vpcConfig.maxAzs,
|
|
89
|
+
natGateways: vpcConfig.natGateways,
|
|
90
|
+
ipAddresses: ec2.IpAddresses.cidr(vpcConfig.cidr),
|
|
91
|
+
subnetConfiguration: subnetConfig,
|
|
92
|
+
// Well-Architected: Security — restrict default SG
|
|
93
|
+
restrictDefaultSecurityGroup: true,
|
|
70
94
|
});
|
|
95
|
+
// Well-Architected: Security — VPC flow logs for network monitoring
|
|
96
|
+
if (vpcConfig.flowLogsEnabled) {
|
|
97
|
+
vpc.addFlowLog('FlowLog', {
|
|
98
|
+
destination: ec2.FlowLogDestination.toCloudWatchLogs(new logs.LogGroup(this, 'VpcFlowLogGroup', {
|
|
99
|
+
logGroupName: `/venturekit/${projectName}/${stage}/vpc-flow-logs`,
|
|
100
|
+
retention: this.toLogRetention(vpcConfig.flowLogsRetentionDays),
|
|
101
|
+
removalPolicy: this.toRemovalPolicy(),
|
|
102
|
+
})),
|
|
103
|
+
trafficType: ec2.FlowLogTrafficType.ALL,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// Well-Architected: Performance — VPC endpoints reduce latency and cost
|
|
107
|
+
if (vpcConfig.enableVpcEndpoints) {
|
|
108
|
+
vpc.addGatewayEndpoint('S3Endpoint', {
|
|
109
|
+
service: ec2.GatewayVpcEndpointAwsService.S3,
|
|
110
|
+
});
|
|
111
|
+
vpc.addGatewayEndpoint('DynamoEndpoint', {
|
|
112
|
+
service: ec2.GatewayVpcEndpointAwsService.DYNAMODB,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
71
115
|
}
|
|
72
116
|
// --- Databases ---
|
|
73
117
|
if (infrastructure?.databases) {
|
|
74
118
|
for (const db of infrastructure.databases) {
|
|
75
|
-
this.
|
|
119
|
+
if (this.isFreeTier) {
|
|
120
|
+
this.createDynamoDBTable(db, projectName, stage);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
this.createDatabase(db, projectName, stage, vpc);
|
|
124
|
+
}
|
|
76
125
|
}
|
|
77
126
|
}
|
|
78
127
|
// --- Storage ---
|
|
@@ -94,17 +143,41 @@ class VentureStack extends cdk.Stack {
|
|
|
94
143
|
}
|
|
95
144
|
}
|
|
96
145
|
// --- Caches ---
|
|
97
|
-
|
|
146
|
+
// Free tier: skip ElastiCache (not free). Use DynamoDB-based caching
|
|
147
|
+
// via createDefaultRateLimiter() from @venturekit/runtime instead.
|
|
148
|
+
if (infrastructure?.caches && !this.isFreeTier) {
|
|
98
149
|
for (const cache of infrastructure.caches) {
|
|
99
150
|
this.createCache(cache, projectName, stage, vpc);
|
|
100
151
|
}
|
|
101
152
|
}
|
|
153
|
+
// Free tier: create a DynamoDB table for rate limiting (always-free tier)
|
|
154
|
+
if (this.isFreeTier) {
|
|
155
|
+
this.createRateLimitTable(projectName, stage);
|
|
156
|
+
}
|
|
157
|
+
// --- Monitoring ---
|
|
158
|
+
if (infrastructure?.monitoring) {
|
|
159
|
+
for (const mon of infrastructure.monitoring) {
|
|
160
|
+
this.createMonitoring(mon, projectName, stage);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// --- Secrets ---
|
|
164
|
+
if (infrastructure?.secrets) {
|
|
165
|
+
for (const secret of infrastructure.secrets) {
|
|
166
|
+
this.createSecret(secret, projectName, stage);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
102
169
|
// --- API Gateway + Lambda routes ---
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
const api = new apigatewayv2.HttpApi(this,
|
|
170
|
+
// Well-Architected: Performance — throttling protects downstream services
|
|
171
|
+
// Well-Architected: Security — CORS preflight with explicit origins in prod
|
|
172
|
+
const api = new apigatewayv2.HttpApi(this, 'Api', {
|
|
106
173
|
apiName: `${projectName}-${stage}`,
|
|
107
|
-
corsPreflight: {
|
|
174
|
+
corsPreflight: apiConfig.cors ? {
|
|
175
|
+
allowOrigins: apiConfig.cors.allowOrigins,
|
|
176
|
+
allowMethods: apiConfig.cors.allowMethods.map((m) => apigatewayv2.CorsHttpMethod[m.toUpperCase()] ?? apigatewayv2.CorsHttpMethod.ANY),
|
|
177
|
+
allowHeaders: apiConfig.cors.allowHeaders,
|
|
178
|
+
allowCredentials: apiConfig.cors.allowCredentials,
|
|
179
|
+
maxAge: cdk.Duration.seconds(apiConfig.cors.maxAgeSec),
|
|
180
|
+
} : {
|
|
108
181
|
allowOrigins: ['*'],
|
|
109
182
|
allowMethods: [
|
|
110
183
|
apigatewayv2.CorsHttpMethod.GET,
|
|
@@ -116,49 +189,178 @@ class VentureStack extends cdk.Stack {
|
|
|
116
189
|
],
|
|
117
190
|
allowHeaders: ['*'],
|
|
118
191
|
},
|
|
192
|
+
disableExecuteApiEndpoint: false,
|
|
193
|
+
});
|
|
194
|
+
// Well-Architected: Operational Excellence — access logging for API
|
|
195
|
+
const apiLogGroup = new logs.LogGroup(this, 'ApiAccessLog', {
|
|
196
|
+
logGroupName: `/venturekit/${projectName}/${stage}/api-access`,
|
|
197
|
+
retention: this.toLogRetention(obsConfig.logs.retentionDays),
|
|
198
|
+
removalPolicy: this.toRemovalPolicy(),
|
|
119
199
|
});
|
|
200
|
+
const defaultStage = api.defaultStage?.node.defaultChild;
|
|
201
|
+
if (defaultStage) {
|
|
202
|
+
defaultStage.accessLogSettings = {
|
|
203
|
+
destinationArn: apiLogGroup.logGroupArn,
|
|
204
|
+
format: JSON.stringify({
|
|
205
|
+
requestId: '$context.requestId',
|
|
206
|
+
ip: '$context.identity.sourceIp',
|
|
207
|
+
method: '$context.httpMethod',
|
|
208
|
+
path: '$context.path',
|
|
209
|
+
status: '$context.status',
|
|
210
|
+
latency: '$context.responseLatency',
|
|
211
|
+
integrationLatency: '$context.integrationLatency',
|
|
212
|
+
}),
|
|
213
|
+
};
|
|
214
|
+
defaultStage.defaultRouteSettings = {
|
|
215
|
+
detailedMetricsEnabled: apiConfig.detailedMetricsEnabled,
|
|
216
|
+
throttlingRateLimit: apiConfig.throttleRateLimit,
|
|
217
|
+
throttlingBurstLimit: apiConfig.throttleBurstLimit,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
120
220
|
new cdk.CfnOutput(this, 'ApiUrl', {
|
|
121
221
|
value: api.apiEndpoint,
|
|
122
222
|
description: 'API Gateway URL',
|
|
123
223
|
});
|
|
224
|
+
// --- Custom Domains ---
|
|
225
|
+
if (infrastructure?.domains) {
|
|
226
|
+
for (const domain of infrastructure.domains) {
|
|
227
|
+
this.createDomain(domain, projectName, stage, api);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// --- Standalone Lambda Functions ---
|
|
231
|
+
// Discover functions from src/functions/ and create Lambda functions.
|
|
232
|
+
// Naming convention: {project}-{stage}-fn-{name}
|
|
233
|
+
const resolvedFunctionsDir = functionsDir
|
|
234
|
+
? path.resolve(projectDir, functionsDir)
|
|
235
|
+
: path.resolve(projectDir, 'src/functions');
|
|
236
|
+
if (fs.existsSync(resolvedFunctionsDir)) {
|
|
237
|
+
const discoveredFunctions = this.discoverFunctionFiles(resolvedFunctionsDir);
|
|
238
|
+
const functionIntents = infrastructure?.functions ?? [];
|
|
239
|
+
for (const fnFile of discoveredFunctions) {
|
|
240
|
+
const intent = functionIntents.find((f) => f.id === fnFile.name);
|
|
241
|
+
this.createStandaloneFunction(fnFile.name, fnFile.handlerFile, projectName, stage, projectDir, intent);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// --- Queue Consumer Lambda Functions ---
|
|
245
|
+
// Discover handlers from src/queues/ and wire them to SQS queues.
|
|
246
|
+
// Naming convention: {project}-{stage}-queue-{name}
|
|
247
|
+
const resolvedQueuesDir = queuesDir
|
|
248
|
+
? path.resolve(projectDir, queuesDir)
|
|
249
|
+
: path.resolve(projectDir, 'src/queues');
|
|
250
|
+
if (fs.existsSync(resolvedQueuesDir)) {
|
|
251
|
+
const discoveredQueues = this.discoverFunctionFiles(resolvedQueuesDir);
|
|
252
|
+
const queueIntents = infrastructure?.queues ?? [];
|
|
253
|
+
for (const qFile of discoveredQueues) {
|
|
254
|
+
const intent = queueIntents.find((q) => q.id === qFile.name);
|
|
255
|
+
this.createQueueConsumer(qFile.name, qFile.handlerFile, projectName, stage, projectDir, intent);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// --- Cron/Scheduled Task Lambda Functions ---
|
|
259
|
+
// Discover handlers from src/crons/ and wire them to EventBridge rules.
|
|
260
|
+
// Naming convention: {project}-{stage}-cron-{name}
|
|
261
|
+
const resolvedCronsDir = cronsDir
|
|
262
|
+
? path.resolve(projectDir, cronsDir)
|
|
263
|
+
: path.resolve(projectDir, 'src/crons');
|
|
264
|
+
if (fs.existsSync(resolvedCronsDir)) {
|
|
265
|
+
const discoveredCrons = this.discoverFunctionFiles(resolvedCronsDir);
|
|
266
|
+
const scheduleIntents = infrastructure?.schedules ?? [];
|
|
267
|
+
for (const cFile of discoveredCrons) {
|
|
268
|
+
const intent = scheduleIntents.find((s) => s.id === cFile.name);
|
|
269
|
+
this.createCronHandler(cFile.name, cFile.handlerFile, projectName, stage, projectDir, intent);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
124
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Well-Architected: Reliability — RDS with stable logical ID so preset
|
|
275
|
+
* changes (e.g. nano→medium) perform an in-place instance class modify,
|
|
276
|
+
* not a replacement. Data is preserved because the CloudFormation resource
|
|
277
|
+
* identity stays the same.
|
|
278
|
+
*
|
|
279
|
+
* Well-Architected: Security — encrypted at rest, credentials in Secrets Manager
|
|
280
|
+
* Well-Architected: Reliability — automated backups, optional multi-AZ
|
|
281
|
+
* Well-Architected: Performance — instance type driven by intent.size
|
|
282
|
+
* Well-Architected: Operational Excellence — auto minor version upgrades
|
|
283
|
+
*/
|
|
125
284
|
createDatabase(intent, projectName, stage, vpc) {
|
|
126
|
-
|
|
285
|
+
// Stable logical ID: 'db-{intent.id}' — never changes when preset changes
|
|
286
|
+
const logicalId = `db-${intent.id}`;
|
|
127
287
|
const instanceType = this.sizeToInstanceType(intent.size || 'small');
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
288
|
+
const engine = intent.type === 'mysql'
|
|
289
|
+
? rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0 })
|
|
290
|
+
: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_16 });
|
|
291
|
+
// Well-Architected: Security — place DB in isolated subnets when available
|
|
292
|
+
const subnetType = vpc.isolatedSubnets?.length
|
|
293
|
+
? ec2.SubnetType.PRIVATE_ISOLATED
|
|
294
|
+
: ec2.SubnetType.PRIVATE_WITH_EGRESS;
|
|
295
|
+
const instance = new rds.DatabaseInstance(this, logicalId, {
|
|
296
|
+
engine,
|
|
132
297
|
instanceType,
|
|
133
298
|
vpc,
|
|
134
|
-
vpcSubnets: { subnetType
|
|
299
|
+
vpcSubnets: { subnetType },
|
|
135
300
|
databaseName: intent.name || intent.id,
|
|
136
301
|
credentials: rds.Credentials.fromGeneratedSecret('postgres'),
|
|
137
|
-
|
|
138
|
-
|
|
302
|
+
// Well-Architected: Reliability — use dataSafety for removal policy
|
|
303
|
+
removalPolicy: this.toRemovalPolicy(),
|
|
304
|
+
deletionProtection: this.dataSafetyConfig.deletionProtection,
|
|
139
305
|
multiAz: intent.highAvailability ?? false,
|
|
306
|
+
// Well-Architected: Security — encryption at rest
|
|
140
307
|
storageEncrypted: intent.encrypted ?? true,
|
|
308
|
+
// Well-Architected: Reliability — automated backups
|
|
141
309
|
backupRetention: intent.backups !== false
|
|
142
310
|
? cdk.Duration.days(intent.backupRetentionDays ?? 7)
|
|
143
311
|
: cdk.Duration.days(0),
|
|
312
|
+
// Well-Architected: Operational Excellence — auto minor version upgrades
|
|
313
|
+
autoMinorVersionUpgrade: true,
|
|
314
|
+
// Well-Architected: Performance — performance insights for medium+ presets
|
|
315
|
+
enablePerformanceInsights: intent.performanceInsights ?? this.envConfig.lambda.memoryMb >= 512,
|
|
316
|
+
// Well-Architected: Reliability — enhanced monitoring for medium+ presets
|
|
317
|
+
monitoringInterval: this.envConfig.lambda.memoryMb >= 512
|
|
318
|
+
? cdk.Duration.seconds(60)
|
|
319
|
+
: undefined,
|
|
144
320
|
});
|
|
145
|
-
|
|
321
|
+
// Well-Architected: Reliability — prevent accidental replacement
|
|
322
|
+
// CFN UpdateReplacePolicy ensures data safety on resource property changes
|
|
323
|
+
const cfnInstance = instance.node.defaultChild;
|
|
324
|
+
if (this.dataSafetyConfig.removalPolicy === 'retain') {
|
|
325
|
+
cfnInstance.cfnOptions.updateReplacePolicy = cdk.CfnDeletionPolicy.RETAIN;
|
|
326
|
+
}
|
|
327
|
+
new cdk.CfnOutput(this, `${logicalId}-endpoint`, {
|
|
146
328
|
value: instance.dbInstanceEndpointAddress,
|
|
147
329
|
description: `Database endpoint for ${intent.id}`,
|
|
148
330
|
});
|
|
149
|
-
new cdk.CfnOutput(this, `${
|
|
331
|
+
new cdk.CfnOutput(this, `${logicalId}-secret-arn`, {
|
|
150
332
|
value: instance.secret?.secretArn || '',
|
|
151
333
|
description: `Database secret ARN for ${intent.id}`,
|
|
152
334
|
});
|
|
153
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* Well-Architected: Security — block public access by default, SSE encryption
|
|
338
|
+
* Well-Architected: Reliability — versioning for backups, dataSafety removal policy
|
|
339
|
+
* Well-Architected: Cost — lifecycle rules for logs/backups to reduce storage cost
|
|
340
|
+
* Well-Architected: Sustainability — intelligent tiering for infrequent access
|
|
341
|
+
*/
|
|
154
342
|
createStorage(intent, projectName, stage) {
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
343
|
+
// Stable logical ID: 'storage-{intent.id}'
|
|
344
|
+
const logicalId = `storage-${intent.id}`;
|
|
345
|
+
const isPublicAssets = intent.purpose === 'assets' && intent.cdn;
|
|
346
|
+
const bucket = new s3.Bucket(this, logicalId, {
|
|
347
|
+
bucketName: `${projectName}-${intent.id}-${stage}`,
|
|
158
348
|
versioned: intent.versioned ?? intent.purpose === 'backups',
|
|
159
|
-
removalPolicy:
|
|
160
|
-
autoDeleteObjects:
|
|
349
|
+
removalPolicy: this.toRemovalPolicy(),
|
|
350
|
+
autoDeleteObjects: this.dataSafetyConfig.removalPolicy === 'destroy',
|
|
351
|
+
// Well-Architected: Security — encryption at rest
|
|
161
352
|
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
353
|
+
// Well-Architected: Security — block public access unless explicit CDN assets
|
|
354
|
+
blockPublicAccess: isPublicAssets
|
|
355
|
+
? new s3.BlockPublicAccess({
|
|
356
|
+
blockPublicAcls: true,
|
|
357
|
+
ignorePublicAcls: true,
|
|
358
|
+
blockPublicPolicy: false,
|
|
359
|
+
restrictPublicBuckets: false,
|
|
360
|
+
})
|
|
361
|
+
: s3.BlockPublicAccess.BLOCK_ALL,
|
|
362
|
+
// Well-Architected: Security — enforce SSL
|
|
363
|
+
enforceSSL: true,
|
|
162
364
|
cors: intent.corsOrigins
|
|
163
365
|
? [
|
|
164
366
|
{
|
|
@@ -173,18 +375,38 @@ class VentureStack extends cdk.Stack {
|
|
|
173
375
|
},
|
|
174
376
|
]
|
|
175
377
|
: undefined,
|
|
378
|
+
// Well-Architected: Cost — lifecycle rules for logs and backups
|
|
379
|
+
lifecycleRules: intent.purpose === 'logs'
|
|
380
|
+
? [{ expiration: cdk.Duration.days(intent.retentionDays ?? 90), id: 'log-expiry' }]
|
|
381
|
+
: intent.purpose === 'backups'
|
|
382
|
+
? [
|
|
383
|
+
{ transitions: [{ storageClass: s3.StorageClass.INFREQUENT_ACCESS, transitionAfter: cdk.Duration.days(30) }], id: 'backup-ia' },
|
|
384
|
+
{ transitions: [{ storageClass: s3.StorageClass.GLACIER, transitionAfter: cdk.Duration.days(90) }], id: 'backup-glacier' },
|
|
385
|
+
]
|
|
386
|
+
: [],
|
|
176
387
|
});
|
|
177
|
-
|
|
388
|
+
// Well-Architected: Reliability — UpdateReplacePolicy for stateful resources
|
|
389
|
+
const cfnBucket = bucket.node.defaultChild;
|
|
390
|
+
if (this.dataSafetyConfig.removalPolicy === 'retain') {
|
|
391
|
+
cfnBucket.cfnOptions.updateReplacePolicy = cdk.CfnDeletionPolicy.RETAIN;
|
|
392
|
+
}
|
|
393
|
+
new cdk.CfnOutput(this, `${logicalId}-bucket-name`, {
|
|
178
394
|
value: bucket.bucketName,
|
|
179
395
|
description: `S3 bucket name for ${intent.id}`,
|
|
180
396
|
});
|
|
181
|
-
new cdk.CfnOutput(this, `${
|
|
397
|
+
new cdk.CfnOutput(this, `${logicalId}-bucket-arn`, {
|
|
182
398
|
value: bucket.bucketArn,
|
|
183
399
|
description: `S3 bucket ARN for ${intent.id}`,
|
|
184
400
|
});
|
|
185
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Well-Architected: Security — Cognito with advanced security features,
|
|
404
|
+
* stable logical ID for safe preset upgrades.
|
|
405
|
+
* Well-Architected: Reliability — dataSafety-driven removal policy
|
|
406
|
+
*/
|
|
186
407
|
createAuth(intent, projectName, stage) {
|
|
187
|
-
|
|
408
|
+
// Stable logical ID: 'auth-{intent.id}'
|
|
409
|
+
const logicalId = `auth-${intent.id}`;
|
|
188
410
|
const passwordPolicy = intent.passwordStrength === 'strong'
|
|
189
411
|
? {
|
|
190
412
|
minLength: 12,
|
|
@@ -200,9 +422,9 @@ class VentureStack extends cdk.Stack {
|
|
|
200
422
|
requireDigits: true,
|
|
201
423
|
requireSymbols: false,
|
|
202
424
|
};
|
|
203
|
-
const userPool = new cognito.UserPool(this,
|
|
204
|
-
userPoolName: `${
|
|
205
|
-
selfSignUpEnabled: true,
|
|
425
|
+
const userPool = new cognito.UserPool(this, logicalId, {
|
|
426
|
+
userPoolName: `${projectName}-${intent.id}-${stage}`,
|
|
427
|
+
selfSignUpEnabled: intent.allowSignUp ?? true,
|
|
206
428
|
signInAliases: {
|
|
207
429
|
email: intent.signInWith?.includes('email') ?? true,
|
|
208
430
|
username: intent.signInWith?.includes('username') ?? false,
|
|
@@ -213,8 +435,20 @@ class VentureStack extends cdk.Stack {
|
|
|
213
435
|
: intent.mfa === 'optional'
|
|
214
436
|
? cognito.Mfa.OPTIONAL
|
|
215
437
|
: cognito.Mfa.OFF,
|
|
216
|
-
|
|
438
|
+
// Well-Architected: Reliability — dataSafety removal policy
|
|
439
|
+
removalPolicy: this.toRemovalPolicy(),
|
|
440
|
+
// Well-Architected: Security — advanced security mode for medium+ presets
|
|
441
|
+
advancedSecurityMode: this.envConfig.lambda.memoryMb >= 512
|
|
442
|
+
? cognito.AdvancedSecurityMode.ENFORCED
|
|
443
|
+
: undefined,
|
|
444
|
+
// Well-Architected: Security — account recovery via verified email
|
|
445
|
+
accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
|
|
217
446
|
});
|
|
447
|
+
// Well-Architected: Reliability — prevent accidental replacement
|
|
448
|
+
const cfnPool = userPool.node.defaultChild;
|
|
449
|
+
if (this.dataSafetyConfig.removalPolicy === 'retain') {
|
|
450
|
+
cfnPool.cfnOptions.updateReplacePolicy = cdk.CfnDeletionPolicy.RETAIN;
|
|
451
|
+
}
|
|
218
452
|
// Create clients
|
|
219
453
|
const clients = intent.clients && intent.clients.length > 0
|
|
220
454
|
? intent.clients
|
|
@@ -223,79 +457,619 @@ class VentureStack extends cdk.Stack {
|
|
|
223
457
|
for (const client of clients) {
|
|
224
458
|
const appClient = userPool.addClient(client.name, {
|
|
225
459
|
authFlows: { userPassword: true, userSrp: true },
|
|
460
|
+
// Well-Architected: Security — prevent user existence errors
|
|
461
|
+
preventUserExistenceErrors: true,
|
|
226
462
|
});
|
|
227
463
|
if (!primaryClientId) {
|
|
228
464
|
primaryClientId = appClient.userPoolClientId;
|
|
229
465
|
}
|
|
230
466
|
}
|
|
231
|
-
new cdk.CfnOutput(this, `${
|
|
467
|
+
new cdk.CfnOutput(this, `${logicalId}-user-pool-id`, {
|
|
232
468
|
value: userPool.userPoolId,
|
|
233
469
|
description: `Cognito User Pool ID for ${intent.id}`,
|
|
234
470
|
});
|
|
235
|
-
new cdk.CfnOutput(this, `${
|
|
471
|
+
new cdk.CfnOutput(this, `${logicalId}-client-id`, {
|
|
236
472
|
value: primaryClientId,
|
|
237
473
|
description: `Cognito Client ID for ${intent.id}`,
|
|
238
474
|
});
|
|
239
475
|
}
|
|
476
|
+
/**
|
|
477
|
+
* Well-Architected: Reliability — DLQ by default, stable logical ID
|
|
478
|
+
* Well-Architected: Security — encryption at rest with SQS managed keys
|
|
479
|
+
* Well-Architected: Performance — visibility timeout derived from handler timeout
|
|
480
|
+
*/
|
|
240
481
|
createQueue(intent, projectName, stage) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
482
|
+
// Stable logical ID: 'queue-{intent.id}'
|
|
483
|
+
const logicalId = `queue-${intent.id}`;
|
|
484
|
+
// Well-Architected: Reliability — always create a DLQ for message safety
|
|
485
|
+
const dlq = new sqs.Queue(this, `${logicalId}-dlq`, {
|
|
486
|
+
queueName: `${projectName}-${intent.id}-dlq-${stage}${intent.type === 'fifo' ? '.fifo' : ''}`,
|
|
487
|
+
fifo: intent.type === 'fifo',
|
|
488
|
+
retentionPeriod: cdk.Duration.days(14),
|
|
489
|
+
// Well-Architected: Security — encryption at rest
|
|
490
|
+
encryption: sqs.QueueEncryption.SQS_MANAGED,
|
|
491
|
+
});
|
|
492
|
+
const queue = new sqs.Queue(this, logicalId, {
|
|
493
|
+
queueName: `${projectName}-${intent.id}-${stage}${intent.type === 'fifo' ? '.fifo' : ''}`,
|
|
252
494
|
fifo: intent.type === 'fifo',
|
|
253
495
|
visibilityTimeout: intent.visibilityTimeoutSeconds
|
|
254
496
|
? cdk.Duration.seconds(intent.visibilityTimeoutSeconds)
|
|
255
|
-
:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
497
|
+
: cdk.Duration.seconds((intent.timeout ?? 30) * 6),
|
|
498
|
+
retentionPeriod: cdk.Duration.days(intent.retentionDays ?? 4),
|
|
499
|
+
// Well-Architected: Security — encryption at rest
|
|
500
|
+
encryption: sqs.QueueEncryption.SQS_MANAGED,
|
|
501
|
+
// Well-Architected: Reliability — DLQ captures failed messages
|
|
502
|
+
deadLetterQueue: {
|
|
503
|
+
queue: dlq,
|
|
504
|
+
maxReceiveCount: intent.maxReceiveCount ?? 3,
|
|
505
|
+
},
|
|
259
506
|
});
|
|
260
|
-
new cdk.CfnOutput(this, `${
|
|
507
|
+
new cdk.CfnOutput(this, `${logicalId}-queue-url`, {
|
|
261
508
|
value: queue.queueUrl,
|
|
262
509
|
description: `SQS Queue URL for ${intent.id}`,
|
|
263
510
|
});
|
|
511
|
+
new cdk.CfnOutput(this, `${logicalId}-dlq-url`, {
|
|
512
|
+
value: dlq.queueUrl,
|
|
513
|
+
description: `SQS DLQ URL for ${intent.id}`,
|
|
514
|
+
});
|
|
264
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Well-Architected: Security — VPC-only access, SG least-privilege
|
|
518
|
+
* Well-Architected: Performance — cache node type driven by intent.size
|
|
519
|
+
* Well-Architected: Reliability — encrypted in transit, snapshot retention for prod
|
|
520
|
+
*/
|
|
265
521
|
createCache(intent, projectName, stage, vpc) {
|
|
266
|
-
|
|
267
|
-
const
|
|
522
|
+
// Stable logical ID: 'cache-{intent.id}'
|
|
523
|
+
const logicalId = `cache-${intent.id}`;
|
|
524
|
+
const securityGroup = new ec2.SecurityGroup(this, `${logicalId}-sg`, {
|
|
268
525
|
vpc,
|
|
269
|
-
description: `Security group for ${
|
|
270
|
-
|
|
526
|
+
description: `Security group for ${projectName}-${intent.id} Redis`,
|
|
527
|
+
// Well-Architected: Security — restrict outbound to VPC only
|
|
528
|
+
allowAllOutbound: false,
|
|
271
529
|
});
|
|
272
530
|
securityGroup.addIngressRule(ec2.Peer.ipv4(vpc.vpcCidrBlock), ec2.Port.tcp(6379), 'Allow Redis access from VPC');
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
531
|
+
// Well-Architected: Security — use isolated subnets when available
|
|
532
|
+
const subnetIds = vpc.isolatedSubnets?.length
|
|
533
|
+
? vpc.isolatedSubnets.map((s) => s.subnetId)
|
|
534
|
+
: vpc.privateSubnets.map((s) => s.subnetId);
|
|
535
|
+
const subnetGroup = new elasticache.CfnSubnetGroup(this, `${logicalId}-subnet-group`, {
|
|
536
|
+
description: `Subnet group for ${projectName}-${intent.id}`,
|
|
537
|
+
subnetIds,
|
|
276
538
|
});
|
|
277
|
-
const
|
|
539
|
+
const cacheNodeType = this.sizeToCacheNodeType(intent.size || 'small');
|
|
540
|
+
const redis = new elasticache.CfnCacheCluster(this, logicalId, {
|
|
278
541
|
engine: 'redis',
|
|
279
|
-
cacheNodeType
|
|
542
|
+
cacheNodeType,
|
|
280
543
|
numCacheNodes: 1,
|
|
281
544
|
vpcSecurityGroupIds: [securityGroup.securityGroupId],
|
|
282
545
|
cacheSubnetGroupName: subnetGroup.ref,
|
|
546
|
+
// Well-Architected: Security — encrypt in transit
|
|
547
|
+
transitEncryptionEnabled: true,
|
|
548
|
+
// Well-Architected: Reliability — snapshots for prod
|
|
549
|
+
snapshotRetentionLimit: this.envConfig.dataSafety === 'strict' ? 7 : 0,
|
|
550
|
+
// Well-Architected: Operational Excellence — auto minor version upgrade
|
|
551
|
+
autoMinorVersionUpgrade: true,
|
|
283
552
|
});
|
|
284
|
-
new cdk.CfnOutput(this, `${
|
|
553
|
+
new cdk.CfnOutput(this, `${logicalId}-endpoint`, {
|
|
285
554
|
value: redis.attrRedisEndpointAddress,
|
|
286
555
|
description: `Redis endpoint for ${intent.id}`,
|
|
287
556
|
});
|
|
288
557
|
}
|
|
558
|
+
createMonitoring(intent, projectName, stage) {
|
|
559
|
+
const resourceName = `${projectName}-${intent.id}`;
|
|
560
|
+
// SNS topic for notifications
|
|
561
|
+
let topic;
|
|
562
|
+
if (intent.notificationArn) {
|
|
563
|
+
topic = sns.Topic.fromTopicArn(this, `${resourceName}-topic`, intent.notificationArn);
|
|
564
|
+
}
|
|
565
|
+
// Create alarms
|
|
566
|
+
if (intent.alarms) {
|
|
567
|
+
for (const alarm of intent.alarms) {
|
|
568
|
+
const comparisonMap = {
|
|
569
|
+
gt: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
570
|
+
gte: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
|
|
571
|
+
lt: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
|
|
572
|
+
lte: cloudwatch.ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD,
|
|
573
|
+
};
|
|
574
|
+
const metric = new cloudwatch.Metric({
|
|
575
|
+
namespace: alarm.namespace || 'AWS/Lambda',
|
|
576
|
+
metricName: alarm.metric,
|
|
577
|
+
period: cdk.Duration.seconds(alarm.periodSeconds ?? 300),
|
|
578
|
+
statistic: alarm.statistic || 'Average',
|
|
579
|
+
dimensionsMap: alarm.dimensions,
|
|
580
|
+
});
|
|
581
|
+
const cwAlarm = new cloudwatch.Alarm(this, `${resourceName}-${alarm.name}`, {
|
|
582
|
+
alarmName: `${projectName}-${stage}-${alarm.name}`,
|
|
583
|
+
alarmDescription: alarm.description,
|
|
584
|
+
metric,
|
|
585
|
+
threshold: alarm.threshold,
|
|
586
|
+
evaluationPeriods: alarm.evaluationPeriods ?? 1,
|
|
587
|
+
comparisonOperator: comparisonMap[alarm.comparison] || cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
588
|
+
});
|
|
589
|
+
if (topic) {
|
|
590
|
+
cwAlarm.addAlarmAction(new cloudwatchActions.SnsAction(topic));
|
|
591
|
+
}
|
|
592
|
+
new cdk.CfnOutput(this, `${resourceName}-${alarm.name}-arn`, {
|
|
593
|
+
value: cwAlarm.alarmArn,
|
|
594
|
+
description: `Alarm ARN for ${alarm.name}`,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// Create dashboard
|
|
599
|
+
if (intent.dashboard) {
|
|
600
|
+
const dashboard = new cloudwatch.Dashboard(this, `${resourceName}-dashboard`, {
|
|
601
|
+
dashboardName: `${projectName}-${stage}-${intent.id}`,
|
|
602
|
+
});
|
|
603
|
+
new cdk.CfnOutput(this, `${resourceName}-dashboard-name`, {
|
|
604
|
+
value: dashboard.dashboardName,
|
|
605
|
+
description: `Dashboard name for ${intent.id}`,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
createSecret(intent, projectName, stage) {
|
|
610
|
+
const resourceName = `${projectName}-${intent.id}`;
|
|
611
|
+
if (intent.store === 'secretsmanager') {
|
|
612
|
+
const secret = new secretsmanager.Secret(this, resourceName, {
|
|
613
|
+
secretName: `${projectName}/${stage}/${intent.id}`,
|
|
614
|
+
description: intent.description || `Secret for ${intent.id}`,
|
|
615
|
+
});
|
|
616
|
+
new cdk.CfnOutput(this, `${resourceName}-secret-arn`, {
|
|
617
|
+
value: secret.secretArn,
|
|
618
|
+
description: `Secret ARN for ${intent.id}`,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
// SSM Parameter Store
|
|
623
|
+
const param = new ssm.StringParameter(this, resourceName, {
|
|
624
|
+
parameterName: `/${projectName}/${stage}/${intent.id}`,
|
|
625
|
+
description: intent.description || `Parameter for ${intent.id}`,
|
|
626
|
+
stringValue: 'PLACEHOLDER — set value after deploy',
|
|
627
|
+
tier: ssm.ParameterTier.STANDARD,
|
|
628
|
+
});
|
|
629
|
+
new cdk.CfnOutput(this, `${resourceName}-param-name`, {
|
|
630
|
+
value: param.parameterName,
|
|
631
|
+
description: `SSM Parameter name for ${intent.id}`,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
createDomain(intent, projectName, stage, api) {
|
|
636
|
+
const resourceName = `${projectName}-${intent.id}`;
|
|
637
|
+
// Certificate: use provided ARN or create a new one
|
|
638
|
+
let certificate;
|
|
639
|
+
if (intent.certificateArn) {
|
|
640
|
+
certificate = acm.Certificate.fromCertificateArn(this, `${resourceName}-cert`, intent.certificateArn);
|
|
641
|
+
}
|
|
642
|
+
else if (intent.hostedZoneId) {
|
|
643
|
+
// Auto-provision with DNS validation via Route53
|
|
644
|
+
const hostedZone = route53.HostedZone.fromHostedZoneAttributes(this, `${resourceName}-zone`, {
|
|
645
|
+
hostedZoneId: intent.hostedZoneId,
|
|
646
|
+
zoneName: intent.domain.split('.').slice(-2).join('.'),
|
|
647
|
+
});
|
|
648
|
+
certificate = new acm.Certificate(this, `${resourceName}-cert`, {
|
|
649
|
+
domainName: intent.domain,
|
|
650
|
+
validation: acm.CertificateValidation.fromDns(hostedZone),
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
// Certificate without DNS validation (manual)
|
|
655
|
+
certificate = new acm.Certificate(this, `${resourceName}-cert`, {
|
|
656
|
+
domainName: intent.domain,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
// Custom domain name for API Gateway
|
|
660
|
+
const domainName = new apigatewayv2.DomainName(this, `${resourceName}-domain`, {
|
|
661
|
+
domainName: intent.domain,
|
|
662
|
+
certificate,
|
|
663
|
+
});
|
|
664
|
+
// API mapping
|
|
665
|
+
new apigatewayv2.ApiMapping(this, `${resourceName}-mapping`, {
|
|
666
|
+
api,
|
|
667
|
+
domainName,
|
|
668
|
+
stage: api.defaultStage,
|
|
669
|
+
});
|
|
670
|
+
// Route53 A record if hosted zone provided
|
|
671
|
+
if (intent.hostedZoneId) {
|
|
672
|
+
const hostedZone = route53.HostedZone.fromHostedZoneAttributes(this, `${resourceName}-zone-record`, {
|
|
673
|
+
hostedZoneId: intent.hostedZoneId,
|
|
674
|
+
zoneName: intent.domain.split('.').slice(-2).join('.'),
|
|
675
|
+
});
|
|
676
|
+
new route53.ARecord(this, `${resourceName}-a-record`, {
|
|
677
|
+
zone: hostedZone,
|
|
678
|
+
recordName: intent.domain,
|
|
679
|
+
target: route53.RecordTarget.fromAlias(new route53Targets.ApiGatewayv2DomainProperties(domainName.regionalDomainName, domainName.regionalHostedZoneId)),
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
new cdk.CfnOutput(this, `${resourceName}-domain-name`, {
|
|
683
|
+
value: intent.domain,
|
|
684
|
+
description: `Custom domain for ${intent.id}`,
|
|
685
|
+
});
|
|
686
|
+
new cdk.CfnOutput(this, `${resourceName}-regional-domain`, {
|
|
687
|
+
value: domainName.regionalDomainName,
|
|
688
|
+
description: `Regional domain name for ${intent.id} (use for DNS CNAME if not using Route53)`,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Free-tier alternative to RDS: create a DynamoDB table.
|
|
693
|
+
* DynamoDB offers 25 GB + 25 RCU/WCU in the always-free tier.
|
|
694
|
+
*
|
|
695
|
+
* Well-Architected: Security — encryption at rest (default)
|
|
696
|
+
* Well-Architected: Reliability — PITR for strict dataSafety
|
|
697
|
+
* Well-Architected: Cost — PAY_PER_REQUEST for unpredictable workloads
|
|
698
|
+
*/
|
|
699
|
+
createDynamoDBTable(intent, projectName, stage) {
|
|
700
|
+
// Stable logical ID: 'dynamo-{intent.id}'
|
|
701
|
+
const logicalId = `dynamo-${intent.id}`;
|
|
702
|
+
const table = new dynamodb.Table(this, logicalId, {
|
|
703
|
+
tableName: `${projectName}-${intent.id}-${stage}`,
|
|
704
|
+
partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
|
|
705
|
+
sortKey: { name: 'sk', type: dynamodb.AttributeType.STRING },
|
|
706
|
+
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
|
|
707
|
+
removalPolicy: this.toRemovalPolicy(),
|
|
708
|
+
// Well-Architected: Reliability — PITR for strict data safety
|
|
709
|
+
pointInTimeRecovery: this.envConfig.dataSafety === 'strict',
|
|
710
|
+
// Well-Architected: Security — encryption at rest (AWS managed key)
|
|
711
|
+
encryption: dynamodb.TableEncryption.AWS_MANAGED,
|
|
712
|
+
timeToLiveAttribute: 'ttl',
|
|
713
|
+
// Well-Architected: Operational Excellence — contributor insights for medium+
|
|
714
|
+
contributorInsightsEnabled: this.envConfig.lambda.memoryMb >= 512,
|
|
715
|
+
});
|
|
716
|
+
// Well-Architected: Reliability — prevent accidental replacement
|
|
717
|
+
const cfnTable = table.node.defaultChild;
|
|
718
|
+
if (this.dataSafetyConfig.removalPolicy === 'retain') {
|
|
719
|
+
cfnTable.cfnOptions.updateReplacePolicy = cdk.CfnDeletionPolicy.RETAIN;
|
|
720
|
+
}
|
|
721
|
+
// GSI for common access patterns
|
|
722
|
+
table.addGlobalSecondaryIndex({
|
|
723
|
+
indexName: 'gsi1',
|
|
724
|
+
partitionKey: { name: 'gsi1pk', type: dynamodb.AttributeType.STRING },
|
|
725
|
+
sortKey: { name: 'gsi1sk', type: dynamodb.AttributeType.STRING },
|
|
726
|
+
projectionType: dynamodb.ProjectionType.ALL,
|
|
727
|
+
});
|
|
728
|
+
new cdk.CfnOutput(this, `${logicalId}-table-name`, {
|
|
729
|
+
value: table.tableName,
|
|
730
|
+
description: `DynamoDB table name for ${intent.id} (free-tier)`,
|
|
731
|
+
});
|
|
732
|
+
new cdk.CfnOutput(this, `${logicalId}-table-arn`, {
|
|
733
|
+
value: table.tableArn,
|
|
734
|
+
description: `DynamoDB table ARN for ${intent.id} (free-tier)`,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Free-tier rate limit table using DynamoDB (always-free tier).
|
|
739
|
+
* Used by createDefaultRateLimiter() from @venturekit/runtime.
|
|
740
|
+
*/
|
|
741
|
+
createRateLimitTable(projectName, stage) {
|
|
742
|
+
const logicalId = 'RateLimitTable';
|
|
743
|
+
const table = new dynamodb.Table(this, logicalId, {
|
|
744
|
+
tableName: `venturekit-rate-limits`,
|
|
745
|
+
partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
|
|
746
|
+
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
|
|
747
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
748
|
+
timeToLiveAttribute: 'ttl',
|
|
749
|
+
encryption: dynamodb.TableEncryption.AWS_MANAGED,
|
|
750
|
+
});
|
|
751
|
+
new cdk.CfnOutput(this, `${logicalId}-table-name`, {
|
|
752
|
+
value: table.tableName,
|
|
753
|
+
description: 'DynamoDB rate limit table (free-tier)',
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
// ─── Helper methods ────────────────────────────────────────────────
|
|
757
|
+
/**
|
|
758
|
+
* Map ResourceSize to RDS instance type.
|
|
759
|
+
* Uses Graviton (T4G) instances for cost efficiency.
|
|
760
|
+
* Changing size triggers an in-place modify, NOT a replacement.
|
|
761
|
+
*/
|
|
289
762
|
sizeToInstanceType(size) {
|
|
290
763
|
switch (size) {
|
|
764
|
+
case 'xlarge':
|
|
765
|
+
return ec2.InstanceType.of(ec2.InstanceClass.R6G, ec2.InstanceSize.LARGE);
|
|
766
|
+
case 'large':
|
|
767
|
+
return ec2.InstanceType.of(ec2.InstanceClass.R6G, ec2.InstanceSize.MEDIUM);
|
|
291
768
|
case 'medium':
|
|
292
769
|
return ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.SMALL);
|
|
293
|
-
case 'large':
|
|
294
|
-
return ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.MEDIUM);
|
|
295
770
|
default: // 'small'
|
|
296
771
|
return ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.MICRO);
|
|
297
772
|
}
|
|
298
773
|
}
|
|
774
|
+
/**
|
|
775
|
+
* Map ResourceSize to ElastiCache node type.
|
|
776
|
+
* Uses Graviton (T4G) instances for cost efficiency.
|
|
777
|
+
* Changing size triggers an in-place modify, NOT a replacement.
|
|
778
|
+
*/
|
|
779
|
+
sizeToCacheNodeType(size) {
|
|
780
|
+
switch (size) {
|
|
781
|
+
case 'xlarge':
|
|
782
|
+
return 'cache.r6g.large';
|
|
783
|
+
case 'large':
|
|
784
|
+
return 'cache.r6g.medium';
|
|
785
|
+
case 'medium':
|
|
786
|
+
return 'cache.t4g.small';
|
|
787
|
+
default: // 'small'
|
|
788
|
+
return 'cache.t4g.micro';
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Convert dataSafety config to CDK RemovalPolicy.
|
|
793
|
+
* Well-Architected: Reliability — prevent accidental data loss.
|
|
794
|
+
*/
|
|
795
|
+
toRemovalPolicy() {
|
|
796
|
+
return this.dataSafetyConfig.removalPolicy === 'retain'
|
|
797
|
+
? cdk.RemovalPolicy.RETAIN
|
|
798
|
+
: cdk.RemovalPolicy.DESTROY;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Convert days to CDK logs.RetentionDays enum.
|
|
802
|
+
* Well-Architected: Cost — don't over-retain logs.
|
|
803
|
+
*/
|
|
804
|
+
toLogRetention(days) {
|
|
805
|
+
if (days <= 1)
|
|
806
|
+
return logs.RetentionDays.ONE_DAY;
|
|
807
|
+
if (days <= 3)
|
|
808
|
+
return logs.RetentionDays.THREE_DAYS;
|
|
809
|
+
if (days <= 5)
|
|
810
|
+
return logs.RetentionDays.FIVE_DAYS;
|
|
811
|
+
if (days <= 7)
|
|
812
|
+
return logs.RetentionDays.ONE_WEEK;
|
|
813
|
+
if (days <= 14)
|
|
814
|
+
return logs.RetentionDays.TWO_WEEKS;
|
|
815
|
+
if (days <= 30)
|
|
816
|
+
return logs.RetentionDays.ONE_MONTH;
|
|
817
|
+
if (days <= 60)
|
|
818
|
+
return logs.RetentionDays.TWO_MONTHS;
|
|
819
|
+
if (days <= 90)
|
|
820
|
+
return logs.RetentionDays.THREE_MONTHS;
|
|
821
|
+
if (days <= 120)
|
|
822
|
+
return logs.RetentionDays.FOUR_MONTHS;
|
|
823
|
+
if (days <= 150)
|
|
824
|
+
return logs.RetentionDays.FIVE_MONTHS;
|
|
825
|
+
if (days <= 180)
|
|
826
|
+
return logs.RetentionDays.SIX_MONTHS;
|
|
827
|
+
if (days <= 365)
|
|
828
|
+
return logs.RetentionDays.ONE_YEAR;
|
|
829
|
+
return logs.RetentionDays.INFINITE;
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Resolve Lambda runtime from preset config.
|
|
833
|
+
*/
|
|
834
|
+
toLambdaRuntime() {
|
|
835
|
+
switch (this.envConfig.lambda.runtime) {
|
|
836
|
+
case 'nodejs22.x':
|
|
837
|
+
return lambda.Runtime.NODEJS_22_X;
|
|
838
|
+
case 'nodejs18.x':
|
|
839
|
+
return lambda.Runtime.NODEJS_18_X;
|
|
840
|
+
default:
|
|
841
|
+
return lambda.Runtime.NODEJS_20_X;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Resolve Lambda architecture from preset config.
|
|
846
|
+
*/
|
|
847
|
+
toLambdaArchitecture() {
|
|
848
|
+
return this.envConfig.lambda.architecture === 'x86_64'
|
|
849
|
+
? lambda.Architecture.X86_64
|
|
850
|
+
: lambda.Architecture.ARM_64;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Recursively discover function files under the functions directory.
|
|
854
|
+
* Returns an array of { name, handlerFile } where name is the
|
|
855
|
+
* dash-delimited path (e.g. `order/create-order.ts` → `order-create-order`).
|
|
856
|
+
*/
|
|
857
|
+
discoverFunctionFiles(dir, baseDir) {
|
|
858
|
+
const root = baseDir ?? dir;
|
|
859
|
+
const results = [];
|
|
860
|
+
if (!fs.existsSync(dir))
|
|
861
|
+
return results;
|
|
862
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
863
|
+
for (const entry of entries) {
|
|
864
|
+
const fullPath = path.join(dir, entry.name);
|
|
865
|
+
if (entry.isDirectory()) {
|
|
866
|
+
results.push(...this.discoverFunctionFiles(fullPath, root));
|
|
867
|
+
}
|
|
868
|
+
else if (/\.(ts|js)$/.test(entry.name) && !entry.name.endsWith('.d.ts')) {
|
|
869
|
+
const relativePath = path.relative(root, fullPath);
|
|
870
|
+
const name = relativePath
|
|
871
|
+
.replace(/\.(ts|js)$/, '')
|
|
872
|
+
.replace(/[\\/]/g, '-');
|
|
873
|
+
results.push({ name, handlerFile: fullPath });
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return results;
|
|
877
|
+
}
|
|
878
|
+
// ─── Lambda function factories ─────────────────────────────────────
|
|
879
|
+
/**
|
|
880
|
+
* Create a standalone Lambda function.
|
|
881
|
+
* Naming convention: {project}-{stage}-fn-{name}
|
|
882
|
+
*
|
|
883
|
+
* Well-Architected: Performance — memory/timeout from preset config
|
|
884
|
+
* Well-Architected: Sustainability — ARM64 Graviton by default
|
|
885
|
+
* Well-Architected: Operational Excellence — structured env vars, log retention from preset
|
|
886
|
+
* Well-Architected: Security — X-Ray tracing from preset config
|
|
887
|
+
*/
|
|
888
|
+
createStandaloneFunction(name, handlerFile, projectName, stage, projectDir, intent) {
|
|
889
|
+
const functionName = `${projectName}-${stage}-fn-${name}`;
|
|
890
|
+
const resourceId = `fn-${name}`;
|
|
891
|
+
const lambdaConfig = this.envConfig.lambda;
|
|
892
|
+
const fn = new lambda.Function(this, resourceId, {
|
|
893
|
+
functionName,
|
|
894
|
+
runtime: this.toLambdaRuntime(),
|
|
895
|
+
architecture: this.toLambdaArchitecture(),
|
|
896
|
+
handler: 'index.main',
|
|
897
|
+
code: lambda.Code.fromAsset(path.dirname(handlerFile), {
|
|
898
|
+
bundling: {
|
|
899
|
+
image: lambda.Runtime.NODEJS_20_X.bundlingImage,
|
|
900
|
+
command: [
|
|
901
|
+
'bash', '-c', [
|
|
902
|
+
'npm install -g esbuild',
|
|
903
|
+
`esbuild ${path.basename(handlerFile)} --bundle --platform=node --target=node20 --outfile=/asset-output/index.js --external:@aws-sdk/*`,
|
|
904
|
+
].join(' && '),
|
|
905
|
+
],
|
|
906
|
+
},
|
|
907
|
+
}),
|
|
908
|
+
// Well-Architected: Performance — memory and timeout from intent or preset
|
|
909
|
+
memorySize: intent?.memorySize ?? lambdaConfig.memoryMb,
|
|
910
|
+
timeout: cdk.Duration.seconds(intent?.timeout ?? lambdaConfig.timeoutSec),
|
|
911
|
+
description: intent?.description ?? `Standalone function: ${name}`,
|
|
912
|
+
environment: {
|
|
913
|
+
VENTURE_PROJECT_NAME: projectName,
|
|
914
|
+
VENTURE_STAGE: stage,
|
|
915
|
+
NODE_OPTIONS: '--enable-source-maps',
|
|
916
|
+
...lambdaConfig.environmentVariables,
|
|
917
|
+
},
|
|
918
|
+
// Well-Architected: Operational Excellence — log retention from preset
|
|
919
|
+
logRetention: this.toLogRetention(lambdaConfig.logRetentionDays),
|
|
920
|
+
// Well-Architected: Security — X-Ray tracing from preset
|
|
921
|
+
tracing: lambdaConfig.tracingEnabled
|
|
922
|
+
? lambda.Tracing.ACTIVE
|
|
923
|
+
: lambda.Tracing.DISABLED,
|
|
924
|
+
// Well-Architected: Performance — reserved concurrency from preset
|
|
925
|
+
reservedConcurrentExecutions: lambdaConfig.reservedConcurrency,
|
|
926
|
+
});
|
|
927
|
+
new cdk.CfnOutput(this, `${resourceId}-name`, {
|
|
928
|
+
value: fn.functionName,
|
|
929
|
+
description: `Lambda function name for ${name}`,
|
|
930
|
+
});
|
|
931
|
+
new cdk.CfnOutput(this, `${resourceId}-arn`, {
|
|
932
|
+
value: fn.functionArn,
|
|
933
|
+
description: `Lambda function ARN for ${name}`,
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Create a queue consumer Lambda function wired to an SQS queue.
|
|
938
|
+
* Naming convention: {project}-{stage}-queue-{name}
|
|
939
|
+
*
|
|
940
|
+
* Well-Architected: Reliability — DLQ on consumer queue, encrypted
|
|
941
|
+
* Well-Architected: Performance — batch size and visibility timeout
|
|
942
|
+
*/
|
|
943
|
+
createQueueConsumer(name, handlerFile, projectName, stage, projectDir, intent) {
|
|
944
|
+
const functionName = `${projectName}-${stage}-queue-${name}`;
|
|
945
|
+
const resourceId = `queue-consumer-${name}`;
|
|
946
|
+
const lambdaConfig = this.envConfig.lambda;
|
|
947
|
+
// Create the SQS queue with DLQ
|
|
948
|
+
const dlq = new sqs.Queue(this, `${resourceId}-dlq`, {
|
|
949
|
+
queueName: `${projectName}-${stage}-${name}-dlq`,
|
|
950
|
+
retentionPeriod: cdk.Duration.days(14),
|
|
951
|
+
encryption: sqs.QueueEncryption.SQS_MANAGED,
|
|
952
|
+
});
|
|
953
|
+
const queue = new sqs.Queue(this, `${resourceId}-sqs`, {
|
|
954
|
+
queueName: `${projectName}-${stage}-${name}`,
|
|
955
|
+
visibilityTimeout: cdk.Duration.seconds(intent?.visibilityTimeoutSeconds ?? (intent?.timeout ?? 30) * 6),
|
|
956
|
+
retentionPeriod: cdk.Duration.days(intent?.retentionDays ?? 4),
|
|
957
|
+
encryption: sqs.QueueEncryption.SQS_MANAGED,
|
|
958
|
+
deadLetterQueue: {
|
|
959
|
+
queue: dlq,
|
|
960
|
+
maxReceiveCount: 3,
|
|
961
|
+
},
|
|
962
|
+
});
|
|
963
|
+
const fn = new lambda.Function(this, resourceId, {
|
|
964
|
+
functionName,
|
|
965
|
+
runtime: this.toLambdaRuntime(),
|
|
966
|
+
architecture: this.toLambdaArchitecture(),
|
|
967
|
+
handler: 'index.main',
|
|
968
|
+
code: lambda.Code.fromAsset(path.dirname(handlerFile), {
|
|
969
|
+
bundling: {
|
|
970
|
+
image: lambda.Runtime.NODEJS_20_X.bundlingImage,
|
|
971
|
+
command: [
|
|
972
|
+
'bash', '-c', [
|
|
973
|
+
'npm install -g esbuild',
|
|
974
|
+
`esbuild ${path.basename(handlerFile)} --bundle --platform=node --target=node20 --outfile=/asset-output/index.js --external:@aws-sdk/*`,
|
|
975
|
+
].join(' && '),
|
|
976
|
+
],
|
|
977
|
+
},
|
|
978
|
+
}),
|
|
979
|
+
memorySize: intent?.memorySize ?? lambdaConfig.memoryMb,
|
|
980
|
+
timeout: cdk.Duration.seconds(intent?.timeout ?? lambdaConfig.timeoutSec),
|
|
981
|
+
description: intent?.description ?? `Queue consumer: ${name}`,
|
|
982
|
+
environment: {
|
|
983
|
+
VENTURE_PROJECT_NAME: projectName,
|
|
984
|
+
VENTURE_STAGE: stage,
|
|
985
|
+
NODE_OPTIONS: '--enable-source-maps',
|
|
986
|
+
...lambdaConfig.environmentVariables,
|
|
987
|
+
},
|
|
988
|
+
logRetention: this.toLogRetention(lambdaConfig.logRetentionDays),
|
|
989
|
+
tracing: lambdaConfig.tracingEnabled
|
|
990
|
+
? lambda.Tracing.ACTIVE
|
|
991
|
+
: lambda.Tracing.DISABLED,
|
|
992
|
+
});
|
|
993
|
+
// Wire Lambda to SQS queue
|
|
994
|
+
fn.addEventSource(new lambdaEventSources.SqsEventSource(queue, {
|
|
995
|
+
batchSize: intent?.batchSize ?? 10,
|
|
996
|
+
}));
|
|
997
|
+
new cdk.CfnOutput(this, `${resourceId}-fn-name`, {
|
|
998
|
+
value: fn.functionName,
|
|
999
|
+
description: `Queue consumer Lambda for ${name}`,
|
|
1000
|
+
});
|
|
1001
|
+
new cdk.CfnOutput(this, `${resourceId}-queue-url`, {
|
|
1002
|
+
value: queue.queueUrl,
|
|
1003
|
+
description: `SQS queue URL for ${name}`,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Create a cron/scheduled task Lambda function wired to an EventBridge rule.
|
|
1008
|
+
* Naming convention: {project}-{stage}-cron-{name}
|
|
1009
|
+
*
|
|
1010
|
+
* Well-Architected: Performance — memory/timeout from preset
|
|
1011
|
+
* Well-Architected: Operational Excellence — log retention, tracing from preset
|
|
1012
|
+
*/
|
|
1013
|
+
createCronHandler(name, handlerFile, projectName, stage, projectDir, intent) {
|
|
1014
|
+
const functionName = `${projectName}-${stage}-cron-${name}`;
|
|
1015
|
+
const resourceId = `cron-${name}`;
|
|
1016
|
+
const lambdaConfig = this.envConfig.lambda;
|
|
1017
|
+
const fn = new lambda.Function(this, resourceId, {
|
|
1018
|
+
functionName,
|
|
1019
|
+
runtime: this.toLambdaRuntime(),
|
|
1020
|
+
architecture: this.toLambdaArchitecture(),
|
|
1021
|
+
handler: 'index.main',
|
|
1022
|
+
code: lambda.Code.fromAsset(path.dirname(handlerFile), {
|
|
1023
|
+
bundling: {
|
|
1024
|
+
image: lambda.Runtime.NODEJS_20_X.bundlingImage,
|
|
1025
|
+
command: [
|
|
1026
|
+
'bash', '-c', [
|
|
1027
|
+
'npm install -g esbuild',
|
|
1028
|
+
`esbuild ${path.basename(handlerFile)} --bundle --platform=node --target=node20 --outfile=/asset-output/index.js --external:@aws-sdk/*`,
|
|
1029
|
+
].join(' && '),
|
|
1030
|
+
],
|
|
1031
|
+
},
|
|
1032
|
+
}),
|
|
1033
|
+
memorySize: intent?.memorySize ?? lambdaConfig.memoryMb,
|
|
1034
|
+
timeout: cdk.Duration.seconds(intent?.timeout ?? 60),
|
|
1035
|
+
description: intent?.description ?? `Cron handler: ${name}`,
|
|
1036
|
+
environment: {
|
|
1037
|
+
VENTURE_PROJECT_NAME: projectName,
|
|
1038
|
+
VENTURE_STAGE: stage,
|
|
1039
|
+
NODE_OPTIONS: '--enable-source-maps',
|
|
1040
|
+
...lambdaConfig.environmentVariables,
|
|
1041
|
+
},
|
|
1042
|
+
logRetention: this.toLogRetention(lambdaConfig.logRetentionDays),
|
|
1043
|
+
tracing: lambdaConfig.tracingEnabled
|
|
1044
|
+
? lambda.Tracing.ACTIVE
|
|
1045
|
+
: lambda.Tracing.DISABLED,
|
|
1046
|
+
});
|
|
1047
|
+
// Wire EventBridge rule if schedule expression is provided
|
|
1048
|
+
if (intent?.schedule) {
|
|
1049
|
+
const scheduleExpr = intent.schedule;
|
|
1050
|
+
const isEnabled = intent.enabled ?? true;
|
|
1051
|
+
let schedule;
|
|
1052
|
+
if ('rate' in scheduleExpr) {
|
|
1053
|
+
schedule = events.Schedule.expression(`rate(${scheduleExpr.rate})`);
|
|
1054
|
+
}
|
|
1055
|
+
else {
|
|
1056
|
+
schedule = events.Schedule.expression(`cron(${scheduleExpr.cron})`);
|
|
1057
|
+
}
|
|
1058
|
+
new events.Rule(this, `${resourceId}-rule`, {
|
|
1059
|
+
ruleName: `${projectName}-${stage}-cron-${name}`,
|
|
1060
|
+
schedule,
|
|
1061
|
+
enabled: isEnabled,
|
|
1062
|
+
targets: [new targets.LambdaFunction(fn)],
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
new cdk.CfnOutput(this, `${resourceId}-fn-name`, {
|
|
1066
|
+
value: fn.functionName,
|
|
1067
|
+
description: `Cron handler Lambda for ${name}`,
|
|
1068
|
+
});
|
|
1069
|
+
new cdk.CfnOutput(this, `${resourceId}-fn-arn`, {
|
|
1070
|
+
value: fn.functionArn,
|
|
1071
|
+
description: `Cron handler Lambda ARN for ${name}`,
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
299
1074
|
}
|
|
300
|
-
exports.VentureStack = VentureStack;
|
|
301
1075
|
//# sourceMappingURL=stack.js.map
|