Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion cdk/src/constructs/agent-vpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ const HTTPS_PORT = 443;
* Properties for the AgentVpc construct.
*/
export interface AgentVpcProps {
/**
* Availability zones to use. Zone names must be resolved for the target
* account because AZ name-to-ID mappings differ between accounts.
*/
readonly availabilityZones?: string[];

/**
* Maximum number of availability zones to use.
* @default 2
Expand Down Expand Up @@ -69,10 +75,13 @@ export class AgentVpc extends Construct {
const maxAzs = props.maxAzs ?? 2;
const natGateways = props.natGateways ?? 1;
const removalPolicy = props.removalPolicy ?? RemovalPolicy.DESTROY;
const availabilityZoneSelection = props.availabilityZones
? { availabilityZones: props.availabilityZones }
: { maxAzs };

// --- VPC ---
this.vpc = new ec2.Vpc(this, 'Vpc', {
maxAzs,
...availabilityZoneSelection,
natGateways,
restrictDefaultSecurityGroup: true,
subnetConfiguration: [
Expand Down
13 changes: 12 additions & 1 deletion cdk/src/stacks/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,18 @@ export class AgentStack extends Stack {
});

// Network isolation — VPC with restricted egress
const agentVpc = new AgentVpc(this, 'AgentVpc');
const agentcoreAvailabilityZonesContext = this.node.tryGetContext(
'agentcoreAvailabilityZones',
) as string | undefined;
const agentcoreAvailabilityZones = agentcoreAvailabilityZonesContext
?.split(',')
.map(zone => zone.trim())
.filter(Boolean);
const agentVpc = new AgentVpc(this, 'AgentVpc', {
availabilityZones: agentcoreAvailabilityZones?.length
? agentcoreAvailabilityZones
: undefined,
});

// DNS Firewall — domain-level egress filtering (observation mode for initial deployment)
const additionalDomains = [...new Set(blueprints.flatMap(b => b.egressAllowlist))];
Expand Down
19 changes: 19 additions & 0 deletions cdk/test/constructs/agent-vpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,25 @@ describe('AgentVpc', () => {
});

describe('AgentVpc with custom props', () => {
test('accepts account-local availability zone names', () => {
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-1d'],
});
const template = Template.fromStack(stack);

template.hasResourceProperties('AWS::EC2::Subnet', {
AvailabilityZone: 'us-east-1b',
});
template.hasResourceProperties('AWS::EC2::Subnet', {
AvailabilityZone: 'us-east-1d',
});
template.resourceCountIs('AWS::EC2::Subnet', 4);
});

test('accepts custom maxAzs', () => {
const app = new App();
const stack = new Stack(app, 'TestStack', {
Expand Down
23 changes: 23 additions & 0 deletions cdk/test/stacks/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1368,3 +1368,26 @@ describe('AgentStack registry gate', () => {
expect(renderedPolicies).not.toContain('bedrock-agentcore:ListRegistryRecords');
});
});

describe('AgentStack AgentCore availability zones', () => {
test('uses account-local zones supplied by workshop context', () => {
const app = new App({
context: {
agentcoreAvailabilityZones: 'us-east-1b,us-east-1d',
enableAgentRegistry: 'false',
},
});
const stack = new AgentStack(app, 'WorkshopAzStack', {
env: { account: '123456789012', region: 'us-east-1' },
});
const template = Template.fromStack(stack);
const subnets = Object.values(template.findResources('AWS::EC2::Subnet')) as Array<{
Properties: { AvailabilityZone?: string };
}>;

expect(subnets).toHaveLength(4);
expect(new Set(subnets.map(subnet => subnet.Properties.AvailabilityZone))).toEqual(
new Set(['us-east-1b', 'us-east-1d']),
);
});
});
64 changes: 59 additions & 5 deletions manage-workshop-stack.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ BLUEPRINT_REPO="${BLUEPRINT_REPO:-aws-samples/sample-abca-playground}"
PARTICIPANT_USERNAME="${PARTICIPANT_USERNAME:-participant@workshop.local}"
PARTICIPANT_SECRET_NAME="${PARTICIPANT_SECRET_NAME:-abca-workshop/participant-credentials}"
ENABLE_AGENT_REGISTRY="${ENABLE_AGENT_REGISTRY:-false}"
AGENTCORE_SUPPORTED_ZONE_IDS="${AGENTCORE_SUPPORTED_ZONE_IDS:-}"

if [[ -z "$AGENTCORE_SUPPORTED_ZONE_IDS" && "$REGION" == "us-east-1" ]]; then
AGENTCORE_SUPPORTED_ZONE_IDS="use1-az1,use1-az2"
fi

export AWS_REGION="$REGION"
export AWS_DEFAULT_REGION="$REGION"
Expand Down Expand Up @@ -41,6 +46,43 @@ prepare_arm64_builder() {
fi
}

bind_cdk_environment() {
CDK_DEFAULT_ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
CDK_DEFAULT_REGION="$REGION"
export CDK_DEFAULT_ACCOUNT
export CDK_DEFAULT_REGION
}

resolve_agentcore_availability_zones() {
if [[ -z "$AGENTCORE_SUPPORTED_ZONE_IDS" ]]; then
return
fi

local zone_id zone_name
local -a zone_ids zone_names
IFS=',' read -r -a zone_ids <<<"$AGENTCORE_SUPPORTED_ZONE_IDS"

for zone_id in "${zone_ids[@]}"; do
zone_id="${zone_id//[[:space:]]/}"
zone_name=$(aws ec2 describe-availability-zones \
--region "$REGION" \
--filters \
"Name=zone-id,Values=${zone_id}" \
"Name=state,Values=available" \
--query 'AvailabilityZones[0].ZoneName' \
--output text)

if [[ -z "$zone_name" || "$zone_name" == "None" ]]; then
echo "AgentCore availability zone ${zone_id} is unavailable in ${REGION}" >&2
return 1
fi
zone_names+=("$zone_name")
done

local IFS=','
printf '%s\n' "${zone_names[*]}"
}

stack_output() {
local output_key="$1"
aws cloudformation describe-stacks \
Expand Down Expand Up @@ -171,14 +213,25 @@ ensure_participant_user() {
deploy_stack() {
install_toolchain
prepare_arm64_builder
bind_cdk_environment

local agentcore_availability_zones
agentcore_availability_zones=$(resolve_agentcore_availability_zones)

pushd cdk >/dev/null
npx cdk bootstrap \
"aws://$(aws sts get-caller-identity --query Account --output text)/${REGION}"
npx cdk deploy "$STACK_NAME" \
--require-approval never \
--context "blueprintRepo=${BLUEPRINT_REPO}" \
npx cdk bootstrap "aws://${CDK_DEFAULT_ACCOUNT}/${CDK_DEFAULT_REGION}"

local -a deploy_args=(
"$STACK_NAME"
--require-approval never
--context "blueprintRepo=${BLUEPRINT_REPO}"
--context "enableAgentRegistry=${ENABLE_AGENT_REGISTRY}"
)
if [[ -n "$agentcore_availability_zones" ]]; then
deploy_args+=(--context "agentcoreAvailabilityZones=${agentcore_availability_zones}")
fi

npx cdk deploy "${deploy_args[@]}"
popd >/dev/null

seed_github_token
Expand Down Expand Up @@ -237,6 +290,7 @@ delete_bootstrap_stack() {

destroy_stack() {
install_toolchain
bind_cdk_environment

if aws cloudformation describe-stacks \
--stack-name "$STACK_NAME" \
Expand Down
Loading