Skip to content

TypeScript Deployment to Amazon Bedrock AgentCore Runtime

This guide covers deploying TypeScript-based Strands agents to Amazon Bedrock AgentCore Runtime using Express and Docker.

  • Node.js 20+
  • Docker installed and running
  • AWS CLI configured with valid credentials
  • AWS account with appropriate permissions
  • ECR repository access

Terminal window
mkdir my-agent-service && cd my-agent-service
npm init -y

Create or update your package.json with the following configuration and dependencies:

{
"name": "my-agent-service",
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsc && node dist/index.js"
},
"dependencies": {
"@strands-agents/sdk": "latest",
"@aws-sdk/client-bedrock-agentcore": "latest",
"express": "^4.18.2",
"zod": "^4.1.12"
},
"devDependencies": {
"@types/express": "^4.17.21",
"typescript": "^5.3.3"
}
}

Then install all dependencies:

Terminal window
npm install

Create tsconfig.json:

{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "./dist",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["*.ts"],
"exclude": ["node_modules", "dist"]
}

Create index.ts with your agent implementation:

import { z } from 'zod'
import * as strands from '@strands-agents/sdk'
import express, { type Request, type Response } from 'express'
const PORT = process.env.PORT || 8080
// Define a custom tool
const calculatorTool = strands.tool({
name: 'calculator',
description: 'Performs basic arithmetic operations',
inputSchema: z.object({
operation: z.enum(['add', 'subtract', 'multiply', 'divide']),
a: z.number(),
b: z.number(),
}),
callback: (input): number => {
switch (input.operation) {
case 'add':
return input.a + input.b
case 'subtract':
return input.a - input.b
case 'multiply':
return input.a * input.b
case 'divide':
return input.a / input.b
}
},
})
// Configure the agent with Amazon Bedrock
const agent = new strands.Agent({
model: new strands.BedrockModel({
region: 'ap-southeast-2', // Change to your preferred region
}),
tools: [calculatorTool],
})
const app = express()
// Health check endpoint (REQUIRED)
app.get('/ping', (_, res) =>
res.json({
status: 'Healthy',
time_of_last_update: Math.floor(Date.now() / 1000),
})
)
// Agent invocation endpoint (REQUIRED)
// AWS sends binary payload, so we use express.raw middleware
app.post('/invocations', express.raw({ type: '*/*' }), async (req, res) => {
try {
// Decode binary payload from AWS SDK
const prompt = new TextDecoder().decode(req.body)
// Invoke the agent
const response = await agent.invoke(prompt)
// Return response
return res.json({ response })
} catch (err) {
console.error('Error processing request:', err)
return res.status(500).json({ error: 'Internal server error' })
}
})
// Start server
app.listen(PORT, () => {
console.log(`🚀 AgentCore Runtime server listening on port ${PORT}`)
console.log(`📍 Endpoints:`)
console.log(` POST http://0.0.0.0:${PORT}/invocations`)
console.log(` GET http://0.0.0.0:${PORT}/ping`)
})

Understanding the Endpoints

AgentCore Runtime requires your service to expose two HTTP endpoints, /ping and /invocations. See HTTP protocol contract for more details.


Compile & Start server

Terminal window
npm run build
npm start

Test health check

Terminal window
curl http://localhost:8080/ping

Test invocation

Terminal window
echo -n "What is 5 plus 3?" | curl -X POST http://localhost:8080/invocations \
-H "Content-Type: application/octet-stream" \
--data-binary @-

Create a Dockerfile for deployment:

FROM --platform=linux/arm64 public.ecr.aws/docker/library/node:latest
WORKDIR /app
# Copy source code
COPY . ./
# Install dependencies
RUN npm install
# Build TypeScript
RUN npm run build
# Expose port
EXPOSE 8080
# Start the application
CMD ["npm", "start"]

Build the image

Terminal window
docker build -t my-agent-service .

Run the container

Terminal window
docker run -p 8081:8080 my-agent-service

Test in another terminal

Terminal window
curl http://localhost:8081/ping

The agent runtime needs an IAM role with permissions to access Bedrock and other AWS services.

The easiest way to create the IAM role is to use the provided script that automates the entire process.

Create a file create-iam-role.sh:

#!/bin/bash
# Script to create IAM role for AWS Bedrock AgentCore Runtime
# Based on the CloudFormation AgentCoreRuntimeExecutionRole
set -e
# Get AWS Account ID and Region
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=${AWS_REGION:-ap-southeast-2}
echo "Creating IAM role for Bedrock AgentCore Runtime..."
echo "Account ID: ${ACCOUNT_ID}"
echo "Region: ${REGION}"
# Role name
ROLE_NAME="BedrockAgentCoreRuntimeRole"
# Create trust policy document
TRUST_POLICY=$(cat <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AssumeRolePolicy",
"Effect": "Allow",
"Principal": {
"Service": "bedrock-agentcore.amazonaws.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "${ACCOUNT_ID}"
},
"ArnLike": {
"aws:SourceArn": "arn:aws:bedrock-agentcore:${REGION}:${ACCOUNT_ID}:*"
}
}
}
]
}
EOF
)
# Create permissions policy document
PERMISSIONS_POLICY=$(cat <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ECRImageAccess",
"Effect": "Allow",
"Action": [
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer"
],
"Resource": "arn:aws:ecr:${REGION}:${ACCOUNT_ID}:repository/*"
},
{
"Sid": "ECRTokenAccess",
"Effect": "Allow",
"Action": "ecr:GetAuthorizationToken",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"logs:DescribeLogStreams",
"logs:CreateLogGroup"
],
"Resource": "arn:aws:logs:${REGION}:${ACCOUNT_ID}:log-group:/aws/bedrock-agentcore/runtimes/*"
},
{
"Effect": "Allow",
"Action": "logs:DescribeLogGroups",
"Resource": "arn:aws:logs:${REGION}:${ACCOUNT_ID}:log-group:*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:${REGION}:${ACCOUNT_ID}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*"
},
{
"Effect": "Allow",
"Action": [
"xray:PutTraceSegments",
"xray:PutTelemetryRecords",
"xray:GetSamplingRules",
"xray:GetSamplingTargets"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "cloudwatch:PutMetricData",
"Resource": "*",
"Condition": {
"StringEquals": {
"cloudwatch:namespace": "bedrock-agentcore"
}
}
},
{
"Sid": "BedrockModelAccess",
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream"
],
"Resource": [
"arn:aws:bedrock:*::foundation-model/*",
"arn:aws:bedrock:${REGION}:${ACCOUNT_ID}:*"
]
}
]
}
EOF
)
# Check if role already exists
if aws iam get-role --role-name ${ROLE_NAME} 2>/dev/null; then
echo "Role ${ROLE_NAME} already exists."
echo "Role ARN: $(aws iam get-role --role-name ${ROLE_NAME} --query 'Role.Arn' --output text)"
exit 0
fi
# Create the IAM role
echo "Creating IAM role: ${ROLE_NAME}"
aws iam create-role \
--role-name ${ROLE_NAME} \
--assume-role-policy-document "${TRUST_POLICY}" \
--description "Service role for AWS Bedrock AgentCore Runtime" \
--tags Key=ManagedBy,Value=Script Key=Purpose,Value=BedrockAgentCore
echo "Attaching permissions policy to role..."
aws iam put-role-policy \
--role-name ${ROLE_NAME} \
--policy-name AgentCoreRuntimeExecutionPolicy \
--policy-document "${PERMISSIONS_POLICY}"
# Get the role ARN
ROLE_ARN=$(aws iam get-role --role-name ${ROLE_NAME} --query 'Role.Arn' --output text)
echo ""
echo "✅ IAM Role created successfully!"
echo ""
echo "Role Name: ${ROLE_NAME}"
echo "Role ARN: ${ROLE_ARN}"
echo ""
echo "Use this ARN in your create-agent-runtime command:"
echo " --role-arn ${ROLE_ARN}"
echo ""
echo "You can also set it as an environment variable:"
echo " export ROLE_ARN=${ROLE_ARN}"

Make the script executable

Terminal window
chmod +x create-iam-role.sh

Run the script

Terminal window
./create-iam-role.sh

Or specify a different region

Terminal window
AWS_REGION=us-east-1 ./create-iam-role.sh

The script will output the role ARN. Save this for the deployment steps.

  1. Go to IAM Console → Roles → Create Role

  2. Select “Custom trust policy” and paste the trust policy above

  3. Attach the required policies:

    • AmazonBedrockFullAccess
    • CloudWatchLogsFullAccess
    • AWSXRayDaemonWriteAccess
  4. Name the role BedrockAgentCoreRuntimeRole


Set Environment Variables

Terminal window
export ACCOUNTID=$(aws sts get-caller-identity --query Account --output text)
export AWS_REGION=ap-southeast-2
// Set the IAM Role ARN
export ROLE_ARN=$(aws iam get-role \
--role-name BedrockAgentCoreRuntimeRole \
--query 'Role.Arn' \
--output text)
// New or Existing ECR repository name
export ECR_REPO=my-agent-service

Create ECR Repository

Create a new ECR repo if it doesn’t yet exist

Terminal window
aws ecr create-repository \
--repository-name ${ECR_REPO} \
--region ${AWS_REGION}

Build and Push Docker Image:

Login to ECR

Terminal window
aws ecr get-login-password --region ${AWS_REGION} | \
docker login --username AWS --password-stdin \
${ACCOUNTID}.dkr.ecr.${AWS_REGION}.amazonaws.com

Build, Tag, and Push

Terminal window
docker build -t ${ECR_REPO} .
docker tag ${ECR_REPO}:latest \
${ACCOUNTID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPO}:latest
docker push ${ACCOUNTID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPO}:latest

Create AgentCore Runtime

Terminal window
aws bedrock-agentcore-control create-agent-runtime \
--agent-runtime-name my_agent_service \
--agent-runtime-artifact containerConfiguration={containerUri=${ACCOUNTID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPO}:latest} \
--role-arn ${ROLE_ARN} \
--network-configuration networkMode=PUBLIC \
--protocol-configuration serverProtocol=HTTP \
--region ${AWS_REGION}

Wait a minute for the runtime to reach “READY” status.

Get runtime ID from the create command output, then check status

Terminal window
aws bedrock-agentcore-control get-agent-runtime \
--agent-runtime-id my-agent-service-XXXXXXXXXX \
--region ${AWS_REGION} \
--query 'status' \
--output text

You can list all runtimes if needed:

Terminal window
aws bedrock-agentcore-control list-agent-runtimes --region ${AWS_REGION}

Create invoke.ts:

Update the YOUR_ACCOUNT_ID and the agentRuntimeArn to the variables we just saw

import {
BedrockAgentCoreClient,
InvokeAgentRuntimeCommand,
} from '@aws-sdk/client-bedrock-agentcore'
const input_text = 'Calculate 5 plus 3 using the calculator tool'
const client = new BedrockAgentCoreClient({
region: 'ap-southeast-2',
})
const input = {
// Generate unique session ID
runtimeSessionId: 'test-session-' + Date.now() + '-' + Math.random().toString(36).substring(7),
// Replace with your actual runtime ARN
agentRuntimeArn:
'arn:aws:bedrock-agentcore:ap-southeast-2:YOUR_ACCOUNT_ID:runtime/my-agent-service-XXXXXXXXXX',
qualifier: 'DEFAULT',
payload: new TextEncoder().encode(input_text),
}
const command = new InvokeAgentRuntimeCommand(input)
const response = await client.send(command)
const textResponse = await response.response.transformToString()
console.log('Response:', textResponse)
Terminal window
npx tsx invoke.ts

Expected output:

Response: {"response":{"type":"agentResult","stopReason":"endTurn","lastMessage":{"type":"message","role":"assistant","content":[{"type":"textBlock","text":"The result of 5 plus 3 is **8**."}]}}}

After making code changes, use this workflow to update your deployed agent.

Build TypeScript

Terminal window
npm run build

Set Environment Variables

Terminal window
export ACCOUNTID=$(aws sts get-caller-identity --query Account --output text)
export AWS_REGION=ap-southeast-2
export ECR_REPO=my-agent-service

Get the IAM Role ARN

Terminal window
export ROLE_ARN=$(aws iam get-role --role-name BedrockAgentCoreRuntimeRole --query 'Role.Arn' --output text)

Build new image

Terminal window
docker build -t ${ACCOUNTID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPO}:latest . --no-cache

Push to ECR

Terminal window
docker push ${ACCOUNTID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPO}:latest

Update runtime

(replace XXXXXXXXXX with your runtime ID)

Terminal window
aws bedrock-agentcore-control update-agent-runtime \
--agent-runtime-id "my-agent-service-XXXXXXXXXX" \
--agent-runtime-artifact "{\"containerConfiguration\": {\"containerUri\": \"${ACCOUNTID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPO}:latest\"}}" \
--role-arn "${ROLE_ARN}" \
--network-configuration "{\"networkMode\": \"PUBLIC\"}" \
--protocol-configuration serverProtocol=HTTP \
--region ${AWS_REGION}

Wait a minute for the update to complete, then test with npx tsx invoke.ts.


Development

  • Test locally with Docker before deploying
  • Use TypeScript strict mode for better type safety
  • Include error handling in all endpoints
  • Log important events for debugging

Deployment

  • Keep IAM permissions minimal (least privilege)
  • Monitor CloudWatch logs after deployment
  • Test thoroughly after each update

TypeScript compilation fails:

Clean, install and build

Terminal window
rm -rf dist node_modules
npm install
npm run build

Docker build fails:

Ensure Docker is running

Terminal window
docker info

Try building without cache

Terminal window
docker build --no-cache -t my-agent-service .

“Access Denied” errors:

  • Verify IAM role trust policy includes your account ID
  • Check role has required permissions
  • Ensure you have permissions to create AgentCore runtimes

ECR authentication expired:

Terminal window
// Re-authenticate
aws ecr get-login-password --region ${AWS_REGION} | \
docker login --username AWS --password-stdin \
${ACCOUNTID}.dkr.ecr.${AWS_REGION}.amazonaws.com

Check CloudWatch logs

Terminal window
aws logs tail /aws/bedrock-agentcore/runtimes/my-agent-service-XXXXXXXXXX-DEFAULT \
--region ${AWS_REGION} \
--since 5m \
--follow

Amazon Bedrock AgentCore provides built-in observability through CloudWatch.

Terminal window
aws logs tail /aws/bedrock-agentcore/runtimes/my-agent-service-XXXXXXXXXX-DEFAULT \
--region ${AWS_REGION} \
--since 1h