From 36e806d394157c1847de18c872d9a5c4694392a8 Mon Sep 17 00:00:00 2001 From: Sundar Raghavan Date: Wed, 4 Mar 2026 04:58:56 -0800 Subject: [PATCH 1/2] feat: add Amazon Bedrock AgentCore Runtime deployment example --- bedrock-agentcore/.DS_Store | Bin 0 -> 6148 bytes bedrock-agentcore/.dockerignore | 8 + bedrock-agentcore/.env.example | 25 +++ bedrock-agentcore/Dockerfile | 16 ++ bedrock-agentcore/README.md | 261 +++++++++++++++++++++++++++++ bedrock-agentcore/agent.py | 101 +++++++++++ bedrock-agentcore/architecture.svg | 107 ++++++++++++ bedrock-agentcore/deploy.py | 72 ++++++++ bedrock-agentcore/kvs_turn.py | 117 +++++++++++++ bedrock-agentcore/requirements.txt | 4 + 10 files changed, 711 insertions(+) create mode 100644 bedrock-agentcore/.DS_Store create mode 100644 bedrock-agentcore/.dockerignore create mode 100644 bedrock-agentcore/.env.example create mode 100644 bedrock-agentcore/Dockerfile create mode 100644 bedrock-agentcore/README.md create mode 100644 bedrock-agentcore/agent.py create mode 100644 bedrock-agentcore/architecture.svg create mode 100644 bedrock-agentcore/deploy.py create mode 100644 bedrock-agentcore/kvs_turn.py create mode 100644 bedrock-agentcore/requirements.txt diff --git a/bedrock-agentcore/.DS_Store b/bedrock-agentcore/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0100K sessions/month, evaluate a channel pooling strategy or use third-party TURN +- No PrivateLink — VPC still needs internet egress for KVS TURN endpoints + +### Option B: Third-party TURN + +Static credentials from [Cloudflare TURN](https://developers.cloudflare.com/calls/turn/), [Twilio](https://www.twilio.com/docs/stun-turn), or [metered.ca](https://www.metered.ca/stun-turn) configured via environment variables. TCP TURN recommended for VPC. + +### Choosing between options + +| Factor | KVS Managed TURN | Third-party TURN | +|---|---|---| +| Best for | AWS-centric deployments | Simplicity, high volume | +| Setup | Moderate (signaling channel + API) | Low (env vars) | +| Credentials | Auto-rotating | Manual or provider-managed | +| Cost (<100K sessions/mo) | ~$0.03/month | Varies by provider | +| Cost (>100K sessions/mo) | Needs pooling strategy | Predictable | + +## Setup + +### 1. Configure VPC + +Create a VPC with private subnets, a NAT Gateway, and an Internet Gateway. If you already have a VPC with internet access, you can reuse it. + +```bash +# Create VPC +VPC_ID=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 \ + --query 'Vpc.VpcId' --output text) +aws ec2 modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-hostnames + +# Create subnets (two AZs for high availability) +PRIVATE_SUBNET_1=$(aws ec2 create-subnet --vpc-id $VPC_ID \ + --cidr-block 10.0.1.0/24 --availability-zone us-west-2a \ + --query 'Subnet.SubnetId' --output text) + +PRIVATE_SUBNET_2=$(aws ec2 create-subnet --vpc-id $VPC_ID \ + --cidr-block 10.0.2.0/24 --availability-zone us-west-2b \ + --query 'Subnet.SubnetId' --output text) + +PUBLIC_SUBNET=$(aws ec2 create-subnet --vpc-id $VPC_ID \ + --cidr-block 10.0.3.0/24 --availability-zone us-west-2a \ + --query 'Subnet.SubnetId' --output text) + +# Internet Gateway +IGW_ID=$(aws ec2 create-internet-gateway \ + --query 'InternetGateway.InternetGatewayId' --output text) +aws ec2 attach-internet-gateway --vpc-id $VPC_ID --internet-gateway-id $IGW_ID + +# NAT Gateway +EIP_ALLOC=$(aws ec2 allocate-address --domain vpc --query 'AllocationId' --output text) +NAT_GW_ID=$(aws ec2 create-nat-gateway --subnet-id $PUBLIC_SUBNET \ + --allocation-id $EIP_ALLOC --query 'NatGateway.NatGatewayId' --output text) +aws ec2 wait nat-gateway-available --nat-gateway-ids $NAT_GW_ID + +# Route tables +PUBLIC_RT=$(aws ec2 create-route-table --vpc-id $VPC_ID \ + --query 'RouteTable.RouteTableId' --output text) +aws ec2 create-route --route-table-id $PUBLIC_RT \ + --destination-cidr-block 0.0.0.0/0 --gateway-id $IGW_ID +aws ec2 associate-route-table --route-table-id $PUBLIC_RT --subnet-id $PUBLIC_SUBNET + +PRIVATE_RT=$(aws ec2 create-route-table --vpc-id $VPC_ID \ + --query 'RouteTable.RouteTableId' --output text) +aws ec2 create-route --route-table-id $PRIVATE_RT \ + --destination-cidr-block 0.0.0.0/0 --nat-gateway-id $NAT_GW_ID +aws ec2 associate-route-table --route-table-id $PRIVATE_RT --subnet-id $PRIVATE_SUBNET_1 +aws ec2 associate-route-table --route-table-id $PRIVATE_RT --subnet-id $PRIVATE_SUBNET_2 + +# Security group +SG_ID=$(aws ec2 create-security-group --group-name agentcore-agent-sg \ + --description "LiveKit agent on AgentCore" --vpc-id $VPC_ID \ + --query 'GroupId' --output text) +``` + +> Subnets must be in [supported Availability Zones](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agentcore-vpc.html). Verify with: `aws ec2 describe-subnets --subnet-ids --query 'Subnets[0].AvailabilityZoneId'` + +### 2. Configure IAM + +Create an IAM role for AgentCore Runtime with: +- `BedrockAgentCoreFullAccess` managed policy +- ECR image pull permissions +- Trust relationship for `bedrock-agentcore.amazonaws.com` + +If using KVS Managed TURN, add: + +```json +{ + "Effect": "Allow", + "Action": [ + "kinesisvideo:DescribeSignalingChannel", + "kinesisvideo:CreateSignalingChannel", + "kinesisvideo:GetSignalingChannelEndpoint", + "kinesisvideo:GetIceServerConfig" + ], + "Resource": "arn:aws:kinesisvideo:us-west-2:*:channel/livekit-agent-turn/*" +} +``` + +### 3. Configure environment + +```bash +cp .env.example .env +``` + +Edit `.env`: +- LiveKit connection: `LIVEKIT_URL`, `LIVEKIT_API_KEY`, `LIVEKIT_API_SECRET` +- AWS credentials: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION` +- TURN provider: set `TURN_PROVIDER=kvs` or `TURN_PROVIDER=static` and fill in the corresponding variables + +### 4. Test locally + +```bash +python -m venv .venv && source .venv/bin/activate +pip install -r requirements.txt +python agent.py download-files +python agent.py console # terminal-only test +python agent.py dev # connects to LiveKit +``` + +### 5. Build and push to ECR + +```bash +ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) +AWS_REGION=us-west-2 + +aws ecr create-repository --repository-name livekit-agentcore-agent --region $AWS_REGION + +aws ecr get-login-password --region $AWS_REGION | \ + docker login --username AWS --password-stdin \ + ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com + +docker buildx build --platform linux/arm64 \ + -t ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/livekit-agentcore-agent:latest \ + --push . +``` + +### 6. Deploy to AgentCore Runtime + +```bash +export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) +export SUBNET_IDS=subnet-XXXXX,subnet-YYYYY +export SECURITY_GROUP_IDS=sg-ZZZZZ +export ROLE_ARN=arn:aws:iam::${AWS_ACCOUNT_ID}:role/AgentCoreRuntimeRole + +python deploy.py +``` + +Save the ARN printed by the script. + +### 7. Invoke the agent + +```python +import boto3, json + +client = boto3.client("bedrock-agentcore", region_name="us-west-2") +response = client.invoke_agent_runtime( + agentRuntimeArn="", + runtimeSessionId="session-" + "a" * 30, + payload=json.dumps({"action": "start"}), + qualifier="DEFAULT", +) +print(json.loads(response["response"].read())) +``` + +Open the [LiveKit Playground](https://agents-playground.livekit.io/) to talk to the agent. + +## Monitoring + +```bash +aws logs tail /aws/bedrock-agentcore/runtimes/-DEFAULT --follow +``` + +## Cleanup + +```bash +# Delete runtime +python -c " +import boto3 +boto3.client('bedrock-agentcore-control', region_name='us-west-2').delete_agent_runtime(agentRuntimeId='') +print('Deleted') +" + +# Delete ECR repo +aws ecr delete-repository --repository-name livekit-agentcore-agent --region us-west-2 --force + +# If using KVS TURN, delete signaling channel +aws kinesisvideo delete-signaling-channel --channel-arn --region us-west-2 +``` + +Don't forget to clean up VPC resources (NAT Gateway, Elastic IP, etc.) to avoid ongoing charges. + +## Files + +| File | Description | +|---|---| +| `agent.py` | LiveKit agent with TURN configuration (KVS + static) | +| `kvs_turn.py` | KVS Managed TURN helper (GetIceServerConfig flow) | +| `deploy.py` | Deployment script using boto3 | +| `Dockerfile` | ARM64 container for AgentCore | +| `requirements.txt` | Python dependencies | +| `.env.example` | Environment variable template | + +## Resources + +- [AgentCore Runtime docs](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/what-is-bedrock-agentcore.html) +- [AgentCore VPC configuration](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agentcore-vpc.html) +- [KVS GetIceServerConfig API](https://docs.aws.amazon.com/kinesisvideostreams/latest/dg/API_signaling_GetIceServerConfig.html) +- [LiveKit Agents docs](https://docs.livekit.io/agents/) +- [LiveKit AWS plugin](https://docs.livekit.io/agents/integrations/aws.md) +- [LiveKit self-hosted deployments](https://docs.livekit.io/deploy/custom/deployments.md) diff --git a/bedrock-agentcore/agent.py b/bedrock-agentcore/agent.py new file mode 100644 index 0000000..dcfdd8d --- /dev/null +++ b/bedrock-agentcore/agent.py @@ -0,0 +1,101 @@ +"""LiveKit voice agent for Amazon Bedrock AgentCore Runtime. + +This is a standard LiveKit voice agent with ICE/TURN configuration +added for VPC deployment. It extends the pattern in the shared +python-agent-example-app/ with two AgentCore-specific changes: + 1. TURN server setup (required for NAT traversal in VPC) + 2. AWS voice pipeline (Transcribe STT + Nova Lite LLM + Polly TTS) + +TURN options (set via TURN_PROVIDER env var): + "kvs" — AWS-native KVS Managed TURN (GetIceServerConfig API) + "static" — Third-party TURN with static credentials (default) + +Usage: + Local: python agent.py console | python agent.py dev + Production: python agent.py start +""" + +import logging +import os + +from dotenv import load_dotenv +from livekit import agents, rtc +from livekit.agents import Agent, AgentServer, AgentSession +from livekit.plugins import aws, silero + +from kvs_turn import get_kvs_ice_servers + +load_dotenv() + +logger = logging.getLogger("agentcore-livekit-agent") + + +class Assistant(Agent): + def __init__(self): + super().__init__( + instructions=( + "You are a helpful voice assistant running on " + "Amazon Bedrock AgentCore Runtime. Your output will " + "be spoken aloud, so keep responses concise and " + "conversational. Avoid special characters, emojis, " + "or formatting that can't be spoken naturally." + ), + ) + + async def on_enter(self): + await self.session.generate_reply( + instructions="Greet the user and offer your assistance." + ) + + +def build_ice_servers() -> list[rtc.IceServer]: + """Build ICE servers from KVS or static env var config. + + TURN is required when running behind a NAT Gateway in a VPC. + """ + provider = os.getenv("TURN_PROVIDER", "static").lower() + + if provider == "kvs": + logger.info("Using KVS Managed TURN") + return get_kvs_ice_servers() + + turn_urls = os.getenv("TURN_SERVER_URLS", "") + if not turn_urls: + logger.warning( + "No TURN server configured. Set TURN_PROVIDER=kvs or " + "set TURN_SERVER_URLS/USERNAME/CREDENTIAL." + ) + return [] + + urls = [u.strip() for u in turn_urls.split(",") if u.strip()] + logger.info("Using static TURN config with %d URL(s)", len(urls)) + + return [ + rtc.IceServer( + urls=urls, + username=os.getenv("TURN_SERVER_USERNAME", ""), + credential=os.getenv("TURN_SERVER_CREDENTIAL", ""), + ) + ] + + +server = AgentServer() + + +@server.rtc_session(agent_name="agentcore-agent") +async def entrypoint(ctx: agents.JobContext): + ice_servers = build_ice_servers() + await ctx.connect(rtc_config=rtc.RtcConfiguration(ice_servers=ice_servers)) + + session = AgentSession( + stt=aws.STT(language="en-US"), + llm=aws.LLM(model="us.amazon.nova-2-lite-v1:0"), + tts=aws.TTS(voice="Ruth", speech_engine="generative", language="en-US"), + vad=silero.VAD.load(), + ) + + await session.start(room=ctx.room, agent=Assistant()) + + +if __name__ == "__main__": + agents.cli.run_app(server) diff --git a/bedrock-agentcore/architecture.svg b/bedrock-agentcore/architecture.svg new file mode 100644 index 0000000..8bae1b4 --- /dev/null +++ b/bedrock-agentcore/architecture.svg @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LiveKit Agent on Amazon Bedrock AgentCore Runtime + + + + AGENTCORE RUNTIME + (VPC · Private Subnets) + + + + Agent Container + livekit-agents + ARM64 · Port 8080 + + + + NAT Gateway + → Internet Gateway + + + + + + + TURN SERVER + + + + ★ KVS Managed TURN + AWS-native · Auto-rotating creds + + + + Third-party TURN + Cloudflare · Twilio · metered.ca + + + or + + + + LIVEKIT + Cloud or Self-hosted + + + + Room + Agent + Users + WebRTC media + + + + Users + Browser · Mobile · SDK + + + + + UDP/TCP + + + + media relay + + + + + + + AWS (your account) + + TURN relay + + LiveKit + + End users + ★ = recommended + diff --git a/bedrock-agentcore/deploy.py b/bedrock-agentcore/deploy.py new file mode 100644 index 0000000..0c0a432 --- /dev/null +++ b/bedrock-agentcore/deploy.py @@ -0,0 +1,72 @@ +"""Deploy LiveKit agent to Amazon Bedrock AgentCore Runtime. + +Creates an AgentCore Runtime with your containerized agent. +Run after building and pushing your Docker image to ECR. + +Usage: + export AWS_ACCOUNT_ID=123456789012 + export SUBNET_IDS=subnet-abc123,subnet-def456 + export SECURITY_GROUP_IDS=sg-abc123 + export ROLE_ARN=arn:aws:iam::123456789012:role/AgentCoreRuntimeRole + python deploy.py +""" + +import os +import sys + +import boto3 + +REGION = os.getenv("AWS_REGION", "us-west-2") +ACCOUNT_ID = os.getenv("AWS_ACCOUNT_ID", "") +ECR_REPO_NAME = os.getenv("ECR_REPO_NAME", "livekit-agentcore-agent") +AGENT_RUNTIME_NAME = os.getenv("AGENT_RUNTIME_NAME", "livekit-voice-agent") +SUBNET_IDS = os.getenv("SUBNET_IDS", "") +SECURITY_GROUP_IDS = os.getenv("SECURITY_GROUP_IDS", "") +ROLE_ARN = os.getenv("ROLE_ARN", "") + + +def deploy(): + missing = [v for v, val in [ + ("AWS_ACCOUNT_ID", ACCOUNT_ID), ("SUBNET_IDS", SUBNET_IDS), + ("SECURITY_GROUP_IDS", SECURITY_GROUP_IDS), ("ROLE_ARN", ROLE_ARN), + ] if not val] + + if missing: + print(f"Error: Missing environment variables: {', '.join(missing)}") + sys.exit(1) + + container_uri = f"{ACCOUNT_ID}.dkr.ecr.{REGION}.amazonaws.com/{ECR_REPO_NAME}:latest" + subnets = [s.strip() for s in SUBNET_IDS.split(",") if s.strip()] + security_groups = [s.strip() for s in SECURITY_GROUP_IDS.split(",") if s.strip()] + + print(f"Deploying: {AGENT_RUNTIME_NAME}") + print(f" Container: {container_uri}") + print(f" Subnets: {subnets}") + print(f" SGs: {security_groups}") + + client = boto3.client("bedrock-agentcore-control", region_name=REGION) + + try: + resp = client.create_agent_runtime( + agentRuntimeName=AGENT_RUNTIME_NAME, + agentRuntimeArtifact={ + "containerConfiguration": {"containerUri": container_uri} + }, + networkConfiguration={ + "networkMode": "VPC", + "networkModeConfig": { + "subnets": subnets, + "securityGroups": security_groups, + }, + }, + roleArn=ROLE_ARN, + ) + print(f"\nCreated! ARN: {resp['agentRuntimeArn']}") + print(f"Status: {resp['status']}") + except client.exceptions.ConflictException: + print(f"Runtime '{AGENT_RUNTIME_NAME}' already exists. Delete it first or use a different name.") + sys.exit(1) + + +if __name__ == "__main__": + deploy() diff --git a/bedrock-agentcore/kvs_turn.py b/bedrock-agentcore/kvs_turn.py new file mode 100644 index 0000000..34765c7 --- /dev/null +++ b/bedrock-agentcore/kvs_turn.py @@ -0,0 +1,117 @@ +"""KVS Managed TURN helper for LiveKit agents. + +Gets temporary TURN credentials from Amazon Kinesis Video Streams +via the GetIceServerConfig API. This provides AWS-native TURN relay +without third-party dependencies. + +Flow: + 1. Get or create a KVS signaling channel (cached after first call) + 2. Get the HTTPS endpoint for that channel + 3. Call GetIceServerConfig to get temporary TURN credentials + 4. Return as LiveKit rtc.IceServer objects + +The signaling channel is only used for TURN credential provisioning — +LiveKit handles all actual signaling. + +Cost: $0.03/month per active signaling channel. +Rate limit: GetIceServerConfig has 5 TPS per channel. +For >100K sessions/month, consider channel pooling or third-party TURN. + +Environment variables: + AWS_REGION — AWS region (default: us-west-2) + KVS_CHANNEL_NAME — Signaling channel name (default: livekit-agent-turn) + +Required IAM permissions: + kinesisvideo:DescribeSignalingChannel + kinesisvideo:CreateSignalingChannel + kinesisvideo:GetSignalingChannelEndpoint + kinesisvideo:GetIceServerConfig +""" + +import logging +import os +from typing import Optional + +import boto3 +from livekit import rtc + +logger = logging.getLogger("kvs-turn") + +AWS_REGION = os.getenv("AWS_REGION", "us-west-2") +CHANNEL_NAME = os.getenv("KVS_CHANNEL_NAME", "livekit-agent-turn") + +_cached_channel_arn: Optional[str] = None +_cached_https_endpoint: Optional[str] = None + + +def _get_or_create_channel() -> str: + """Get existing or create new KVS signaling channel. Returns ARN.""" + global _cached_channel_arn + if _cached_channel_arn: + return _cached_channel_arn + + kvs = boto3.client("kinesisvideo", region_name=AWS_REGION) + try: + resp = kvs.describe_signaling_channel(ChannelName=CHANNEL_NAME) + _cached_channel_arn = resp["ChannelInfo"]["ChannelARN"] + logger.info("Using existing KVS channel: %s", CHANNEL_NAME) + except kvs.exceptions.ResourceNotFoundException: + resp = kvs.create_signaling_channel( + ChannelName=CHANNEL_NAME, ChannelType="SINGLE_MASTER" + ) + _cached_channel_arn = resp["ChannelARN"] + logger.info("Created KVS signaling channel: %s", CHANNEL_NAME) + + return _cached_channel_arn + + +def _get_https_endpoint(channel_arn: str) -> str: + """Get HTTPS endpoint for the signaling channel.""" + global _cached_https_endpoint + if _cached_https_endpoint: + return _cached_https_endpoint + + kvs = boto3.client("kinesisvideo", region_name=AWS_REGION) + resp = kvs.get_signaling_channel_endpoint( + ChannelARN=channel_arn, + SingleMasterChannelEndpointConfiguration={ + "Protocols": ["HTTPS"], + "Role": "MASTER", + }, + ) + _cached_https_endpoint = resp["ResourceEndpointList"][0]["ResourceEndpoint"] + return _cached_https_endpoint + + +def get_kvs_ice_servers() -> list[rtc.IceServer]: + """Get TURN ICE servers from KVS. + + Returns LiveKit rtc.IceServer objects with temporary credentials. + Only TURN URIs are returned (STUN filtered — not useful behind NAT). + """ + channel_arn = _get_or_create_channel() + endpoint = _get_https_endpoint(channel_arn) + + signaling = boto3.client( + "kinesis-video-signaling", + region_name=AWS_REGION, + endpoint_url=endpoint, + ) + resp = signaling.get_ice_server_config( + ChannelARN=channel_arn, Service="TURN" + ) + + ice_servers = [] + for server in resp["IceServerList"]: + turn_urls = [u for u in server["Uris"] if u.startswith("turn:")] + if turn_urls: + ice_servers.append( + rtc.IceServer( + urls=turn_urls, + username=server.get("Username", ""), + credential=server.get("Password", ""), + ) + ) + + logger.info("Got %d TURN server(s) from KVS channel '%s'", len(ice_servers), CHANNEL_NAME) + return ice_servers diff --git a/bedrock-agentcore/requirements.txt b/bedrock-agentcore/requirements.txt new file mode 100644 index 0000000..936c2e4 --- /dev/null +++ b/bedrock-agentcore/requirements.txt @@ -0,0 +1,4 @@ +livekit-agents[silero]~=1.4 +livekit-plugins-aws~=1.4 +python-dotenv +boto3 From fcb901ebaff412bd6591686634647e68c04f8fb4 Mon Sep 17 00:00:00 2001 From: Sundar Raghavan Date: Wed, 4 Mar 2026 05:00:07 -0800 Subject: [PATCH 2/2] feat: add Amazon Bedrock AgentCore Runtime deployment example --- bedrock-agentcore/.DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 bedrock-agentcore/.DS_Store diff --git a/bedrock-agentcore/.DS_Store b/bedrock-agentcore/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0