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
134 changes: 134 additions & 0 deletions eventbridge-apidestination-agentcore-cdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Amazon EventBridge API Destination to Amazon Bedrock AgentCore Runtime

This pattern demonstrates **Lambda-less, event-driven invocation of an AI agent**: an EventBridge rule delivers events directly to an Amazon Bedrock AgentCore Runtime endpoint via an API Destination, authenticated with Cognito machine-to-machine (M2M) OAuth. No Lambda function, no glue code.

The CDK stack is **fully self-contained** — it builds and deploys the AgentCore Runtime (from the bundled `agent-code/` Docker image) alongside the EventBridge plumbing, so a single `cdk deploy` gives you a working, testable pattern.

![Architecture](architecture.png)

```
EventBridge Rule ──▶ API Destination (HTTPS + OAuth) ──▶ AgentCore Runtime
│ │ │
custom event bus Connection: Cognito async processing
(demo.orders) client_credentials JWT (ack < 5s, work in
background)
```

Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/

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](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example.

## How it works

1. An event (e.g. `source: demo.orders`, `detail-type: OrderCreated`) is published to a custom event bus.
2. An EventBridge rule matches the event and forwards it to an **API Destination** whose endpoint is the AgentCore Runtime `InvokeAgentRuntime` HTTPS API.
3. The API Destination's **Connection** obtains an OAuth access token from a **Cognito user pool token endpoint** using the `client_credentials` grant, and attaches it as a Bearer token.
4. The AgentCore Runtime validates the JWT against the Cognito user pool (inbound identity / `customJwtAuthorizer`), **acknowledges the request within 5 seconds**, and processes the event **asynchronously**.
5. Failed deliveries (after 3 retries) are sent to an SQS dead-letter queue.

## Key technical details

### 1. The 5-second timeout → async execution

EventBridge API Destinations enforce a hard **5-second response timeout**. Agent reasoning takes much longer than that. The AgentCore Runtime therefore runs in **asynchronous mode**: the agent entrypoint returns an acknowledgment immediately (HTTP 2xx) and continues working in the background. See [`agent-code/agent.py`](agent-code/agent.py) for the implementation — it uses `asyncio.create_task` to kick off the real work, then returns `{"status": "accepted"}` well within the 5-second window.

```python
from bedrock_agentcore import BedrockAgentCoreApp
import asyncio

app = BedrockAgentCoreApp()

@app.entrypoint
async def invoke(payload):
# Kick off long-running agent work in the background
asyncio.create_task(process_event(payload))
# Acknowledge within the 5-second API Destination timeout
return {"status": "accepted"}
```

### 2. The URL-encoding gotcha → use the agent ID, not the ARN

API Destinations **automatically decode `%XX` sequences** in the endpoint URL. A URL-encoded runtime ARN in the path (containing `:` and `/`) gets decoded back and breaks the request signature/routing.

The fix: use the **agent runtime ID in the path** and pass the **account ID as a query parameter**. Per the AWS docs: *"When you use the agent ID instead of the full ARN, you don't need to URL-encode the identifier."* The stack derives this URL automatically from the runtime it creates (`CfnRuntime.attrAgentRuntimeId`):

```
https://bedrock-agentcore.<region>.amazonaws.com/runtimes/<agentRuntimeId>/invocations?accountId=<accountId>&qualifier=DEFAULT
```

### 3. Authentication → Cognito M2M (client_credentials)

The stack creates:
- A **Cognito user pool** with a hosted domain (provides the `/oauth2/token` endpoint)
- A **resource server** (`agentcore`) with a custom scope (`agentcore/invoke`)
- An **app client** with a secret and the `client_credentials` grant

The EventBridge Connection is configured with OAuth (client credentials) against the Cognito token endpoint. The AgentCore Runtime's `customJwtAuthorizer` is wired to the **same** user pool at creation time (its `discoveryUrl` and `allowedClients` reference the pool and app client this stack creates), so there is no manual post-deploy step.

## Prerequisites

- [AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) with sufficient permissions
- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cli.html) installed and configured
- [Node.js 20+](https://nodejs.org/en/download/) and npm
- [AWS CDK CLI](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) (`npm i -g aws-cdk`), bootstrapped in the target account/region
- [Docker](https://docs.docker.com/get-docker/) installed and running (the CDK build packages the agent into a container image)
- Access to the Amazon Bedrock model your agent uses (the bundled agent uses the [Strands](https://strandsagents.com/) default model; enable model access in the Amazon Bedrock console for your region)

## Deployment

1. Clone and enter the pattern directory:

```bash
git clone https://github.com/aws-samples/serverless-patterns
cd serverless-patterns/eventbridge-apidestination-agentcore-cdk/cdk
npm install
```

2. Deploy. The stack builds the agent container image, deploys the AgentCore Runtime, and wires up EventBridge — all in one command:

```bash
cdk deploy
```

3. Note the stack outputs — in particular `EventBusName`, `AgentRuntimeId`, and `DeadLetterQueueUrl`. No further configuration is required: the runtime's JWT authorizer already trusts the Cognito app client created by this stack.

## Testing

Publish a test event to the custom bus (`EventBusName` output):

```bash
aws events put-events --entries '[
{
"EventBusName": "agentcore-events",
"Source": "demo.orders",
"DetailType": "OrderCreated",
"Detail": "{\"orderId\": \"12345\", \"prompt\": \"Summarize this order and flag any anomalies.\"}"
}
]'
```

Verify the invocation:

1. **AgentCore Runtime logs** — check CloudWatch Logs for the runtime (`/aws/bedrock-agentcore/runtimes/<AgentRuntimeId>-DEFAULT`) to see the event arrive and background processing run.
2. **Connection health** — `aws events describe-connection --name agentcore-cognito-oauth` should show `AUTHORIZED`.
3. **Failures** — if delivery fails after retries, events land in the DLQ:

```bash
aws sqs receive-message --queue-url <DeadLetterQueueUrl output>
```

Common failure causes:
- HTTP 401/403 in the DLQ → the Connection couldn't obtain or present a valid token (check the Connection status and the Cognito app client secret).
- Timeouts → the agent isn't acknowledging within 5 seconds (keep the entrypoint async; see `agent-code/agent.py`).

## Cleanup

```bash
cdk destroy
```

---

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

SPDX-License-Identifier: MIT-0
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
__pycache__
*.pyc
.git
.gitignore
.venv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__
*.pyc
.venv
20 changes: 20 additions & 0 deletions eventbridge-apidestination-agentcore-cdk/agent-code/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"]
50 changes: 50 additions & 0 deletions eventbridge-apidestination-agentcore-cdk/agent-code/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""
Minimal async AgentCore Runtime entrypoint for the
EventBridge API Destination -> AgentCore Runtime pattern.

Why async: EventBridge API Destinations enforce a hard 5-second response
timeout on the target endpoint. Agent reasoning (an LLM call via Strands)
routinely takes longer than that, so this entrypoint acknowledges the
request immediately (HTTP 2xx, well under 5s) and continues the actual
agent work in a background asyncio task.

This is intentionally minimal so the pattern deploys and can be tested
end-to-end. Swap the Strands `Agent()` call for your own tools/model
config as needed.
"""
import asyncio
import logging

from bedrock_agentcore import BedrockAgentCoreApp
from strands import Agent

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = BedrockAgentCoreApp()
agent = Agent(model="us.anthropic.claude-haiku-4-5-20251001-v1:0")


async def process_event(payload: dict) -> None:
"""Runs the actual agent reasoning after the HTTP response has
already been returned to EventBridge. Errors here are logged only:
there is no caller left to report back to."""
prompt = payload.get("prompt", "Summarize this event.")
order_id = payload.get("orderId", "unknown")
try:
result = agent(prompt)
logger.info("orderId=%s agent result: %s", order_id, result)
except Exception:
logger.exception("orderId=%s agent invocation failed", order_id)


@app.entrypoint
async def invoke(payload: dict) -> dict:
# Fire-and-forget the real work so we can return well within the
# API Destination's 5-second timeout.
asyncio.create_task(process_event(payload))
return {"status": "accepted", "orderId": payload.get("orderId")}


if __name__ == "__main__":
app.run()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
strands-agents==1.50.2
bedrock-agentcore==1.18.1
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions eventbridge-apidestination-agentcore-cdk/cdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
cdk.out
*.js
!jest.config.js
*.d.ts
.cdk.staging
*.tsbuildinfo
14 changes: 14 additions & 0 deletions eventbridge-apidestination-agentcore-cdk/cdk/bin/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { EventBridgeAgentCoreStack } from '../lib/eventbridge-agentcore-stack';

const app = new cdk.App();

new EventBridgeAgentCoreStack(app, 'EventBridgeAgentCoreStack', {
description:
'ServerlessLand pattern: EventBridge API Destination -> AgentCore Runtime (Lambda-less event-driven agent invocation)',
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
});
21 changes: 21 additions & 0 deletions eventbridge-apidestination-agentcore-cdk/cdk/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"app": "npx ts-node --prefer-ts-exts bin/app.ts",
"watch": {
"include": ["**"],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"node_modules",
"cdk.out"
]
},
"context": {
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/aws-iam:standardizedServicePrincipals": true
}
}
Loading