TypeScript Deployment to Amazon Bedrock AgentCore Runtime¶
This guide covers deploying TypeScript-based Strands agents to Amazon Bedrock AgentCore Runtime using Express and Docker.
Prerequisites¶
- Node.js 20+
- Docker installed and running
- AWS CLI configured with valid credentials
- AWS account with appropriate permissions
- ECR repository access
Step 1: Project Setup¶
Create Project Structure¶
mkdir my-agent-service && cd my-agent-service
npm init -y
Install Dependencies¶
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:
npm install
Configure TypeScript¶
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"]
}
Step 2: Create Your Agent¶
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.
Step 3: Test Locally¶
Compile & Start server
npm run build
npm start
Test health check
curl http://localhost:8080/ping
Test invocation
echo -n "What is 5 plus 3?" | curl -X POST http://localhost:8080/invocations \
-H "Content-Type: application/octet-stream" \
--data-binary @-
Step 4: Create Dockerfile¶
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"]
Test Docker Build Locally¶
Build the image
docker build -t my-agent-service .
Run the container
docker run -p 8081:8080 my-agent-service
Test in another terminal
curl http://localhost:8081/ping
Step 5: Create IAM Role¶
The agent runtime needs an IAM role with permissions to access Bedrock and other AWS services.
Option 1: Using a Script (Recommended)¶
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
chmod +x create-iam-role.sh
Run the script
./create-iam-role.sh
Or specify a different region
AWS_REGION=us-east-1 ./create-iam-role.sh
The script will output the role ARN. Save this for the deployment steps.
Option 2: Using AWS Console¶
- Go to IAM Console → Roles → Create Role
- Select "Custom trust policy" and paste the trust policy above
-
Attach the required policies:
- AmazonBedrockFullAccess
- CloudWatchLogsFullAccess
- AWSXRayDaemonWriteAccess
-
Name the role
BedrockAgentCoreRuntimeRole
Step 6: Deploy to AWS¶
Set Environment Variables
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
aws ecr create-repository \
--repository-name ${ECR_REPO} \
--region ${AWS_REGION}
Build and Push Docker Image:
Login to ECR
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
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
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}
Verify Deployment Status¶
Wait a minute for the runtime to reach "READY" status.
Get runtime ID from the create command output, then check status
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:
aws bedrock-agentcore-control list-agent-runtimes --region ${AWS_REGION}
Step 7: Test Your Deployment¶
Create Test Script¶
Create invoke.ts:
Update the
YOUR_ACCOUNT_IDand theagentRuntimeArnto 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)
Run the Test¶
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**."}]}}}
Step 8: Update Your Deployment¶
After making code changes, use this workflow to update your deployed agent.
Build TypeScript
npm run build
Set Environment Variables
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
export ROLE_ARN=$(aws iam get-role --role-name BedrockAgentCoreRuntimeRole --query 'Role.Arn' --output text)
Build new image
docker build -t ${ACCOUNTID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPO}:latest . --no-cache
Push to ECR
docker push ${ACCOUNTID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPO}:latest
Update runtime
(replace XXXXXXXXXX with your runtime ID)
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.
Best Practices¶
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
Troubleshooting¶
Build Errors¶
TypeScript compilation fails:
Clean, install and build
rm -rf dist node_modules
npm install
npm run build
Docker build fails:
Ensure Docker is running
docker info
Try building without cache
docker build --no-cache -t my-agent-service .
Deployment Errors¶
"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:
// Re-authenticate
aws ecr get-login-password --region ${AWS_REGION} | \
docker login --username AWS --password-stdin \
${ACCOUNTID}.dkr.ecr.${AWS_REGION}.amazonaws.com
Runtime Errors¶
Check CloudWatch logs
aws logs tail /aws/bedrock-agentcore/runtimes/my-agent-service-XXXXXXXXXX-DEFAULT \
--region ${AWS_REGION} \
--since 5m \
--follow
Observability¶
Amazon Bedrock AgentCore provides built-in observability through CloudWatch.
View Recent Logs¶
aws logs tail /aws/bedrock-agentcore/runtimes/my-agent-service-XXXXXXXXXX-DEFAULT \
--region ${AWS_REGION} \
--since 1h