Skip to content

Deploying AWS Bedrock AgentCore with CDK: a quickstart

A CDK guide for deploying a minimal Strands agent on AgentCore Runtime — parameterized stack, arm64 build, deploy and invoke, and the IAM and Marketplace prerequisites you need before the first call.

The setup

A companion post walks through what AgentCore Runtime is at the architecture level: container, identity, memory, gateway. This post picks up where that ends and asks the deployment question: AgentCore is now reachable from CDK through @aws-cdk/aws-bedrock-agentcore-alpha, so what does a working trial look like end-to-end? In practice the code is short and the alpha L2 carries most of the weight, but a few IAM and Marketplace prerequisites need to land before the first invoke returns.

What we are building

A single CDK stack that deploys a minimal Strands agent on AgentCore Runtime in eu-central-1, plus the build script and the boto3 invoke helper. The full layout:

text
agent-core-cdk/├── bin/app.ts                    # CDK app, reads context from cdk.json├── lib/agent-runtime-stack.ts    # Runtime construct + 2 IAM statements├── cdk.json                      # region, modelId, runtimeName, etc.├── agent/│   ├── main.py                   # Strands BedrockModel + @app.entrypoint│   └── requirements.txt          # bedrock-agentcore + strands-agents├── scripts/│   ├── build-agent.sh            # uv pip --python-platform aarch64-manylinux2014│   └── invoke.py                 # boto3 bedrock-agentcore client└── package.json

Roughly 150 lines of TypeScript and Python total. Region, model, and runtime config sit in cdk.json so the Stack class itself stays parameter-driven and one configuration change moves you across regions or models. The interesting part is not the volume; it is which lines you write and which the alpha L2 writes for you. Full source: github.com/ayhansipahi/agent-core-cdk.

App entry and config

cdk.json carries the runtime parameters as CDK context, so the Stack class stays a pure construct and the values change without code edits:

json
{  "app": "npx ts-node --prefer-ts-exts bin/app.ts",  "context": {    "agentCore:region": "eu-central-1",    "agentCore:modelId": "anthropic.claude-sonnet-4-5-20250929-v1:0",    "agentCore:runtimeName": "helloAgent",    "agentCore:inferenceProfilePrefix": "eu",    "agentCore:description": "Minimal Strands agent on AgentCore Runtime"  }}

bin/app.ts reads, validates, and passes them into the stack:

typescript
const region = app.node.tryGetContext('agentCore:region') as string;const modelId = app.node.tryGetContext('agentCore:modelId') as string;const runtimeName = app.node.tryGetContext('agentCore:runtimeName') as string;const inferenceProfilePrefix = app.node.tryGetContext(  'agentCore:inferenceProfilePrefix',) as string;const runtimeDescription = app.node.tryGetContext('agentCore:description') as string;
if (!region || !modelId || !runtimeName || !inferenceProfilePrefix || !runtimeDescription) {  throw new Error(    'Missing required cdk.json context: agentCore:{region, modelId, runtimeName, inferenceProfilePrefix, description}',  );}
new AgentRuntimeStack(app, 'AgentCoreCdkStack', {  env: { region, account: process.env.CDK_DEFAULT_ACCOUNT },  modelId,  runtimeName,  inferenceProfilePrefix,  runtimeDescription,});

The throw is a deploy-time guard — synth fails fast if any key is missing instead of producing a stack with undefined ARNs. To switch regions or models, edit cdk.json and re-deploy; the Stack class itself never holds a string literal for any of these.

The CDK stack

lib/agent-runtime-stack.ts takes the four config props and produces the whole infrastructure surface — two manual PolicyStatement blocks, one Runtime construct, one CfnOutput, two stack-level tags:

typescript
import * as cdk from 'aws-cdk-lib';import { Construct } from 'constructs';import * as iam from 'aws-cdk-lib/aws-iam';import * as agentcore from '@aws-cdk/aws-bedrock-agentcore-alpha';import * as path from 'path';
export interface AgentRuntimeStackProps extends cdk.StackProps {  readonly modelId: string;  readonly inferenceProfilePrefix: string;  readonly runtimeName: string;  readonly runtimeDescription: string;}
export class AgentRuntimeStack extends cdk.Stack {  constructor(scope: Construct, id: string, props: AgentRuntimeStackProps) {    super(scope, id, props);
    const inferenceProfileId = `${props.inferenceProfilePrefix}.${props.modelId}`;
    const artifact = agentcore.AgentRuntimeArtifact.fromCodeAsset({      path: path.join(__dirname, '..', 'agent', 'dist'),      runtime: agentcore.AgentCoreRuntime.PYTHON_3_13,      entrypoint: ['main.py'],    });
    const runtime = new agentcore.Runtime(this, 'AgentRuntime', {      runtimeName: props.runtimeName,      agentRuntimeArtifact: artifact,      description: props.runtimeDescription,    });
    runtime.role.addToPrincipalPolicy(      new iam.PolicyStatement({        sid: 'BedrockInvokeModel',        actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'],        resources: [          `arn:aws:bedrock:${this.region}::foundation-model/${props.modelId}`,          `arn:aws:bedrock:*::foundation-model/${props.modelId}`,          `arn:aws:bedrock:${this.region}:${this.account}:inference-profile/${inferenceProfileId}`,        ],      }),    );
    runtime.role.addToPrincipalPolicy(      new iam.PolicyStatement({        sid: 'BedrockMarketplaceSubscriptionCheck',        actions: [          'aws-marketplace:Subscribe',          'aws-marketplace:Unsubscribe',          'aws-marketplace:ViewSubscriptions',        ],        resources: ['*'],      }),    );
    new cdk.CfnOutput(this, 'AgentRuntimeArn', {      value: runtime.agentRuntimeArn,    });
    cdk.Tags.of(this).add('Project', 'agent-core-cdk');    cdk.Tags.of(this).add('ManagedBy', 'cdk');  }}

Three details worth pausing on. The Bedrock invoke statement lists three ARNs, not one: the regional foundation model, a wildcard region, and the inference profile (constructed at runtime from props.inferenceProfilePrefix + props.modelId). EU cross-region inference uses the eu. prefixed profile, and the wildcard region is required because the inference profile fans out to other regions internally. The Marketplace statement scopes resources to * because Marketplace API actions do not support resource-level IAM. And the runtime name has to be camelCase because the alpha L2 inherits CFN naming constraints — hyphens fail at deploy-time, not synth-time.

The agent code

agent/main.py reads the model ID and region from environment. AWS_REGION mirrors cdk.json; MODEL_ID defaults to the eu.-prefixed inference profile because the agent calls Bedrock directly and needs the prefixed ID, while the stack constructs the same value at synth time from inferenceProfilePrefix + modelId. The same file runs locally with agentcore dev and in the deployed runtime:

python
import os
from bedrock_agentcore.runtime import BedrockAgentCoreAppfrom strands import Agentfrom strands.models import BedrockModel
MODEL_ID = os.environ.get("MODEL_ID", "eu.anthropic.claude-sonnet-4-5-20250929-v1:0")REGION = os.environ.get("AWS_REGION", "eu-central-1")
app = BedrockAgentCoreApp()
model = BedrockModel(model_id=MODEL_ID, region_name=REGION)agent = Agent(model=model)

@app.entrypointdef invoke(payload, context):    user_message = payload.get("prompt", "Hello!")    response = agent(user_message)    return {"result": str(response)}

BedrockAgentCoreApp is the runtime contract; it spins up the HTTP server on port 8080 inside the AgentCore container and routes every invoke_agent_runtime call to the function decorated with @app.entrypoint. The default MODEL_ID carries the eu. regional prefix because the runtime executes in eu-central-1 and Bedrock requires the prefixed profile for cross-region inference. AgentCore injects AWS_REGION automatically; MODEL_ID can be overridden through the Runtime construct's environmentVariables prop if you ever want it different from the cdk.json value.

The arm64 build hop

AgentCore Runtime executes only on arm64. A macOS or x86_64 host that runs pip install -r requirements.txt --target=dist will produce x86_64 wheels and the container will fail with an exec format error on first invoke. The build script forces the right platform:

bash
uv pip install \  --python-platform aarch64-manylinux2014 \  --python-version 3.13 \  --target="$DIST_DIR" \  --only-binary=:all: \  -r agent/requirements.txt
cp agent/main.py "$DIST_DIR/"

--only-binary=:all: is the safety net. If a dependency has no arm64 wheel and pip falls back to building from source, the source build runs on the host arch and the resulting .so files break in the container. :all: makes pip fail loudly instead. uv in place of pip is a ~4-6 second versus ~30 second difference on this dependency set, but plain pip --platform works too. Output goes to agent/dist/, which is what AgentRuntimeArtifact.fromCodeAsset zips and uploads.

First deploy: 66 seconds

cdk deploy from a clean slate took 66.13s in this trial: CFN bootstrap reuse, Runtime resource creation, IAM role creation, and the S3 asset upload through the CDK staging bucket. Subsequent deploys are faster because the Runtime resource is already there: an IAM-only update came in at 30.48s, a description bump that forced a new Runtime version landed at 24.9s, and an idempotent re-deploy with no diff at 21.25s.

That was the easy half. Three IAM/billing prerequisites stand between this and the first successful invoke.

Prerequisites you may not see in the docs

Three layers gate the first invoke. The CDK stack above already covers the first two; the third is account-level and worth checking before the first deploy.

1. Bedrock model invoke (in the stack). The runtime role needs bedrock:InvokeModel against the foundation model and the inference profile. Without it, the invoke returns:

text
not authorized to perform bedrock:InvokeModel

The first PolicyStatement in the stack covers this — three ARN forms (regional, wildcard, inference profile).

2. Marketplace subscription validation (in the stack). Newer Anthropic models on Bedrock validate an AWS Marketplace subscription on each ConverseStream call (the Sonnet 4.5 model card lists a Marketplace product ID: prod-mxcfnwvpd6kb4). Without aws-marketplace:Subscribe, Unsubscribe, and ViewSubscriptions on the runtime role, the invoke returns:

text
Model access is denied due to IAM user or service role is not authorized to perform therequired AWS Marketplace actions (aws-marketplace:ViewSubscriptions, aws-marketplace:Subscribe).

The minimal IAM example in the AWS AgentCore docs assumes an Anthropic model but does not include these actions; the second PolicyStatement in the stack covers them.

3. Account payment instrument (account-level, not IaC). With both IAM blocks correct, the Marketplace subscription itself can still fail at the billing layer:

text
Model access is denied due to INVALID_PAYMENT_INSTRUMENT: A valid payment instrumentmust be provided. Your AWS Marketplace subscription for this model cannot be completedat this time.

Fix in the AWS Billing console (not the Marketplace one): Account → Payment preferences → Add card. Roughly two minutes for propagation, no redeploy.

The L2 alpha bonus

Inspecting the runtime role after deploy with aws iam get-role-policy revealed seven policy statements that the L2 construct attached on its own. None of these are in the stack code; they are what agentcore.Runtime writes for you:

SidServicePurpose
LogGroupAccesslogsDescribeLogStreams, CreateLogGroup for the runtime's own log group
DescribeLogGroupslogsDescribeLogGroups broader scope, console visibility
LogStreamAccesslogsCreateLogStream, PutLogEvents for invoke logs
XRayAccessxrayPutTraceSegments, PutTelemetryRecords, GetSamplingRules, GetSamplingTargets
CloudWatchMetricscloudwatchPutMetricData scoped to namespace bedrock-agentcore
GetAgentAccessTokenbedrock-agentcoreGetWorkloadAccessToken, GetWorkloadAccessTokenForJWT, GetWorkloadAccessTokenForUserId
S3AssetReads3GetObject, GetObjectVersion on arn:aws:s3:::cdk-hnb659fds-assets-<account>-<region>/*

Plus the two manual statements above (Bedrock invoke + Marketplace), the runtime role ends up with nine statements total. Written by hand on top of the L1 CfnRuntime, this is roughly 80 lines of JSON you do not maintain. That is the case for staying on the alpha L2 even with the version-pin caveat.

The numbers

All measurements from the same POC session in eu-central-1, fresh runtimeSessionId per invoke:

OperationServer-sideClient-sideNotes
Deploy 1 (initial create)66.13sCFN bootstrap reuse, Runtime + IAM + S3 asset
Deploy 2 (IAM-only update)30.48sMarketplace block added
Deploy 3 (description bump)24.9sForced new Runtime version, fresh container
Deploy 4 (idempotent re-deploy)21.25sNo-diff floor
Invoke 1 (cold container)6.244s10.16sFirst call, container boot included
Invoke 2 (warm)2.476s5.79sSecond call, same client process
Invoke 33.387s7.41sContainer appears reused across fresh sessions

Client-side overhead sits around 3.5-4 seconds across all three invokes: boto3 import, archive resolve, AgentCore session create, network round-trip. For SLO measurement, take the server-side timing; the client-side number is dominated by Python and SDK startup.

Closing

AgentCore-on-CDK is a small surface — about 150 lines of TypeScript and Python with cdk.json on the side. The two things to bring with you that the AWS docs minimal example does not flag: the Marketplace IAM block on the runtime role for newer Anthropic models, and a payment method on the account before the first invoke. The recommendation: stay on the alpha L2 (@aws-cdk/aws-bedrock-agentcore-alpha) because it injects seven policy statements you would otherwise hand-roll on top of the L1 CfnRuntime, and pin the version because alpha qualifiers are not semver-stable; the trial here ran on 2.252.0-alpha.0. Drop to L1 only if an alpha qualifier is a hard compliance blocker.

The natural next step from this stack is the AgentCore Memory and Gateway constructs, which the same alpha module exposes; both layer onto the same Runtime role this trial set up.

References

Related Posts