Deploying a CrewAI Multi-Agent Crew to Amazon Bedrock AgentCore
AWS AgentCore Guide

Full source for this project: https://github.com/ratishjain12/crewai-bedrock-agentcore
Amazon Bedrock AgentCore is AWS's answer to a problem every team building agents eventually hits: your agent works great on your laptop, and then you need to actually run it somewhere with scaling, session isolation, observability, and a secure invocation boundary without hand-rolling that infrastructure yourself.
This post walks through taking a real multi-agent CrewAI crew - a two-agent "Research Crew" that researches a topic and turns the findings into a report from crewai run on a laptop to a secured, containerized deployment on AgentCore Runtime. Every step here was actually run against a live AWS account, including the mistakes.
What is Bedrock AgentCore, actually?
Strip away the marketing and AgentCore is a managed runtime with one real contract: give it a container (or a zipped Python package) that speaks HTTP on port 8080 — POST /invocations for the actual work, GET /ping for health and AgentCore handles the rest: provisioning, scaling, session isolation, IAM, logging, and (optionally) JWT-based auth in front of it. It's explicitly framework-agnostic. Nothing about the contract cares whether the code behind it is Strands, LangChain, LangGraph, or in this case, CrewAI.
That framework-agnosticism matters here: AgentCore's own CLI ships built-in project templates for Strands, LangChain/LangGraph, GoogleADK, OpenAI Agents, and Autogen but not CrewAI. The escape hatch is a BYO (bring your own code) agent: you register a code location and an entrypoint file, and AgentCore doesn't care what's inside as long as it implements the HTTP contract.
The architecture
Here's what we ended up with:
Walking it left to right:
Client - anything calling the agent: a script, another service,
agentcore invoke.Identity Provider (Cognito) / IAM-JWT Auth - the front door. AgentCore Runtime supports two inbound-auth modes:
AWS_IAM(SigV4-signed requests, the default) orCUSTOM_JWT(bearer tokens from any OIDC-compliant issuer - Cognito here). This layer validates the request before it ever reaches your code.AgentCore Runtime → MicroVM → Docker Container → Agent Application Code - each invocation gets an isolated microVM running your container. Your code (
main.pyin this project) is the innermost layer — it never sees infrastructure concerns, just a payload in and a result out.Execution Role - the IAM role AgentCore assumes on your container's behalf, scoped to exactly what it needs.
ECR - where the container image lives; the Execution Role pulls it to start the runtime.
CloudWatch - logs and traces from every invocation land here automatically.
Bedrock (LLM) - where the actual model calls go. The Execution Role, not the client, is what's authorized to invoke Bedrock - the client never touches AWS credentials at all.
The demo: a CrewAI Research Crew
The crew is intentionally simple - two agents, sequential process:
Researcher — gathers information on a topic using
SerperDevTool(web search), backed bybedrock/apac.amazon.nova-lite-v1:0.Analyst — turns the researcher's output into a structured markdown report, backed by the same model.
It's defined declaratively, CrewAI's JSON-first format - crew.jsonc for the crew/task graph, agents/researcher.jsonc and agents/analyst.jsonc for each agent's role/goal/backstory/model/tools:
// agents/researcher.jsonc
{
"role": "Senior Research Specialist for {topic}",
"goal": "Find comprehensive and accurate information about {topic}...",
"llm": "bedrock/apac.amazon.nova-lite-v1:0",
"tools": ["SerperDevTool"]
}
Locally, crewai run reads this straight out of pyproject.toml's [tool.crewai] block. Neither crew.jsonc nor the loader cares whether you're running on a laptop or inside AgentCore - which is exactly what makes the port to a BYO agent almost mechanical.
Wiring it into AgentCore
A BYO agent needs exactly one thing beyond the existing project: an entrypoint that speaks the AgentCore HTTP contract. CrewAI conveniently exposes a load_crew() helper that does the JSON-first loading programmatically, so the entrypoint is a thin wrapper:
# app/ResearchAgent/main.py
from pathlib import Path
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from crewai.project.crew_loader import load_crew
app = BedrockAgentCoreApp()
CREW_PATH = Path(__file__).parent / "crew.jsonc"
@app.entrypoint
def invoke(payload, context):
crew, default_inputs = load_crew(CREW_PATH)
topic = payload.get("prompt", default_inputs.get("topic", ""))
result = crew.kickoff(inputs={**default_inputs, "topic": topic})
return {"result": result.raw}
if __name__ == "__main__":
app.run()
BedrockAgentCoreApp handles the HTTP contract (/invocations, /ping) — the crew itself doesn't know it's running inside AgentCore at all.
The agent is registered in agentcore/agentcore.json as a flat resource entry — codeLocation, entrypoint, build type, environment variables, and (as it turned out) auth config, all declared, not templated:
{
"runtimes": [{
"name": "ResearchAgent",
"build": "Container",
"entrypoint": "main.py",
"codeLocation": "app/ResearchAgent/",
"envVars": [
{ "name": "SERPER_API_KEY", "value": "..." },
{ "name": "AWS_DEFAULT_REGION", "value": "ap-south-1" }
]
}]
}
Two bugs that only show up in deployment
Everything above sounds clean in hindsight. It wasn't, the first time through — and both failures are worth calling out because neither is obvious from the CrewAI or AgentCore docs alone.
1. The package was too big for a CodeZip build. AgentCore's default build type zips your code and its dependencies and caps the artifact at 250 MB. crewai[bedrock,tools] blew past that at 285 MB. The reason: the tools extra doesn't scope dependencies per-tool — it pulls in crewai-tools' entire dependency tree, because the package ships all ~40 tool wrappers as one unit. The researcher agent uses exactly one tool (SerperDevTool, a thin requests wrapper), but that one line in pyproject.toml also drags in chromadb, lancedb, pyarrow, onnxruntime, pymupdf, and kubernetes — dependencies for RAG tools and PDF tools and a Kubernetes tool we never touch. The fix was switching the runtime's build type from CodeZip to Container — a container image isn't subject to the 250 MB cap, so the same heavy dependency tree just... works. (The leaner fix, for anyone hitting this without wanting to move to containers, is dropping the tools extra and hand-writing a ten-line Serper tool against requests instead.)
2. Cross-region model IDs silently picked the wrong region. The crew uses bedrock/apac.amazon.nova-lite-v1:0 — a cross-region inference profile that only routes within APAC regions. Locally, everything worked, because the AWS CLI's default profile region was ap-south-1. Deployed, every invocation failed with "The provided model identifier is invalid." The cause: CrewAI's Bedrock LLM provider builds its own boto3 session and only checks AWS_DEFAULT_REGION / AWS_REGION_NAME env vars for region — it never falls back to ~/.aws/config the way a bare boto3.Session() does. With neither env var set, it silently defaulted to us-east-1, where an APAC-only inference profile is, correctly, rejected. The fix was one env var — AWS_DEFAULT_REGION=ap-south-1 — set explicitly in both agentcore/.env.local (for local agentcore dev) and the runtime's envVars in agentcore.json (for the deployed container). Two separate places, because — importantly — .env.local never reaches the deployed runtime. It lives outside the container's codeLocation and is read only by the local dev server; production env vars are a completely separate, explicit declaration.
Locking down the front door: Cognito JWT auth
By default, AgentCore Runtime protects invocations with IAM/SigV4 — fine for service-to-service calls already inside your AWS account, but not something you'd hand to, say, a frontend client. Swapping in JWT auth is a matter of setting two fields on the runtime:
"authorizerType": "CUSTOM_JWT",
"authorizerConfiguration": {
"customJwtAuthorizer": {
"discoveryUrl": "https://cognito-idp.<region>.amazonaws.com/<pool-id>/.well-known/openid-configuration",
"allowedClients": ["<app-client-id>"]
}
}
The catch: AgentCore doesn't provision the identity provider for you — there's no agentcore add cognito. Cognito (user pool, resource server, domain, app client) is a plain AWS resource you create yourself; AgentCore's CLI only consumes the resulting discovery URL. This project's scripts/cognito/ directory has three small scripts for exactly that lifecycle: create-cognito.sh provisions everything and prints the config block above; get-bearer-token.sh does the client-credentials token exchange for testing; destroy-cognito.sh tears it back down. Once CUSTOM_JWT is set, plain boto3 invoke_agent_runtime calls stop working — SigV4 signing and JWT bearer tokens are mutually exclusive per runtime, so invocation becomes a raw HTTPS POST with an Authorization: Bearer <token> header instead.
Closing thoughts
None of the individual pieces here are exotic — CrewAI's JSON-first crews, AgentCore's BYO-agent model, Cognito client-credentials grants. What's worth taking away is that the two real failures — a silent dependency bloat past a packaging limit, and a silent region fallback inside a third-party LLM provider — both looked like AgentCore problems at first and were actually one level down, in how CrewAI resolves its own dependencies and its own AWS region. Deploying an existing framework onto a new runtime surfaces exactly these kinds of assumptions the framework was making that never had to be explicit before.
Full source for this project: https://github.com/ratishjain12/crewai-bedrock-agentcore





