Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c881055
feat(core): add CDK-level VPC support
svidgen Jul 28, 2026
3080bff
fix(vpc): direct CDK endpoint registration, full BB smoke tests, pers…
svidgen Jul 28, 2026
f61d356
fix(vpc): add Database/Aurora smoke test, Realtime subscribe test, Au…
svidgen Jul 28, 2026
8796448
refactor(vpc): simplify VPC implementation
svidgen Jul 28, 2026
cf8dbf2
fix: regenerate package-lock.json for test-infra workspace
svidgen Jul 28, 2026
0f8fcee
ci: rename VPC job to 'E2E VPC Smoke' + add persistent VPC ensure step
svidgen Jul 28, 2026
e374d58
fix: add no-op test script to test-infra (npm test --workspaces requi…
svidgen Jul 29, 2026
52addf8
fix(vpc-smoke): wire deploy/destroy lifecycle matching comprehensive …
svidgen Jul 29, 2026
f48abb8
fix(ci): capture VPC ID from test-infra deploy and export to env
svidgen Jul 29, 2026
43b8d25
fix(vpc-smoke): scope Vpc.fromLookup to a Stack (CDK requirement)
svidgen Jul 29, 2026
76b5212
fix(vpc-smoke): use ApiNamespace + RPC client pattern (matching compr…
svidgen Jul 29, 2026
a4902bb
fix(vpc-smoke): remove cross-app type import (use any for smoke test …
svidgen Jul 29, 2026
29af392
fix(vpc-smoke): use standard createLambdaHandler pattern (not BB re-e…
svidgen Jul 29, 2026
3f98a6b
fix(vpc): create endpoints in app scope + disable auto-provisioning i…
svidgen Jul 29, 2026
18896b4
fix(vpc): correct GatewayVpcEndpoint constructor (service is not an a…
svidgen Jul 29, 2026
a74631a
fix(vpc-smoke): add missing aws-blocks/package.json (client.js module…
svidgen Jul 29, 2026
8dbb503
fix(vpc-smoke): register aws-blocks/ as workspace (enables module res…
svidgen Jul 29, 2026
c53714c
fix(vpc-smoke): rename aws-blocks subpackage to avoid workspace name …
svidgen Jul 29, 2026
b5fa1ec
fix: restore aws-blocks name + regenerate lock file
svidgen Jul 29, 2026
e701c00
fix(vpc-smoke): use relative import for backend (can't share workspac…
svidgen Jul 29, 2026
afbb6ec
fix: restore package-lock.json (from main + install for new packages)
svidgen Jul 29, 2026
2461199
fix(vpc-smoke): use direct HTTP/RPC calls instead of client module im…
svidgen Jul 29, 2026
8516a0d
fix(vpc-smoke): use correct JSON-RPC 2.0 format for API calls
svidgen Jul 29, 2026
d0130a1
fix(vpc-smoke): make teardown best-effort (non-fatal if destroy fails)
svidgen Jul 30, 2026
4213fe6
fix(vpc-smoke): eliminate teardown failures — destroy all stacks, kee…
svidgen Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .changeset/simplify-vpc-implementation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@aws-blocks/core": minor
"@aws-blocks/bb-kv-store": patch
"@aws-blocks/bb-distributed-table": patch
"@aws-blocks/bb-file-bucket": patch
"@aws-blocks/bb-data": patch
"@aws-blocks/bb-distributed-data": patch
"@aws-blocks/bb-async-job": patch
"@aws-blocks/bb-agent": patch
"@aws-blocks/bb-knowledge-base": patch
"@aws-blocks/bb-email-client": patch
"@aws-blocks/bb-app-setting": patch
"@aws-blocks/bb-realtime": patch
"@aws-blocks/bb-auth-cognito": patch
"@aws-blocks/bb-auth-oidc": patch
---

Simplify VPC implementation: replace `registerVpcEndpoint` (instanceof-based) with two explicit methods (`registerVpcGatewayEndpoint` / `registerVpcInterfaceEndpoint`), simplify `BlocksVpcOptions` to `{ vpc, subnets?, provisionEndpoints? }`, and strip persistent test VPC to bare minimum.
12 changes: 10 additions & 2 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ jobs:
run: npm run test:e2e:sandbox

e2e-sandbox-vpc:
name: E2E Sandbox (VPC Smoke)
name: E2E VPC Smoke
needs: [build-and-test-local, detect-changes]
if: needs.detect-changes.outputs.source-changed == 'true' && github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
Expand Down Expand Up @@ -293,7 +293,15 @@ jobs:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1

- name: E2E Test Sandbox (VPC Smoke)
- name: Ensure persistent test VPC
run: cd test-infra && npx cdk deploy --require-approval never --outputs-file outputs.json

- name: Export VPC ID
run: |
VPC_ID=$(cat test-infra/outputs.json | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['BlocksTestVpc']['VpcId'])")
echo "VPC_TEST_VPC_ID=$VPC_ID" >> "$GITHUB_ENV"

- name: E2E VPC Smoke
run: npm run test:e2e:sandbox:vpc

e2e-production:
Expand Down
177 changes: 177 additions & 0 deletions docs/design/VPC-DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# VPC Support — Design

> **Status:** Implemented (PR #277, branch `feat/vpc-support`)

---

**Package:** `@aws-blocks/core` (CDK-level option)
**AWS Services:** Amazon VPC, EC2 (subnets, NAT gateways, security groups, VPC endpoints)

---

## Purpose

Place an AWS Blocks application in a VPC with a single prop on `BlocksStack`/`BlocksBackend`. The framework handles Lambda placement, endpoint provisioning (based on BB requirements), and security group wiring.

---

## API Surface

### BlocksVpcOptions

```typescript
interface BlocksVpcOptions {
/** The VPC to place Lambdas and VPC-resident resources into. */
vpc: ec2.IVpc;

/**
* Subnet selection for Lambda and all Blocks-managed compute placement.
* @default { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }
*/
subnets?: ec2.SubnetSelection;

/**
* Whether to auto-provision VPC endpoints based on BB registrations.
* Set to `false` to disable (e.g., when using a shared VPC that already has endpoints).
* @default true
*/
provisionEndpoints?: boolean;
}
```

### Customer Usage

```typescript
import * as ec2 from 'aws-cdk-lib/aws-ec2';

const vpc = new ec2.Vpc(app, 'AppVpc', { maxAzs: 2, natGateways: 1 });

// Simplest: pass VPC, Blocks provisions endpoints automatically
await BlocksStack.create(app, stackName, {
backendHandlerPath: join(__dirname, 'index.handler.ts'),
backendCDKPath: join(__dirname, 'index.ts'),
vpc: { vpc },
});

// Bring existing VPC with pre-provisioned endpoints
const sharedVpc = ec2.Vpc.fromLookup(app, 'SharedVpc', { vpcId: 'vpc-abc123' });
await BlocksStack.create(app, stackName, {
backendHandlerPath: join(__dirname, 'index.handler.ts'),
backendCDKPath: join(__dirname, 'index.ts'),
vpc: { vpc: sharedVpc, provisionEndpoints: false },
});
```

---

## BB Endpoint Registration

Each Building Block declares what VPC endpoints it needs via two explicit methods on the `Scope` class. No `instanceof` detection — each BB calls the method matching its endpoint type directly.

### Registration API

```typescript
// On Scope (core/cdk)
protected registerVpcGatewayEndpoint(service: ec2.GatewayVpcEndpointAwsService): void;
protected registerVpcInterfaceEndpoint(service: ec2.InterfaceVpcEndpointAwsService): void;
```

### Per-BB Declarations

| Building Block | Registration Call |
|----------------|-----------------|
| bb-kv-store | `this.registerVpcGatewayEndpoint(ec2.GatewayVpcEndpointAwsService.DYNAMODB)` |
| bb-distributed-table | `this.registerVpcGatewayEndpoint(ec2.GatewayVpcEndpointAwsService.DYNAMODB)` |
| bb-file-bucket | `this.registerVpcGatewayEndpoint(ec2.GatewayVpcEndpointAwsService.S3)` |
| bb-data | `this.registerVpcInterfaceEndpoint(ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER)` + `...RDS_DATA` |
| bb-async-job | `this.registerVpcInterfaceEndpoint(ec2.InterfaceVpcEndpointAwsService.SQS)` |
| bb-agent | `this.registerVpcInterfaceEndpoint(ec2.InterfaceVpcEndpointAwsService.BEDROCK_RUNTIME)` |
| bb-knowledge-base | `this.registerVpcInterfaceEndpoint(ec2.InterfaceVpcEndpointAwsService.BEDROCK_RUNTIME)` |
| bb-email-client | `this.registerVpcInterfaceEndpoint(ec2.InterfaceVpcEndpointAwsService.SES)` |
| bb-app-setting | `this.registerVpcInterfaceEndpoint(ec2.InterfaceVpcEndpointAwsService.SSM)` |
| bb-realtime | `this.registerVpcInterfaceEndpoint(ec2.InterfaceVpcEndpointAwsService.APIGATEWAY)` |
| bb-auth-cognito | `this.registerVpcInterfaceEndpoint(ec2.InterfaceVpcEndpointAwsService.SSM)` |
| bb-auth-oidc | `this.registerVpcInterfaceEndpoint(ec2.InterfaceVpcEndpointAwsService.SSM)` |
| bb-distributed-data | None (DSQL uses public HTTPS, reachable via NAT) |

### Always-Added Endpoints

The framework always adds these interface endpoints when `provisionEndpoints !== false`:

- **CloudWatch Logs** — Lambda needs it for log delivery from within VPC
- **SSM** — Used by auth BBs and AppSetting

### Collection and Provisioning

After all BBs are constructed, `finalizeVpc` walks the construct tree, collects all registered gateway and interface endpoints, deduplicates by service name, and provisions them on the VPC. Gateway endpoints are free; interface endpoints cost ~$7.20/month/AZ.

---

## Internal VPC Context

```typescript
interface VpcContext {
readonly vpc: ec2.IVpc;
readonly lambdaSecurityGroup: ec2.ISecurityGroup;
readonly lambdaSubnets: ec2.SubnetSelection;
selectSubnets(role: SubnetRole): ec2.SubnetSelection;
}
```

Set on the scope during `initializeVpc()`. BBs like `bb-data` read this via `getVpcContext(scope)` to discover the shared VPC and place Aurora in the correct subnets.

---

## Testing Strategy

### Persistent Test VPC (Bare Minimum)

The persistent test VPC stack (`test-infra/vpc-test-stack.ts`) contains only:

- VPC with public / private / isolated subnets (2 AZs)
- 1 NAT gateway
- VPC ID output

No pre-provisioned endpoints. No Aurora cluster. No security groups beyond defaults.

The test app provisions its own endpoints via `provisionEndpoints: true`, testing the real auto-detection path end-to-end.

### Per-Test Aurora

The `vpc-smoke` test app instantiates `new Database(scope, 'db')` with **no** `connection` option. The Database BB detects the VPC context and creates its own Aurora Serverless v2 cluster in the shared VPC's isolated subnets. The test runs `SELECT 1` and insert/read operations against this self-provisioned Aurora.

This avoids:
- A persistent Aurora cluster ($50+/month idle costs)
- External secret ARN management
- Cross-stack coupling between test infra and test app

### Test App Structure

```
test-apps/vpc-smoke/
├── aws-blocks/
│ ├── index.ts # Instantiates KVStore, DistributedTable, FileBucket, AsyncJob, AppSetting, Realtime, AuthCognito, Database, Logger, Metrics, Tracer
│ ├── index.cdk.ts # Looks up persistent test VPC, passes vpc: { vpc, provisionEndpoints: true }
│ └── index.handler.ts # Re-exports BB instances
└── package.json
```

---

## Phased Implementation

### Phase 1: CDK-level VPC (this PR)

- `vpc` prop on `BlocksStack` / `BlocksBackend`
- `registerVpcGatewayEndpoint()` / `registerVpcInterfaceEndpoint()` on `Scope`
- Per-BB endpoint declarations in each BB's CDK constructor
- Finalization: collect + deduplicate + provision endpoints
- Lambda placement in private subnets + security group
- `bb-data` refactor: use shared VPC when available
- `BlocksVpcOptions`: `{ vpc, subnets?, provisionEndpoints? }`

### Phase 2: Per-handler VPC (after configurable compute)

- `VpcNetwork` Building Block
- `network` option on individual compute targets
- Per-handler scope tree walks for requirement collection
43 changes: 41 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@
"test-apps/pipeline",
"test-apps/db-pull-typecheck",
"scripts/agent-bench",
"test-apps/telemetry"
"test-apps/telemetry",
"test-infra"
],
"scripts": {
"prepare": "husky && bash scripts/setup-git-secrets.sh",
Expand Down
4 changes: 4 additions & 0 deletions packages/bb-agent/src/index.cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Scope } from '@aws-blocks/core/cdk';
import type { ScopeParent } from '@aws-blocks/core';
import { DistributedTable } from '@aws-blocks/bb-distributed-table';
Expand Down Expand Up @@ -30,6 +31,9 @@ export class Agent extends Scope {
constructor(scope: ScopeParent, id: string, config?: any) {
super(id, { parent: scope });

// Register VPC endpoint for Bedrock access
this.registerVpcInterfaceEndpoint(ec2.InterfaceVpcEndpointAwsService.BEDROCK_RUNTIME);

this.handler.addToRolePolicy(new PolicyStatement({
actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream', 'bedrock:GetFoundationModel', 'bedrock:ListFoundationModels', 'bedrock:GetInferenceProfile'],
resources: [
Expand Down
4 changes: 4 additions & 0 deletions packages/bb-app-setting/src/index.cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import * as ssm from 'aws-cdk-lib/aws-ssm';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as cr from 'aws-cdk-lib/custom-resources';
import { Scope, registerConfig, DEFAULT_NODE_RUNTIME } from '@aws-blocks/core/cdk';
import type { ScopeParent } from '@aws-blocks/core';
Expand Down Expand Up @@ -49,6 +50,9 @@
constructor(scope: ScopeParent, id: string, options: AppSettingOptions<T>) {
super(id, { parent: scope });

// Register VPC endpoint for SSM access
this.registerVpcInterfaceEndpoint(ec2.InterfaceVpcEndpointAwsService.SSM);

// `external` is package-internal (set only by fromExisting), not on the
// public AppSettingOptions — read it via the internal options type.
const external = (options as InternalAppSettingOptions<T>).external ?? false;
Expand Down Expand Up @@ -257,7 +261,7 @@
secretInitFn.addToRolePolicy(new iam.PolicyStatement({
actions: ['ssm:PutParameter', 'ssm:DeleteParameter', 'ssm:AddTagsToResource'],
resources: cdk.Lazy.list({
produce: () => state!.parameterNames.map(name =>

Check warning on line 264 in packages/bb-app-setting/src/index.cdk.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/style/noNonNullAssertion

Forbidden non-null assertion.
stack.formatArn({
service: 'ssm',
resource: 'parameter',
Expand All @@ -284,7 +288,7 @@
new cdk.CustomResource(stack, 'BlocksSecretsBulk', {
serviceToken: provider.serviceToken,
properties: {
ParameterNames: cdk.Lazy.list({ produce: () => state!.parameterNames }),

Check warning on line 291 in packages/bb-app-setting/src/index.cdk.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/style/noNonNullAssertion

Forbidden non-null assertion.
StackName: (() => { let s = stack; while (s.nestedStackParent) s = s.nestedStackParent; return s.stackName; })(),
},
});
Expand Down
4 changes: 4 additions & 0 deletions packages/bb-async-job/src/index.cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@
// SPDX-License-Identifier: Apache-2.0

import { Duration } from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Queue, QueueEncryption } from 'aws-cdk-lib/aws-sqs';
import { SqsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources';
import { Scope } from '@aws-blocks/core/cdk';
import { registerConfig } from '@aws-blocks/core/cdk';
import type { ScopeParent } from '@aws-blocks/core';
import type {
AsyncJobContext,
AsyncJobOptions,
SubmitOptions,

Check warning on line 14 in packages/bb-async-job/src/index.cdk.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/correctness/noUnusedImports

Several of these imports are unused.
} from './types.js';
import { AsyncJobErrors } from './errors.js';

Check warning on line 16 in packages/bb-async-job/src/index.cdk.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/correctness/noUnusedImports

This import is unused.

export { AsyncJobErrors } from './errors.js';
export type { AsyncJobContext, AsyncJobOptions, SubmitOptions } from './types.js';
Expand All @@ -24,6 +25,9 @@
constructor(scope: ScopeParent, id: string, options: AsyncJobOptions<T>) {
super(id, { parent: scope });

// Register VPC endpoint for SQS access
this.registerVpcInterfaceEndpoint(ec2.InterfaceVpcEndpointAwsService.SQS);

const maxRetries = options.maxRetries ?? 3;
const batchSize = options.batchSize ?? 1;

Expand Down
5 changes: 5 additions & 0 deletions packages/bb-auth-cognito/src/index.cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
*/

import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as iam from 'aws-cdk-lib/aws-iam';
import type * as lambda from 'aws-cdk-lib/aws-lambda';
Expand Down Expand Up @@ -67,12 +68,16 @@
export class AuthCognito<const O extends AuthCognitoOptions = AuthCognitoOptions> extends Scope {
public readonly userPool: cognito.IUserPool;
public readonly userPoolClient: cognito.IUserPoolClient;
private readonly sessions: KVStore;

Check warning on line 71 in packages/bb-auth-cognito/src/index.cdk.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/correctness/noUnusedPrivateClassMembers

This private class member is defined but never used.
/** Admin opt-in, captured for the IAM grant in `grantCognitoPermissions`. */
private readonly adminOptions?: AdminOptions;

constructor(scope: ScopeParent, id: string, options?: O) {
super(id, { parent: scope });

// Register VPC endpoint for SSM (session secret storage)
this.registerVpcInterfaceEndpoint(ec2.InterfaceVpcEndpointAwsService.SSM);

// `AuthCognitoOptions` is all-optional; the cast is sound by the type bound.
const opts: AuthCognitoOptions = options ?? ({} as O);
this.adminOptions = opts.admin;
Expand Down
4 changes: 4 additions & 0 deletions packages/bb-auth-oidc/src/index.cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@
* the provider helpers.
*/

import { type ScopeParent } from '@aws-blocks/core';

Check failure on line 31 in packages/bb-auth-oidc/src/index.cdk.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/style/useImportType

All these imports are only used as types.
import { Scope, registerConfig } from '@aws-blocks/core/cdk';
import { AppSetting } from '@aws-blocks/bb-app-setting';
import { KVStore } from '@aws-blocks/bb-kv-store';
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import type * as lambda from 'aws-cdk-lib/aws-lambda';
import type { IDependable } from 'constructs';
Expand Down Expand Up @@ -92,6 +93,9 @@
constructor(scope: ScopeParent, id: string, options: AuthOIDCOptions<P>) {
super(id, { parent: scope });

// Register VPC endpoint for SSM (cookie secret storage)
this.registerVpcInterfaceEndpoint(ec2.InterfaceVpcEndpointAwsService.SSM);

this.callbackPath = options.callbackPath ?? DEFAULT_CALLBACK_PATH;
this.signOutPath = options.signOutPath ?? DEFAULT_SIGNOUT_PATH;

Expand Down Expand Up @@ -175,7 +179,7 @@
client.node.addDependency(dep);
}

const fn = this.handler as lambda.Function;

Check warning on line 182 in packages/bb-auth-oidc/src/index.cdk.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/correctness/noUnusedVariables

This variable fn is unused.
const envPrefix = `BLOCKS_AUTH_OIDC_COGNITO_${this.fullId.toUpperCase().replace(/[^A-Z0-9]/g, '_')}`;
registerConfig(this, `${envPrefix}_POOL_ID`, pool.userPoolId);
registerConfig(this, `${envPrefix}_CLIENT_ID`, client.userPoolClientId);
Expand Down
Loading
Loading