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
94 changes: 94 additions & 0 deletions python/03-integrate/scheduling/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Scheduled Agents — EventBridge Scheduler + AgentCore

Invoke a [Strands Agents](https://github.com/strands-agents/sdk-python) agent running on [Bedrock AgentCore](https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore.html) on a recurring schedule using **Amazon EventBridge Scheduler**.

## Architecture

```
EventBridge Scheduler ──▶ EventBridge Bus ──▶ Rule + InputTransformer ──▶ API Destination ──▶ AgentCore Runtime
Cognito OAuth
(client_credentials)
```

1. **EventBridge Scheduler** fires on a cron/rate expression and puts an event on a custom EventBridge bus
2. A **Rule** matches events from `scheduler.agentcore` with detail-type `ScheduledAgentInvocation`
3. An **InputTransformer** extracts `$.detail.prompt` into `{"prompt": "<value>"}`
4. An **EventBridge Connection** acquires a Cognito OAuth token via `client_credentials`
5. An **API Destination** POSTs to the AgentCore `/invocations` endpoint with `Authorization: Bearer <jwt>`
6. **AgentCore** validates the JWT and runs the Strands agent

> **Why the extra hop?** EventBridge Scheduler can't target API Destinations directly.
> Scheduler puts an event on a bus, then a Rule routes it to the API Destination.

## Prerequisites

- AWS CLI v2, SAM CLI, Docker (with buildx)
- A Bedrock AgentCore–enabled AWS account

## Project Structure

```
scheduling/
├── agent/
│ ├── agent.py # Strands agent with get_current_time tool
│ ├── Dockerfile
│ └── requirements.txt
├── deploy.sh # Full deployment script (7 steps)
├── template.yaml # SAM template (Cognito, EventBridge bus, DLQ)
└── README.md
```

## Deploy

```bash
# Defaults: rate(1 hour), us-east-1
./deploy.sh

# Custom schedule and region
SCHEDULE_EXPR="cron(0 9 * * ? *)" AWS_REGION=us-west-2 ./deploy.sh
```

The deploy script handles everything in 7 steps:
1. Creates an ECR repository
2. Builds and pushes the agent container (ARM64)
3. Creates the AgentCore execution IAM role
4. Creates the AgentCore Runtime
5. Deploys the SAM stack (Cognito + EventBridge bus + DLQ)
6. Wires up the EventBridge Connection, API Destination, and Rule
7. Creates the EventBridge Schedule

### Environment Variables

| Variable | Default | Description |
|---|---|---|
| `STACK_NAME` | `scheduled-agentcore` | CloudFormation stack name |
| `AGENT_NAME` | `scheduled_agentcore_agent` | AgentCore runtime name |
| `AWS_REGION` | `us-east-1` | AWS region |
| `SCHEDULE_EXPR` | `rate(1 hour)` | EventBridge schedule expression |

## Post-Deploy

After deployment, configure the AgentCore JWT authorizer with the Cognito values printed at the end of the deploy script output.

## Test Manually

```bash
aws events put-events --entries '[{
"EventBusName": "scheduled-agentcore-bus",
"Source": "scheduler.agentcore",
"DetailType": "ScheduledAgentInvocation",
"Detail": "{\"prompt\": \"Hello from manual test!\"}"
}]' --region us-east-1
```

## Cleanup

```bash
STACK_NAME=scheduled-agentcore
REGION=us-east-1

aws scheduler delete-schedule --name ${STACK_NAME}-schedule --region ${REGION}
aws events delete-connection --name ${STACK_NAME}-connection --region ${REGION}
aws cloudformation delete-stack --stack-name ${STACK_NAME} --region ${REGION}
```
9 changes: 9 additions & 0 deletions python/03-integrate/scheduling/agent/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM --platform=linux/arm64 ghcr.io/astral-sh/uv:python3.11-bookworm-slim

WORKDIR /app
COPY requirements.txt .
RUN uv pip install --system -r requirements.txt
COPY agent.py .

EXPOSE 8080
CMD ["python", "agent.py"]
28 changes: 28 additions & 0 deletions python/03-integrate/scheduling/agent/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import json
from datetime import datetime, timezone
from strands import Agent, tool
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands.models.bedrock import BedrockModel

app = BedrockAgentCoreApp()
model = BedrockModel(model_id="amazon.nova-pro-v1:0")


@tool
def get_current_time() -> str:
"""Return the current UTC timestamp."""
return datetime.now(timezone.utc).isoformat()


agent = Agent(model=model, tools=[get_current_time])


@app.entrypoint
def invoke(payload):
prompt = payload.get("prompt", "You were triggered by a schedule. Summarize what time it is and confirm you are running.")
result = agent(prompt)
return {"message": result.message}


if __name__ == "__main__":
app.run()
2 changes: 2 additions & 0 deletions python/03-integrate/scheduling/agent/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
strands-agents
bedrock-agentcore
163 changes: 163 additions & 0 deletions python/03-integrate/scheduling/deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#!/usr/bin/env bash
set -euo pipefail

# ── Configuration ──────────────────────────────────────────────
STACK_NAME="${STACK_NAME:-scheduled-agentcore}"
AGENT_NAME="${AGENT_NAME:-scheduled_agentcore_agent}"
REGION="${AWS_REGION:-us-east-1}"
SCHEDULE_EXPR="${SCHEDULE_EXPR:-rate(1 hour)}"
SCHEDULE_PAYLOAD="${SCHEDULE_PAYLOAD:-{\"prompt\":\"You are triggered by a schedule. Report the current time and confirm you are running.\"}}"
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
ECR_REPO="${STACK_NAME}-agent"
ECR_URI="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${ECR_REPO}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

echo "══════════════════════════════════════════════════════════"
echo " Account: ${ACCOUNT_ID}"
echo " Region: ${REGION}"
echo " Stack: ${STACK_NAME}"
echo " Schedule: ${SCHEDULE_EXPR}"
echo "══════════════════════════════════════════════════════════"

# ── Step 1: ECR ────────────────────────────────────────────────
echo -e "\n▶ Step 1/5: ECR repository..."
aws ecr describe-repositories --repository-names "${ECR_REPO}" --region "${REGION}" >/dev/null 2>&1 \
|| aws ecr create-repository --repository-name "${ECR_REPO}" --region "${REGION}" --output text >/dev/null

aws ecr get-login-password --region "${REGION}" \
| docker login --username AWS --password-stdin "${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com" 2>/dev/null

# ── Step 2: Build & push container ─────────────────────────────
echo -e "\n▶ Step 2/5: Building agent container (ARM64)..."
docker buildx build --platform linux/arm64 \
-t "${ECR_URI}:latest" --push "${SCRIPT_DIR}/agent"

# ── Step 3: AgentCore execution role ───────────────────────────
echo -e "\n▶ Step 3/5: AgentCore execution role..."
ROLE_NAME="${STACK_NAME}-AgentCoreRole"

ROLE_ARN=$(aws iam get-role --role-name "${ROLE_NAME}" --query 'Role.Arn' --output text 2>/dev/null || echo "")
if [ -z "${ROLE_ARN}" ] || [ "${ROLE_ARN}" = "None" ]; then
ROLE_ARN=$(aws iam create-role \
--role-name "${ROLE_NAME}" \
--assume-role-policy-document "{
\"Version\":\"2012-10-17\",
\"Statement\":[{
\"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}:*\"}
}
}]
}" --query 'Role.Arn' --output text)
echo " Created: ${ROLE_ARN}"
else
echo " Exists: ${ROLE_ARN}"
fi

aws iam put-role-policy --role-name "${ROLE_NAME}" \
--policy-name AgentCorePolicy \
--policy-document "{
\"Version\":\"2012-10-17\",
\"Statement\":[
{\"Effect\":\"Allow\",\"Action\":[\"ecr:BatchGetImage\",\"ecr:GetDownloadUrlForLayer\"],\"Resource\":\"arn:aws:ecr:${REGION}:${ACCOUNT_ID}:repository/${ECR_REPO}\"},
{\"Effect\":\"Allow\",\"Action\":\"ecr:GetAuthorizationToken\",\"Resource\":\"*\"},
{\"Effect\":\"Allow\",\"Action\":[\"logs:CreateLogGroup\",\"logs:DescribeLogStreams\"],\"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\"}}},
{\"Effect\":\"Allow\",\"Action\":[\"bedrock:InvokeModel\",\"bedrock:InvokeModelWithResponseStream\"],\"Resource\":[\"arn:aws:bedrock:*::foundation-model/*\",\"arn:aws:bedrock:${REGION}:${ACCOUNT_ID}:*\"]}
]
}"

sleep 10

# ── Step 4: Create AgentCore Runtime ──────────────────────────
echo -e "\n▶ Step 4/5: AgentCore Runtime..."
AGENT_ARN=""
RUNTIMES=$(aws bedrock-agentcore-control list-agent-runtimes --region "${REGION}" --query "agentRuntimes[?agentRuntimeName=='${AGENT_NAME}'].agentRuntimeArn" --output text 2>/dev/null || echo "")
if [ -n "${RUNTIMES}" ] && [ "${RUNTIMES}" != "None" ]; then
AGENT_ARN="${RUNTIMES}"
echo " Exists: ${AGENT_ARN}"
else
AGENT_ARN=$(aws bedrock-agentcore-control create-agent-runtime \
--agent-runtime-name "${AGENT_NAME}" \
--agent-runtime-artifact "{\"containerConfiguration\":{\"containerUri\":\"${ECR_URI}:latest\"}}" \
--network-configuration '{"networkMode":"PUBLIC"}' \
--role-arn "${ROLE_ARN}" \
--region "${REGION}" \
--query 'agentRuntimeArn' --output text)
echo " Created: ${AGENT_ARN}"

echo " Waiting for ACTIVE status..."
for i in $(seq 1 30); do
STATUS=$(aws bedrock-agentcore-control get-agent-runtime \
--agent-runtime-id "${AGENT_ARN}" --region "${REGION}" \
--query 'status' --output text 2>/dev/null || echo "CREATING")
printf " [%02d/30] %s\n" "$i" "${STATUS}"
[ "${STATUS}" = "ACTIVE" ] && break
sleep 10
done
fi

ENCODED_ARN=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${AGENT_ARN}', safe=''))")
AGENT_ENDPOINT="https://bedrock-agentcore.${REGION}.amazonaws.com/runtimes/${ENCODED_ARN}/invocations"

# ── Step 5: SAM deploy (all infrastructure) ────────────────────
echo -e "\n▶ Step 5/5: Deploying SAM stack..."
cd "${SCRIPT_DIR}"
sam build --use-container 2>/dev/null || sam build
sam deploy \
--stack-name "${STACK_NAME}" \
--region "${REGION}" \
--resolve-s3 \
--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \
--parameter-overrides \
"ParameterKey=AgentCoreEndpoint,ParameterValue=${AGENT_ENDPOINT}" \
"ParameterKey=AgentName,ParameterValue=${AGENT_NAME}" \
"ParameterKey=ScheduleExpression,ParameterValue='${SCHEDULE_EXPR}'" \
"ParameterKey=SchedulePayload,ParameterValue='${SCHEDULE_PAYLOAD}'" \
--no-confirm-changeset

# ── Read outputs ───────────────────────────────────────────────
get_output() {
aws cloudformation describe-stacks --stack-name "${STACK_NAME}" --region "${REGION}" \
--query "Stacks[0].Outputs[?OutputKey=='$1'].OutputValue" --output text
}

POOL_ID=$(get_output CognitoUserPoolId)
CLIENT_ID=$(get_output CognitoAppClientId)
EVENT_BUS_NAME="${STACK_NAME}-bus"

# ── Done ──────────────────────────────────────────────────────
echo ""
echo "══════════════════════════════════════════════════════════"
echo " ✅ Deployment complete!"
echo ""
echo " AgentCore ARN: ${AGENT_ARN}"
echo " Schedule: ${STACK_NAME}-schedule (${SCHEDULE_EXPR})"
echo " EventBus: ${EVENT_BUS_NAME}"
echo " Connection: ${STACK_NAME}-connection"
echo " API Dest: ${STACK_NAME}-dest"
echo ""
echo " Test manually:"
echo " aws events put-events --entries '[{"
echo " \"EventBusName\":\"${EVENT_BUS_NAME}\","
echo " \"Source\":\"scheduler.agentcore\","
echo " \"DetailType\":\"ScheduledAgentInvocation\","
echo " \"Detail\":\"{\\\"prompt\\\":\\\"Hello from manual test!\\\"}\"}"
echo " }]' --region ${REGION}"
echo ""
echo " Manage schedule:"
echo " aws scheduler get-schedule --name ${STACK_NAME}-schedule --region ${REGION}"
echo " aws scheduler update-schedule --name ${STACK_NAME}-schedule --state DISABLED ... # pause"
echo ""
echo " ⚠️ Configure AgentCore JWT authorizer:"
echo " Discovery URL: https://cognito-idp.${REGION}.amazonaws.com/${POOL_ID}/.well-known/openid-configuration"
echo " Allowed Audiences: ${CLIENT_ID}"
echo " Allowed Clients: ${CLIENT_ID}"
echo " Allowed Scopes: ${AGENT_NAME}/invoke"
echo "══════════════════════════════════════════════════════════"
Loading
Loading