In this workshop you will:
- Configure IRSA (IAM Roles for Service Accounts) so the A2A proxy can call AgentCore without static AWS credentials
- Deploy a thin A2A proxy that wraps an AWS Bedrock AgentCore runtime agent
- Route traffic to it through Solo Agent Gateway using the A2A protocol
- Apply enterprise policies (JWT auth, rate limiting) via
EnterpriseAgentgatewayPolicy
┌──────────────────────────────────────────────────────────────┐
│ EKS Cluster │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Solo Agent Gateway (enterprise-agentgateway) │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ JWT Auth │ │ Rate Limiter │ │ CORS │ │ │
│ │ │ (policy) │ │ (policy) │ │ (policy) │ │ │
│ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │
│ │ └────────────────┼────────────────┘ │ │
│ │ │ │ │
│ │ ┌──────┴───────┐ │ │
│ │ │ HTTPRoute │ │ │
│ │ │ (agentcore) │ │ │
│ │ └──────┬───────┘ │ │
│ │ │ OTel metrics + traces │ │
│ └──────────────────────────┼─────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────┼──────── ns: agentcore ──────┐ │
│ │ ▼ │ │
│ │ ┌────────────────────────────────────────────────┐ │ │
│ │ │ A2A Proxy (pre-built image / gunicorn) │ │ │
│ │ │ │ │ │
│ │ │ /.well-known/agent.json → Agent Card │ │ │
│ │ │ POST / (tasks/send) → InvokeAgentRuntime │ │ │
│ │ │ │ │ │
│ │ │ serviceAccountName: agentcore-proxy │ │ │
│ │ │ (IRSA — no static AWS creds) │ │ │
│ │ │ │ │ │
│ │ │ Service: appProtocol: kgateway.dev/a2a │ │ │
│ │ └────────────────────┬───────────────────────────┘ │ │
│ └───────────────────────┼────────────────────────────────┘ │
│ │ │
└──────────────────────────┼───────────────────────────────────┘
│ SigV4-signed HTTPS (via IRSA)
▼
┌──────────────────────────────┐
│ AWS Bedrock AgentCore │
│ InvokeAgentRuntime API │
│ │
│ ┌────────────────────────┐ │
│ │ Your Agent Runtime │ │
│ │ (Claude-powered) │ │
│ └────────────────────────┘ │
└──────────────────────────────┘
| Concern | Static credentials | IRSA |
|---|---|---|
| Credential type | Long-lived access keys (or short-lived SSO tokens) | Temporary STS credentials, auto-refreshed |
| Rotation | Manual (SSO tokens expire every 1-12 hours) | Automatic — EKS injects fresh tokens |
| Blast radius | Any pod with Secret access | Single service account only |
| Secret management | K8s Secret with AWS_ACCESS_KEY_ID, etc. |
No secrets to manage |
| Code changes | None | None — boto3 picks up IRSA automatically |
AgentCore's InvokeAgentRuntime API requires AWS SigV4 signing, which Agent Gateway
doesn't support natively for the bedrock-agentcore service. The proxy handles SigV4
via boto3 (using IRSA credentials), while exposing a standard A2A interface (tasks/send)
that Agent Gateway can route, observe, and apply policies to.
- An EKS cluster with Solo Enterprise Agent Gateway installed
kubectl,aws, andeksctlCLI tools installed and configured- AWS IAM permissions to create roles and OIDC providers
- An AgentCore runtime agent deployed (you'll need the ARN)
- A container registry (ECR) to push the proxy image
# Your AgentCore agent ARN (without the /runtime-endpoint/DEFAULT suffix)
export AGENT_RUNTIME_ARN="arn:aws:bedrock-agentcore:us-east-1:986112284769:runtime/rvennam_agent-tEJ8OxEBo1"
export AWS_REGION="us-east-1"
export CLUSTER_NAME="<your-eks-cluster-name>"
# Auto-detect account ID
export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
# Agent Gateway details (adjust to your cluster)
export GATEWAY_NAME="agentgateway"
export GATEWAY_NS="enterprise-agentgateway"
# Get the gateway external address
export GW_ADDR=$(kubectl get svc -n $GATEWAY_NS $GATEWAY_NAME \
-o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}")
export GW_PORT=$(kubectl get svc -n $GATEWAY_NS $GATEWAY_NAME \
-o jsonpath="{.spec.ports[0].port}")
echo "Gateway: http://${GW_ADDR}:${GW_PORT}"
echo "Account: ${ACCOUNT_ID}"IRSA lets the proxy pod assume an IAM role directly. boto3 picks up the credentials automatically — no static keys, no secret rotation.
eksctl utils associate-iam-oidc-provider \
--cluster $CLUSTER_NAME \
--approveexport OIDC_ISSUER=$(aws eks describe-cluster \
--name $CLUSTER_NAME \
--query "cluster.identity.oidc.issuer" \
--output text)
export OIDC_ID=$(echo $OIDC_ISSUER | sed 's|.*/id/||')
echo "OIDC Issuer: $OIDC_ISSUER"
echo "OIDC ID: $OIDC_ID"cat > /tmp/agentcore-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "bedrock-agentcore:InvokeAgentRuntime",
"Resource": [
"${AGENT_RUNTIME_ARN}",
"${AGENT_RUNTIME_ARN}/*"
]
}
]
}
EOF
aws iam create-policy \
--policy-name AgentCoreProxyAccess \
--policy-document file:///tmp/agentcore-policy.json \
--query 'Policy.Arn' \
--output textThis creates a Kubernetes service account annotated with an IAM role that
only the agentcore-proxy SA in the agentcore namespace can assume.
kubectl create ns agentcore
eksctl create iamserviceaccount \
--name agentcore-proxy \
--namespace agentcore \
--cluster $CLUSTER_NAME \
--attach-policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/AgentCoreProxyAccess \
--approveManual alternative (if not using eksctl)
Create the trust policy:
cat > /tmp/trust-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::${ACCOUNT_ID}:oidc-provider/oidc.eks.${AWS_REGION}.amazonaws.com/id/${OIDC_ID}"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.${AWS_REGION}.amazonaws.com/id/${OIDC_ID}:sub": "system:serviceaccount:agentcore:agentcore-proxy",
"oidc.eks.${AWS_REGION}.amazonaws.com/id/${OIDC_ID}:aud": "sts.amazonaws.com"
}
}
}
]
}
EOF
aws iam create-role \
--role-name AgentCoreProxyIRSA \
--assume-role-policy-document file:///tmp/trust-policy.json \
--description "IRSA role for AgentCore A2A proxy"
aws iam attach-role-policy \
--role-name AgentCoreProxyIRSA \
--policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/AgentCoreProxyAccess
export ROLE_ARN="arn:aws:iam::${ACCOUNT_ID}:role/AgentCoreProxyIRSA"
kubectl create ns agentcore
kubectl create sa agentcore-proxy -n agentcore
kubectl annotate sa agentcore-proxy -n agentcore \
eks.amazonaws.com/role-arn=$ROLE_ARNkubectl get sa agentcore-proxy -n agentcore \
-o jsonpath='{.metadata.annotations}' | python3 -m json.toolYou should see eks.amazonaws.com/role-arn pointing to your IAM role.
The proxy code is a Flask app that:
- Serves an A2A Agent Card at
/.well-known/agent.json - Handles
tasks/sendJSON-RPC requests - Translates them to AgentCore InvokeAgentRuntime API calls with SigV4
- Returns A2A-formatted responses
# Create the ECR repository
aws ecr create-repository \
--repository-name agentcore-a2a-proxy \
--region $AWS_REGION 2>/dev/null || true
# Build and push (from the agentcore-mcp-bridge/ directory)
cd agentcore-mcp-bridge/
docker build --platform linux/amd64 -t ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/agentcore-a2a-proxy:latest .
aws ecr get-login-password --region $AWS_REGION | \
docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com
docker push ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/agentcore-a2a-proxy:latest
cd -Note that:
serviceAccountName: agentcore-proxy— the IRSA-annotated SA from Step 2- No
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY, orAWS_SESSION_TOKENenv vars - No ConfigMap volume — the code is baked into the container image
- gunicorn on port 8000 (production-ready)
kubectl apply -f - <<EOF
# --- Deployment ---
apiVersion: apps/v1
kind: Deployment
metadata:
name: agentcore-a2a-proxy
namespace: agentcore
labels:
app: agentcore-a2a-proxy
spec:
replicas: 2
selector:
matchLabels:
app: agentcore-a2a-proxy
template:
metadata:
labels:
app: agentcore-a2a-proxy
spec:
serviceAccountName: agentcore-proxy
containers:
- name: proxy
image: ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/agentcore-a2a-proxy:latest
ports:
- containerPort: 8000
env:
- name: AGENT_RUNTIME_ARN
value: "${AGENT_RUNTIME_ARN}"
- name: AWS_REGION
value: "${AWS_REGION}"
- name: AGENT_NAME
value: "AgentCore Agent"
- name: AGENT_DESCRIPTION
value: "AI assistant powered by AWS Bedrock AgentCore"
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 3
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8000
periodSeconds: 30
---
# --- Service with A2A appProtocol ---
apiVersion: v1
kind: Service
metadata:
name: agentcore-a2a-proxy
namespace: agentcore
labels:
app: agentcore-a2a-proxy
spec:
selector:
app: agentcore-a2a-proxy
ports:
- protocol: TCP
port: 8000
targetPort: 8000
appProtocol: kgateway.dev/a2a
type: ClusterIP
---
# --- HTTPRoute ---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: agentcore
namespace: agentcore
spec:
parentRefs:
- name: ${GATEWAY_NAME}
namespace: ${GATEWAY_NS}
rules:
- backendRefs:
- name: agentcore-a2a-proxy
port: 8000
EOFkubectl wait --for=condition=ready pod -l app=agentcore-a2a-proxy \
-n agentcore --timeout=120sConfirm the EKS webhook injected credentials into the pod:
POD=$(kubectl get pods -n agentcore -l app=agentcore-a2a-proxy \
-o jsonpath='{.items[0].metadata.name}')
echo "=== IRSA Environment Variables ==="
kubectl get pod $POD -n agentcore \
-o jsonpath='{range .spec.containers[0].env[*]}{.name}={.value}{"\n"}{end}' \
| grep -E "AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE"
echo ""
echo "=== Projected Token Volume ==="
kubectl get pod $POD -n agentcore \
-o jsonpath='{.spec.volumes[?(@.name=="aws-iam-token")]}' | python3 -m json.toolYou should see:
AWS_ROLE_ARNset to your IAM role ARNAWS_WEB_IDENTITY_TOKEN_FILEset to/var/run/secrets/eks.amazonaws.com/serviceaccount/token- A projected volume named
aws-iam-token
kubectl describe httproute agentcore -n agentcoreYou should see Reason: Accepted and Status: True.
curl -s http://${GW_ADDR}:${GW_PORT}/.well-known/agent.json | jqExpected output:
{
"name": "AgentCore Agent",
"description": "AI assistant powered by AWS Bedrock AgentCore",
"skills": [
{
"id": "chat",
"name": "Chat",
"description": "Have a conversation with the AgentCore agent."
}
],
"version": "1.0.0"
}curl -s -X POST http://${GW_ADDR}:${GW_PORT}/ \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "1",
"method": "tasks/send",
"params": {
"id": "task-1",
"message": {
"role": "user",
"parts": [{"type": "text", "text": "Hello from Agent Gateway!"}]
}
}
}' | jqThe request flows: curl -> Agent Gateway -> A2A proxy (IRSA) -> AgentCore -> Claude
Now that traffic is flowing through Agent Gateway, you can apply
EnterpriseAgentgatewayPolicy resources to the HTTPRoute.
Require a valid JWT on every request. Replace the issuer/JWKS with your IdP.
kubectl apply -f - <<EOF
apiVersion: enterpriseagentgateway.solo.io/v1alpha1
kind: EnterpriseAgentgatewayPolicy
metadata:
name: agentcore-jwt-auth
namespace: agentcore
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: agentcore
traffic:
jwtAuthentication:
mode: Strict
providers:
- issuer: "\${KEYCLOAK_ISSUER}"
audiences: ["agentcore"]
jwks:
remote:
jwksPath: "\${KEYCLOAK_JWKS_PATH}"
backendRef:
name: keycloak
namespace: keycloak
kind: Service
port: 8080
EOFTest without a token (should get 401):
curl -v -X POST http://${GW_ADDR}:${GW_PORT}/ \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":"1","method":"tasks/send","params":{"id":"t1","message":{"role":"user","parts":[{"type":"text","text":"test"}]}}}'
# Expected: HTTP 401 UnauthorizedTest with a valid token (should get 200):
TOKEN=$(curl -s -X POST "${KEYCLOAK_ISSUER}/protocol/openid-connect/token" \
-d "grant_type=client_credentials" \
-d "client_id=agentcore-client" \
-d "client_secret=${CLIENT_SECRET}" | jq -r .access_token)
curl -s -X POST http://${GW_ADDR}:${GW_PORT}/ \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"jsonrpc":"2.0","id":"1","method":"tasks/send","params":{"id":"t1","message":{"role":"user","parts":[{"type":"text","text":"Hello with auth!"}]}}}' | jqStep 1: Create the RateLimitConfig in the gateway's namespace:
kubectl apply -f - <<EOF
apiVersion: ratelimit.solo.io/v1alpha1
kind: RateLimitConfig
metadata:
name: agentcore-rate-limit
namespace: ${GATEWAY_NS}
spec:
raw:
descriptors:
- key: generic_key
value: counter
rateLimit:
requestsPerUnit: 3
unit: MINUTE
rateLimits:
- actions:
- genericKey:
descriptorValue: counter
type: REQUEST
EOFStep 2: Create the EnterpriseAgentgatewayPolicy targeting the HTTPRoute:
kubectl apply -f - <<EOF
apiVersion: enterpriseagentgateway.solo.io/v1alpha1
kind: EnterpriseAgentgatewayPolicy
metadata:
name: agentcore-rate-limit
namespace: agentcore
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: agentcore
traffic:
entRateLimit:
global:
rateLimitConfigRefs:
- name: agentcore-rate-limit
namespace: ${GATEWAY_NS}
EOFTest rate limiting (send 6 requests — the first ~3 succeed, then 429):
for i in $(seq 1 6); do
CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST http://${GW_ADDR}:${GW_PORT}/ \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":"'$i'","method":"tasks/send","params":{"id":"t'$i'","message":{"role":"user","parts":[{"type":"text","text":"ping"}]}}}')
echo "Request $i: HTTP $CODE"
donekubectl apply -f - <<EOF
apiVersion: enterpriseagentgateway.solo.io/v1alpha1
kind: EnterpriseAgentgatewayPolicy
metadata:
name: agentcore-cors
namespace: agentcore
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: agentcore
traffic:
cors:
allowOrigins:
- exact: "https://your-app.example.com"
allowMethods:
- POST
- GET
- OPTIONS
allowHeaders:
- Content-Type
- Authorization
maxAge: 86400s
EOFIf your cluster has the OTel stack deployed (Prometheus + Grafana + Tempo), Agent Gateway automatically exports metrics and traces for all traffic flowing through it, including the A2A requests to your AgentCore agent.
# Port-forward to Grafana
kubectl port-forward svc/grafana-prometheus-grafana -n monitoring 3000:80 &
# Open http://localhost:3000 and look for agentgateway dashboardskubectl logs -n agentcore -l app=agentcore-a2a-proxy -f# Remove Kubernetes resources
kubectl delete ns agentcore
# Remove rate limit config from gateway namespace
kubectl delete ratelimitconfig agentcore-rate-limit -n ${GATEWAY_NS} 2>/dev/null
# Remove IRSA service account and IAM role
eksctl delete iamserviceaccount \
--name agentcore-proxy \
--namespace agentcore \
--cluster $CLUSTER_NAME
# Remove the IAM policy
aws iam delete-policy \
--policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/AgentCoreProxyAccess
# Remove the ECR repository (optional)
aws ecr delete-repository \
--repository-name agentcore-a2a-proxy \
--region $AWS_REGION \
--force| What | How |
|---|---|
| AWS credentials | IRSA — automatic, short-lived, no secrets |
| AgentCore agent invocation | boto3 invoke_agent_runtime() with SigV4 (credentials from IRSA) |
| Container image | Pre-built from ECR with gunicorn |
| Protocol to Agent Gateway | A2A (JSON-RPC tasks/send) via appProtocol: kgateway.dev/a2a |
| Gateway routing | HTTPRoute -> Service (A2A) |
| JWT authentication | EnterpriseAgentgatewayPolicy traffic.jwtAuthentication |
| Rate limiting | RateLimitConfig + EnterpriseAgentgatewayPolicy traffic.entRateLimit |
| CORS | EnterpriseAgentgatewayPolicy traffic.cors |
| Observability | Built-in OTel metrics/traces through Agent Gateway |