Skip to content

Commit 2891801

Browse files
author
bgagent
committed
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.
1 parent a7f04df commit 2891801

4 files changed

Lines changed: 62 additions & 20 deletions

File tree

cdk/src/constructs/agentcore-azs.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,23 @@ export interface ResolveAgentCoreAzsOptions {
100100
* or lists fewer than {@link MIN_AGENTCORE_AZS} zones.
101101
*/
102102
export function resolveAgentCoreAzOverride(node: Node): string[] | undefined {
103-
const override = node.tryGetContext(AGENTCORE_AZS_CONTEXT_KEY);
104-
if (override === undefined || override === null) {
103+
const raw = node.tryGetContext(AGENTCORE_AZS_CONTEXT_KEY);
104+
if (raw === undefined || raw === null) {
105105
return undefined;
106106
}
107+
// `cdk.context.json` delivers a real array, but `-c key=value` on the CLI
108+
// (the recovery path this feature exists for) delivers a raw string. Parse the
109+
// string form so both behave identically. A non-JSON string — a true typo —
110+
// is left as-is and fails the Array.isArray check below with the same clear,
111+
// key-named error.
112+
let override: unknown = raw;
113+
if (typeof raw === 'string') {
114+
try {
115+
override = JSON.parse(raw);
116+
} catch {
117+
override = raw;
118+
}
119+
}
107120
if (!Array.isArray(override)) {
108121
throw new Error(
109122
`Context '${AGENTCORE_AZS_CONTEXT_KEY}' must be a JSON array of availability-zone names `
@@ -176,10 +189,11 @@ async function defaultDescribeAzs(region: string): Promise<AvailabilityZoneInfo[
176189
* context value always wins (works even in env-agnostic synth, which is how
177190
* the CI-built artifact and production stack pin zones).
178191
* 2. **Auto-pin (default path)** — when synth has a concrete account + region,
179-
* resolve the account's name -> zone-ID mapping and intersect it with
180-
* {@link AGENTCORE_SUPPORTED_AZ_IDS} for the region, so a fresh local
181-
* `cdk deploy` lands only in supported zones without account-specific
182-
* guesswork.
192+
* resolve the account's name -> zone-ID mapping, intersect it with
193+
* {@link AGENTCORE_SUPPORTED_AZ_IDS} for the region, and pin the first
194+
* {@link MIN_AGENTCORE_AZS} supported zones (matching AgentVpc's default
195+
* `maxAzs`), so a fresh local `cdk deploy` lands only in supported zones
196+
* without account-specific guesswork or widening the topology.
183197
* 3. **Fallback** — env-agnostic synth (token/undefined account or region), an
184198
* unknown region, a failed lookup, or fewer than {@link MIN_AGENTCORE_AZS}
185199
* supported zones all return `undefined`, leaving CDK's default AZ selection
@@ -220,7 +234,11 @@ export async function resolveAgentCoreAzs(options: ResolveAgentCoreAzsOptions):
220234
const zones = await describeAzs(region);
221235
const names = selectSupportedAzNames(region, zones);
222236
if (names.length >= MIN_AGENTCORE_AZS) {
223-
pinnedZones = names;
237+
// Pin exactly MIN_AGENTCORE_AZS zones — the HA floor, matching AgentVpc's
238+
// default `maxAzs` (2). Pinning every supported zone would silently widen
239+
// an account that deploys fine today from 2 AZs to all supported (often 3
240+
// -> 6 subnets), so cap the selection to keep the topology stable.
241+
pinnedZones = names.slice(0, MIN_AGENTCORE_AZS);
224242
} else {
225243
Annotations.of(scope).addWarning(
226244
`[AgentCore AZs] Found only ${names.length} AgentCore-supported availability zone(s) in `

cdk/test/constructs/agentcore-azs.test.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,26 @@ describe('resolveAgentCoreAzOverride', () => {
8484
).toEqual(override);
8585
});
8686

87-
it('throws on a non-array override (typo guard)', () => {
87+
it('throws on a bare (non-JSON) string override (typo guard)', () => {
8888
expect(() =>
8989
resolveAgentCoreAzOverride(nodeWithContext({ [AGENTCORE_AZS_CONTEXT_KEY]: 'us-east-1b' })),
9090
).toThrow(/must be a JSON array/);
9191
});
9292

93+
it('parses a JSON-string array (the `-c key=value` CLI form)', () => {
94+
// CDK delivers `-c agentcore:availabilityZones=[...]` as a raw string, not a
95+
// parsed array — the documented mid-rollback recovery path must still work.
96+
expect(
97+
resolveAgentCoreAzOverride(nodeWithContext({ [AGENTCORE_AZS_CONTEXT_KEY]: '["us-east-1b","us-east-1c"]' })),
98+
).toEqual(['us-east-1b', 'us-east-1c']);
99+
});
100+
101+
it('throws on a JSON string that does not parse to an array', () => {
102+
expect(() =>
103+
resolveAgentCoreAzOverride(nodeWithContext({ [AGENTCORE_AZS_CONTEXT_KEY]: '"us-east-1b"' })),
104+
).toThrow(/must be a JSON array/);
105+
});
106+
93107
it('throws on a non-string / empty entry', () => {
94108
expect(() =>
95109
resolveAgentCoreAzOverride(nodeWithContext({ [AGENTCORE_AZS_CONTEXT_KEY]: ['us-east-1b', ''] })),
@@ -166,15 +180,17 @@ describe('resolveAgentCoreAzs', () => {
166180
expect(describeAzs).not.toHaveBeenCalled();
167181
});
168182

169-
it('auto-pins to the account supported zone names for a concrete env', async () => {
183+
it('auto-pins the first two supported zone names (capped) for a concrete env', async () => {
184+
// US_EAST_1_ZONES has three supported zones (az2/az4/az1 -> a/b/d); auto-pin
185+
// caps at two to match AgentVpc's default maxAzs and avoid widening topology.
170186
const describeAzs = jest.fn<Promise<AvailabilityZoneInfo[]>, [string]>().mockResolvedValue(US_EAST_1_ZONES);
171187
const result = await resolveAgentCoreAzs({
172188
scope: scopeWithContext(),
173189
account: '123456789012',
174190
region: 'us-east-1',
175191
describeAzs,
176192
});
177-
expect(result).toEqual(['us-east-1a', 'us-east-1b', 'us-east-1d']);
193+
expect(result).toEqual(['us-east-1a', 'us-east-1b']);
178194
expect(describeAzs).toHaveBeenCalledWith('us-east-1');
179195
});
180196

docs/guides/DEPLOYMENT_GUIDE.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,22 +171,26 @@ Triggers via `workflow_run` when `build.yml` completes successfully. The pipelin
171171

172172
**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.
173173

174-
**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.
174+
**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.
175175

176-
**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:
176+
**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:
177177

178178
1. Discover your account's zone name-to-ID mapping:
179179
```bash
180180
aws ec2 describe-availability-zones --region <region> \
181181
--query 'AvailabilityZones[].[ZoneName,ZoneId]' --output text
182182
```
183-
2. Pick the zone **names** whose **IDs** are in the AgentCore-supported set for the region (at least two, for high availability).
184-
3. Set the override in `cdk/cdk.context.json` (or via `-c`), then redeploy:
183+
2. Pick at least two zone **names** whose **IDs** are in the AgentCore-supported set for the region (high-availability floor).
184+
3. Set the override — either in `cdk/cdk.context.json`:
185185
```json
186186
{ "agentcore:availabilityZones": ["us-east-1b", "us-east-1c"] }
187187
```
188+
or on the CLI (note the value is a JSON array):
189+
```bash
190+
cdk deploy -c 'agentcore:availabilityZones=["us-east-1b","us-east-1c"]'
191+
```
188192

189-
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`.
193+
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`.
190194

191195
### DNS Query Log Config replacement cascade (upgrading from pre-v0.5)
192196

docs/src/content/docs/getting-started/Deployment-guide.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,22 +175,26 @@ Triggers via `workflow_run` when `build.yml` completes successfully. The pipelin
175175

176176
**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.
177177

178-
**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.
178+
**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.
179179

180-
**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:
180+
**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:
181181

182182
1. Discover your account's zone name-to-ID mapping:
183183
```bash
184184
aws ec2 describe-availability-zones --region <region> \
185185
--query 'AvailabilityZones[].[ZoneName,ZoneId]' --output text
186186
```
187-
2. Pick the zone **names** whose **IDs** are in the AgentCore-supported set for the region (at least two, for high availability).
188-
3. Set the override in `cdk/cdk.context.json` (or via `-c`), then redeploy:
187+
2. Pick at least two zone **names** whose **IDs** are in the AgentCore-supported set for the region (high-availability floor).
188+
3. Set the override — either in `cdk/cdk.context.json`:
189189
```json
190190
{ "agentcore:availabilityZones": ["us-east-1b", "us-east-1c"] }
191191
```
192+
or on the CLI (note the value is a JSON array):
193+
```bash
194+
cdk deploy -c 'agentcore:availabilityZones=["us-east-1b","us-east-1c"]'
195+
```
192196

193-
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`.
197+
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`.
194198

195199
### DNS Query Log Config replacement cascade (upgrading from pre-v0.5)
196200

0 commit comments

Comments
 (0)