@aws-blocks/core 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/cdk/blocks-backend.d.ts +4 -1
- package/dist/cdk/blocks-backend.d.ts.map +1 -1
- package/dist/cdk/blocks-backend.js +56 -10
- package/dist/cdk/blocks-backend.test.js +71 -1
- package/dist/cdk/blocks-defaults.d.ts +11 -0
- package/dist/cdk/blocks-defaults.d.ts.map +1 -1
- package/dist/cdk/blocks-stack.test.js +23 -3
- package/dist/cdk/compute/compute.d.ts +80 -2
- package/dist/cdk/compute/compute.d.ts.map +1 -1
- package/dist/cdk/compute/compute.js +57 -3
- package/dist/cdk/config-registry.test.js +12 -0
- package/dist/cdk/dashboard-registry.d.ts +41 -0
- package/dist/cdk/dashboard-registry.d.ts.map +1 -0
- package/dist/cdk/dashboard-registry.js +61 -0
- package/dist/cdk/index.d.ts +67 -11
- package/dist/cdk/index.d.ts.map +1 -1
- package/dist/cdk/index.js +100 -18
- package/dist/cdk/internal.d.ts +3 -1
- package/dist/cdk/internal.d.ts.map +1 -1
- package/dist/cdk/internal.js +4 -1
- package/dist/cdk/tracer-registry.d.ts +31 -0
- package/dist/cdk/tracer-registry.d.ts.map +1 -0
- package/dist/cdk/tracer-registry.js +49 -0
- package/dist/cdk/vpc-requirements-registry.d.ts +33 -0
- package/dist/cdk/vpc-requirements-registry.d.ts.map +1 -0
- package/dist/cdk/vpc-requirements-registry.js +46 -0
- package/dist/cdk/vpc-types.d.ts +151 -0
- package/dist/cdk/vpc-types.d.ts.map +1 -0
- package/dist/cdk/vpc-types.js +3 -0
- package/dist/cdk/vpc.d.ts +59 -0
- package/dist/cdk/vpc.d.ts.map +1 -0
- package/dist/cdk/vpc.js +298 -0
- package/dist/cdk/vpc.test.d.ts +2 -0
- package/dist/cdk/vpc.test.d.ts.map +1 -0
- package/dist/cdk/vpc.test.js +285 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +5 -0
- package/dist/hosting.d.ts.map +1 -1
- package/dist/hosting.js +2 -0
- package/dist/hosting.test.js +39 -1
- package/dist/index.cdk.d.ts +2 -1
- package/dist/index.cdk.d.ts.map +1 -1
- package/dist/index.cdk.js +1 -1
- package/dist/lambda-handler.js +9 -2
- package/dist/lambda-handler.test.js +61 -1
- package/dist/raw-route.d.ts +15 -1
- package/dist/raw-route.d.ts.map +1 -1
- package/dist/raw-route.js +96 -12
- package/dist/raw-route.test.js +332 -1
- package/dist/scripts/dev-server.d.ts.map +1 -1
- package/dist/scripts/dev-server.js +11 -0
- package/dist/scripts/extract-ts-types.d.ts.map +1 -1
- package/dist/scripts/extract-ts-types.js +107 -23
- package/dist/scripts/extract-ts-types.test.js +225 -26
- package/dist/scripts/generate-spec.d.ts.map +1 -1
- package/dist/scripts/generate-spec.js +14 -5
- package/dist/scripts/generate-spec.test.js +93 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -1
- package/src/cdk/blocks-backend.test.ts +144 -60
- package/src/cdk/blocks-backend.ts +298 -239
- package/src/cdk/blocks-defaults.ts +12 -0
- package/src/cdk/blocks-stack.test.ts +32 -13
- package/src/cdk/compute/compute.ts +105 -3
- package/src/cdk/config-registry.test.ts +14 -0
- package/src/cdk/dashboard-registry.ts +68 -0
- package/src/cdk/index.ts +426 -298
- package/src/cdk/internal.ts +6 -2
- package/src/cdk/tracer-registry.ts +54 -0
- package/src/cdk/vpc-requirements-registry.ts +63 -0
- package/src/cdk/vpc-types.ts +158 -0
- package/src/cdk/vpc.test.ts +348 -0
- package/src/cdk/vpc.ts +336 -0
- package/src/errors.ts +5 -0
- package/src/hosting.test.ts +59 -1
- package/src/hosting.ts +3 -0
- package/src/index.cdk.ts +7 -0
- package/src/lambda-handler.test.ts +79 -1
- package/src/lambda-handler.ts +11 -2
- package/src/raw-route.test.ts +427 -1
- package/src/raw-route.ts +125 -12
- package/src/scripts/dev-server.ts +12 -1
- package/src/scripts/extract-ts-types.test.ts +228 -26
- package/src/scripts/extract-ts-types.ts +104 -20
- package/src/scripts/generate-spec.test.ts +101 -0
- package/src/scripts/generate-spec.ts +15 -5
- package/src/version.ts +1 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type * as ec2 from 'aws-cdk-lib/aws-ec2';
|
|
2
|
+
/**
|
|
3
|
+
* A subnet role — the kind of subnet a Building Block needs, expressed as an
|
|
4
|
+
* intent the VPC resolves to a concrete {@link ec2.SubnetSelection}:
|
|
5
|
+
*
|
|
6
|
+
* - `'private-with-egress'` — private subnets with outbound internet access via
|
|
7
|
+
* a NAT gateway. Required by anything that must reach a public AWS endpoint at
|
|
8
|
+
* runtime (e.g. a service with no interface endpoint).
|
|
9
|
+
* - `'isolated'` — private subnets with no internet route at all. Best for
|
|
10
|
+
* resources reached entirely over VPC endpoints or in-VPC (e.g. a database).
|
|
11
|
+
* - `'public'` — subnets with a direct internet gateway route.
|
|
12
|
+
*/
|
|
13
|
+
export type SubnetRole = 'private-with-egress' | 'isolated' | 'public';
|
|
14
|
+
/**
|
|
15
|
+
* Options for VPC integration on BlocksStack / BlocksBackend.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const vpc = new ec2.Vpc(app, 'AppVpc', { maxAzs: 2, natGateways: 1 });
|
|
20
|
+
* await BlocksStack.create(app, stackName, {
|
|
21
|
+
* backendHandlerPath: join(__dirname, 'index.handler.ts'),
|
|
22
|
+
* backendCDKPath: join(__dirname, 'index.ts'),
|
|
23
|
+
* defaults: { ...BlocksPresets.production, vpc: { network: vpc } },
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export interface BlocksVpcOptions {
|
|
28
|
+
/**
|
|
29
|
+
* The VPC to place Lambdas and VPC-resident resources into.
|
|
30
|
+
* Create this however you like — standard CDK:
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const vpc = new ec2.Vpc(stack, 'AppVpc', { maxAzs: 2, natGateways: 1 });
|
|
34
|
+
* // or
|
|
35
|
+
* const vpc = ec2.Vpc.fromLookup(stack, 'SharedVpc', { vpcId: 'vpc-abc123' });
|
|
36
|
+
*/
|
|
37
|
+
network: ec2.IVpc;
|
|
38
|
+
/**
|
|
39
|
+
* Subnet selection for Lambda and Blocks-managed compute placement.
|
|
40
|
+
*
|
|
41
|
+
* Any subnets you select must belong to {@link network}. This is not
|
|
42
|
+
* validated at synth — CDK can't check it for an imported subnet (which
|
|
43
|
+
* carries no `vpcId`) — so a mismatch surfaces as a CloudFormation error at
|
|
44
|
+
* deploy, not a synth-time failure.
|
|
45
|
+
*
|
|
46
|
+
* @default { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }
|
|
47
|
+
*/
|
|
48
|
+
subnets?: ec2.SubnetSelection;
|
|
49
|
+
/**
|
|
50
|
+
* Whether to auto-provision VPC endpoints based on BB registrations.
|
|
51
|
+
* Set to `false` to disable (e.g., when using a shared VPC that already has endpoints).
|
|
52
|
+
*
|
|
53
|
+
* @default true
|
|
54
|
+
*/
|
|
55
|
+
provisionEndpoints?: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* VPC requirements declared by a Building Block.
|
|
59
|
+
* Supplied to the `BuildingBlockScope` constructor and collected in the central
|
|
60
|
+
* requirements registry; `finalizeVpc` pulls them to provision endpoints, and the
|
|
61
|
+
* lazy VPC uses `requiresVpc` to decide whether to derive a VPC when none was
|
|
62
|
+
* provided.
|
|
63
|
+
*/
|
|
64
|
+
export interface VpcRequirements {
|
|
65
|
+
/** Gateway VPC endpoints this BB needs (e.g., S3, DynamoDB). */
|
|
66
|
+
gatewayEndpoints?: ec2.GatewayVpcEndpointAwsService[];
|
|
67
|
+
/** Interface VPC endpoints this BB needs (e.g., SQS, SSM, Secrets Manager). */
|
|
68
|
+
interfaceEndpoints?: ec2.InterfaceVpcEndpointAwsService[];
|
|
69
|
+
/**
|
|
70
|
+
* Whether this BB **cannot function without a VPC** — i.e. it must run in (or
|
|
71
|
+
* provision resources into) a VPC regardless of whether the customer asked
|
|
72
|
+
* for one. When any registered requirement sets this and no `vpc` was
|
|
73
|
+
* provided, the framework **lazily derives** a default VPC (see
|
|
74
|
+
* `getOrCreateVpc`) rather than erroring — mirroring how bb-data creates its
|
|
75
|
+
* own VPC when no shared one exists.
|
|
76
|
+
*
|
|
77
|
+
* Most BBs leave this unset: they reach AWS services from the AWS-managed
|
|
78
|
+
* Lambda network and only *benefit* from a VPC (endpoints) when one is present.
|
|
79
|
+
* Set it only for a BB whose resources are intrinsically VPC-resident.
|
|
80
|
+
*
|
|
81
|
+
* @default false
|
|
82
|
+
*/
|
|
83
|
+
requiresVpc?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Whether the BB's **parent runtime** — the shared Blocks handler Lambda (or,
|
|
86
|
+
* in future, a container) that executes this BB's operations — must be able to
|
|
87
|
+
* reach the internet (outbound egress) for the BB to work at runtime.
|
|
88
|
+
*
|
|
89
|
+
* Set this when your BB's runtime code calls a service it can only reach over
|
|
90
|
+
* the public internet. The canonical case: a service with no VPC endpoint
|
|
91
|
+
* (e.g. Aurora DSQL) is reached over a public HTTPS endpoint, so the runtime
|
|
92
|
+
* must sit in a subnet with an egress route; if it's placed in isolated
|
|
93
|
+
* subnets the deploy still succeeds but every call times out at runtime.
|
|
94
|
+
*
|
|
95
|
+
* The framework **validates** this at synth against the runtime's resolved
|
|
96
|
+
* placement (egress is satisfied by a `private-with-egress` or `public`
|
|
97
|
+
* selection) and fails the build with an actionable message on a mismatch. It
|
|
98
|
+
* never moves the runtime — reassigning a customer's explicit placement is not
|
|
99
|
+
* a BB's responsibility, so an unsatisfiable requirement is an error, not a
|
|
100
|
+
* silent relocation.
|
|
101
|
+
*
|
|
102
|
+
* This constrains the BB's **host**. To place compute the BB provisions
|
|
103
|
+
* **itself** (e.g. an Aurora cluster), resolve a subnet inline in your
|
|
104
|
+
* constructor via {@link VpcContext.selectSubnets} instead.
|
|
105
|
+
*
|
|
106
|
+
* @default false
|
|
107
|
+
*/
|
|
108
|
+
requiresEgress?: boolean;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The minimal shape {@link VpcContext.selectSubnets} needs from a Building
|
|
112
|
+
* Block to produce an instructive, BB-named error — just its `fullId`. Typed
|
|
113
|
+
* structurally so `vpc-types.ts` stays type-only and free of a dependency on
|
|
114
|
+
* the `Scope` class.
|
|
115
|
+
*/
|
|
116
|
+
export interface SubnetScope {
|
|
117
|
+
readonly fullId: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Internal VPC context propagated through the construct tree.
|
|
121
|
+
* Set by the CDK-level VPC option. BBs read this to determine their VPC placement.
|
|
122
|
+
* @internal
|
|
123
|
+
*/
|
|
124
|
+
export interface VpcContext {
|
|
125
|
+
readonly vpc: ec2.IVpc;
|
|
126
|
+
readonly computeSecurityGroup: ec2.ISecurityGroup;
|
|
127
|
+
readonly computeSubnets: ec2.SubnetSelection;
|
|
128
|
+
/**
|
|
129
|
+
* Resolve a {@link SubnetRole} to a concrete {@link ec2.SubnetSelection} for a
|
|
130
|
+
* resource this Building Block provisions itself, verifying the VPC actually
|
|
131
|
+
* has subnets of that role.
|
|
132
|
+
*
|
|
133
|
+
* Prefer this over building an `ec2.SubnetSelection` by hand: if the VPC has
|
|
134
|
+
* no matching subnet, it throws an actionable, BB-named error at synth
|
|
135
|
+
* ("`KVStore 'app/cache'` needs an 'isolated' subnet, but VPC 'vpc-…' has
|
|
136
|
+
* none …") instead of the opaque CDK "no subnet groups" error thrown later.
|
|
137
|
+
*
|
|
138
|
+
* @param scope the BB requesting the subnet (its `fullId` names the error)
|
|
139
|
+
* @param role the subnet role the BB's own resource needs
|
|
140
|
+
* @param opts.fallback an alternate role to use when `role` is absent from
|
|
141
|
+
* the VPC. Provide it to **explicitly** allow graceful degradation (e.g.
|
|
142
|
+
* Aurora over the Data API works from `'private-with-egress'` when there is
|
|
143
|
+
* no isolated tier); omit it to require `role` strictly and fail otherwise.
|
|
144
|
+
* The downgrade is never silent — it only happens when you opt in here.
|
|
145
|
+
* @throws if the VPC has neither `role` nor (when given) `opts.fallback`
|
|
146
|
+
*/
|
|
147
|
+
selectSubnets(scope: SubnetScope, role: SubnetRole, opts?: {
|
|
148
|
+
fallback?: SubnetRole;
|
|
149
|
+
}): ec2.SubnetSelection;
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=vpc-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vpc-types.d.ts","sourceRoot":"","sources":["../../src/cdk/vpc-types.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,GAAG,MAAM,qBAAqB,CAAC;AAEhD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,UAAU,GAAG,qBAAqB,GAAG,UAAU,GAAG,QAAQ,CAAC;AAEvE;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,gBAAgB;IAChC;;;;;;;;OAQG;IACH,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC;IAElB;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,GAAG,CAAC,eAAe,CAAC;IAE9B;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC/B,gEAAgE;IAChE,gBAAgB,CAAC,EAAE,GAAG,CAAC,4BAA4B,EAAE,CAAC;IACtD,+EAA+E;IAC/E,kBAAkB,CAAC,EAAE,GAAG,CAAC,8BAA8B,EAAE,CAAC;IAC1D;;;;;;;;;;;;;OAaG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC;IACvB,QAAQ,CAAC,oBAAoB,EAAE,GAAG,CAAC,cAAc,CAAC;IAClD,QAAQ,CAAC,cAAc,EAAE,GAAG,CAAC,eAAe,CAAC;IAC7C;;;;;;;;;;;;;;;;;;OAkBG;IACH,aAAa,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,UAAU,CAAA;KAAE,GAAG,GAAG,CAAC,eAAe,CAAC;CAC3G"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as ec2 from 'aws-cdk-lib/aws-ec2';
|
|
2
|
+
import type { Construct } from 'constructs';
|
|
3
|
+
import type { BlocksVpcOptions, VpcContext } from './vpc-types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Set the VPC context on a scope (BlocksStack or BlocksBackend).
|
|
6
|
+
* Called during stack creation when `vpc` prop is provided.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export declare function setVpcContext(scope: Construct, context: VpcContext): void;
|
|
10
|
+
/**
|
|
11
|
+
* Get the VPC context from a scope by walking up the construct tree.
|
|
12
|
+
* Used by BBs (e.g., bb-data) to discover the shared VPC.
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export declare function getVpcContext(scope: Construct): VpcContext | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Does any registered Building Block *require* a VPC (i.e. cannot function
|
|
18
|
+
* without one)? Used to decide whether to lazily derive a VPC when the customer
|
|
19
|
+
* didn't provide one. A BB that merely benefits from endpoints does NOT count —
|
|
20
|
+
* only `requiresVpc: true`.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export declare function anyRequirementNeedsVpc(scope: Construct): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Get the framework-owned VPC for a stack, creating one on first use.
|
|
26
|
+
*
|
|
27
|
+
* VPC is a **derived** resource: when a Building Block (or, later, a container
|
|
28
|
+
* compute) requires one and the customer didn't bring their own, Blocks
|
|
29
|
+
* materializes a sensible default here — the same create-if-absent pattern
|
|
30
|
+
* `bb-data` uses for Aurora, lifted to the framework so the whole app shares a
|
|
31
|
+
* single VPC. Keyed by a Symbol on the stack so it's a true singleton: the first
|
|
32
|
+
* caller creates, everyone else reuses. Passing a customer VPC (`vpc:` prop)
|
|
33
|
+
* pre-seeds this via {@link initializeVpc}, so this default is only built when
|
|
34
|
+
* nothing was provided.
|
|
35
|
+
*
|
|
36
|
+
* The default has both a `private-with-egress` tier (for a runtime that needs
|
|
37
|
+
* outbound access — the derivation trigger) and public subnets for the NAT
|
|
38
|
+
* gateway. NAT has a real cost, so this is only created on genuine need, and
|
|
39
|
+
* `create()` emits a notice when it does.
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export declare function getOrCreateVpc(scope: Construct): ec2.IVpc;
|
|
43
|
+
/**
|
|
44
|
+
* Initialize VPC support on a Blocks scope (BlocksStack or BlocksBackend).
|
|
45
|
+
* Creates the security group, sets VPC context, and returns the VpcContext
|
|
46
|
+
* that is used for Lambda placement configuration.
|
|
47
|
+
*
|
|
48
|
+
* Called during setupBlocksInfra when `vpc` prop is present.
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
export declare function initializeVpc(scope: Construct, options: BlocksVpcOptions): VpcContext;
|
|
52
|
+
/**
|
|
53
|
+
* Finalize VPC: query all BuildingBlockScope children for their VPC requirements,
|
|
54
|
+
* deduplicate, and provision endpoints.
|
|
55
|
+
* Called after all BBs are constructed (alongside finalizeConfigRegistry).
|
|
56
|
+
* @internal
|
|
57
|
+
*/
|
|
58
|
+
export declare function finalizeVpc(scope: Construct, options: BlocksVpcOptions): void;
|
|
59
|
+
//# sourceMappingURL=vpc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vpc.d.ts","sourceRoot":"","sources":["../../src/cdk/vpc.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,GAAG,MAAM,qBAAqB,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,OAAO,KAAK,EAAE,gBAAgB,EAA2B,UAAU,EAAmB,MAAM,gBAAgB,CAAC;AA4B7G;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI,CAEzE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,GAAG,SAAS,CAQtE;AAID;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAEhE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,CAOzD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,gBAAgB,GAAG,UAAU,CAkDrF;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAyH7E"}
|
package/dist/cdk/vpc.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { Annotations } from 'aws-cdk-lib';
|
|
4
|
+
import * as ec2 from 'aws-cdk-lib/aws-ec2';
|
|
5
|
+
import { getVpcRequirements as getRegisteredVpcRequirements } from './vpc-requirements-registry.js';
|
|
6
|
+
const VPC_CONTEXT_KEY = Symbol.for('BLOCKS_VPC_CONTEXT');
|
|
7
|
+
/** Map a subnet role to its concrete CDK subnet type. */
|
|
8
|
+
function subnetTypeForRole(role) {
|
|
9
|
+
switch (role) {
|
|
10
|
+
case 'isolated':
|
|
11
|
+
return ec2.SubnetType.PRIVATE_ISOLATED;
|
|
12
|
+
case 'public':
|
|
13
|
+
return ec2.SubnetType.PUBLIC;
|
|
14
|
+
case 'private-with-egress':
|
|
15
|
+
return ec2.SubnetType.PRIVATE_WITH_EGRESS;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Does the VPC actually contain at least one subnet of the given role? */
|
|
19
|
+
function vpcHasRole(vpc, role) {
|
|
20
|
+
switch (role) {
|
|
21
|
+
case 'isolated':
|
|
22
|
+
return vpc.isolatedSubnets.length > 0;
|
|
23
|
+
case 'public':
|
|
24
|
+
return vpc.publicSubnets.length > 0;
|
|
25
|
+
case 'private-with-egress':
|
|
26
|
+
return vpc.privateSubnets.length > 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Set the VPC context on a scope (BlocksStack or BlocksBackend).
|
|
31
|
+
* Called during stack creation when `vpc` prop is provided.
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
export function setVpcContext(scope, context) {
|
|
35
|
+
scope[VPC_CONTEXT_KEY] = context;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get the VPC context from a scope by walking up the construct tree.
|
|
39
|
+
* Used by BBs (e.g., bb-data) to discover the shared VPC.
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export function getVpcContext(scope) {
|
|
43
|
+
let current = scope;
|
|
44
|
+
while (current) {
|
|
45
|
+
const ctx = current[VPC_CONTEXT_KEY];
|
|
46
|
+
if (ctx)
|
|
47
|
+
return ctx;
|
|
48
|
+
current = current.node.scope;
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const LAZY_VPC_KEY = Symbol.for('BLOCKS_LAZY_VPC');
|
|
53
|
+
/**
|
|
54
|
+
* Does any registered Building Block *require* a VPC (i.e. cannot function
|
|
55
|
+
* without one)? Used to decide whether to lazily derive a VPC when the customer
|
|
56
|
+
* didn't provide one. A BB that merely benefits from endpoints does NOT count —
|
|
57
|
+
* only `requiresVpc: true`.
|
|
58
|
+
* @internal
|
|
59
|
+
*/
|
|
60
|
+
export function anyRequirementNeedsVpc(scope) {
|
|
61
|
+
return getRegisteredVpcRequirements(scope).some((r) => r.requirements.requiresVpc === true);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get the framework-owned VPC for a stack, creating one on first use.
|
|
65
|
+
*
|
|
66
|
+
* VPC is a **derived** resource: when a Building Block (or, later, a container
|
|
67
|
+
* compute) requires one and the customer didn't bring their own, Blocks
|
|
68
|
+
* materializes a sensible default here — the same create-if-absent pattern
|
|
69
|
+
* `bb-data` uses for Aurora, lifted to the framework so the whole app shares a
|
|
70
|
+
* single VPC. Keyed by a Symbol on the stack so it's a true singleton: the first
|
|
71
|
+
* caller creates, everyone else reuses. Passing a customer VPC (`vpc:` prop)
|
|
72
|
+
* pre-seeds this via {@link initializeVpc}, so this default is only built when
|
|
73
|
+
* nothing was provided.
|
|
74
|
+
*
|
|
75
|
+
* The default has both a `private-with-egress` tier (for a runtime that needs
|
|
76
|
+
* outbound access — the derivation trigger) and public subnets for the NAT
|
|
77
|
+
* gateway. NAT has a real cost, so this is only created on genuine need, and
|
|
78
|
+
* `create()` emits a notice when it does.
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
export function getOrCreateVpc(scope) {
|
|
82
|
+
const holder = scope;
|
|
83
|
+
const existing = holder[LAZY_VPC_KEY];
|
|
84
|
+
if (existing)
|
|
85
|
+
return existing;
|
|
86
|
+
const vpc = new ec2.Vpc(scope, 'BlocksVpc', { maxAzs: 2, natGateways: 1 });
|
|
87
|
+
holder[LAZY_VPC_KEY] = vpc;
|
|
88
|
+
return vpc;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Initialize VPC support on a Blocks scope (BlocksStack or BlocksBackend).
|
|
92
|
+
* Creates the security group, sets VPC context, and returns the VpcContext
|
|
93
|
+
* that is used for Lambda placement configuration.
|
|
94
|
+
*
|
|
95
|
+
* Called during setupBlocksInfra when `vpc` prop is present.
|
|
96
|
+
* @internal
|
|
97
|
+
*/
|
|
98
|
+
export function initializeVpc(scope, options) {
|
|
99
|
+
const { network: vpc, subnets } = options;
|
|
100
|
+
// Default Lambda placement to private-with-egress. Guard the default: if the
|
|
101
|
+
// caller didn't specify `subnets` and the VPC has no such tier, fail now with
|
|
102
|
+
// an actionable message instead of letting Lambda placement throw a cryptic
|
|
103
|
+
// CDK error later. (A caller who *explicitly* passes an isolated selection is
|
|
104
|
+
// honored — per-BB `requiresEgress` validation catches BBs that can't run there.)
|
|
105
|
+
if (!subnets && !vpcHasRole(vpc, 'private-with-egress')) {
|
|
106
|
+
throw new Error(`VPC '${vpc.vpcId}' has no private-with-egress subnets, which Blocks uses for Lambda ` +
|
|
107
|
+
`placement by default. Add a PRIVATE_WITH_EGRESS subnet tier (a private subnet with a ` +
|
|
108
|
+
`NAT gateway), or pass 'vpc.subnets' to choose a different placement explicitly. ` +
|
|
109
|
+
`See packages/blocks/VPC.md.`);
|
|
110
|
+
}
|
|
111
|
+
const resolvedSubnets = subnets ?? { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS };
|
|
112
|
+
const computeSecurityGroup = new ec2.SecurityGroup(scope, 'BlocksLambdaSg', {
|
|
113
|
+
vpc,
|
|
114
|
+
description: 'Security group for Blocks Lambda functions in VPC',
|
|
115
|
+
allowAllOutbound: true,
|
|
116
|
+
});
|
|
117
|
+
const context = {
|
|
118
|
+
vpc,
|
|
119
|
+
computeSecurityGroup,
|
|
120
|
+
computeSubnets: resolvedSubnets,
|
|
121
|
+
selectSubnets(scope, role, opts) {
|
|
122
|
+
if (vpcHasRole(vpc, role)) {
|
|
123
|
+
return { subnetType: subnetTypeForRole(role) };
|
|
124
|
+
}
|
|
125
|
+
// Requested role is absent. Fall back only if the BB explicitly opted in
|
|
126
|
+
// AND the fallback role actually exists — never downgrade silently.
|
|
127
|
+
if (opts?.fallback && vpcHasRole(vpc, opts.fallback)) {
|
|
128
|
+
return { subnetType: subnetTypeForRole(opts.fallback) };
|
|
129
|
+
}
|
|
130
|
+
const wanted = opts?.fallback ? `'${role}' (or '${opts.fallback}')` : `'${role}'`;
|
|
131
|
+
throw new Error(`${scope.fullId} needs a ${wanted} subnet, but VPC '${vpc.vpcId}' has none. ` +
|
|
132
|
+
`Add a matching subnet tier to your VPC (e.g. a 'subnetConfiguration' entry of the ` +
|
|
133
|
+
`required type), or place the Building Block differently. ` +
|
|
134
|
+
`See packages/blocks/VPC.md for guidance.`);
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
setVpcContext(scope, context);
|
|
138
|
+
return context;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Finalize VPC: query all BuildingBlockScope children for their VPC requirements,
|
|
142
|
+
* deduplicate, and provision endpoints.
|
|
143
|
+
* Called after all BBs are constructed (alongside finalizeConfigRegistry).
|
|
144
|
+
* @internal
|
|
145
|
+
*/
|
|
146
|
+
export function finalizeVpc(scope, options) {
|
|
147
|
+
const { network: vpc, subnets } = options;
|
|
148
|
+
// Whether the runtime's placement provides egress (outbound internet). This is
|
|
149
|
+
// the only capability the requirement check needs — a BB either needs egress
|
|
150
|
+
// or it doesn't. Resolved from the actual selected subnets rather than guessed
|
|
151
|
+
// from a subnet role, so an explicit subnet *list* (no subnetType) is handled
|
|
152
|
+
// correctly. `undefined` = couldn't determine (e.g. an imported VPC whose
|
|
153
|
+
// subnets aren't known at synth) → we skip validation with a warning rather
|
|
154
|
+
// than fabricate an answer.
|
|
155
|
+
const placementHasEgress = placementProvidesEgress(vpc, subnets);
|
|
156
|
+
const gatewayEndpoints = [];
|
|
157
|
+
const interfaceEndpoints = [];
|
|
158
|
+
// Pull requirements from the central registry — every BuildingBlockScope
|
|
159
|
+
// self-registers in its constructor, so this is a faithful census without a
|
|
160
|
+
// separate tree walk (mirrors how config/compute registries are consumed).
|
|
161
|
+
for (const { fullId, requirements: reqs } of getRegisteredVpcRequirements(scope)) {
|
|
162
|
+
if (reqs.gatewayEndpoints) {
|
|
163
|
+
gatewayEndpoints.push(...reqs.gatewayEndpoints);
|
|
164
|
+
}
|
|
165
|
+
if (reqs.interfaceEndpoints) {
|
|
166
|
+
interfaceEndpoints.push(...reqs.interfaceEndpoints);
|
|
167
|
+
}
|
|
168
|
+
// Validate the BB's runtime egress need against where the runtime is
|
|
169
|
+
// actually placed. We never move the runtime (that's the customer's
|
|
170
|
+
// explicit choice) — an unsatisfiable requirement is a synth error, not a
|
|
171
|
+
// silent relocation. This turns an otherwise-silent runtime failure (e.g.
|
|
172
|
+
// DSQL in an isolated Lambda: deploys clean, times out on every call) into
|
|
173
|
+
// an actionable build error.
|
|
174
|
+
if (reqs.requiresEgress) {
|
|
175
|
+
if (placementHasEgress === false) {
|
|
176
|
+
throw new Error(`${fullId} requires its runtime to reach the internet (outbound egress), but the ` +
|
|
177
|
+
`Blocks runtime is placed in subnets with no egress route. ` +
|
|
178
|
+
`Set 'vpc.subnets' to a 'private-with-egress' (or 'public') selection, or remove ` +
|
|
179
|
+
`the Building Block that needs it. See packages/blocks/VPC.md.`);
|
|
180
|
+
}
|
|
181
|
+
if (placementHasEgress === undefined) {
|
|
182
|
+
// Couldn't determine egress (e.g. imported VPC with unknown subnets).
|
|
183
|
+
// Don't fabricate a pass/fail — warn so a real mismatch isn't silent.
|
|
184
|
+
Annotations.of(scope).addWarningV2('blocks:vpc:egress-unverified', `${fullId} requires runtime egress, but Blocks couldn't determine whether the ` +
|
|
185
|
+
`configured subnets provide it (e.g. an imported VPC). Ensure the runtime's subnets ` +
|
|
186
|
+
`have an outbound internet route. See packages/blocks/VPC.md.`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (options.provisionEndpoints === false) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// Always add the S3 gateway endpoint. The runtime pulls config/secrets and
|
|
194
|
+
// migrations from S3 at cold start, so an in-VPC runtime needs it. Gateway
|
|
195
|
+
// endpoints are free (route-table entries, no ENI), so this is unconditional.
|
|
196
|
+
gatewayEndpoints.push(ec2.GatewayVpcEndpointAwsService.S3);
|
|
197
|
+
// Provision gateway endpoints (deduplicated). Gateway endpoints attach to
|
|
198
|
+
// route tables, not ENIs, so they have no security-group layer.
|
|
199
|
+
const provisionedGateway = new Set();
|
|
200
|
+
for (const service of gatewayEndpoints) {
|
|
201
|
+
const key = service.name;
|
|
202
|
+
if (provisionedGateway.has(key))
|
|
203
|
+
continue;
|
|
204
|
+
provisionedGateway.add(key);
|
|
205
|
+
const constructId = `VpcGw${key.replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
206
|
+
new ec2.GatewayVpcEndpoint(scope, constructId, { vpc, service });
|
|
207
|
+
}
|
|
208
|
+
// Always add CloudWatch Logs — every in-VPC Lambda needs it for log delivery,
|
|
209
|
+
// regardless of which Building Blocks are present. SSM is NOT added here: it
|
|
210
|
+
// flows from BB requirements (AppSetting and the auth blocks, which compose
|
|
211
|
+
// AppSetting, declare it), so an app that uses neither doesn't pay for an
|
|
212
|
+
// unused interface endpoint.
|
|
213
|
+
interfaceEndpoints.push(ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS);
|
|
214
|
+
// Dedicated security group for the interface endpoints. Without an explicit
|
|
215
|
+
// SG, CDK creates a default that allows 443 from the entire VPC CIDR — on a
|
|
216
|
+
// bring-your-own VPC that exposes every endpoint to unrelated workloads. Scope
|
|
217
|
+
// ingress to just the Blocks Lambda SG so only our functions can reach them.
|
|
218
|
+
const ctx = getVpcContext(scope);
|
|
219
|
+
const endpointSecurityGroup = new ec2.SecurityGroup(scope, 'BlocksVpcEndpointSg', {
|
|
220
|
+
vpc,
|
|
221
|
+
description: 'Blocks interface VPC endpoints — 443 from the Blocks Lambda only',
|
|
222
|
+
allowAllOutbound: true,
|
|
223
|
+
});
|
|
224
|
+
if (ctx) {
|
|
225
|
+
endpointSecurityGroup.addIngressRule(ec2.Peer.securityGroupId(ctx.computeSecurityGroup.securityGroupId), ec2.Port.tcp(443), 'HTTPS from Blocks Lambda');
|
|
226
|
+
}
|
|
227
|
+
// Provision interface endpoints (deduplicated)
|
|
228
|
+
const provisionedInterface = new Set();
|
|
229
|
+
for (const service of interfaceEndpoints) {
|
|
230
|
+
const key = service.name;
|
|
231
|
+
if (provisionedInterface.has(key))
|
|
232
|
+
continue;
|
|
233
|
+
provisionedInterface.add(key);
|
|
234
|
+
const constructId = `VpcIf${key.replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
235
|
+
new ec2.InterfaceVpcEndpoint(scope, constructId, {
|
|
236
|
+
vpc,
|
|
237
|
+
service,
|
|
238
|
+
privateDnsEnabled: true,
|
|
239
|
+
securityGroups: [endpointSecurityGroup],
|
|
240
|
+
// `open: false` suppresses CDK's default "allow 443 from the whole VPC
|
|
241
|
+
// CIDR" ingress rule. Our dedicated SG already allows 443 from just the
|
|
242
|
+
// Blocks Lambda SG; without this, CDK would re-widen access to the entire
|
|
243
|
+
// VPC — the exact broadening this dedicated SG exists to prevent.
|
|
244
|
+
open: false,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Does the runtime's subnet placement provide egress (an outbound internet
|
|
250
|
+
* route)? Returns `true`/`false` when determinable, or `undefined` when it
|
|
251
|
+
* can't be determined at synth (e.g. an imported VPC whose subnets aren't
|
|
252
|
+
* known) — callers should warn rather than assume.
|
|
253
|
+
*
|
|
254
|
+
* Resolved from the actual subnets, not from a subnet *role*, so an explicit
|
|
255
|
+
* subnet **list** (which carries no `subnetType`) is handled correctly. For a
|
|
256
|
+
* multi-subnet selection, egress is only reported when **every** selected
|
|
257
|
+
* subnet has it (a runtime may land in any of them).
|
|
258
|
+
*/
|
|
259
|
+
function placementProvidesEgress(vpc, selection) {
|
|
260
|
+
// Default placement (no selection) is PRIVATE_WITH_EGRESS — see initializeVpc.
|
|
261
|
+
if (!selection) {
|
|
262
|
+
return vpc.privateSubnets.length > 0 ? true : undefined;
|
|
263
|
+
}
|
|
264
|
+
// Explicit type: egress iff it's the egress tier; isolated/public are known.
|
|
265
|
+
if (selection.subnetType !== undefined) {
|
|
266
|
+
switch (selection.subnetType) {
|
|
267
|
+
case ec2.SubnetType.PRIVATE_WITH_EGRESS:
|
|
268
|
+
return true;
|
|
269
|
+
case ec2.SubnetType.PUBLIC:
|
|
270
|
+
return true; // public subnets route to an internet gateway
|
|
271
|
+
case ec2.SubnetType.PRIVATE_ISOLATED:
|
|
272
|
+
return false;
|
|
273
|
+
default:
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Explicit subnet list (or filter): resolve the concrete subnets and check
|
|
278
|
+
// that all of them are egress-capable. If resolution yields nothing usable,
|
|
279
|
+
// we can't tell.
|
|
280
|
+
try {
|
|
281
|
+
const { subnets: selected } = vpc.selectSubnets(selection);
|
|
282
|
+
if (selected.length === 0)
|
|
283
|
+
return undefined;
|
|
284
|
+
// A subnet is egress-capable if it's one of the VPC's private (with-egress)
|
|
285
|
+
// or public subnets. Isolated subnets are in neither list.
|
|
286
|
+
const egressCapable = new Set([
|
|
287
|
+
...vpc.privateSubnets.map((s) => s.subnetId),
|
|
288
|
+
...vpc.publicSubnets.map((s) => s.subnetId),
|
|
289
|
+
]);
|
|
290
|
+
// If the VPC exposes no subnet inventory (imported VPC), we can't classify.
|
|
291
|
+
if (egressCapable.size === 0 && vpc.isolatedSubnets.length === 0)
|
|
292
|
+
return undefined;
|
|
293
|
+
return selected.every((s) => egressCapable.has(s.subnetId));
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vpc.test.d.ts","sourceRoot":"","sources":["../../src/cdk/vpc.test.ts"],"names":[],"mappings":""}
|