Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,6 @@ terraform.rc
*.idea

# This file gets generated as part of Maven shade plugin which can be ignored
dependency-reduced-pom.xml
dependency-reduced-pom.xml

.history
10 changes: 10 additions & 0 deletions cloudfront-agentcore-runtime-cdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
*.swp
package-lock.json
__pycache__
.pytest_cache
.venv
*.egg-info
*.pyc
cdk.out
.DS_Store
.bedrock_agentcore
157 changes: 157 additions & 0 deletions cloudfront-agentcore-runtime-cdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# CloudFront to Amazon Bedrock AgentCore Runtime

This pattern demonstrates how to proxy requests to Amazon Bedrock AgentCore Runtime through CloudFront with OAuth 2.0 authentication, supporting all three [AgentCore Runtime service contracts](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-service-contract.html): A2A, HTTP, and MCP protocols.

Benefits of using CloudFront in front of AgentCore Runtime:
- Global edge caching and low-latency access
- DDoS protection via AWS Shield Standard (included)
- Optional AWS WAF integration for IP rate limiting, geo-blocking, and bot protection
- Custom domain support with SSL/TLS certificates
- Request/response transformation via Amazon CloudFront Functions
- Centralized access logging and monitoring

Learn more about this pattern at [Serverless Land Patterns](https://serverlessland.com/patterns/cloudfront-agentcore-runtime-cdk)

Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the AWS Pricing page for details. You are responsible for any AWS costs incurred. No warranty is implied in this example.

## Pre-requisites

- [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in.
- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured
- [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
- [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) installed and configured
- [Docker](https://docs.docker.com/get-docker/) installed and running
- [Python 3.12](https://www.python.org/downloads/) installed

## Deployment Instructions

1. Clone the GitHub repository:

```shell
git clone https://github.com/aws-samples/serverless-patterns
cd serverless-patterns/cloudfront-agentcore-runtime-cdk
```

2. Create and activate a Python virtual environment:

```shell
python3 -m venv .venv
source .venv/bin/activate
```

3. Install the required dependencies:

```shell
pip3 install -r requirements.txt
```

4. Deploy the stacks:

```shell
cdk deploy --all
```

5. After `cdk deploy --all` completes, verify the deployment:
1. Check the CDK outputs for `UserPoolId`, `UserPoolClientId`, and `DistributionUrl`.
2. Verify the CloudFront distribution is deployed:
```shell
aws cloudfront list-distributions --query "DistributionList.Items[*].[Id,Status,DomainName]" --output table
```
3. Confirm the distribution status shows "Deployed".

## How it works

This pattern creates:

1. **Amazon Bedrock AgentCore Runtimes** - Three agents supporting A2A, HTTP, and MCP protocols
2. **Amazon Cognito User Pool** for JWT authentication
3. **CloudFront Distribution** that proxies requests to AgentCore Runtimes:
- `/a2a/*` - A2A protocol agent
- `/rest/*` - HTTP protocol agent
- `/mcp/*` - MCP protocol agent

**Architecture Flow:**
1. Client sends request to CloudFront with Bearer token
2. CloudFront Function strips path prefix (/a2a, /rest, /mcp)
3. Request is forwarded to the appropriate AgentCore Runtime
4. AgentCore validates JWT token
5. Response is returned through the same path

## Testing

1. Get Pool ID, Client ID, and CloudFront URL from the CDK outputs after running the `cdk deploy` command. Look for `UserPoolId`, `UserPoolClientId`, and `DistributionUrl` in the outputs.

2. Get a bearer token:

```shell
cd test
./get_token.sh
```

Example output:
```
Cognito Token Generator
Get Pool ID and Client ID from CDK stack outputs

Pool ID []: us-west-2_xxxxxx
Client ID []: xxxxxxxxxxxxxxxxxx
Username []: testuser
Password:
Region [us-west-2]:
export BEARER_TOKEN="eyJraWQiOi..."
```

3. Set environment variables (get DistributionUrl from AgentcoreCloudFrontStack outputs):

```shell
export CF_URL="https://<distribution-id>.cloudfront.net"
export BEARER_TOKEN="<token-from-step-2>"
```

4. Test A2A protocol:

```shell
python test_a2a.py
```

You can also use tools like [A2A Inspector](https://a2a-inspector.vercel.app/) with the agent card URL:
```
https://<distribution-id>.cloudfront.net/a2a/
```

5. Test HTTP protocol:

```shell
python test_http.py
```

6. Test MCP protocol (requires `mcp` package):

```shell
python test_mcp.py
```

## Optional: Update A2A Agent Card URL

If you want A2A clients to discover your agent via CloudFront instead of the direct AgentCore Runtime URL, run this script after deployment:

```shell
cd ..
./scripts/update_a2a_cloudfront_url.sh
```

This configures the A2A agent to advertise the CloudFront URL in its agent card (`/.well-known/agent-card.json`). This is required when using A2A-compatible clients that rely on the agent card for endpoint discovery.

## Cleanup

Delete the stacks:

```bash
cdk destroy --all
```

---

Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.

SPDX-License-Identifier: MIT-0
20 changes: 20 additions & 0 deletions cloudfront-agentcore-runtime-cdk/agent-code/a2a/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM public.ecr.aws/docker/library/python:3.12-slim

WORKDIR /app

RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

RUN useradd -m -u 1000 bedrock_agentcore
USER bedrock_agentcore

EXPOSE 9000

COPY . .

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:9000/ping || exit 1

CMD ["python", "agent.py"]
44 changes: 44 additions & 0 deletions cloudfront-agentcore-runtime-cdk/agent-code/a2a/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import logging
import os
from strands import Agent
from strands.multiagent.a2a import A2AServer
from a2a.types import AgentSkill
import uvicorn
from fastapi import FastAPI

logging.basicConfig(level=logging.INFO)

runtime_url = os.environ.get('CLOUDFRONT_URL', os.environ.get('AGENTCORE_RUNTIME_URL', 'http://127.0.0.1:9000/'))

strands_agent = Agent(
name="Test Agent",
description="A helpful assistant that answers questions clearly and concisely.",
callback_handler=None
)

a2a_server = A2AServer(
agent=strands_agent,
http_url=runtime_url,
serve_at_root=True,
enable_a2a_compliant_streaming=True,
skills=[
AgentSkill(
id="general-assistant",
name="General Assistant",
description="Answers questions and provides helpful information",
tags=["assistant", "qa"],
examples=["What is the capital of France?"]
)
]
)

a2a_app = a2a_server.to_fastapi_app()

@a2a_app.get("/ping")
def ping():
return {"status": "healthy"}

app = a2a_app

if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=9000)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
strands-agents[a2a]
bedrock-agentcore
uvicorn
fastapi
20 changes: 20 additions & 0 deletions cloudfront-agentcore-runtime-cdk/agent-code/http/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM public.ecr.aws/docker/library/python:3.12-slim

WORKDIR /app

RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

RUN useradd -m -u 1000 bedrock_agentcore
USER bedrock_agentcore

EXPOSE 8080

COPY . .

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/ping || exit 1

CMD ["python", "agent.py"]
17 changes: 17 additions & 0 deletions cloudfront-agentcore-runtime-cdk/agent-code/http/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from strands import Agent
from bedrock_agentcore import BedrockAgentCoreApp

app = BedrockAgentCoreApp()
agent = Agent()

@app.entrypoint
async def agent_invocation(payload):
user_message = payload.get(
"prompt", "No prompt found in input, please provide a json payload with prompt key"
)
stream = agent.stream_async(user_message)
async for event in stream:
yield event

if __name__ == "__main__":
app.run()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
strands-agents
bedrock-agentcore
20 changes: 20 additions & 0 deletions cloudfront-agentcore-runtime-cdk/agent-code/mcp/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM public.ecr.aws/docker/library/python:3.12-slim

WORKDIR /app

RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

RUN useradd -m -u 1000 bedrock_agentcore
USER bedrock_agentcore

EXPOSE 8000

COPY . .

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/mcp || exit 1

CMD ["python", "agent.py"]
18 changes: 18 additions & 0 deletions cloudfront-agentcore-runtime-cdk/agent-code/mcp/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from mcp.server.fastmcp import FastMCP

mcp = FastMCP(host="0.0.0.0", stateless_http=True)

@mcp.tool()
def add_numbers(a: int, b: int) -> int:
return a + b

@mcp.tool()
def multiply_numbers(a: int, b: int) -> int:
return a * b

@mcp.tool()
def greet_user(name: str) -> str:
return f"Hello, {name}!"

if __name__ == "__main__":
mcp.run(transport="streamable-http")
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mcp
29 changes: 29 additions & 0 deletions cloudfront-agentcore-runtime-cdk/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env python3

import aws_cdk as cdk
import os

from infra.agentcore_stack import AgentcoreStack
from infra.cloudfront_stack import CloudFrontStack

app = cdk.App()

region = os.environ.get("CDK_DEFAULT_REGION")
if not region:
raise ValueError("CDK_DEFAULT_REGION environment variable must be set. Run: export CDK_DEFAULT_REGION=<your-region>")

agentcore_stack = AgentcoreStack(app, "AgentCoreAgentsStack",
env=cdk.Environment(region=region),
cross_region_references=True
)

cloudfront_stack = CloudFrontStack(app, "CloudFrontToAgentCoreStack",
env=cdk.Environment(region=region),
cross_region_references=True,
a2a_agent_runtime_arn=agentcore_stack.a2a_agent_runtime_arn,
http_agent_runtime_arn=agentcore_stack.http_agent_runtime_arn,
mcp_agent_runtime_arn=agentcore_stack.mcp_agent_runtime_arn
)
cloudfront_stack.add_dependency(agentcore_stack)

app.synth()
Loading