@venturekit/infra 0.0.0-dev.20260701100017 → 0.0.0-dev.20260704225856
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/README.md +1 -5
- package/dist/cdk/app-stack.d.ts +88 -0
- package/dist/cdk/app-stack.d.ts.map +1 -1
- package/dist/cdk/app-stack.js +292 -0
- package/dist/cdk/app-stack.js.map +1 -1
- package/dist/cdk/data-stack.d.ts +3 -4
- package/dist/cdk/data-stack.d.ts.map +1 -1
- package/dist/cdk/data-stack.js +44 -25
- package/dist/cdk/data-stack.js.map +1 -1
- package/dist/cdk/edge-stack.d.ts +2 -0
- package/dist/cdk/edge-stack.d.ts.map +1 -1
- package/dist/cdk/edge-stack.js +123 -17
- package/dist/cdk/edge-stack.js.map +1 -1
- package/dist/cdk/identity-stack.d.ts.map +1 -1
- package/dist/cdk/identity-stack.js +16 -1
- package/dist/cdk/identity-stack.js.map +1 -1
- package/dist/cdk/messaging-stack.d.ts +15 -0
- package/dist/cdk/messaging-stack.d.ts.map +1 -1
- package/dist/cdk/messaging-stack.js +49 -0
- package/dist/cdk/messaging-stack.js.map +1 -1
- package/dist/cdk/shared/ssm-keys.d.ts +20 -10
- package/dist/cdk/shared/ssm-keys.d.ts.map +1 -1
- package/dist/cdk/shared/ssm-keys.js +13 -5
- package/dist/cdk/shared/ssm-keys.js.map +1 -1
- package/dist/venture.d.ts +10 -5
- package/dist/venture.d.ts.map +1 -1
- package/dist/venture.js +16 -7
- package/dist/venture.js.map +1 -1
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -29,13 +29,11 @@ Create a `vk.config.ts` at your project root:
|
|
|
29
29
|
```typescript
|
|
30
30
|
import { defineVenture } from '@venturekit/infra';
|
|
31
31
|
import { base } from './config/base';
|
|
32
|
-
import { security } from './config/security';
|
|
33
32
|
import { dev } from './config/dev';
|
|
34
33
|
import { prod } from './config/prod';
|
|
35
34
|
|
|
36
35
|
export default defineVenture({
|
|
37
36
|
base,
|
|
38
|
-
security,
|
|
39
37
|
envs: { dev, prod },
|
|
40
38
|
routesDir: 'src/routes',
|
|
41
39
|
});
|
|
@@ -46,7 +44,6 @@ export default defineVenture({
|
|
|
46
44
|
```typescript
|
|
47
45
|
export default defineVenture({
|
|
48
46
|
base,
|
|
49
|
-
security,
|
|
50
47
|
envs: { dev, prod },
|
|
51
48
|
routesDir: 'src/routes',
|
|
52
49
|
infrastructure: {
|
|
@@ -213,12 +210,11 @@ VPC itself is not replaced and all other resources stay put.
|
|
|
213
210
|
Creates a VentureKit application from a `VentureDefinition`:
|
|
214
211
|
|
|
215
212
|
- `base` — `BaseConfig` (project identity)
|
|
216
|
-
- `security` — `SecurityConfig` (scopes, clients)
|
|
217
213
|
- `envs` — `{ dev?, stage?, prod? }` environment configurations
|
|
218
214
|
- `routesDir` — path to file-based routes directory
|
|
219
215
|
- `infrastructure?` — `VentureIntent` for declarative infrastructure
|
|
220
216
|
|
|
221
|
-
### `getResolvedConfig(base,
|
|
217
|
+
### `getResolvedConfig(base, envInput)`
|
|
222
218
|
|
|
223
219
|
Returns the fully resolved `ResolvedConfig` for the current environment. Useful for accessing config values in handler code.
|
|
224
220
|
|
package/dist/cdk/app-stack.d.ts
CHANGED
|
@@ -126,6 +126,21 @@ export interface AppStackImports {
|
|
|
126
126
|
storageBucketNamesById?: Map<string, string>;
|
|
127
127
|
/** SQS queue ARNs per `queues` intent — for grant + ESM source. */
|
|
128
128
|
queueArnsById?: Map<string, string>;
|
|
129
|
+
/**
|
|
130
|
+
* EventBridge bus names per OWNED `events` intent. Injected into the
|
|
131
|
+
* Lambda environment as `EVENT_BUS_<ID>_NAME` (+ `VENTURE_EVENT_BUS_NAME`
|
|
132
|
+
* for the primary) so `@venturekit/runtime`'s `publishEvent()` resolves
|
|
133
|
+
* the bus without an SSM read. Subscriber-only intents (those with
|
|
134
|
+
* `external.busName`) are NOT in this map — they reference the bus by
|
|
135
|
+
* name on the rule side and never publish.
|
|
136
|
+
*/
|
|
137
|
+
eventBusNamesById?: Map<string, string>;
|
|
138
|
+
/**
|
|
139
|
+
* EventBridge bus ARNs per OWNED `events` intent — the `events:PutEvents`
|
|
140
|
+
* IAM scoping target on the shared Lambda role, and the handle the app
|
|
141
|
+
* stack re-imports to attach owned-bus subscription rules.
|
|
142
|
+
*/
|
|
143
|
+
eventBusArnsById?: Map<string, string>;
|
|
129
144
|
/** SNS notify events topic ARNs per `notify` intent. */
|
|
130
145
|
notifyEventsTopicArnsById?: Map<string, string>;
|
|
131
146
|
/** CloudFront CDN domain per storage intent that has `cdn:true`. */
|
|
@@ -389,6 +404,79 @@ export declare class VentureAppStack extends cdk.Stack {
|
|
|
389
404
|
* directly to the caller, where they belong.
|
|
390
405
|
*/
|
|
391
406
|
private createStandaloneFunction;
|
|
407
|
+
/**
|
|
408
|
+
* Materialise every `events[]` intent's subscriptions as EventBridge
|
|
409
|
+
* `Rule`s on the target bus. Each rule routes events matching its
|
|
410
|
+
* pattern (source + detail-type) to ONE in-venture target — an SQS
|
|
411
|
+
* queue (delivered as a message its `queueBatchHandler` consumer
|
|
412
|
+
* reads, at-least-once) or a Lambda (invoked async).
|
|
413
|
+
*
|
|
414
|
+
* Bus resolution:
|
|
415
|
+
* - OWNED bus → re-imported from the messaging tier by ARN
|
|
416
|
+
* (`imports.eventBusArnsById`).
|
|
417
|
+
* - `external.busName` → imported by name (`fromEventBusName`);
|
|
418
|
+
* same account + region, owner venture deployed first. This is how
|
|
419
|
+
* a vertical venture attaches a rule to the control-plane
|
|
420
|
+
* venture's bus without either calling the other.
|
|
421
|
+
*/
|
|
422
|
+
private createEventSubscriptions;
|
|
423
|
+
/**
|
|
424
|
+
* One EventBridge `Rule` on `bus` for subscription `sub`. The pattern
|
|
425
|
+
* defaults to `{ source: [busSource], detailType: sub.detailTypes }`,
|
|
426
|
+
* with `sub.sources` / `sub.pattern` overriding (the raw `pattern`
|
|
427
|
+
* escape hatch is shallow-merged LAST so it always wins). CDK's target
|
|
428
|
+
* constructs add the resource policy that lets EventBridge invoke the
|
|
429
|
+
* Lambda / send to the queue — for a cross-stack imported queue that
|
|
430
|
+
* is an `AWS::SQS::QueuePolicy` in this app stack, leaving the
|
|
431
|
+
* messaging-tier queue untouched.
|
|
432
|
+
*/
|
|
433
|
+
private createEventRule;
|
|
434
|
+
/**
|
|
435
|
+
* Render declared `monitoring` intents into CloudWatch alarms
|
|
436
|
+
* (REQUIREMENTS §7.4). Until this existed a `MonitoringIntent` in
|
|
437
|
+
* vk.config.ts was a SILENT NO-OP — the intent reached the stack but
|
|
438
|
+
* nothing materialised a `cloudwatch.Alarm`.
|
|
439
|
+
*
|
|
440
|
+
* Each `AlarmIntent` becomes one `cloudwatch.Alarm` over a
|
|
441
|
+
* `cloudwatch.Metric` built verbatim from the intent (namespace /
|
|
442
|
+
* metric / statistic / period / threshold / comparison /
|
|
443
|
+
* evaluationPeriods / dimensions). When the parent `MonitoringIntent`
|
|
444
|
+
* carries a `notificationArn`, the alarm's ALARM action publishes to
|
|
445
|
+
* that (imported) SNS topic.
|
|
446
|
+
*
|
|
447
|
+
* # Dimension ergonomics
|
|
448
|
+
*
|
|
449
|
+
* Lambda function names and EventBridge rule names are clamped /
|
|
450
|
+
* prefixed at synth (`clampLambdaFunctionName`,
|
|
451
|
+
* `${project}-${stage}-cron-${name}`), so a consumer can't predict
|
|
452
|
+
* them in vk.config.ts. To keep the §7.4 Lambda-error / cron-failure
|
|
453
|
+
* alarms declarable, two dimension keys accept a LOGICAL handler name
|
|
454
|
+
* and are resolved here to the physical name:
|
|
455
|
+
* - `FunctionName` — matched against every Lambda this stack created
|
|
456
|
+
* (route slug, queue / cron / standalone handler name, notify
|
|
457
|
+
* dispatcher/bounce id) → the CDK `functionName` token.
|
|
458
|
+
* - `RuleName` — matched against declared cron handler names →
|
|
459
|
+
* `${project}-${stage}-cron-${name}`.
|
|
460
|
+
* Any other dimension value (an explicit physical name, an RDS
|
|
461
|
+
* `DBInstanceIdentifier`, a custom-metric dimension) passes through
|
|
462
|
+
* verbatim. (RDS identifiers live in the data stack, so they're not
|
|
463
|
+
* auto-resolvable here — supply the physical id, or see the
|
|
464
|
+
* cross-stack follow-up.)
|
|
465
|
+
*/
|
|
466
|
+
private createMonitoring;
|
|
467
|
+
/**
|
|
468
|
+
* Resolve an AlarmIntent's dimensions, mapping the logical
|
|
469
|
+
* `FunctionName` / `RuleName` conventions to physical names (see
|
|
470
|
+
* `createMonitoring`). Returns a fresh object; never mutates input.
|
|
471
|
+
*/
|
|
472
|
+
private resolveAlarmDimensions;
|
|
473
|
+
/**
|
|
474
|
+
* Find a Lambda this stack created by its LOGICAL name — a route
|
|
475
|
+
* slug, a queue / cron / standalone handler name, or a notify intent
|
|
476
|
+
* id. Returns undefined when nothing matches (the caller then treats
|
|
477
|
+
* the value as an explicit physical name).
|
|
478
|
+
*/
|
|
479
|
+
private findLambdaByLogicalName;
|
|
392
480
|
/**
|
|
393
481
|
* Attach IAM grants to the shared Lambda role for every imported
|
|
394
482
|
* resource. Called once during construction, before any Lambda is
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-stack.d.ts","sourceRoot":"","sources":["../../src/cdk/app-stack.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AAKH,OAAO,KAAK,GAAG,MAAM,aAAa,CAAC;AACnC,OAAO,KAAK,GAAG,MAAM,qBAAqB,CAAC;AAC3C,OAAO,KAAK,GAAG,MAAM,qBAAqB,CAAC;AAC3C,OAAO,KAAK,MAAM,MAAM,wBAAwB,CAAC;AAMjD,OAAO,KAAK,YAAY,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"app-stack.d.ts","sourceRoot":"","sources":["../../src/cdk/app-stack.ts"],"names":[],"mappings":"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"}
|
package/dist/cdk/app-stack.js
CHANGED
|
@@ -79,6 +79,8 @@ import * as apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2';
|
|
|
79
79
|
import * as apigatewayv2Integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
|
|
80
80
|
import * as sns from 'aws-cdk-lib/aws-sns';
|
|
81
81
|
import * as snsSubscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
|
|
82
|
+
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
|
|
83
|
+
import * as cloudwatchActions from 'aws-cdk-lib/aws-cloudwatch-actions';
|
|
82
84
|
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
|
|
83
85
|
import * as logs from 'aws-cdk-lib/aws-logs';
|
|
84
86
|
import { DATA_SAFETY_CONFIG, } from '@venturekit/core';
|
|
@@ -88,6 +90,16 @@ import { bundleHandlerCodeWithEsbuild, inlineStubLambdaCode, toLambdaArchitectur
|
|
|
88
90
|
import { discoverRouteFiles, } from './shared/route-discovery.js';
|
|
89
91
|
import { discoverWsRoutes } from './shared/ws-discovery.js';
|
|
90
92
|
import { attachWebSocketApi, } from './shared/websocket-construct.js';
|
|
93
|
+
/**
|
|
94
|
+
* Provider-agnostic `AlarmIntent.comparison` → CloudWatch operator.
|
|
95
|
+
* Defined once at module scope so `createMonitoring` stays a lookup.
|
|
96
|
+
*/
|
|
97
|
+
const ALARM_COMPARISON = {
|
|
98
|
+
gt: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
99
|
+
gte: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
|
|
100
|
+
lt: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
|
|
101
|
+
lte: cloudwatch.ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD,
|
|
102
|
+
};
|
|
91
103
|
/**
|
|
92
104
|
* Convert an intent id (e.g. `user-uploads`) to upper-snake-case
|
|
93
105
|
* (`USER_UPLOADS`) suitable for use in env-var names. Without this,
|
|
@@ -284,6 +296,19 @@ export class VentureAppStack extends cdk.Stack {
|
|
|
284
296
|
// coordinated Lambda rollout anyway. RETAIN under strict isn't
|
|
285
297
|
// appropriate; on a stack delete we're tearing down every Lambda
|
|
286
298
|
// that uses the key, so retaining the key alone serves no purpose.
|
|
299
|
+
//
|
|
300
|
+
// # Rotation runbook (S5 — manual, dual-secret verify window)
|
|
301
|
+
// This key is symmetric, so it cannot use Secrets Manager's
|
|
302
|
+
// built-in (asymmetric DB-credential) rotation. Rotate WITHOUT
|
|
303
|
+
// downtime via a two-key overlap:
|
|
304
|
+
// 1. Add a SECOND secret (`…/internal-hmac-next`) and roll it to
|
|
305
|
+
// all functions as a fallback VERIFY key (sign with old, accept
|
|
306
|
+
// either) — a brief deploy where verification accepts both.
|
|
307
|
+
// 2. Flip signing to the new key; keep accepting the old one until
|
|
308
|
+
// every in-flight token (state tokens are short-lived) expires.
|
|
309
|
+
// 3. Remove the old key and the dual-verify fallback.
|
|
310
|
+
// Because issued tokens are short-lived, the overlap window can be
|
|
311
|
+
// minutes. Until tooling automates this, treat it as an ops task.
|
|
287
312
|
this.internalHmacSecret = new secretsmanager.Secret(this, 'internalHmacSecret', {
|
|
288
313
|
secretName: `venturekit/${projectName}/${stage}/internal-hmac`,
|
|
289
314
|
description: 'VentureKit internal Lambda-to-Lambda HMAC signing key. Auto-generated.',
|
|
@@ -435,6 +460,18 @@ export class VentureAppStack extends cdk.Stack {
|
|
|
435
460
|
}
|
|
436
461
|
}
|
|
437
462
|
}
|
|
463
|
+
// ─── Event-bus env injection (owned `events` intents) ──────
|
|
464
|
+
// `publishEvent()` resolves the destination bus from
|
|
465
|
+
// `EVENT_BUS_<ID>_NAME`, falling back to `VENTURE_EVENT_BUS_NAME`
|
|
466
|
+
// (the FIRST declared owned bus). Subscriber-only intents
|
|
467
|
+
// (`external.busName`) never publish and so are absent from the map.
|
|
468
|
+
if (imports.eventBusNamesById && imports.eventBusNamesById.size > 0) {
|
|
469
|
+
const [, primaryBusName] = imports.eventBusNamesById.entries().next().value;
|
|
470
|
+
this.baseEnvVars['VENTURE_EVENT_BUS_NAME'] = primaryBusName;
|
|
471
|
+
for (const [id, busName] of imports.eventBusNamesById) {
|
|
472
|
+
this.baseEnvVars[`EVENT_BUS_${upperSnake(id)}_NAME`] = busName;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
438
475
|
// ─── IAM grants on the shared Lambda role ──────────────────
|
|
439
476
|
// Every grant attached to `this.lambdaRole` benefits every
|
|
440
477
|
// Lambda in the stack (routes, queue consumers, schedules,
|
|
@@ -653,6 +690,24 @@ export class VentureAppStack extends cdk.Stack {
|
|
|
653
690
|
this.provisionNotifyHandlers(infrastructure.notify, props.projectDir, imports);
|
|
654
691
|
}
|
|
655
692
|
}
|
|
693
|
+
// ─── EventBridge subscriptions (events[] rules → targets) ──
|
|
694
|
+
// Outside the `projectDir` guard above (like monitoring below) so a
|
|
695
|
+
// queue-target subscription — whose target ARN comes from imports,
|
|
696
|
+
// not handler discovery — still renders for projects that ship no
|
|
697
|
+
// handler code. Placed AFTER the guard so `{ function }` targets
|
|
698
|
+
// resolve to Lambdas discovered above. Owner-side bus creation lives
|
|
699
|
+
// in the messaging stack; here we only attach the rules + targets.
|
|
700
|
+
if (infrastructure.events && infrastructure.events.length > 0) {
|
|
701
|
+
this.createEventSubscriptions(infrastructure.events, imports);
|
|
702
|
+
}
|
|
703
|
+
// ─── CloudWatch alarms (monitoring) ───────────────────────
|
|
704
|
+
// Outside the `projectDir` guard above so data-only projects
|
|
705
|
+
// (custom-namespace or RDS alarms) still render; placed last so
|
|
706
|
+
// the FunctionName / RuleName dimension resolution can see every
|
|
707
|
+
// Lambda + cron rule created above.
|
|
708
|
+
if (infrastructure.monitoring && infrastructure.monitoring.length > 0) {
|
|
709
|
+
this.createMonitoring(infrastructure.monitoring);
|
|
710
|
+
}
|
|
656
711
|
}
|
|
657
712
|
/**
|
|
658
713
|
* Build the Lambda code asset for a handler file. Production path:
|
|
@@ -913,6 +968,227 @@ export class VentureAppStack extends cdk.Stack {
|
|
|
913
968
|
});
|
|
914
969
|
this.functionLambdas.set(name, fn);
|
|
915
970
|
}
|
|
971
|
+
/**
|
|
972
|
+
* Materialise every `events[]` intent's subscriptions as EventBridge
|
|
973
|
+
* `Rule`s on the target bus. Each rule routes events matching its
|
|
974
|
+
* pattern (source + detail-type) to ONE in-venture target — an SQS
|
|
975
|
+
* queue (delivered as a message its `queueBatchHandler` consumer
|
|
976
|
+
* reads, at-least-once) or a Lambda (invoked async).
|
|
977
|
+
*
|
|
978
|
+
* Bus resolution:
|
|
979
|
+
* - OWNED bus → re-imported from the messaging tier by ARN
|
|
980
|
+
* (`imports.eventBusArnsById`).
|
|
981
|
+
* - `external.busName` → imported by name (`fromEventBusName`);
|
|
982
|
+
* same account + region, owner venture deployed first. This is how
|
|
983
|
+
* a vertical venture attaches a rule to the control-plane
|
|
984
|
+
* venture's bus without either calling the other.
|
|
985
|
+
*/
|
|
986
|
+
createEventSubscriptions(eventsIntents, imports) {
|
|
987
|
+
for (const intent of eventsIntents) {
|
|
988
|
+
const subs = intent.subscriptions ?? [];
|
|
989
|
+
if (subs.length === 0)
|
|
990
|
+
continue;
|
|
991
|
+
let bus;
|
|
992
|
+
if (intent.external?.busName) {
|
|
993
|
+
bus = events.EventBus.fromEventBusName(this, `events-${intent.id}-imported`, intent.external.busName);
|
|
994
|
+
}
|
|
995
|
+
else {
|
|
996
|
+
const busArn = imports.eventBusArnsById?.get(intent.id);
|
|
997
|
+
if (!busArn) {
|
|
998
|
+
throw new Error(`[venturekit] app stack: events intent '${intent.id}' has ` +
|
|
999
|
+
`subscriptions but no bus to attach them to. Either the ` +
|
|
1000
|
+
`messaging stack must create the bus (CLI passes its ARN via ` +
|
|
1001
|
+
`imports.eventBusArnsById) or the intent must set ` +
|
|
1002
|
+
`'external.busName' to attach rules to a bus owned by another ` +
|
|
1003
|
+
`venture.`);
|
|
1004
|
+
}
|
|
1005
|
+
bus = events.EventBus.fromEventBusArn(this, `events-${intent.id}-owned`, busArn);
|
|
1006
|
+
}
|
|
1007
|
+
const defaultSource = intent.source ?? this.projectName;
|
|
1008
|
+
for (const sub of subs) {
|
|
1009
|
+
this.createEventRule(intent, sub, bus, defaultSource, imports);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* One EventBridge `Rule` on `bus` for subscription `sub`. The pattern
|
|
1015
|
+
* defaults to `{ source: [busSource], detailType: sub.detailTypes }`,
|
|
1016
|
+
* with `sub.sources` / `sub.pattern` overriding (the raw `pattern`
|
|
1017
|
+
* escape hatch is shallow-merged LAST so it always wins). CDK's target
|
|
1018
|
+
* constructs add the resource policy that lets EventBridge invoke the
|
|
1019
|
+
* Lambda / send to the queue — for a cross-stack imported queue that
|
|
1020
|
+
* is an `AWS::SQS::QueuePolicy` in this app stack, leaving the
|
|
1021
|
+
* messaging-tier queue untouched.
|
|
1022
|
+
*/
|
|
1023
|
+
createEventRule(intent, sub, bus, defaultSource, imports) {
|
|
1024
|
+
const resourceId = `events-${intent.id}-${sub.id}`;
|
|
1025
|
+
const pattern = {
|
|
1026
|
+
source: sub.sources ?? [defaultSource],
|
|
1027
|
+
...(sub.detailTypes ? { detailType: sub.detailTypes } : {}),
|
|
1028
|
+
...(sub.pattern ?? {}),
|
|
1029
|
+
};
|
|
1030
|
+
let target;
|
|
1031
|
+
if (sub.target?.queue) {
|
|
1032
|
+
const queueArn = imports.queueArnsById?.get(sub.target.queue);
|
|
1033
|
+
if (!queueArn) {
|
|
1034
|
+
throw new Error(`[venturekit] app stack: events subscription '${intent.id}/${sub.id}' ` +
|
|
1035
|
+
`targets queue '${sub.target.queue}' but no such queue is declared. ` +
|
|
1036
|
+
`Add a queues[] intent with id='${sub.target.queue}'.`);
|
|
1037
|
+
}
|
|
1038
|
+
const queue = sqs.Queue.fromQueueArn(this, `${resourceId}-queue`, queueArn);
|
|
1039
|
+
// Deliver ONLY the event `detail` as the SQS body (not the full
|
|
1040
|
+
// EventBridge envelope). This matches VentureKit's queue
|
|
1041
|
+
// convention — a `queueBatchHandler` consumer's `body` is the
|
|
1042
|
+
// payload — so subscribing a queue to an event is indistinguishable
|
|
1043
|
+
// from `sendMessage(queueId, detail)`. The publisher bakes
|
|
1044
|
+
// `__trace` INTO the detail, so trace propagation survives.
|
|
1045
|
+
target = new eventsTargets.SqsQueue(queue, {
|
|
1046
|
+
message: events.RuleTargetInput.fromEventPath('$.detail'),
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
else if (sub.target?.function) {
|
|
1050
|
+
const fn = this.findLambdaByLogicalName(sub.target.function);
|
|
1051
|
+
if (!fn) {
|
|
1052
|
+
throw new Error(`[venturekit] app stack: events subscription '${intent.id}/${sub.id}' ` +
|
|
1053
|
+
`targets function '${sub.target.function}' but no such Lambda was ` +
|
|
1054
|
+
`created. Add a handler at src/functions/${sub.target.function}.ts.`);
|
|
1055
|
+
}
|
|
1056
|
+
target = new eventsTargets.LambdaFunction(fn);
|
|
1057
|
+
}
|
|
1058
|
+
else {
|
|
1059
|
+
throw new Error(`[venturekit] app stack: events subscription '${intent.id}/${sub.id}' ` +
|
|
1060
|
+
`must declare a target: { queue: '<id>' } or { function: '<id>' }.`);
|
|
1061
|
+
}
|
|
1062
|
+
new events.Rule(this, `${resourceId}-rule`, {
|
|
1063
|
+
ruleName: clampLambdaFunctionName(`${this.projectName}-${this.stage}-events-${intent.id}-${sub.id}`),
|
|
1064
|
+
eventBus: bus,
|
|
1065
|
+
eventPattern: pattern,
|
|
1066
|
+
targets: [target],
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Render declared `monitoring` intents into CloudWatch alarms
|
|
1071
|
+
* (REQUIREMENTS §7.4). Until this existed a `MonitoringIntent` in
|
|
1072
|
+
* vk.config.ts was a SILENT NO-OP — the intent reached the stack but
|
|
1073
|
+
* nothing materialised a `cloudwatch.Alarm`.
|
|
1074
|
+
*
|
|
1075
|
+
* Each `AlarmIntent` becomes one `cloudwatch.Alarm` over a
|
|
1076
|
+
* `cloudwatch.Metric` built verbatim from the intent (namespace /
|
|
1077
|
+
* metric / statistic / period / threshold / comparison /
|
|
1078
|
+
* evaluationPeriods / dimensions). When the parent `MonitoringIntent`
|
|
1079
|
+
* carries a `notificationArn`, the alarm's ALARM action publishes to
|
|
1080
|
+
* that (imported) SNS topic.
|
|
1081
|
+
*
|
|
1082
|
+
* # Dimension ergonomics
|
|
1083
|
+
*
|
|
1084
|
+
* Lambda function names and EventBridge rule names are clamped /
|
|
1085
|
+
* prefixed at synth (`clampLambdaFunctionName`,
|
|
1086
|
+
* `${project}-${stage}-cron-${name}`), so a consumer can't predict
|
|
1087
|
+
* them in vk.config.ts. To keep the §7.4 Lambda-error / cron-failure
|
|
1088
|
+
* alarms declarable, two dimension keys accept a LOGICAL handler name
|
|
1089
|
+
* and are resolved here to the physical name:
|
|
1090
|
+
* - `FunctionName` — matched against every Lambda this stack created
|
|
1091
|
+
* (route slug, queue / cron / standalone handler name, notify
|
|
1092
|
+
* dispatcher/bounce id) → the CDK `functionName` token.
|
|
1093
|
+
* - `RuleName` — matched against declared cron handler names →
|
|
1094
|
+
* `${project}-${stage}-cron-${name}`.
|
|
1095
|
+
* Any other dimension value (an explicit physical name, an RDS
|
|
1096
|
+
* `DBInstanceIdentifier`, a custom-metric dimension) passes through
|
|
1097
|
+
* verbatim. (RDS identifiers live in the data stack, so they're not
|
|
1098
|
+
* auto-resolvable here — supply the physical id, or see the
|
|
1099
|
+
* cross-stack follow-up.)
|
|
1100
|
+
*/
|
|
1101
|
+
createMonitoring(monitoring) {
|
|
1102
|
+
for (const intent of monitoring) {
|
|
1103
|
+
const alarms = intent.alarms ?? [];
|
|
1104
|
+
if (alarms.length === 0 && !intent.dashboard)
|
|
1105
|
+
continue;
|
|
1106
|
+
// Import the notification topic ONCE per intent — a per-alarm
|
|
1107
|
+
// import would collide on the construct id.
|
|
1108
|
+
const topic = intent.notificationArn
|
|
1109
|
+
? sns.Topic.fromTopicArn(this, `alarm-topic-${intent.id}`, intent.notificationArn)
|
|
1110
|
+
: undefined;
|
|
1111
|
+
const createdAlarms = [];
|
|
1112
|
+
for (const a of alarms) {
|
|
1113
|
+
const alarm = new cloudwatch.Alarm(this, `alarm-${intent.id}-${a.name}`, {
|
|
1114
|
+
alarmName: `${this.projectName}-${this.stage}-${a.name}`,
|
|
1115
|
+
alarmDescription: a.description,
|
|
1116
|
+
metric: new cloudwatch.Metric({
|
|
1117
|
+
namespace: a.namespace ?? 'AWS/Lambda',
|
|
1118
|
+
metricName: a.metric,
|
|
1119
|
+
statistic: a.statistic ?? 'Average',
|
|
1120
|
+
period: cdk.Duration.seconds(a.periodSeconds ?? 300),
|
|
1121
|
+
dimensionsMap: this.resolveAlarmDimensions(a.dimensions),
|
|
1122
|
+
}),
|
|
1123
|
+
threshold: a.threshold,
|
|
1124
|
+
comparisonOperator: ALARM_COMPARISON[a.comparison] ??
|
|
1125
|
+
cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
1126
|
+
evaluationPeriods: a.evaluationPeriods ?? 1,
|
|
1127
|
+
// Failure/error alarms stay green when the metric simply
|
|
1128
|
+
// hasn't reported (no invocations = no errors).
|
|
1129
|
+
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
|
|
1130
|
+
});
|
|
1131
|
+
if (topic) {
|
|
1132
|
+
alarm.addAlarmAction(new cloudwatchActions.SnsAction(topic));
|
|
1133
|
+
}
|
|
1134
|
+
createdAlarms.push(alarm);
|
|
1135
|
+
}
|
|
1136
|
+
if (intent.dashboard) {
|
|
1137
|
+
const dashboard = new cloudwatch.Dashboard(this, `dashboard-${intent.id}`, {
|
|
1138
|
+
dashboardName: `${this.projectName}-${this.stage}-${intent.id}`,
|
|
1139
|
+
});
|
|
1140
|
+
if (createdAlarms.length > 0) {
|
|
1141
|
+
dashboard.addWidgets(new cloudwatch.AlarmStatusWidget({
|
|
1142
|
+
title: `${intent.id} alarms`,
|
|
1143
|
+
alarms: createdAlarms,
|
|
1144
|
+
width: 24,
|
|
1145
|
+
}));
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Resolve an AlarmIntent's dimensions, mapping the logical
|
|
1152
|
+
* `FunctionName` / `RuleName` conventions to physical names (see
|
|
1153
|
+
* `createMonitoring`). Returns a fresh object; never mutates input.
|
|
1154
|
+
*/
|
|
1155
|
+
resolveAlarmDimensions(dimensions) {
|
|
1156
|
+
if (!dimensions)
|
|
1157
|
+
return {};
|
|
1158
|
+
const resolved = {};
|
|
1159
|
+
for (const [key, value] of Object.entries(dimensions)) {
|
|
1160
|
+
if (key === 'FunctionName') {
|
|
1161
|
+
const fn = this.findLambdaByLogicalName(value);
|
|
1162
|
+
// `fn.functionName` renders as an in-stack CFN `Ref`, but a
|
|
1163
|
+
// CloudWatch dimension needs the LITERAL name the metric is
|
|
1164
|
+
// published under. Every Lambda is built with an explicit
|
|
1165
|
+
// physical name (see `buildLambda`), so read it off the L1.
|
|
1166
|
+
const cfnFn = fn?.node.defaultChild;
|
|
1167
|
+
resolved[key] = cfnFn?.functionName ?? value;
|
|
1168
|
+
}
|
|
1169
|
+
else if (key === 'RuleName' && this.scheduleLambdas.has(value)) {
|
|
1170
|
+
resolved[key] = `${this.projectName}-${this.stage}-cron-${value}`;
|
|
1171
|
+
}
|
|
1172
|
+
else {
|
|
1173
|
+
resolved[key] = value;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return resolved;
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Find a Lambda this stack created by its LOGICAL name — a route
|
|
1180
|
+
* slug, a queue / cron / standalone handler name, or a notify intent
|
|
1181
|
+
* id. Returns undefined when nothing matches (the caller then treats
|
|
1182
|
+
* the value as an explicit physical name).
|
|
1183
|
+
*/
|
|
1184
|
+
findLambdaByLogicalName(name) {
|
|
1185
|
+
return (this.routeLambdas.get(name) ??
|
|
1186
|
+
this.scheduleLambdas.get(name) ??
|
|
1187
|
+
this.queueLambdas.get(name) ??
|
|
1188
|
+
this.functionLambdas.get(name) ??
|
|
1189
|
+
this.notifyDispatcherLambdas.get(name) ??
|
|
1190
|
+
this.notifyBounceLambdas.get(name));
|
|
1191
|
+
}
|
|
916
1192
|
/**
|
|
917
1193
|
* Attach IAM grants to the shared Lambda role for every imported
|
|
918
1194
|
* resource. Called once during construction, before any Lambda is
|
|
@@ -1020,6 +1296,22 @@ export class VentureAppStack extends cdk.Stack {
|
|
|
1020
1296
|
}));
|
|
1021
1297
|
}
|
|
1022
1298
|
}
|
|
1299
|
+
// ── EventBridge PutEvents on OWNED buses ─────────────────
|
|
1300
|
+
// Scoped to the ARNs of the buses this venture owns, so a handler
|
|
1301
|
+
// can `publishEvent()` to them but not to arbitrary buses. Pure
|
|
1302
|
+
// subscribers (external.busName) own no bus and get no grant — they
|
|
1303
|
+
// only RECEIVE via rules, which EventBridge delivers under its own
|
|
1304
|
+
// service principal, not the Lambda role.
|
|
1305
|
+
if (imports.eventBusArnsById) {
|
|
1306
|
+
const busArns = [...imports.eventBusArnsById.values()];
|
|
1307
|
+
if (busArns.length > 0) {
|
|
1308
|
+
this.lambdaRole.addToPrincipalPolicy(new iam.PolicyStatement({
|
|
1309
|
+
effect: iam.Effect.ALLOW,
|
|
1310
|
+
actions: ['events:PutEvents'],
|
|
1311
|
+
resources: busArns,
|
|
1312
|
+
}));
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1023
1315
|
}
|
|
1024
1316
|
/**
|
|
1025
1317
|
* Provision dispatcher + bounce-handler Lambdas per declared
|