From 00011f0d8802937a886abcde366154ef53217dc4 Mon Sep 17 00:00:00 2001 From: bgagent Date: Tue, 16 Jun 2026 15:32:27 -0400 Subject: [PATCH 1/3] 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); + }); }); From 392b55e3b462c0001953a59fb268005c824ebfc8 Mon Sep 17 00:00:00 2001 From: bgagent Date: Wed, 15 Jul 2026 11:12:18 -0400 Subject: [PATCH 2/3] fix(cdk): auto-select AgentCore-supported AZs by default (#353) Addresses review on #358: a manual AZ pin alone left the default deploy path broken for affected accounts. The stack now auto-selects AgentCore-supported availability zones, with the context override kept as a validated safety valve. - New constructs/agentcore-azs.ts: - AGENTCORE_SUPPORTED_AZ_IDS: per-region map of supported physical zone IDs (from the AWS AgentCore VPC docs), not a us-east-1-only comment presented as universal. - resolveAgentCoreAzOverride: validates agentcore:availabilityZones, mirroring resolveBedrockModelIds (rejects non-array / empty / non- string entries and <2 zones for HA) with a clear synth error. - selectSupportedAzNames: pure name<->id intersection. - resolveAgentCoreAzs: override wins; else, when synth has a concrete account+region, DescribeAvailabilityZones -> intersect -> pin AZ names; env-agnostic synth / unknown region / lookup failure fall back to CDK's default selection (surfaced via a synth warning, not a masked empty result). - main.ts is now async and threads the resolved names into AgentStack (new AgentStackProps.availabilityZones) -> AgentVpc. - Docs: DEPLOYMENT_GUIDE 'Known deployment issues' + QUICK_START note and troubleshooting row cover the discover-mapping -> pick-IDs -> set- context operator path. - Tests: agent-vpc asserts subnet AvailabilityZone == pinned names plus an env-agnostic Fn::GetAZs fallback; agentcore-azs covers the map, override validation, selection, and resolver branches. Adds @aws-sdk/client-ec2 for the synth-time lookup (loaded lazily). --- cdk/package.json | 1 + cdk/src/constructs/agent-vpc.ts | 19 +- cdk/src/constructs/agentcore-azs.ts | 239 +++++++++++++++++ cdk/src/main.ts | 106 +++++--- cdk/src/stacks/agent.ts | 41 +-- cdk/test/constructs/agent-vpc.test.ts | 22 ++ cdk/test/constructs/agentcore-azs.test.ts | 199 ++++++++++++++ docs/guides/DEPLOYMENT_GUIDE.md | 25 ++ docs/guides/QUICK_START.mdx | 3 +- .../docs/getting-started/Deployment-guide.md | 25 ++ .../docs/getting-started/Quick-start.mdx | 3 +- yarn.lock | 252 ++++++++++++++++++ 12 files changed, 867 insertions(+), 68 deletions(-) create mode 100644 cdk/src/constructs/agentcore-azs.ts create mode 100644 cdk/test/constructs/agentcore-azs.test.ts diff --git a/cdk/package.json b/cdk/package.json index be16744a..e99d8234 100644 --- a/cdk/package.json +++ b/cdk/package.json @@ -19,6 +19,7 @@ "@aws-sdk/client-bedrock-agentcore": "^3.1078.0", "@aws-sdk/client-bedrock-runtime": "^3.1078.0", "@aws-sdk/client-dynamodb": "^3.1078.0", + "@aws-sdk/client-ec2": "^3.1078.0", "@aws-sdk/client-ecs": "^3.1078.0", "@aws-sdk/client-lambda": "^3.1078.0", "@aws-sdk/client-s3": "^3.1078.0", diff --git a/cdk/src/constructs/agent-vpc.ts b/cdk/src/constructs/agent-vpc.ts index 529c0759..12d7fc50 100644 --- a/cdk/src/constructs/agent-vpc.ts +++ b/cdk/src/constructs/agent-vpc.ts @@ -54,17 +54,16 @@ export interface AgentVpcProps { * * 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: + * name → zone-ID mapping. The supported zone-ID set differs per region and + * can change over time — see the AWS + * {@link https://aws.github.io/bedrock-agentcore-starter-toolkit/user-guide/security/agentcore-vpc/#supported-availability-zones Supported Availability Zones} + * table, and the per-region `AGENTCORE_SUPPORTED_AZ_IDS` map in + * `constructs/agentcore-azs.ts`. * - * ```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. + * Callers normally don't set this directly: `resolveAgentCoreAzs` (invoked + * from `main.ts`) auto-selects supported zone names for the target account, + * or honors the validated `agentcore:availabilityZones` context override, and + * passes the result through {@link AgentStackProps.availabilityZones}. * * When provided, takes precedence over {@link maxAzs}. * @default - CDK selects the first `maxAzs` zones by name diff --git a/cdk/src/constructs/agentcore-azs.ts b/cdk/src/constructs/agentcore-azs.ts new file mode 100644 index 00000000..2d3d1a8e --- /dev/null +++ b/cdk/src/constructs/agentcore-azs.ts @@ -0,0 +1,239 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { Annotations, Token } from 'aws-cdk-lib'; +import { Construct, Node } from 'constructs'; + +/** + * AgentCore-supported physical **Availability Zone IDs** per region. + * + * AgentCore Runtime (and the built-in Code Interpreter / Browser tools) only + * places its elastic network interfaces in a subset of each region's zones. If + * the VPC subnets land in an unsupported zone the + * `AWS::BedrockAgentCore::Runtime` resource fails to stabilize + * (`NotStabilized` — "subnets are in unsupported availability zones") and rolls + * back the whole stack. + * + * The constraint is published in terms of **zone IDs** (e.g. `use1-az1`), which + * are stable across accounts, NOT zone *names* (e.g. `us-east-1a`) which are + * aliased per-account. Keep this map aligned with the AWS documentation: + * https://aws.github.io/bedrock-agentcore-starter-toolkit/user-guide/security/agentcore-vpc/#supported-availability-zones + * + * A region absent from this map is treated as "no known constraint" — the + * auto-pin logic leaves AZ selection to CDK's default and operators can still + * pin explicitly via the {@link AGENTCORE_AZS_CONTEXT_KEY} context override. + */ +export const AGENTCORE_SUPPORTED_AZ_IDS: Readonly> = { + 'us-east-1': ['use1-az1', 'use1-az2', 'use1-az4'], + 'us-east-2': ['use2-az1', 'use2-az2', 'use2-az3'], + 'us-west-2': ['usw2-az1', 'usw2-az2', 'usw2-az3'], + 'ap-southeast-1': ['apse1-az1', 'apse1-az2', 'apse1-az3'], + 'ap-southeast-2': ['apse2-az1', 'apse2-az2', 'apse2-az3'], + 'ap-south-1': ['aps1-az1', 'aps1-az2', 'aps1-az3'], + 'ap-northeast-1': ['apne1-az1', 'apne1-az2', 'apne1-az4'], + 'eu-west-1': ['euw1-az1', 'euw1-az2', 'euw1-az3'], + 'eu-central-1': ['euc1-az1', 'euc1-az2', 'euc1-az3'], +}; + +/** + * CDK context key whose value (a JSON array of AZ **names**, e.g. + * `["us-east-1b", "us-east-1c"]`) overrides the auto-selected zones. Set it in + * `cdk.context.json`, `cdk.json` `context`, or via + * `-c 'agentcore:availabilityZones=["us-east-1b","us-east-1c"]'`. + */ +export const AGENTCORE_AZS_CONTEXT_KEY = 'agentcore:availabilityZones'; + +/** Minimum AZ count — AgentCore high-availability guidance is >=2 zones. */ +const MIN_AGENTCORE_AZS = 2; + +/** A single Availability Zone's name and its stable physical zone ID. */ +export interface AvailabilityZoneInfo { + /** Account-aliased zone name, e.g. `us-east-1a`. */ + readonly zoneName: string; + /** Stable physical zone ID, e.g. `use1-az1`. */ + readonly zoneId: string; +} + +/** Signature for the injectable `DescribeAvailabilityZones` lookup. */ +export type DescribeAzsFn = (region: string) => Promise; + +/** Options for {@link resolveAgentCoreAzs}. */ +export interface ResolveAgentCoreAzsOptions { + /** Scope used to read context and surface synth-time warnings (the `App`). */ + readonly scope: Construct; + /** Target account (from `CDK_DEFAULT_ACCOUNT`); undefined/token = env-agnostic. */ + readonly account?: string; + /** Target region (from `CDK_DEFAULT_REGION`); undefined/token = env-agnostic. */ + readonly region?: string; + /** + * Availability-zone lookup. Defaults to a live EC2 + * `DescribeAvailabilityZones` call; injectable so tests need no AWS access. + */ + readonly describeAzs?: DescribeAzsFn; +} + +/** + * Validates and returns the optional {@link AGENTCORE_AZS_CONTEXT_KEY} override. + * + * Mirrors the loud-fail contract of `resolveBedrockModelIds` (bedrock-models.ts): + * a malformed override fails synth with a clear message naming the key and the + * expected JSON shape, rather than silently pinning nothing. + * + * @returns the validated AZ-name array, or `undefined` when the key is unset. + * @throws if the value is not a JSON array, contains a non-string/empty entry, + * or lists fewer than {@link MIN_AGENTCORE_AZS} zones. + */ +export function resolveAgentCoreAzOverride(node: Node): string[] | undefined { + const override = node.tryGetContext(AGENTCORE_AZS_CONTEXT_KEY); + if (override === undefined || override === null) { + return undefined; + } + if (!Array.isArray(override)) { + throw new Error( + `Context '${AGENTCORE_AZS_CONTEXT_KEY}' must be a JSON array of availability-zone names ` + + `(e.g. ["us-east-1b", "us-east-1c"]); got ${JSON.stringify(override)}.`, + ); + } + for (const az of override) { + if (typeof az !== 'string' || az.trim().length === 0) { + throw new Error( + `Context '${AGENTCORE_AZS_CONTEXT_KEY}' entries must be non-empty availability-zone-name ` + + `strings; got ${JSON.stringify(az)}.`, + ); + } + } + if (override.length < MIN_AGENTCORE_AZS) { + throw new Error( + `Context '${AGENTCORE_AZS_CONTEXT_KEY}' must list at least ${MIN_AGENTCORE_AZS} zones for ` + + `AgentCore high availability; got ${JSON.stringify(override)}.`, + ); + } + return override as string[]; +} + +/** + * Pure selection: given the account's AZ (name, id) pairs, returns the zone + * *names* whose physical zone IDs are AgentCore-supported for `region`. + * + * Returns an empty array when the region has no known constraint (absent from + * {@link AGENTCORE_SUPPORTED_AZ_IDS}) or none of the account's zones match. + */ +export function selectSupportedAzNames(region: string, zones: readonly AvailabilityZoneInfo[]): string[] { + const supported = AGENTCORE_SUPPORTED_AZ_IDS[region]; + if (!supported) { + return []; + } + const supportedIds = new Set(supported); + return zones.filter(zone => supportedIds.has(zone.zoneId)).map(zone => zone.zoneName); +} + +/** + * Live `DescribeAvailabilityZones` lookup (default {@link DescribeAzsFn}). + * + * The `@aws-sdk/client-ec2` module is imported dynamically so it is only loaded + * when auto-pin actually runs (concrete env, no override) — not during + * env-agnostic synth or in unit tests, which inject their own lookup. + */ +async function defaultDescribeAzs(region: string): Promise { + const { EC2Client, DescribeAvailabilityZonesCommand } = await import('@aws-sdk/client-ec2'); + const client = new EC2Client({ region }); + const response = await client.send( + new DescribeAvailabilityZonesCommand({ + // Standard AZs only — exclude Local Zones / Wavelength / Outposts. + Filters: [{ Name: 'zone-type', Values: ['availability-zone'] }], + }), + ); + const zones: AvailabilityZoneInfo[] = []; + for (const zone of response.AvailabilityZones ?? []) { + if (zone.ZoneName && zone.ZoneId) { + zones.push({ zoneName: zone.ZoneName, zoneId: zone.ZoneId }); + } + } + return zones; +} + +/** + * Resolves the Availability-Zone *names* the AgentCore VPC should pin to. + * + * Resolution order: + * 1. **Operator override** — a validated {@link AGENTCORE_AZS_CONTEXT_KEY} + * context value always wins (works even in env-agnostic synth, which is how + * the CI-built artifact and production stack pin zones). + * 2. **Auto-pin (default path)** — when synth has a concrete account + region, + * resolve the account's name -> zone-ID mapping and intersect it with + * {@link AGENTCORE_SUPPORTED_AZ_IDS} for the region, so a fresh local + * `cdk deploy` lands only in supported zones without account-specific + * guesswork. + * 3. **Fallback** — env-agnostic synth (token/undefined account or region), an + * unknown region, a failed lookup, or fewer than {@link MIN_AGENTCORE_AZS} + * supported zones all return `undefined`, leaving CDK's default AZ selection + * in place. Degraded cases (2 & 3 boundary) surface a synth warning rather + * than failing, so unaffected regions and offline synth still deploy. + * + * @returns AZ names to pass to `ec2.Vpc({ availabilityZones })`, or `undefined` + * to keep the default `maxAzs` selection. + */ +export async function resolveAgentCoreAzs(options: ResolveAgentCoreAzsOptions): Promise { + const { scope, account, region } = options; + + // 1. Explicit, validated override wins (throws loudly if malformed). + const override = resolveAgentCoreAzOverride(scope.node); + if (override) { + return override; + } + + // 2. Auto-pin needs a concrete account + region. Env-agnostic synth uses + // token placeholders (the CI-built artifact + production stack) — pin via + // the override there instead. + if (!account || !region || Token.isUnresolved(account) || Token.isUnresolved(region)) { + return undefined; + } + + // 3. No published constraint for this region — don't guess. + if (!AGENTCORE_SUPPORTED_AZ_IDS[region]) { + return undefined; + } + + const describeAzs = options.describeAzs ?? defaultDescribeAzs; + + // The return sits outside the try/catch so a failed lookup degrades to the + // default AZ selection (surfaced as a warning) instead of masking the error + // with an empty result inside the catch (ts-silent-success-masking / AI004). + let pinnedZones: string[] | undefined; + try { + const zones = await describeAzs(region); + const names = selectSupportedAzNames(region, zones); + if (names.length >= MIN_AGENTCORE_AZS) { + pinnedZones = names; + } else { + Annotations.of(scope).addWarning( + `[AgentCore AZs] Found only ${names.length} AgentCore-supported availability zone(s) in ` + + `${region} (need >=${MIN_AGENTCORE_AZS}); using CDK's default AZ selection. Pin zones ` + + `explicitly via context '${AGENTCORE_AZS_CONTEXT_KEY}' if the deploy hits unsupported zones.`, + ); + } + } catch (err) { + Annotations.of(scope).addWarning( + `[AgentCore AZs] Could not resolve AgentCore-supported availability zones for ${region} ` + + `(${err instanceof Error ? err.message : String(err)}); using CDK's default AZ selection. ` + + `Pin zones explicitly via context '${AGENTCORE_AZS_CONTEXT_KEY}' if the deploy hits unsupported zones.`, + ); + } + return pinnedZones; +} diff --git a/cdk/src/main.ts b/cdk/src/main.ts index c8bc2cc8..cc594df4 100644 --- a/cdk/src/main.ts +++ b/cdk/src/main.ts @@ -19,6 +19,7 @@ import { App, Aspects, Tags } from 'aws-cdk-lib'; import { AwsSolutionsChecks } from 'cdk-nag'; +import { resolveAgentCoreAzs } from './constructs/agentcore-azs'; import { AgentStack } from './stacks/agent'; // for development, use account/region from cdk cli @@ -27,53 +28,78 @@ const devEnv = { region: process.env.CDK_DEFAULT_REGION, }; -const app = new App(); +/** + * Synthesizes the app. Async because AgentCore-supported availability zones are + * resolved from the account's zone mapping at synth time (a live + * `DescribeAvailabilityZones` call) when a concrete account/region is bound. + * Env-agnostic synth and the validated context override never touch AWS. + */ +async function main(): Promise { + const app = new App(); + + Aspects.of(app).add(new AwsSolutionsChecks()); + + const stackName = app.node.tryGetContext('stackName') ?? 'backgroundagent-dev'; -Aspects.of(app).add(new AwsSolutionsChecks()); + // Auto-pin the VPC to AgentCore-supported AZs (or honor the validated + // `agentcore:availabilityZones` override). Undefined => CDK default selection. + const availabilityZones = await resolveAgentCoreAzs({ + scope: app, + account: devEnv.account, + region: devEnv.region, + }); -const stackName = app.node.tryGetContext('stackName') ?? 'backgroundagent-dev'; + const stack = new AgentStack( + app, + stackName, + { + env: devEnv, + availabilityZones, + description: 'ABCA Development Stack (uksb-wt64nei4u6)', + }, + ); -const stack = new AgentStack( - app, - stackName, - { - env: devEnv, - description: 'ABCA Development Stack (uksb-wt64nei4u6)', - }, -); + const computeType = app.node.tryGetContext('compute_type') ?? 'agentcore'; -const computeType = app.node.tryGetContext('compute_type') ?? 'agentcore'; + // Route53 Resolver resources where tag changes trigger replacement cascades. + // Config: treats ANY property change (including tags) as requiring replacement. + // Association: depends on Config's physical ID; if Config is replaced, the + // Association update fails on the one-association-per-VPC constraint. + const excludeResourceTypes = [ + 'AWS::Route53Resolver::ResolverQueryLoggingConfig', + 'AWS::Route53Resolver::ResolverQueryLoggingConfigAssociation', + ]; -// Route53 Resolver resources where tag changes trigger replacement cascades. -// Config: treats ANY property change (including tags) as requiring replacement. -// Association: depends on Config's physical ID; if Config is replaced, the -// Association update fails on the one-association-per-VPC constraint. -const excludeResourceTypes = [ - 'AWS::Route53Resolver::ResolverQueryLoggingConfig', - 'AWS::Route53Resolver::ResolverQueryLoggingConfigAssociation', -]; + Tags.of(stack).add('compute_type', computeType, { excludeResourceTypes }); -Tags.of(stack).add('compute_type', computeType, { excludeResourceTypes }); + const githubTagKeys = [ + 'sha', + 'ref', + 'ref-type', + 'actor', + 'head-ref', + 'base-ref', + 'pr-number', + 'run-id', + 'run-attempt', + 'event', + 'workflow', + 'repository', + 'clean', + ] as const; -const githubTagKeys = [ - 'sha', - 'ref', - 'ref-type', - 'actor', - 'head-ref', - 'base-ref', - 'pr-number', - 'run-id', - 'run-attempt', - 'event', - 'workflow', - 'repository', - 'clean', -] as const; + for (const key of githubTagKeys) { + const value = app.node.tryGetContext(`github:${key}`); + Tags.of(stack).add(`github:${key}`, value || 'none', { excludeResourceTypes }); + } -for (const key of githubTagKeys) { - const value = app.node.tryGetContext(`github:${key}`); - Tags.of(stack).add(`github:${key}`, value || 'none', { excludeResourceTypes }); + app.synth(); } -app.synth(); +// Surface any synth-time failure (e.g. a malformed `agentcore:availabilityZones` +// override) as a non-zero exit. `void` satisfies no-floating-promises; throwing +// from the handler triggers an unhandled rejection so the CDK CLI fails loudly. +void main().catch((err: unknown) => { + process.exitCode = 1; + throw err; +}); diff --git a/cdk/src/stacks/agent.ts b/cdk/src/stacks/agent.ts index d748c0d3..924cd750 100644 --- a/cdk/src/stacks/agent.ts +++ b/cdk/src/stacks/agent.ts @@ -69,8 +69,23 @@ const RUNTIME_SESSION_TIMEOUT_HOURS = 8; /** Index of the stage segment in a split API Gateway URL. */ const API_URL_STAGE_SEGMENT_INDEX = 3; +/** Properties for {@link AgentStack}. */ +export interface AgentStackProps extends StackProps { + /** + * Availability-zone *names* to pin the AgentCore VPC (and therefore the + * Runtime ENIs) into, so they land only in AgentCore-supported zones. + * + * Resolved in `main.ts` via `resolveAgentCoreAzs` — the validated + * `agentcore:availabilityZones` context override, else auto-selected from the + * account's supported zones when synth has a concrete account/region. Leave + * `undefined` (env-agnostic synth, unknown region) to keep CDK's default + * `maxAzs` selection. + */ + readonly availabilityZones?: string[]; +} + export class AgentStack extends Stack { - constructor(scope: Construct, id: string, props: StackProps = {}) { + constructor(scope: Construct, id: string, props: AgentStackProps = {}) { super(scope, id, props); // Build context is repo root (not agent/) so the Dockerfile can COPY @@ -200,23 +215,17 @@ export class AgentStack extends Stack { }, ]); - // Network isolation — VPC with restricted egress + // Network isolation — VPC with restricted egress. // 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; + // region, and AZ *names* are aliased per-account, so the default maxAzs + // selection can land the Runtime ENIs in an unsupported zone and fail the + // deploy. `props.availabilityZones` carries the AZ names resolved in + // main.ts (`resolveAgentCoreAzs`): the validated `agentcore:availabilityZones` + // override, else auto-selected from the account's AgentCore-supported zones + // when synth has a concrete account/region. Left undefined otherwise, so the + // construct keeps CDK's default AZ selection. See constructs/agentcore-azs.ts. const agentVpc = new AgentVpc(this, 'AgentVpc', { - ...(agentCoreAzs ? { availabilityZones: agentCoreAzs } : {}), + ...(props.availabilityZones ? { availabilityZones: props.availabilityZones } : {}), }); // DNS Firewall — domain-level egress filtering (observation mode for initial deployment) diff --git a/cdk/test/constructs/agent-vpc.test.ts b/cdk/test/constructs/agent-vpc.test.ts index 2fcc1c88..1c206662 100644 --- a/cdk/test/constructs/agent-vpc.test.ts +++ b/cdk/test/constructs/agent-vpc.test.ts @@ -163,6 +163,28 @@ describe('AgentVpc with custom props', () => { // 2 explicit AZs × 2 subnet types = 4 subnets template.resourceCountIs('AWS::EC2::Subnet', 4); + + // Subnets are pinned to the requested AZ *names* — the whole point of the + // fix (a wrong-count assertion would pass even if AZs were unpinned). + template.hasResourceProperties('AWS::EC2::Subnet', { AvailabilityZone: 'us-east-1b' }); + template.hasResourceProperties('AWS::EC2::Subnet', { AvailabilityZone: 'us-east-1c' }); + }); + + test('env-agnostic synth falls back to Fn::GetAZs (no pinning, no crash)', () => { + const app = new App(); + // No env → account/region are tokens. The production AgentStack synthesizes + // this way, so auto-pin is skipped and CDK selects AZs at deploy time. + const stack = new Stack(app, 'TestStack'); + new AgentVpc(stack, 'AgentVpc'); + const template = Template.fromStack(stack); + + // Default maxAzs (2) → 4 subnets; AZ resolved at deploy via Fn::GetAZs. + template.resourceCountIs('AWS::EC2::Subnet', 4); + template.hasResourceProperties('AWS::EC2::Subnet', { + AvailabilityZone: { + 'Fn::Select': Match.arrayWith([{ 'Fn::GetAZs': '' }]), + }, + }); }); test('availabilityZones with 3 zones creates 6 subnets', () => { diff --git a/cdk/test/constructs/agentcore-azs.test.ts b/cdk/test/constructs/agentcore-azs.test.ts new file mode 100644 index 00000000..1e2bde42 --- /dev/null +++ b/cdk/test/constructs/agentcore-azs.test.ts @@ -0,0 +1,199 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { App, Stack } from 'aws-cdk-lib'; +import { Annotations, Match } from 'aws-cdk-lib/assertions'; +import { + AGENTCORE_AZS_CONTEXT_KEY, + AGENTCORE_SUPPORTED_AZ_IDS, + AvailabilityZoneInfo, + resolveAgentCoreAzOverride, + resolveAgentCoreAzs, + selectSupportedAzNames, +} from '../../src/constructs/agentcore-azs'; + +function nodeWithContext(context?: Record) { + const app = new App({ context }); + return new Stack(app, 'TestStack').node; +} + +function scopeWithContext(context?: Record): Stack { + const app = new App({ context }); + return new Stack(app, 'TestStack'); +} + +// Realistic us-east-1 mapping: supported IDs are use1-az1/az2/az4. +const US_EAST_1_ZONES: AvailabilityZoneInfo[] = [ + { zoneName: 'us-east-1a', zoneId: 'use1-az2' }, // supported + { zoneName: 'us-east-1b', zoneId: 'use1-az4' }, // supported + { zoneName: 'us-east-1c', zoneId: 'use1-az6' }, // unsupported + { zoneName: 'us-east-1d', zoneId: 'use1-az1' }, // supported + { zoneName: 'us-east-1e', zoneId: 'use1-az3' }, // unsupported + { zoneName: 'us-east-1f', zoneId: 'use1-az5' }, // unsupported +]; + +describe('AGENTCORE_SUPPORTED_AZ_IDS', () => { + it('covers the documented AgentCore VPC regions', () => { + for (const region of [ + 'us-east-1', 'us-east-2', 'us-west-2', + 'ap-southeast-1', 'ap-southeast-2', 'ap-south-1', 'ap-northeast-1', + 'eu-west-1', 'eu-central-1', + ]) { + expect(AGENTCORE_SUPPORTED_AZ_IDS[region]).toBeDefined(); + } + }); + + it('lists at least two unique physical zone IDs per region', () => { + for (const [region, ids] of Object.entries(AGENTCORE_SUPPORTED_AZ_IDS)) { + expect(ids.length).toBeGreaterThanOrEqual(2); + expect(new Set(ids).size).toBe(ids.length); + for (const id of ids) { + // Physical zone-ID shape, e.g. use1-az1 (NOT a zone name like us-east-1a). + expect(id).toMatch(/^[a-z]+[0-9]+-az[0-9]+$/); + } + expect(region).toMatch(/^[a-z]{2}-[a-z]+-\d$/); + } + }); +}); + +describe('resolveAgentCoreAzOverride', () => { + it('returns undefined when the context key is unset', () => { + expect(resolveAgentCoreAzOverride(nodeWithContext())).toBeUndefined(); + }); + + it('returns the validated array when provided', () => { + const override = ['us-east-1b', 'us-east-1c']; + expect( + resolveAgentCoreAzOverride(nodeWithContext({ [AGENTCORE_AZS_CONTEXT_KEY]: override })), + ).toEqual(override); + }); + + it('throws on a non-array override (typo guard)', () => { + expect(() => + resolveAgentCoreAzOverride(nodeWithContext({ [AGENTCORE_AZS_CONTEXT_KEY]: 'us-east-1b' })), + ).toThrow(/must be a JSON array/); + }); + + it('throws on a non-string / empty entry', () => { + expect(() => + resolveAgentCoreAzOverride(nodeWithContext({ [AGENTCORE_AZS_CONTEXT_KEY]: ['us-east-1b', ''] })), + ).toThrow(/non-empty availability-zone-name/); + }); + + it('throws when fewer than two zones are listed (HA guidance)', () => { + expect(() => + resolveAgentCoreAzOverride(nodeWithContext({ [AGENTCORE_AZS_CONTEXT_KEY]: ['us-east-1b'] })), + ).toThrow(/at least 2 zones/); + }); +}); + +describe('selectSupportedAzNames', () => { + it('returns the zone names whose IDs are AgentCore-supported', () => { + expect(selectSupportedAzNames('us-east-1', US_EAST_1_ZONES)).toEqual([ + 'us-east-1a', + 'us-east-1b', + 'us-east-1d', + ]); + }); + + it('returns an empty array for a region with no known constraint', () => { + expect(selectSupportedAzNames('eu-north-1', US_EAST_1_ZONES)).toEqual([]); + }); + + it('returns an empty array when no account zone matches the supported set', () => { + const zones: AvailabilityZoneInfo[] = [{ zoneName: 'us-east-1c', zoneId: 'use1-az6' }]; + expect(selectSupportedAzNames('us-east-1', zones)).toEqual([]); + }); +}); + +describe('resolveAgentCoreAzs', () => { + it('returns the validated override without calling the AZ lookup', async () => { + const describeAzs = jest.fn, [string]>(); + const result = await resolveAgentCoreAzs({ + scope: scopeWithContext({ [AGENTCORE_AZS_CONTEXT_KEY]: ['us-east-1b', 'us-east-1c'] }), + account: '123456789012', + region: 'us-east-1', + describeAzs, + }); + expect(result).toEqual(['us-east-1b', 'us-east-1c']); + expect(describeAzs).not.toHaveBeenCalled(); + }); + + it('rethrows a malformed override (fails synth loudly)', async () => { + await expect( + resolveAgentCoreAzs({ + scope: scopeWithContext({ [AGENTCORE_AZS_CONTEXT_KEY]: 'us-east-1b' }), + account: '123456789012', + region: 'us-east-1', + }), + ).rejects.toThrow(/must be a JSON array/); + }); + + it('falls back (undefined) for env-agnostic synth without touching AWS', async () => { + const describeAzs = jest.fn, [string]>(); + expect(await resolveAgentCoreAzs({ scope: scopeWithContext(), account: undefined, region: undefined, describeAzs })) + .toBeUndefined(); + expect(await resolveAgentCoreAzs({ scope: scopeWithContext(), account: '123456789012', region: undefined, describeAzs })) + .toBeUndefined(); + expect(describeAzs).not.toHaveBeenCalled(); + }); + + it('falls back (undefined) for a region with no known constraint', async () => { + const describeAzs = jest.fn, [string]>(); + const result = await resolveAgentCoreAzs({ + scope: scopeWithContext(), + account: '123456789012', + region: 'eu-north-1', + describeAzs, + }); + expect(result).toBeUndefined(); + expect(describeAzs).not.toHaveBeenCalled(); + }); + + it('auto-pins to the account supported zone names for a concrete env', async () => { + const describeAzs = jest.fn, [string]>().mockResolvedValue(US_EAST_1_ZONES); + const result = await resolveAgentCoreAzs({ + scope: scopeWithContext(), + account: '123456789012', + region: 'us-east-1', + describeAzs, + }); + expect(result).toEqual(['us-east-1a', 'us-east-1b', 'us-east-1d']); + expect(describeAzs).toHaveBeenCalledWith('us-east-1'); + }); + + it('warns and falls back when fewer than two supported zones are found', async () => { + const scope = scopeWithContext(); + const describeAzs = jest.fn, [string]>().mockResolvedValue([ + { zoneName: 'us-east-1a', zoneId: 'use1-az1' }, + { zoneName: 'us-east-1c', zoneId: 'use1-az6' }, + ]); + const result = await resolveAgentCoreAzs({ scope, account: '123456789012', region: 'us-east-1', describeAzs }); + expect(result).toBeUndefined(); + Annotations.fromStack(scope).hasWarning('*', Match.stringLikeRegexp('AgentCore AZs')); + }); + + it('warns and falls back when the AZ lookup fails', async () => { + const scope = scopeWithContext(); + const describeAzs = jest.fn, [string]>().mockRejectedValue(new Error('DescribeAZ boom')); + const result = await resolveAgentCoreAzs({ scope, account: '123456789012', region: 'us-east-1', describeAzs }); + expect(result).toBeUndefined(); + Annotations.fromStack(scope).hasWarning('*', Match.stringLikeRegexp('AgentCore AZs')); + }); +}); diff --git a/docs/guides/DEPLOYMENT_GUIDE.md b/docs/guides/DEPLOYMENT_GUIDE.md index a32b0922..c27ec196 100644 --- a/docs/guides/DEPLOYMENT_GUIDE.md +++ b/docs/guides/DEPLOYMENT_GUIDE.md @@ -163,6 +163,31 @@ Triggers via `workflow_run` when `build.yml` completes successfully. The pipelin ## Known deployment issues +### AgentCore unsupported Availability Zones + +**Affects:** Fresh deploys in accounts whose default Availability Zones don't line up with the zones AgentCore supports for the region. + +**Symptom:** The `AWS::BedrockAgentCore::Runtime` resource fails to stabilize (`NotStabilized` — "subnets are in unsupported availability zones") and the stack rolls back. + +**Root cause:** AgentCore Runtime only places its network interfaces in a subset of each region's Availability Zones, published as physical **zone IDs** (e.g. `use1-az1`, `use1-az2`, `use1-az4` for `us-east-1`). Zone IDs are stable across accounts, but zone *names* (`us-east-1a`) are aliased per-account — so `us-east-1a` can map to a different physical zone in your account than in another. Left to its default, CDK picks zones by name and can land the Runtime subnets in an unsupported zone. See the AWS [Supported Availability Zones](https://aws.github.io/bedrock-agentcore-starter-toolkit/user-guide/security/agentcore-vpc/#supported-availability-zones) table for the per-region set. + +**Default behavior (auto-pin):** When the stack is synthesized with a concrete account and region — the usual local `cdk deploy` / `mise //cdk:deploy` path — it resolves your account's zone name-to-ID mapping (`ec2:DescribeAvailabilityZones`) and pins the VPC to the AZ *names* whose IDs are AgentCore-supported. No action needed for supported regions. + +**When you must pin manually:** Env-agnostic synthesis skips auto-pin (there's no bound account to resolve the mapping) — this includes the CI-built artifact deployed by `deploy.yml`, and any region not yet in the built-in supported-AZ map. In those cases, set the `agentcore:availabilityZones` context override: + +1. Discover your account's zone name-to-ID mapping: + ```bash + aws ec2 describe-availability-zones --region \ + --query 'AvailabilityZones[].[ZoneName,ZoneId]' --output text + ``` +2. Pick the zone **names** whose **IDs** are in the AgentCore-supported set for the region (at least two, for high availability). +3. Set the override in `cdk/cdk.context.json` (or via `-c`), then redeploy: + ```json + { "agentcore:availabilityZones": ["us-east-1b", "us-east-1c"] } + ``` + +The override is validated at synth time: a non-array, an empty/non-string entry, or fewer than two zones fails the synth with a message naming the key and expected shape. The supported-AZ map and resolution logic live in `cdk/src/constructs/agentcore-azs.ts`. + ### DNS Query Log Config replacement cascade (upgrading from pre-v0.5) **Affects:** Stacks deployed *before* the tag-exclusion fix ([#222](https://github.com/aws-samples/sample-autonomous-cloud-coding-agents/pull/222)). Stacks created after this fix are not affected. diff --git a/docs/guides/QUICK_START.mdx b/docs/guides/QUICK_START.mdx index 45e85151..ee0d33ca 100644 --- a/docs/guides/QUICK_START.mdx +++ b/docs/guides/QUICK_START.mdx @@ -72,7 +72,7 @@ If `lib/bin/bgagent.js` is missing, run `mise run build` from `cli/` (or repeat ::: -> **Note:** `mise run build` includes CDK synthesis, which queries AWS for availability zones. Your active AWS credentials must have at least `ec2:DescribeAvailabilityZones` permission, or the build will fail. If you use named profiles, make sure `AWS_PROFILE` is set before running the build. +> **Note:** `mise run build` includes CDK synthesis, which queries AWS for availability zones. Your active AWS credentials must have at least `ec2:DescribeAvailabilityZones` permission, or the build will fail. If you use named profiles, make sure `AWS_PROFILE` is set before running the build. A local deploy uses that same call to auto-pin the VPC to AgentCore-supported zones; if you deploy from an env-agnostic artifact or hit "unsupported availability zones", set the `agentcore:availabilityZones` override — see [Known deployment issues](./DEPLOYMENT_GUIDE.md#agentcore-unsupported-availability-zones). ## Step 2 - Prepare a repository @@ -514,6 +514,7 @@ Here is what the platform did after you ran `node lib/bin/bgagent.js submit`: | CDK deploy prompts for approval and hangs | Non-interactive terminal (CI/CD, scripts) | Pass `--require-approval never` to `cdk deploy` (Step 3 uses it) or use an interactive terminal | | Deploy rolled back; can't redeploy (`ROLLBACK_COMPLETE`) | A first-create failure leaves the stack un-updatable | `mise //cdk:destroy` (or delete the stack), then deploy again. Do **not** force-delete past stuck VPC resources — it orphans the VPC, and VPCs are quota-capped per Region | | Stack stuck in `DELETE_FAILED` on a security group / subnet | AgentCore's service-managed (Hyperplane) ENIs reclaim asynchronously after the runtime is gone | Wait ~20–40 min for AWS to release the ENIs, then retry `mise //cdk:destroy`. You cannot force-detach an `amazon-aws`-owned ENI | +| Runtime rollback: `NotStabilized` / "subnets are in unsupported availability zones" | VPC subnets landed in AZs AgentCore doesn't support (auto-pin skipped for env-agnostic synth, or region not in the built-in map) | Set the `agentcore:availabilityZones` override to supported zone names — see [Known deployment issues](./DEPLOYMENT_GUIDE.md#agentcore-unsupported-availability-zones) | | `put-secret-value` returns double-dot endpoint | `REGION` variable is empty | Set `REGION=us-east-1` (or your actual region) before running the command | | Model / Bedrock errors in logs (`not available on your bedrock`, zero tokens) | Model not entitled for the account or Region, wrong `modelId` shape, or missing Marketplace / FTU steps | Follow **Amazon Bedrock before your first task** above; confirm [model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) and use an [inference profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-use.html) ID such as `us.anthropic.claude-sonnet-4-6` where required; keep `grantInvoke` in `agent.ts` aligned with that model | | `REPO_NOT_ONBOARDED` on task submit | Blueprint `repo` does not match what you passed to the CLI | Confirm `BLUEPRINT_REPO`, CDK context `blueprintRepo`, or the `repo` prop on the `Blueprint` in `cdk/src/stacks/agent.ts` resolves to exactly the same `owner/repo` you pass to the CLI | diff --git a/docs/src/content/docs/getting-started/Deployment-guide.md b/docs/src/content/docs/getting-started/Deployment-guide.md index 01f6e2ce..c36653b1 100644 --- a/docs/src/content/docs/getting-started/Deployment-guide.md +++ b/docs/src/content/docs/getting-started/Deployment-guide.md @@ -167,6 +167,31 @@ Triggers via `workflow_run` when `build.yml` completes successfully. The pipelin ## Known deployment issues +### AgentCore unsupported Availability Zones + +**Affects:** Fresh deploys in accounts whose default Availability Zones don't line up with the zones AgentCore supports for the region. + +**Symptom:** The `AWS::BedrockAgentCore::Runtime` resource fails to stabilize (`NotStabilized` — "subnets are in unsupported availability zones") and the stack rolls back. + +**Root cause:** AgentCore Runtime only places its network interfaces in a subset of each region's Availability Zones, published as physical **zone IDs** (e.g. `use1-az1`, `use1-az2`, `use1-az4` for `us-east-1`). Zone IDs are stable across accounts, but zone *names* (`us-east-1a`) are aliased per-account — so `us-east-1a` can map to a different physical zone in your account than in another. Left to its default, CDK picks zones by name and can land the Runtime subnets in an unsupported zone. See the AWS [Supported Availability Zones](https://aws.github.io/bedrock-agentcore-starter-toolkit/user-guide/security/agentcore-vpc/#supported-availability-zones) table for the per-region set. + +**Default behavior (auto-pin):** When the stack is synthesized with a concrete account and region — the usual local `cdk deploy` / `mise //cdk:deploy` path — it resolves your account's zone name-to-ID mapping (`ec2:DescribeAvailabilityZones`) and pins the VPC to the AZ *names* whose IDs are AgentCore-supported. No action needed for supported regions. + +**When you must pin manually:** Env-agnostic synthesis skips auto-pin (there's no bound account to resolve the mapping) — this includes the CI-built artifact deployed by `deploy.yml`, and any region not yet in the built-in supported-AZ map. In those cases, set the `agentcore:availabilityZones` context override: + +1. Discover your account's zone name-to-ID mapping: + ```bash + aws ec2 describe-availability-zones --region \ + --query 'AvailabilityZones[].[ZoneName,ZoneId]' --output text + ``` +2. Pick the zone **names** whose **IDs** are in the AgentCore-supported set for the region (at least two, for high availability). +3. Set the override in `cdk/cdk.context.json` (or via `-c`), then redeploy: + ```json + { "agentcore:availabilityZones": ["us-east-1b", "us-east-1c"] } + ``` + +The override is validated at synth time: a non-array, an empty/non-string entry, or fewer than two zones fails the synth with a message naming the key and expected shape. The supported-AZ map and resolution logic live in `cdk/src/constructs/agentcore-azs.ts`. + ### DNS Query Log Config replacement cascade (upgrading from pre-v0.5) **Affects:** Stacks deployed *before* the tag-exclusion fix ([#222](https://github.com/aws-samples/sample-autonomous-cloud-coding-agents/pull/222)). Stacks created after this fix are not affected. diff --git a/docs/src/content/docs/getting-started/Quick-start.mdx b/docs/src/content/docs/getting-started/Quick-start.mdx index e12fb871..a1a32ae0 100644 --- a/docs/src/content/docs/getting-started/Quick-start.mdx +++ b/docs/src/content/docs/getting-started/Quick-start.mdx @@ -72,7 +72,7 @@ If `lib/bin/bgagent.js` is missing, run `mise run build` from `cli/` (or repeat ::: -> **Note:** `mise run build` includes CDK synthesis, which queries AWS for availability zones. Your active AWS credentials must have at least `ec2:DescribeAvailabilityZones` permission, or the build will fail. If you use named profiles, make sure `AWS_PROFILE` is set before running the build. +> **Note:** `mise run build` includes CDK synthesis, which queries AWS for availability zones. Your active AWS credentials must have at least `ec2:DescribeAvailabilityZones` permission, or the build will fail. If you use named profiles, make sure `AWS_PROFILE` is set before running the build. A local deploy uses that same call to auto-pin the VPC to AgentCore-supported zones; if you deploy from an env-agnostic artifact or hit "unsupported availability zones", set the `agentcore:availabilityZones` override — see [Known deployment issues](/sample-autonomous-cloud-coding-agents/getting-started/deployment-guide#agentcore-unsupported-availability-zones). ## Step 2 - Prepare a repository @@ -514,6 +514,7 @@ Here is what the platform did after you ran `node lib/bin/bgagent.js submit`: | CDK deploy prompts for approval and hangs | Non-interactive terminal (CI/CD, scripts) | Pass `--require-approval never` to `cdk deploy` (Step 3 uses it) or use an interactive terminal | | Deploy rolled back; can't redeploy (`ROLLBACK_COMPLETE`) | A first-create failure leaves the stack un-updatable | `mise //cdk:destroy` (or delete the stack), then deploy again. Do **not** force-delete past stuck VPC resources — it orphans the VPC, and VPCs are quota-capped per Region | | Stack stuck in `DELETE_FAILED` on a security group / subnet | AgentCore's service-managed (Hyperplane) ENIs reclaim asynchronously after the runtime is gone | Wait ~20–40 min for AWS to release the ENIs, then retry `mise //cdk:destroy`. You cannot force-detach an `amazon-aws`-owned ENI | +| Runtime rollback: `NotStabilized` / "subnets are in unsupported availability zones" | VPC subnets landed in AZs AgentCore doesn't support (auto-pin skipped for env-agnostic synth, or region not in the built-in map) | Set the `agentcore:availabilityZones` override to supported zone names — see [Known deployment issues](/sample-autonomous-cloud-coding-agents/getting-started/deployment-guide#agentcore-unsupported-availability-zones) | | `put-secret-value` returns double-dot endpoint | `REGION` variable is empty | Set `REGION=us-east-1` (or your actual region) before running the command | | Model / Bedrock errors in logs (`not available on your bedrock`, zero tokens) | Model not entitled for the account or Region, wrong `modelId` shape, or missing Marketplace / FTU steps | Follow **Amazon Bedrock before your first task** above; confirm [model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) and use an [inference profile](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-use.html) ID such as `us.anthropic.claude-sonnet-4-6` where required; keep `grantInvoke` in `agent.ts` aligned with that model | | `REPO_NOT_ONBOARDED` on task submit | Blueprint `repo` does not match what you passed to the CLI | Confirm `BLUEPRINT_REPO`, CDK context `blueprintRepo`, or the `repo` prop on the `Blueprint` in `cdk/src/stacks/agent.ts` resolves to exactly the same `owner/repo` you pass to the CLI | diff --git a/yarn.lock b/yarn.lock index 04b6df25..71cda37c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -478,6 +478,21 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@aws-sdk/client-ec2@^3.1078.0": + version "3.1087.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-ec2/-/client-ec2-3.1087.0.tgz#eb234305bbaada457aa7d75d75956c3e210df672" + integrity sha512-4QcrDvc5mBA3mfhWTTiv32ZIYUhVdxT89b1faimTfU29B4e7Kb2//+WtXN1hbIx6W+tZZ/wriwylapRfL2AhEQ== + dependencies: + "@aws-sdk/core" "^3.975.2" + "@aws-sdk/credential-provider-node" "^3.972.68" + "@aws-sdk/middleware-sdk-ec2" "^3.972.46" + "@aws-sdk/types" "^3.974.1" + "@smithy/core" "^3.29.3" + "@smithy/fetch-http-handler" "^5.6.5" + "@smithy/node-http-handler" "^4.9.5" + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@aws-sdk/client-ecs@^3.1078.0": version "3.1081.0" resolved "https://registry.yarnpkg.com/@aws-sdk/client-ecs/-/client-ecs-3.1081.0.tgz#05f57706640d9f3e6853fbb5d302c7940b91f32a" @@ -565,6 +580,20 @@ bowser "^2.11.0" tslib "^2.6.2" +"@aws-sdk/core@^3.975.2": + version "3.975.2" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.975.2.tgz#743f5ccf805721da3fd330155f5e374e8dbc8969" + integrity sha512-iyeXwziyjJpixq5OmhsIyrSWx8vwcI7gDo4yRUC3EP7NQtOo9iAJiIEc3G+/HkhtNXqOhofiCK7Lc34Sq+fJWg== + dependencies: + "@aws-sdk/types" "^3.974.1" + "@aws-sdk/xml-builder" "^3.972.35" + "@aws/lambda-invoke-store" "^0.3.0" + "@smithy/core" "^3.29.3" + "@smithy/signature-v4" "^5.6.3" + "@smithy/types" "^4.16.1" + bowser "^2.11.0" + tslib "^2.6.2" + "@aws-sdk/credential-provider-env@^3.972.55": version "3.972.55" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.55.tgz#1129acf8860db362a30a02a531387fd399448268" @@ -576,6 +605,17 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@aws-sdk/credential-provider-env@^3.972.58": + version "3.972.58" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.58.tgz#74592896db48dc9fec7852ba0e842c06c13f0f52" + integrity sha512-vyGtvK1rY940eq7JT0yIGKuZ+2kpPSJcHibSvGlit5oiMFDamzC7cxBGLl4FLnd6suihMXDI2FSF2dL6TmBqPA== + dependencies: + "@aws-sdk/core" "^3.975.2" + "@aws-sdk/types" "^3.974.1" + "@smithy/core" "^3.29.3" + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@aws-sdk/credential-provider-http@^3.972.57": version "3.972.57" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.57.tgz#4f73bb2f0e03525ab11e001ac18c39cb120dd14c" @@ -589,6 +629,19 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@aws-sdk/credential-provider-http@^3.972.60": + version "3.972.60" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.60.tgz#da1d2646fb1bee732436b75ab5cda18f06554727" + integrity sha512-g9b9YzDrD5pcKiPBJfCSXRfFMrA39eR0guUhZ5SRm+7vMAVc43+effxbcamxBjSd5bUhrdKo5te/yQuWurLXLA== + dependencies: + "@aws-sdk/core" "^3.975.2" + "@aws-sdk/types" "^3.974.1" + "@smithy/core" "^3.29.3" + "@smithy/fetch-http-handler" "^5.6.5" + "@smithy/node-http-handler" "^4.9.5" + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@aws-sdk/credential-provider-ini@^3.972.62": version "3.972.62" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.62.tgz#a2ca7fc7a1899c0a4a38e501baa666cf1449f2e9" @@ -608,6 +661,25 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@aws-sdk/credential-provider-ini@^3.973.2": + version "3.973.2" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.2.tgz#c66b59528b985baeb620926a4582f8d821f7f5f4" + integrity sha512-Yr7yxNyQ8aHt9Ww0RPFUZx+xiem+vl7vuwhP0tniTijoesJNV5jou9HCgVpI0GEPAF+89TkOvilE5uRrZJnjaw== + dependencies: + "@aws-sdk/core" "^3.975.2" + "@aws-sdk/credential-provider-env" "^3.972.58" + "@aws-sdk/credential-provider-http" "^3.972.60" + "@aws-sdk/credential-provider-login" "^3.972.64" + "@aws-sdk/credential-provider-process" "^3.972.58" + "@aws-sdk/credential-provider-sso" "^3.973.2" + "@aws-sdk/credential-provider-web-identity" "^3.972.64" + "@aws-sdk/nested-clients" "^3.997.32" + "@aws-sdk/types" "^3.974.1" + "@smithy/core" "^3.29.3" + "@smithy/credential-provider-imds" "^4.4.7" + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@aws-sdk/credential-provider-login@^3.972.61": version "3.972.61" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.61.tgz#f5ba696bae9af3505ae5f3e2a70d7e216d0d37f6" @@ -620,6 +692,18 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@aws-sdk/credential-provider-login@^3.972.64": + version "3.972.64" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.64.tgz#d323b16bf9857f2248b234e5f83e033f3981d50d" + integrity sha512-YQoSI4d6kXvoenoG/0Jv/PqaAuukHzGmGXGyHBQYeEUNsYovlNAn/Sw1wp/WQbhcQ3HsEMGgjEahvD3igz6ecQ== + dependencies: + "@aws-sdk/core" "^3.975.2" + "@aws-sdk/nested-clients" "^3.997.32" + "@aws-sdk/types" "^3.974.1" + "@smithy/core" "^3.29.3" + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@aws-sdk/credential-provider-node@^3.972.61", "@aws-sdk/credential-provider-node@^3.972.64": version "3.972.64" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.64.tgz#282456c24bad616faefe2a6c68701913f8289c10" @@ -637,6 +721,23 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@aws-sdk/credential-provider-node@^3.972.68": + version "3.972.68" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.68.tgz#300fd3c97c01f9669c34dc2def1f7e29d3b0ff7c" + integrity sha512-4akjzW9CjorByYfqXBXmYUh/h7Io3U4DtVgGGh9TQraZ7ZlyJqNyHwDRGiUFnHD+BTOeTbCesCa4sJaK7BGZ7A== + dependencies: + "@aws-sdk/credential-provider-env" "^3.972.58" + "@aws-sdk/credential-provider-http" "^3.972.60" + "@aws-sdk/credential-provider-ini" "^3.973.2" + "@aws-sdk/credential-provider-process" "^3.972.58" + "@aws-sdk/credential-provider-sso" "^3.973.2" + "@aws-sdk/credential-provider-web-identity" "^3.972.64" + "@aws-sdk/types" "^3.974.1" + "@smithy/core" "^3.29.3" + "@smithy/credential-provider-imds" "^4.4.7" + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@aws-sdk/credential-provider-process@^3.972.55": version "3.972.55" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.55.tgz#bebd30d0065ca34f64e14a870f54a09f23cf11e2" @@ -648,6 +749,17 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@aws-sdk/credential-provider-process@^3.972.58": + version "3.972.58" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.58.tgz#ac55b2271ea5672e8b00e390f6b00f9f2433ad2b" + integrity sha512-1nYitRCaDmXWUrpBJt6WlcGjLx1JVsMY8rlYuHHsTYTSaYikbixYdQSyINN2VYq1F798uTO9qHAzytL25M8g3A== + dependencies: + "@aws-sdk/core" "^3.975.2" + "@aws-sdk/types" "^3.974.1" + "@smithy/core" "^3.29.3" + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@aws-sdk/credential-provider-sso@^3.972.61": version "3.972.61" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.61.tgz#8c15846c65d2f03bfca3347626e1fcd1a0f40726" @@ -661,6 +773,19 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@aws-sdk/credential-provider-sso@^3.973.2": + version "3.973.2" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.2.tgz#bdf55f712b8ddf70b18be483a4c8432c6e2e876c" + integrity sha512-pjMLaLU/JZi5lVfmR14V1OZqRBTuMHf6AwGNZA0K9hK+JKtO3jcLBarfD8iq5oc8cSowvc/9R32sqMVXZPo6xQ== + dependencies: + "@aws-sdk/core" "^3.975.2" + "@aws-sdk/nested-clients" "^3.997.32" + "@aws-sdk/token-providers" "3.1087.0" + "@aws-sdk/types" "^3.974.1" + "@smithy/core" "^3.29.3" + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@aws-sdk/credential-provider-web-identity@^3.972.61": version "3.972.61" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.61.tgz#1938cc425e6156acd673484f10b437c5c4c85158" @@ -673,6 +798,18 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@aws-sdk/credential-provider-web-identity@^3.972.64": + version "3.972.64" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.64.tgz#fdeee7ac25a1d9e6c791184ba93eb18c74eeea34" + integrity sha512-7Buc7p0OvDHW7iBsu4b+YdS0WnaFBDGKDfbVQqaac9dkWiSiUtIoarBDsA1RmOVXZijaZJDoHJFIQiicQvWRlQ== + dependencies: + "@aws-sdk/core" "^3.975.2" + "@aws-sdk/nested-clients" "^3.997.32" + "@aws-sdk/types" "^3.974.1" + "@smithy/core" "^3.29.3" + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@aws-sdk/dynamodb-codec@^3.973.26", "@aws-sdk/dynamodb-codec@^3.973.29": version "3.973.29" resolved "https://registry.yarnpkg.com/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.973.29.tgz#ea72996b2e1f2c687254c69c357fa4278e42f1da" @@ -744,6 +881,18 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@aws-sdk/middleware-sdk-ec2@^3.972.46": + version "3.972.46" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-ec2/-/middleware-sdk-ec2-3.972.46.tgz#9fb2f43ab8033084ee9684785ec4d81b9fac5db6" + integrity sha512-H73wH+QnWnXHFKIRpewMkLLczp+Fkmb/bkMutW13ABb3xLKbfPU/TTdOADZ+f0ZE+RbQ6K1y+PfjEziL2cRxPA== + dependencies: + "@aws-sdk/core" "^3.975.2" + "@aws-sdk/types" "^3.974.1" + "@smithy/core" "^3.29.3" + "@smithy/signature-v4" "^5.6.3" + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@aws-sdk/middleware-sdk-s3@^3.972.60": version "3.972.60" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.60.tgz#2aef54e9f4b352268f2dc9a0b3fe628795826512" @@ -783,6 +932,20 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@aws-sdk/nested-clients@^3.997.32": + version "3.997.32" + resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.997.32.tgz#3ddb2ccfc5f8a50041eedc5981c441dd984f419b" + integrity sha512-6Yj2fr9XF67cndITea48rchTdVr3VGx6PN47bIKNinJAjLkmaIlz/4EBPCgJ8UmhVopiXmeAuPLI3+DXDDbMhQ== + dependencies: + "@aws-sdk/core" "^3.975.2" + "@aws-sdk/signature-v4-multi-region" "^3.996.40" + "@aws-sdk/types" "^3.974.1" + "@smithy/core" "^3.29.3" + "@smithy/fetch-http-handler" "^5.6.5" + "@smithy/node-http-handler" "^4.9.5" + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@aws-sdk/s3-presigned-post@^3.1078.0": version "3.1081.0" resolved "https://registry.yarnpkg.com/@aws-sdk/s3-presigned-post/-/s3-presigned-post-3.1081.0.tgz#ed27961af8e772e23745e7ff34f34083bc69c291" @@ -818,6 +981,16 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@aws-sdk/signature-v4-multi-region@^3.996.40": + version "3.996.40" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.40.tgz#f8caa07c2463e24d7a13bf2d5a77d7083dc0ecda" + integrity sha512-wrGZ/authosokclY1DXsiWT/1WjfCI22FuZGgdcilF+XLTXs5dCjAtiFYSPsEToZkbm3Lj2YP8PoWg0yoMNu0g== + dependencies: + "@aws-sdk/types" "^3.974.1" + "@smithy/signature-v4" "^5.6.3" + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@aws-sdk/token-providers@3.1078.0": version "3.1078.0" resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.1078.0.tgz#554be2f2c42f21191f31aead718bcedf2047926d" @@ -842,6 +1015,18 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@aws-sdk/token-providers@3.1087.0": + version "3.1087.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.1087.0.tgz#0cd1b7c41a092340a287a2d009027ef88bc7635b" + integrity sha512-umM+qNq16f2fH+VLM5MqXW4ORNQAjk+TOSto73xbUHcKaU41L48j786r3UWQYlejeJk37NlvRYgxBT+MBkfaYQ== + dependencies: + "@aws-sdk/core" "^3.975.2" + "@aws-sdk/nested-clients" "^3.997.32" + "@aws-sdk/types" "^3.974.1" + "@smithy/core" "^3.29.3" + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@aws-sdk/types@^3.222.0", "@aws-sdk/types@^3.973.15": version "3.973.15" resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.973.15.tgz#98a4860bed33c32c7088924d0ab52f9eabbdf7c3" @@ -850,6 +1035,14 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@aws-sdk/types@^3.974.1": + version "3.974.1" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.974.1.tgz#ac3f8943d2a04c1ced470452494b7723b926714e" + integrity sha512-W0IQZR0eaBqlBFIIofMapaWkw1W0U+Xi4dvW+BqwmCEMd8Ng2U6IhkxuPSjMVnR8klLjfuS9PeZWUl1N6UaZdg== + dependencies: + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@aws-sdk/util-dynamodb@^3.996.5": version "3.996.5" resolved "https://registry.yarnpkg.com/@aws-sdk/util-dynamodb/-/util-dynamodb-3.996.5.tgz#2ad647e1532c20c76570a878e49f058a8d21e012" @@ -865,6 +1058,14 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@aws-sdk/xml-builder@^3.972.35": + version "3.972.35" + resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.972.35.tgz#8affa66cb8836168b66dd3a76eeebb8165ed9672" + integrity sha512-pXzaWe3evZhjxDXAlMnqISe/XefTCGwBJG4nFTXaWSgAnMkqPEhxEPqJNhhpGesEvKFhvNpnozJJ4GTL11bRYw== + dependencies: + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@aws/durable-execution-sdk-js@^2.0.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@aws/durable-execution-sdk-js/-/durable-execution-sdk-js-2.1.0.tgz#a020f8f3eae0fd3b577ad55397e454241a9e3029" @@ -2744,6 +2945,14 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@smithy/core@^3.29.3", "@smithy/core@^3.29.4": + version "3.29.4" + resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.29.4.tgz#ff4f9ba70e0276c892cd3dad5dbef54fc4945464" + integrity sha512-G1GRglAabzEhqghJMBAd54FkRS7SAFGHEwbhcI9r+O+LIMuFsLyXkLZkCoFSgAglRu8s/URVXJB0hglq3ZipIg== + dependencies: + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@smithy/credential-provider-imds@^4.4.5": version "4.4.6" resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.6.tgz#048ad961282016fc2d46d457f43beac8ca8f0e24" @@ -2753,6 +2962,15 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@smithy/credential-provider-imds@^4.4.7": + version "4.4.9" + resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.9.tgz#7ae40b9e1faab4256197335353463b3788591e13" + integrity sha512-2nfV4qRKiYeXU4zD2vvSCfg5dfp/BuhrM73vt7q9gzBhxs4rbPxXY21wo+kyI3bRmXcEGRnCLTaW8O437jzHIg== + dependencies: + "@smithy/core" "^3.29.4" + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@smithy/fetch-http-handler@^5.6.2": version "5.6.3" resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.3.tgz#7b4aec28f89bd1e3aa00dfae0d0bd30079c58341" @@ -2762,6 +2980,15 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@smithy/fetch-http-handler@^5.6.5": + version "5.6.6" + resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.6.tgz#eee36126fb4f9cd4aee278ae8f20fb6a83f02b07" + integrity sha512-NHLgAlORUFZjn5ZfhYuyyKMlXA1WLYOdGxEhyNxrPpbJzoacGbl0chn1lN2KiZ8mpNVk0tV5607CSYlYs/OFgw== + dependencies: + "@smithy/core" "^3.29.4" + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@smithy/is-array-buffer@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz#f84f0d9f9a36601a9ca9381688bd1b726fd39111" @@ -2778,6 +3005,15 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@smithy/node-http-handler@^4.9.5": + version "4.9.6" + resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.9.6.tgz#a1756af48d8f1250aaa7225e74a98d95900383fb" + integrity sha512-odd+HYx3OLcXRSEz0ZeF3JQdSYdK8QnRgA2N87cPW7coWIbKfRk7a9VQjfeWQLqnzrDLk23KMEn46p8N7M/JFg== + dependencies: + "@smithy/core" "^3.29.4" + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@smithy/protocol-http@^5.5.5": version "5.5.6" resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-5.5.6.tgz#62db7e22b4446168ad091b8fe90d71b05c0fd8bd" @@ -2795,6 +3031,15 @@ "@smithy/types" "^4.15.1" tslib "^2.6.2" +"@smithy/signature-v4@^5.6.3": + version "5.6.5" + resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.6.5.tgz#163dce1523903d251c86da30a0725e6801072d20" + integrity sha512-MO5VEhwVl0BN7xVoVeNrZfiUFoQtqxUbgl6/RwOTlMMxCSjblG8twSrVTwz3J4w9WZxd2rBfBAUXjH77agspBg== + dependencies: + "@smithy/core" "^3.29.4" + "@smithy/types" "^4.16.1" + tslib "^2.6.2" + "@smithy/types@^4.15.1": version "4.15.1" resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.15.1.tgz#cdad0fdf4c5d1f5788dc21a3f1fb6af523d79da0" @@ -2802,6 +3047,13 @@ dependencies: tslib "^2.6.2" +"@smithy/types@^4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.16.1.tgz#19e199c234829a51c085caf63f0bb17bb80187e4" + integrity sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg== + dependencies: + tslib "^2.6.2" + "@smithy/util-buffer-from@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz#6fc88585165ec73f8681d426d96de5d402021e4b" From 2891801edf8af7a6c56589ea6a4fa10b21560604 Mon Sep 17 00:00:00 2001 From: bgagent Date: Wed, 15 Jul 2026 15:33:29 -0400 Subject: [PATCH 3/3] fix(cdk): accept -c JSON-string AZ override + cap auto-pin at 2 (#353) Follow-up to the auto-pin review on #358. - resolveAgentCoreAzOverride now JSON-parses a string context value, so the documented '-c agentcore:availabilityZones=[...]' recovery path works identically to the cdk.context.json array form (CDK delivers -c values as raw strings). A non-JSON string is left as-is and still fails the array check with the same clear, key-named error, so true typos error out. - Auto-pin now pins the first MIN_AGENTCORE_AZS (2) supported zones instead of every match, matching AgentVpc's default maxAzs so enabling auto-pin no longer silently widens a working 2-AZ account to all supported zones (e.g. 3 AZs / 6 subnets). - DEPLOYMENT_GUIDE: sharpened the contrast that auto-pin is local-deploy-only and the CI/CD (deploy.yml) artifact is env-agnostic and must set the override; added an explicit -c example now that it parses. - Tests: added JSON-string override (success) and JSON-non-array (throws) cases; updated the auto-pin test to assert the 2-zone cap. --- cdk/src/constructs/agentcore-azs.ts | 32 +++++++++++++++---- cdk/test/constructs/agentcore-azs.test.ts | 22 +++++++++++-- docs/guides/DEPLOYMENT_GUIDE.md | 14 +++++--- .../docs/getting-started/Deployment-guide.md | 14 +++++--- 4 files changed, 62 insertions(+), 20 deletions(-) diff --git a/cdk/src/constructs/agentcore-azs.ts b/cdk/src/constructs/agentcore-azs.ts index 2d3d1a8e..fb7ad06a 100644 --- a/cdk/src/constructs/agentcore-azs.ts +++ b/cdk/src/constructs/agentcore-azs.ts @@ -100,10 +100,23 @@ export interface ResolveAgentCoreAzsOptions { * or lists fewer than {@link MIN_AGENTCORE_AZS} zones. */ export function resolveAgentCoreAzOverride(node: Node): string[] | undefined { - const override = node.tryGetContext(AGENTCORE_AZS_CONTEXT_KEY); - if (override === undefined || override === null) { + const raw = node.tryGetContext(AGENTCORE_AZS_CONTEXT_KEY); + if (raw === undefined || raw === null) { return undefined; } + // `cdk.context.json` delivers a real array, but `-c key=value` on the CLI + // (the recovery path this feature exists for) delivers a raw string. Parse the + // string form so both behave identically. A non-JSON string — a true typo — + // is left as-is and fails the Array.isArray check below with the same clear, + // key-named error. + let override: unknown = raw; + if (typeof raw === 'string') { + try { + override = JSON.parse(raw); + } catch { + override = raw; + } + } if (!Array.isArray(override)) { throw new Error( `Context '${AGENTCORE_AZS_CONTEXT_KEY}' must be a JSON array of availability-zone names ` @@ -176,10 +189,11 @@ async function defaultDescribeAzs(region: string): Promise zone-ID mapping and intersect it with - * {@link AGENTCORE_SUPPORTED_AZ_IDS} for the region, so a fresh local - * `cdk deploy` lands only in supported zones without account-specific - * guesswork. + * resolve the account's name -> zone-ID mapping, intersect it with + * {@link AGENTCORE_SUPPORTED_AZ_IDS} for the region, and pin the first + * {@link MIN_AGENTCORE_AZS} supported zones (matching AgentVpc's default + * `maxAzs`), so a fresh local `cdk deploy` lands only in supported zones + * without account-specific guesswork or widening the topology. * 3. **Fallback** — env-agnostic synth (token/undefined account or region), an * unknown region, a failed lookup, or fewer than {@link MIN_AGENTCORE_AZS} * supported zones all return `undefined`, leaving CDK's default AZ selection @@ -220,7 +234,11 @@ export async function resolveAgentCoreAzs(options: ResolveAgentCoreAzsOptions): const zones = await describeAzs(region); const names = selectSupportedAzNames(region, zones); if (names.length >= MIN_AGENTCORE_AZS) { - pinnedZones = names; + // Pin exactly MIN_AGENTCORE_AZS zones — the HA floor, matching AgentVpc's + // default `maxAzs` (2). Pinning every supported zone would silently widen + // an account that deploys fine today from 2 AZs to all supported (often 3 + // -> 6 subnets), so cap the selection to keep the topology stable. + pinnedZones = names.slice(0, MIN_AGENTCORE_AZS); } else { Annotations.of(scope).addWarning( `[AgentCore AZs] Found only ${names.length} AgentCore-supported availability zone(s) in ` diff --git a/cdk/test/constructs/agentcore-azs.test.ts b/cdk/test/constructs/agentcore-azs.test.ts index 1e2bde42..1bc6a700 100644 --- a/cdk/test/constructs/agentcore-azs.test.ts +++ b/cdk/test/constructs/agentcore-azs.test.ts @@ -84,12 +84,26 @@ describe('resolveAgentCoreAzOverride', () => { ).toEqual(override); }); - it('throws on a non-array override (typo guard)', () => { + it('throws on a bare (non-JSON) string override (typo guard)', () => { expect(() => resolveAgentCoreAzOverride(nodeWithContext({ [AGENTCORE_AZS_CONTEXT_KEY]: 'us-east-1b' })), ).toThrow(/must be a JSON array/); }); + it('parses a JSON-string array (the `-c key=value` CLI form)', () => { + // CDK delivers `-c agentcore:availabilityZones=[...]` as a raw string, not a + // parsed array — the documented mid-rollback recovery path must still work. + expect( + resolveAgentCoreAzOverride(nodeWithContext({ [AGENTCORE_AZS_CONTEXT_KEY]: '["us-east-1b","us-east-1c"]' })), + ).toEqual(['us-east-1b', 'us-east-1c']); + }); + + it('throws on a JSON string that does not parse to an array', () => { + expect(() => + resolveAgentCoreAzOverride(nodeWithContext({ [AGENTCORE_AZS_CONTEXT_KEY]: '"us-east-1b"' })), + ).toThrow(/must be a JSON array/); + }); + it('throws on a non-string / empty entry', () => { expect(() => resolveAgentCoreAzOverride(nodeWithContext({ [AGENTCORE_AZS_CONTEXT_KEY]: ['us-east-1b', ''] })), @@ -166,7 +180,9 @@ describe('resolveAgentCoreAzs', () => { expect(describeAzs).not.toHaveBeenCalled(); }); - it('auto-pins to the account supported zone names for a concrete env', async () => { + it('auto-pins the first two supported zone names (capped) for a concrete env', async () => { + // US_EAST_1_ZONES has three supported zones (az2/az4/az1 -> a/b/d); auto-pin + // caps at two to match AgentVpc's default maxAzs and avoid widening topology. const describeAzs = jest.fn, [string]>().mockResolvedValue(US_EAST_1_ZONES); const result = await resolveAgentCoreAzs({ scope: scopeWithContext(), @@ -174,7 +190,7 @@ describe('resolveAgentCoreAzs', () => { region: 'us-east-1', describeAzs, }); - expect(result).toEqual(['us-east-1a', 'us-east-1b', 'us-east-1d']); + expect(result).toEqual(['us-east-1a', 'us-east-1b']); expect(describeAzs).toHaveBeenCalledWith('us-east-1'); }); diff --git a/docs/guides/DEPLOYMENT_GUIDE.md b/docs/guides/DEPLOYMENT_GUIDE.md index c27ec196..5d3de7e3 100644 --- a/docs/guides/DEPLOYMENT_GUIDE.md +++ b/docs/guides/DEPLOYMENT_GUIDE.md @@ -171,22 +171,26 @@ Triggers via `workflow_run` when `build.yml` completes successfully. The pipelin **Root cause:** AgentCore Runtime only places its network interfaces in a subset of each region's Availability Zones, published as physical **zone IDs** (e.g. `use1-az1`, `use1-az2`, `use1-az4` for `us-east-1`). Zone IDs are stable across accounts, but zone *names* (`us-east-1a`) are aliased per-account — so `us-east-1a` can map to a different physical zone in your account than in another. Left to its default, CDK picks zones by name and can land the Runtime subnets in an unsupported zone. See the AWS [Supported Availability Zones](https://aws.github.io/bedrock-agentcore-starter-toolkit/user-guide/security/agentcore-vpc/#supported-availability-zones) table for the per-region set. -**Default behavior (auto-pin):** When the stack is synthesized with a concrete account and region — the usual local `cdk deploy` / `mise //cdk:deploy` path — it resolves your account's zone name-to-ID mapping (`ec2:DescribeAvailabilityZones`) and pins the VPC to the AZ *names* whose IDs are AgentCore-supported. No action needed for supported regions. +**Default behavior (auto-pin) — local deploy only:** When the stack is synthesized with a concrete account and region — a local `cdk deploy` / `mise //cdk:deploy`, which resolves credentials at synth time — it reads your account's zone name-to-ID mapping (`ec2:DescribeAvailabilityZones`) and pins the VPC to the first two AZ *names* whose IDs are AgentCore-supported (two matches AgentVpc's default `maxAzs`, so enabling this doesn't widen an already-working topology). No action needed on that path for the regions in the built-in map. -**When you must pin manually:** Env-agnostic synthesis skips auto-pin (there's no bound account to resolve the mapping) — this includes the CI-built artifact deployed by `deploy.yml`, and any region not yet in the built-in supported-AZ map. In those cases, set the `agentcore:availabilityZones` context override: +**The CI/CD-deployed stack is NOT auto-pinned.** Auto-pin needs a bound account at synth time, and the pipeline doesn't have one: `build.yml` synthesizes `cdk.out` credential-less (env-agnostic), and `deploy.yml` deploys that pre-built artifact (`--app cdk/cdk.out`). So a team deploying through the pipeline falls back to CDK's default AZ selection and **must set the `agentcore:availabilityZones` override to pin zones**. The same applies to any region not yet in the built-in supported-AZ map. To set the override: 1. Discover your account's zone name-to-ID mapping: ```bash aws ec2 describe-availability-zones --region \ --query 'AvailabilityZones[].[ZoneName,ZoneId]' --output text ``` -2. Pick the zone **names** whose **IDs** are in the AgentCore-supported set for the region (at least two, for high availability). -3. Set the override in `cdk/cdk.context.json` (or via `-c`), then redeploy: +2. Pick at least two zone **names** whose **IDs** are in the AgentCore-supported set for the region (high-availability floor). +3. Set the override — either in `cdk/cdk.context.json`: ```json { "agentcore:availabilityZones": ["us-east-1b", "us-east-1c"] } ``` + or on the CLI (note the value is a JSON array): + ```bash + cdk deploy -c 'agentcore:availabilityZones=["us-east-1b","us-east-1c"]' + ``` -The override is validated at synth time: a non-array, an empty/non-string entry, or fewer than two zones fails the synth with a message naming the key and expected shape. The supported-AZ map and resolution logic live in `cdk/src/constructs/agentcore-azs.ts`. +The override is validated at synth time (both forms parse identically): a non-array, an empty/non-string entry, or fewer than two zones fails the synth with a message naming the key and expected shape. The supported-AZ map and resolution logic live in `cdk/src/constructs/agentcore-azs.ts`. ### DNS Query Log Config replacement cascade (upgrading from pre-v0.5) diff --git a/docs/src/content/docs/getting-started/Deployment-guide.md b/docs/src/content/docs/getting-started/Deployment-guide.md index c36653b1..2035170f 100644 --- a/docs/src/content/docs/getting-started/Deployment-guide.md +++ b/docs/src/content/docs/getting-started/Deployment-guide.md @@ -175,22 +175,26 @@ Triggers via `workflow_run` when `build.yml` completes successfully. The pipelin **Root cause:** AgentCore Runtime only places its network interfaces in a subset of each region's Availability Zones, published as physical **zone IDs** (e.g. `use1-az1`, `use1-az2`, `use1-az4` for `us-east-1`). Zone IDs are stable across accounts, but zone *names* (`us-east-1a`) are aliased per-account — so `us-east-1a` can map to a different physical zone in your account than in another. Left to its default, CDK picks zones by name and can land the Runtime subnets in an unsupported zone. See the AWS [Supported Availability Zones](https://aws.github.io/bedrock-agentcore-starter-toolkit/user-guide/security/agentcore-vpc/#supported-availability-zones) table for the per-region set. -**Default behavior (auto-pin):** When the stack is synthesized with a concrete account and region — the usual local `cdk deploy` / `mise //cdk:deploy` path — it resolves your account's zone name-to-ID mapping (`ec2:DescribeAvailabilityZones`) and pins the VPC to the AZ *names* whose IDs are AgentCore-supported. No action needed for supported regions. +**Default behavior (auto-pin) — local deploy only:** When the stack is synthesized with a concrete account and region — a local `cdk deploy` / `mise //cdk:deploy`, which resolves credentials at synth time — it reads your account's zone name-to-ID mapping (`ec2:DescribeAvailabilityZones`) and pins the VPC to the first two AZ *names* whose IDs are AgentCore-supported (two matches AgentVpc's default `maxAzs`, so enabling this doesn't widen an already-working topology). No action needed on that path for the regions in the built-in map. -**When you must pin manually:** Env-agnostic synthesis skips auto-pin (there's no bound account to resolve the mapping) — this includes the CI-built artifact deployed by `deploy.yml`, and any region not yet in the built-in supported-AZ map. In those cases, set the `agentcore:availabilityZones` context override: +**The CI/CD-deployed stack is NOT auto-pinned.** Auto-pin needs a bound account at synth time, and the pipeline doesn't have one: `build.yml` synthesizes `cdk.out` credential-less (env-agnostic), and `deploy.yml` deploys that pre-built artifact (`--app cdk/cdk.out`). So a team deploying through the pipeline falls back to CDK's default AZ selection and **must set the `agentcore:availabilityZones` override to pin zones**. The same applies to any region not yet in the built-in supported-AZ map. To set the override: 1. Discover your account's zone name-to-ID mapping: ```bash aws ec2 describe-availability-zones --region \ --query 'AvailabilityZones[].[ZoneName,ZoneId]' --output text ``` -2. Pick the zone **names** whose **IDs** are in the AgentCore-supported set for the region (at least two, for high availability). -3. Set the override in `cdk/cdk.context.json` (or via `-c`), then redeploy: +2. Pick at least two zone **names** whose **IDs** are in the AgentCore-supported set for the region (high-availability floor). +3. Set the override — either in `cdk/cdk.context.json`: ```json { "agentcore:availabilityZones": ["us-east-1b", "us-east-1c"] } ``` + or on the CLI (note the value is a JSON array): + ```bash + cdk deploy -c 'agentcore:availabilityZones=["us-east-1b","us-east-1c"]' + ``` -The override is validated at synth time: a non-array, an empty/non-string entry, or fewer than two zones fails the synth with a message naming the key and expected shape. The supported-AZ map and resolution logic live in `cdk/src/constructs/agentcore-azs.ts`. +The override is validated at synth time (both forms parse identically): a non-array, an empty/non-string entry, or fewer than two zones fails the synth with a message naming the key and expected shape. The supported-AZ map and resolution logic live in `cdk/src/constructs/agentcore-azs.ts`. ### DNS Query Log Config replacement cascade (upgrading from pre-v0.5)