Skip to content
Ayhan Sipahi Ayhan Sipahi

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, with IAM and Marketplace prerequisites.

The setup

A companion post walks through what AgentCore Runtime is at the architecture level: container, identity, memory, gateway. The deployment question starts where that ends: AgentCore is now reachable from CDK through @aws-cdk/aws-bedrock-agentcore-alpha, so what does a working trial look like end-to-end? Short answer: take the alpha L2 and pin its version. The stack code stays short because the construct 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:

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 stays parameter-driven. One configuration change moves you across regions or models. What matters here is the split: 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:

{
  "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:

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:

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: 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:

import os

from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent
from 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.entrypoint
def 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:

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 cuts this dependency set from ~30 seconds to ~4-6 seconds, 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 a green deploy 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:

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:

Model access is denied due to IAM user or service role is not authorized to perform the
required 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:

Model access is denied due to INVALID_PAYMENT_INSTRUMENT: A valid payment instrument
must be provided. Your AWS Marketplace subscription for this model cannot be completed
at 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>/*

With 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, that is roughly 80 lines of JSON you do not maintain.

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.13sn/aCFN bootstrap reuse, Runtime + IAM + S3 asset
Deploy 2 (IAM-only update)30.48sn/aMarketplace block added
Deploy 3 (description bump)24.9sn/aForced new Runtime version, fresh container
Deploy 4 (idempotent re-deploy)21.25sn/aNo-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 alpha L2 default holds as long as you pin the version, since 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