From 00011f0d8802937a886abcde366154ef53217dc4 Mon Sep 17 00:00:00 2001 From: bgagent Date: Tue, 16 Jun 2026 15:32:27 -0400 Subject: [PATCH] fix(cdk): allow pinning AgentVpc to AgentCore-supported availability zones (#353) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AgentCore only supports a subset of physical availability zones per region. AZ names are aliased per-account to physical zone IDs, so the default maxAzs selection can land in a zone AgentCore does not support, causing the AWS::BedrockAgentCore::Runtime resource to fail with NotStabilized. Changes: - Add optional `availabilityZones` prop to AgentVpcProps — when provided it takes precedence over maxAzs so the VPC is pinned to specific AZ names. - Wire up the CDK context key `agentcore:availabilityZones` in agent.ts so affected accounts can set it in cdk.context.json or via -c flag without touching construct code. - Add tests for the new prop (explicit AZs override maxAzs, 3-zone case). Usage for affected accounts: cdk deploy -c agentcore:availabilityZones='["us-east-1b","us-east-1c"]' Or in cdk.context.json: { "agentcore:availabilityZones": ["us-east-1b", "us-east-1c"] } Closes #353 --- cdk/src/constructs/agent-vpc.ts | 41 ++++++++++++++++++++++++++- cdk/src/stacks/agent.ts | 18 +++++++++++- cdk/test/constructs/agent-vpc.test.ts | 29 +++++++++++++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/cdk/src/constructs/agent-vpc.ts b/cdk/src/constructs/agent-vpc.ts index d8d5546e..529c0759 100644 --- a/cdk/src/constructs/agent-vpc.ts +++ b/cdk/src/constructs/agent-vpc.ts @@ -32,10 +32,45 @@ const HTTPS_PORT = 443; export interface AgentVpcProps { /** * Maximum number of availability zones to use. + * + * Ignored when {@link availabilityZones} is provided (CDK does not allow + * both `maxAzs` and an explicit zone list on the same VPC). * @default 2 */ readonly maxAzs?: number; + /** + * Explicit list of availability-zone *names* (e.g. `['us-east-1b', 'us-east-1c']`) + * to place the VPC — and therefore the AgentCore Runtime ENIs — into. + * + * AgentCore only supports a subset of the physical availability zones in a + * region, and AZ *names* are aliased per-account to physical zone IDs (so + * `us-east-1a` is not the same physical zone across accounts). When CDK is + * left to pick zones by name (the `maxAzs` default) it can land the Runtime + * subnets in a zone AgentCore does not support, and the + * `AWS::BedrockAgentCore::Runtime` resource fails to stabilize with + * `NotStabilized` ("subnets are in unsupported availability zones"), rolling + * back the whole stack. + * + * Pin this to AZ names whose physical zone IDs are AgentCore-supported to + * make a fresh deploy deterministic regardless of the account's + * name → zone-ID mapping. Discover the mapping with: + * + * ```sh + * aws ec2 describe-availability-zones --region \ + * --query 'AvailabilityZones[].[ZoneName,ZoneId]' --output text + * ``` + * + * then choose names whose zone IDs are in the AgentCore-supported set for + * the region (for `us-east-1` at time of writing: `use1-az1`, `use1-az2`, + * `use1-az4`). The error message returned by a failed Runtime creation also + * lists the currently supported zone IDs. + * + * When provided, takes precedence over {@link maxAzs}. + * @default - CDK selects the first `maxAzs` zones by name + */ + readonly availabilityZones?: string[]; + /** * Number of NAT gateways to provision. * @default 1 @@ -71,8 +106,12 @@ export class AgentVpc extends Construct { const removalPolicy = props.removalPolicy ?? RemovalPolicy.DESTROY; // --- VPC --- + // When explicit AZs are provided (to target AgentCore-supported physical + // zones), pass them directly and omit maxAzs — CDK does not allow both. this.vpc = new ec2.Vpc(this, 'Vpc', { - maxAzs, + ...(props.availabilityZones + ? { availabilityZones: props.availabilityZones } + : { maxAzs }), natGateways, restrictDefaultSecurityGroup: true, subnetConfiguration: [ diff --git a/cdk/src/stacks/agent.ts b/cdk/src/stacks/agent.ts index 17e13ebb..1fbbca18 100644 --- a/cdk/src/stacks/agent.ts +++ b/cdk/src/stacks/agent.ts @@ -199,7 +199,23 @@ export class AgentStack extends Stack { ]); // Network isolation — VPC with restricted egress - const agentVpc = new AgentVpc(this, 'AgentVpc'); + // AgentCore only supports a subset of physical availability zones per + // region (for us-east-1: use1-az1, use1-az2, use1-az4). AZ *names* are + // aliased per-account, so the default maxAzs selection can land in an + // unsupported zone and cause a deploy failure. Use the CDK context key + // `agentcore:availabilityZones` to pin to account-specific AZ names whose + // physical zone IDs are AgentCore-supported. + // + // Discover your mapping: + // aws ec2 describe-availability-zones --region us-east-1 \ + // --query 'AvailabilityZones[].[ZoneName,ZoneId]' --output text + // + // Then set in cdk.context.json or via -c: + // "agentcore:availabilityZones": ["us-east-1b", "us-east-1c"] + const agentCoreAzs = this.node.tryGetContext('agentcore:availabilityZones') as string[] | undefined; + const agentVpc = new AgentVpc(this, 'AgentVpc', { + ...(agentCoreAzs ? { availabilityZones: agentCoreAzs } : {}), + }); // DNS Firewall — domain-level egress filtering (observation mode for initial deployment) const additionalDomains = [...new Set(blueprints.flatMap(b => b.egressAllowlist))]; diff --git a/cdk/test/constructs/agent-vpc.test.ts b/cdk/test/constructs/agent-vpc.test.ts index 49614d8d..2fcc1c88 100644 --- a/cdk/test/constructs/agent-vpc.test.ts +++ b/cdk/test/constructs/agent-vpc.test.ts @@ -149,4 +149,33 @@ describe('AgentVpc with custom props', () => { template.resourceCountIs('AWS::EC2::NatGateway', 2); }); + + test('accepts explicit availabilityZones and ignores maxAzs', () => { + const app = new App(); + const stack = new Stack(app, 'TestStack', { + env: { account: '123456789012', region: 'us-east-1' }, + }); + new AgentVpc(stack, 'AgentVpc', { + availabilityZones: ['us-east-1b', 'us-east-1c'], + maxAzs: 3, // should be ignored when availabilityZones is provided + }); + const template = Template.fromStack(stack); + + // 2 explicit AZs × 2 subnet types = 4 subnets + template.resourceCountIs('AWS::EC2::Subnet', 4); + }); + + test('availabilityZones with 3 zones creates 6 subnets', () => { + const app = new App(); + const stack = new Stack(app, 'TestStack', { + env: { account: '123456789012', region: 'us-east-1' }, + }); + new AgentVpc(stack, 'AgentVpc', { + availabilityZones: ['us-east-1b', 'us-east-1c', 'us-east-1d'], + }); + const template = Template.fromStack(stack); + + // 3 AZs × 2 subnet types = 6 subnets + template.resourceCountIs('AWS::EC2::Subnet', 6); + }); });