by Reilly Manton, Wesley Petry, Scott Wainner, Gabriel Fuentes, Mark Paguay
A serverless A2A gateway that provides the complete three-layer architecture required for enterprise agent deployments: management, control, and data layers.
Amazon Bedrock AgentCore provides a complete gateway solution for MCP (Model Context Protocol) with management, access control, and data proxying. However, AgentCore has no native A2A (Agent-to-Agent) protocol gateway. While AgentCore Runtime can host A2A servers with OAuth 2.0 authentication, it does not provide the management, control, and data layers needed to operate multiple A2A agents behind a single domain. Existing A2A solutions only implement a management layer (agent discovery/registry) but lack the control and data layers, rendering them ineffective for corporate environments where security, access control, and centralized routing are critical.
This gateway fills that gap by implementing all three layers, using OAuth 2.0 client credentials flow for backend authentication — the same auth model that A2A and AgentCore Runtime use natively:
Management Layer - Agent Discovery & Registry
- Centralized agent registry with metadata and capabilities
- Dynamic agent card caching and URL rewriting
- Agent lifecycle management (register, sync, activate/deactivate)
- Multi-backend support for standard A2A servers
Control Layer - Fine-Grained Access Control (FGAC)
- JWT-based authentication via Cognito
- Scope-based permissions (who can access which agents)
- Lambda authorizer generates agent-specific IAM policies
- Unauthorized requests blocked at API Gateway level (never reach Lambdas)
- 5-minute policy caching for performance
- Audit trail through CloudWatch logs
Data Layer - Centralized Request Proxying
- Single domain endpoint for all agents (
/agents/{agentId}) - OAuth 2.0 client credentials flow for backend authentication
- SSE streaming support for real-time responses
- Request/response transformation and validation
The gateway hosts multiple A2A agents at a single domain with path-based routing (/agents/{agentId}). Each path acts as an independent A2A server from the client's perspective - standard A2A clients work without modification.
Key Features:
- ✅ Fully A2A protocol compliant
- ✅ Dual protocol binding support (HTTP+JSON/REST and JSON-RPC)
- ✅ Fine-grained access control via Cognito JWT scopes
- ✅ Per-user rate limiting via DynamoDB
- ✅ Semantic search for agent discovery via S3 Vectors
- ✅ Async task lifecycle support (get, cancel) via
contextIdsession routing - ✅ SSE streaming support for
message:streamoperations - ✅ OAuth 2.0 Client Credentials flow for backend authentication
- ✅ Serverless architecture (API Gateway + Lambda + DynamoDB)
- ✅ Native support for AWS Bedrock AgentCore Runtime backends
API Gateway (REST API)
- Two static routes that never change when agents are added/removed
- Lambda authorizer for JWT validation and FGAC
- Response streaming enabled via Lambda Web Adapter
Lambda Functions
- Authorizer: JWT validation, FGAC lookup in DynamoDB, generates IAM policies with agent-specific resource ARNs
- Registry: Agent discovery with permission filtering (returns only agents user can access)
- Search: Semantic agent discovery using S3 Vectors embeddings
- Proxy (Container): Routes A2A requests to backends with OAuth authentication, supports real-time SSE streaming via FastAPI + Lambda Web Adapter
- Admin: Agent registration and management (requires
gateway:adminscope)
DynamoDB Tables
- AgentRegistry: Maps agent IDs to backend URLs, auth configs, cached agent cards
- Permissions: Maps user scopes to allowed agents and rate limits
- RateLimitCounters: Tracks per-user request counts per minute (auto-expires via TTL)
Secrets Manager
- Stores OAuth client secrets for backend authentication
- Secrets referenced by ARN, never stored in DynamoDB
The gateway supports both A2A protocol bindings as defined in the A2A specification:
POST /agents/{agentId}/message:send- Send message (buffered)POST /agents/{agentId}/message:stream- Send message (SSE streaming)GET /agents/{agentId}/.well-known/agent-card.json- Get Agent CardPOST /agents/{agentId}/tasks:get- Get task by IDPOST /agents/{agentId}/tasks:cancel- Cancel a task
POST /agents/{agentId}with{"jsonrpc": "2.0", "method": "SendMessage", ...}POST /agents/{agentId}with{"jsonrpc": "2.0", "method": "SendStreamingMessage", ...}POST /agents/{agentId}with{"jsonrpc": "2.0", "method": "GetTask", ...}POST /agents/{agentId}with{"jsonrpc": "2.0", "method": "CancelTask", ...}
The gateway automatically detects the protocol binding based on the request format and translates to JSON-RPC for all backend communication.
The gateway translates all inbound requests to JSON-RPC format for backend communication. This provides a consistent interface regardless of which protocol binding clients use. Backends should implement JSON-RPC handlers for A2A operations.
- Terraform >= 1.5.0
- Python 3.12
- AWS CLI configured
- Docker (container runtime for building proxy Lambda)
- S3 bucket for Terraform state
- API Gateway CloudWatch Logging role configured in your account (one-time per account per region — set under API Gateway Settings → Logging in the AWS Console)
Edit terraform/backend.tf - uncomment the S3 backend block and set your bucket name:
terraform {
backend "s3" {
bucket = "your-terraform-state-bucket"
key = "a2a-gateway/terraform.tfstate"
region = "us-east-1"
encrypt = true
}
}Note: The S3 backend is commented out by default. If you skip this step, Terraform will use local state storage, which is fine for individual testing but not recommended for team environments.
cd terraform
cp terraform.tfvars.example terraform.tfvarsEdit terraform.tfvars (set your region and naming preferences):
aws_region = "us-east-1" # or your preferred region
project_name = "a2a-gateway"
environment = "poc"Build the zip package for non-container Lambdas (Authorizer, Registry, Search, Admin):
./scripts/build_lambda_package.shcd terraform
terraform init
terraform plan # Review what will be created
terraform applyThis creates everything in one go:
- DynamoDB tables (AgentRegistry, Permissions)
- Cognito User Pool
- ECR repository for the proxy container
- Builds and pushes the proxy container image automatically (requires Docker)
- 5 Lambda functions (Authorizer, Registry, Search, Proxy container, Admin)
- API Gateway with Lambda Authorizer and response streaming
- IAM roles and policies
- Secrets Manager setup
- Automatically updates Lambda env vars with the API Gateway URL
Note: The proxy Lambda container is built and pushed automatically by Terraform. The other Lambdas use the zip package built in step 3.
cd ..
python3 scripts/seed_permissions.py <permissions-table-name> us-east-1Get the table name from Terraform outputs: terraform output permissions_table_name
To deploy the gateway without internet-facing endpoints, enable private deployment mode. This attaches all Lambdas to a VPC, creates VPC endpoints for all AWS services, and switches API Gateway from REGIONAL to PRIVATE.
What "private deployment" means: This mode makes the gateway's own infrastructure private — the API Gateway endpoint is only reachable from within the VPC (or via VPN/Direct Connect/Transit Gateway), and all Lambda functions run inside private subnets. It does not provide a fully air-gapped environment. Outbound internet connectivity is still required for OAuth token exchange with backend agents (see below). In enterprise environments, this private gateway would typically sit within an existing VPC architecture that already provides managed egress through NAT Gateways, Transit Gateways, or similar. This sample provisions the VPC endpoints but leaves the outbound connectivity path to the deployer, since it depends on your network topology.
You have two options: let the gateway create a new VPC, or bring your own.
Edit terraform.tfvars:
enable_private_deployment = true
vpc_cidr = "10.0.0.0/16"
enable_bedrock_endpoint = true # required for semantic search and Bedrock AgentCore backendsDeploy into an existing VPC that you manage. The gateway creates only VPC endpoints and attaches Lambdas to your subnets — it does not create or modify your VPC, subnets, route tables, or security groups.
Edit terraform.tfvars:
enable_private_deployment = true
existing_vpc_id = "vpc-0123456789abcdef0"
existing_subnet_ids = ["subnet-aaa", "subnet-bbb"]
existing_route_table_ids = ["rtb-aaa"]
existing_lambda_security_group_id = "sg-lambda"
existing_vpc_endpoint_security_group_id = "sg-vpce"
enable_bedrock_endpoint = trueBYOVPC requirements:
- Subnets: At least 2 private subnets in different AZs (for Interface VPC endpoint high availability)
- Lambda security group: Must allow egress on port 443 to the VPC CIDR (for Interface endpoints) and to
0.0.0.0/0or the DynamoDB/S3 prefix lists (for Gateway endpoints) - VPC endpoint security group: Must allow ingress on port 443 from the Lambda security group and from the VPC CIDR
- Route tables: The route table(s) associated with your private subnets (needed for DynamoDB and S3 Gateway endpoints)
- DNS: The VPC must have
enableDnsSupportandenableDnsHostnamesset totrue(required for Interface VPC endpoint private DNS)
What gets created in your VPC: 9 VPC endpoints (DynamoDB, S3, Secrets Manager, execute-api, CloudWatch Logs, ECR API, ECR DKR, S3 Vectors, and optionally Bedrock Runtime). On terraform destroy, all gateway resources including these endpoints are removed — your VPC, subnets, route tables, and security groups are never touched.
Then re-run terraform apply. The API Gateway will only be accessible from within the VPC or via VPN/Direct Connect/Transit Gateway.
Outbound Connectivity Requirement: The Proxy Lambda performs OAuth 2.0 client credentials flow to authenticate with backend agents. Cognito's OAuth token endpoint (/oauth2/token) is hosted on a public domain and is not accessible via AWS PrivateLink. Your private VPC must have outbound internet connectivity for this token exchange. In enterprise environments, this is typically provided by:
- A NAT Gateway in a public subnet
- A Transit Gateway routing to a shared egress VPC
- AWS Direct Connect or VPN with internet breakout
This applies to any external OAuth provider, not just Cognito. The gateway's VPC endpoints handle all AWS service traffic (DynamoDB, S3, Secrets Manager, etc.) privately — only the OAuth token exchange requires outbound connectivity.
Note: The integration timeout quota defaults to 29 seconds. For long-running agent calls, request an increase for "Maximum integration timeout in milliseconds" in the AWS Service Quotas console.
Export your gateway URL and obtain a JWT token:
# Get gateway configuration
GATEWAY_URL=$(cd terraform && terraform output -raw api_gateway_url)
TOKEN_ENDPOINT=$(cd terraform && terraform output -raw cognito_token_endpoint)
CLIENT_ID=$(cd terraform && terraform output -raw cognito_client_id)
CLIENT_SECRET=$(cd terraform && terraform output -raw cognito_client_secret)
# Obtain JWT with required scopes
TOKEN_RESPONSE=$(curl -s -X POST $TOKEN_ENDPOINT \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&scope=a2a-gateway/gateway:admin a2a-gateway/billing:read")
export JWT=$(echo $TOKEN_RESPONSE | jq -r .access_token)
echo "JWT obtained: ${JWT:0:50}..."The gateway supports two types of A2A backends:
curl -X POST $GATEWAY_URL/admin/agents/register \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"agentId": "my-agent",
"name": "My Custom Agent",
"backendUrl": "https://your-backend.example.com",
"agentCardUrl": "https://your-backend.example.com/.well-known/agent-card.json",
"authConfig": {
"type": "oauth2_client_credentials",
"tokenUrl": "https://your-auth.example.com/oauth/token",
"clientId": "your-client-id",
"clientSecret": "your-client-secret",
"scopes": ["agent:invoke"]
}
}'Note: For Bedrock AgentCore Runtime backends, ensure the agent ARN is URL-encoded in the backendUrl. The AgentCore agent must be deployed with OAuth authentication using the customJWTAuthorizer — configure it with allowedClients (not allowedAudience) set to your Cognito client ID. This is required because Cognito client_credentials tokens include a client_id claim but not the standard aud claim that allowedAudience validates against. See the AgentCore A2A protocol contract and Deploy A2A servers in AgentCore Runtime for deployment details.
List all agents you have access to:
curl $GATEWAY_URL/agents \
-H "Authorization: Bearer $JWT" | jq .Example response:
[
{
"name": "Calculator Agent",
"description": "A calculator agent that can perform basic arithmetic operations.",
"url": "https://your-gateway.execute-api.us-east-1.amazonaws.com/v1/agents/bedrock-agent",
"protocolVersion": "0.3.0",
"skills": [
{
"id": "calculator",
"name": "calculator",
"description": "Calculator powered by SymPy..."
}
],
"capabilities": {
"streaming": true
}
}
]Search for agents using natural language queries:
curl -X POST $GATEWAY_URL/search \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{"query": "calculator math arithmetic", "topK": 5}' | jq .Example response:
{
"results": [
{
"agentCard": {
"name": "Calculator Agent",
"description": "A calculator agent that can perform basic arithmetic operations.",
"url": "https://your-gateway.execute-api.us-east-1.amazonaws.com/v1/agents/bedrock-agent"
},
"score": 0.89
}
],
"query": "calculator math arithmetic",
"totalMatches": 1
}The search uses Amazon Titan Text Embeddings V2 to generate vector embeddings stored in S3 Vectors. Results are filtered by user permissions - you only see agents you have access to.
Fetch a specific agent's capabilities:
curl $GATEWAY_URL/agents/bedrock-agent/.well-known/agent-card.json \
-H "Authorization: Bearer $JWT" | jq .Send a message using RESTful URLs (buffered response):
curl -X POST $GATEWAY_URL/agents/bedrock-agent/message:send \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"message": {
"messageId": "msg-123",
"role": "ROLE_USER",
"parts": [{"text": "Calculate 2 + 2"}]
}
}' | jq .Send a message using JSON-RPC format (method in body):
curl -X POST $GATEWAY_URL/agents/bedrock-agent \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "req-123",
"method": "SendMessage",
"params": {
"message": {
"messageId": "msg-123",
"role": "ROLE_USER",
"parts": [{"text": "Calculate 2 + 2"}]
}
}
}' | jq .Example response (streaming chunks from Bedrock AgentCore):
[
{
"contextId": "a1a7915d-078b-478c-a7b5-9c23a4883a8c",
"kind": "message",
"messageId": "b8f185a5-fecd-4736-be3c-4d48806e66c2",
"parts": [{"kind": "text", "text": "The result of"}],
"role": "agent",
"taskId": "5be2cf00-6c20-4e2f-a4d4-7e0f90f47c8f"
},
{
"contextId": "a1a7915d-078b-478c-a7b5-9c23a4883a8c",
"kind": "message",
"messageId": "fbb7e32e-bfee-439b-8bca-9b2f50f227ad",
"parts": [{"kind": "text", "text": " 2 + 2 is"}],
"role": "agent",
"taskId": "5be2cf00-6c20-4e2f-a4d4-7e0f90f47c8f"
},
{
"contextId": "a1a7915d-078b-478c-a7b5-9c23a4883a8c",
"kind": "message",
"messageId": "ad214099-d7d2-4b3e-931b-e363a4edde84",
"parts": [{"kind": "text", "text": " **4**."}],
"role": "agent",
"taskId": "5be2cf00-6c20-4e2f-a4d4-7e0f90f47c8f"
}
]For streaming responses using RESTful URLs (SSE):
curl -N -X POST $GATEWAY_URL/agents/bedrock-agent/message:stream \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"message": {
"messageId": "msg-456",
"role": "ROLE_USER",
"parts": [{"text": "What is 101 * 11?"}]
}
}'For streaming responses using JSON-RPC format:
curl -N -X POST $GATEWAY_URL/agents/bedrock-agent \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "req-456",
"method": "SendStreamingMessage",
"params": {
"message": {
"messageId": "msg-456",
"role": "ROLE_USER",
"parts": [{"text": "What is 101 * 11?"}]
}
}
}'For agents that support async task lifecycle, the gateway passes through task operations using contextId for session routing. The contextId ensures follow-up requests (get, cancel) hit the same backend container that handled the original message:send.
# HTTP+JSON/REST
curl -X POST $GATEWAY_URL/agents/my-async-agent/tasks:get \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"id": "task-uuid-from-send-response",
"contextId": "context-uuid-from-send-response"
}'
# JSON-RPC
curl -X POST $GATEWAY_URL/agents/my-async-agent \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "req-002",
"method": "GetTask",
"params": {
"id": "task-uuid-from-send-response",
"contextId": "context-uuid-from-send-response"
}
}'curl -X POST $GATEWAY_URL/agents/my-async-agent/tasks:cancel \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"id": "task-uuid",
"contextId": "context-uuid"
}'# 1. Get JWT
TOKEN_RESPONSE=$(curl -s -X POST $TOKEN_ENDPOINT \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&scope=a2a-gateway/gateway:admin a2a-gateway/billing:read")
export JWT=$(echo $TOKEN_RESPONSE | jq -r .access_token)
# 2. Discover agents
curl $GATEWAY_URL/agents -H "Authorization: Bearer $JWT" | jq '.[].name'
# 3. Get agent card
curl $GATEWAY_URL/agents/bedrock-agent/.well-known/agent-card.json \
-H "Authorization: Bearer $JWT" | jq '.skills[].name'
# 4. Send message
curl -X POST $GATEWAY_URL/agents/bedrock-agent/message:send \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"message": {
"messageId": "msg-001",
"role": "ROLE_USER",
"parts": [{"text": "Calculate the square root of 144"}]
}
}' | jq '.[].parts[].text' | tr -d '\n' && echoPOST /admin/agents/register- Register new backend agentPOST /admin/agents/{agentId}/sync- Refresh Agent Card cachePATCH /admin/agents/{agentId}/status- Set agent active/inactive
This gateway implements core A2A messaging operations with support for both protocol bindings. Below is the full compliance status:
| Binding | Status | Notes |
|---|---|---|
| HTTP+JSON/REST | ✅ Supported | RESTful URLs like /message:send |
| JSON-RPC | ✅ Supported | Single endpoint with method in body |
| Operation | Endpoint | Status | Notes |
|---|---|---|---|
| Agent Discovery | GET /agents |
✅ Supported | Registry with permission filtering |
| Get Agent Card | GET /agents/{id}/.well-known/agent-card.json |
✅ Supported | Cached with URL rewriting |
| Send Message | POST /agents/{id}/message:send |
✅ Supported | Buffered response |
| Stream Message | POST /agents/{id}/message:stream |
✅ Supported | Real-time SSE streaming |
| Get Task | POST /agents/{id}/tasks:get |
✅ Supported | Pass-through to backend |
| Cancel Task | POST /agents/{id}/tasks:cancel |
✅ Supported | Pass-through to backend |
| Subscribe to Task | POST /tasks/{id}:subscribe |
❌ Not Implemented | |
| Push Notifications | POST /tasks/{id}/pushNotificationConfigs |
❌ Not Implemented | Webhook-based async updates |
| Extended Agent Card | GET /extendedAgentCard |
❌ Not Implemented | User-specific capabilities |
Task Management: Task get and cancel are supported as opaque pass-throughs to the backend agent. The gateway does not store task state itself — the backend agent is responsible for task persistence.
Integration Timeout: API Gateway REST API has a default 29-second integration timeout. This gateway is configured for 300 seconds (5 minutes), but you must request a quota increase from AWS Support to enable timeouts beyond 29 seconds. Without the quota increase, requests will timeout at 29 seconds regardless of the configured value. Request the "Amazon API Gateway - REST API integration timeout" quota increase in the AWS Service Quotas console.
"Unauthorized" errors: Check your JWT is valid and not expired. Tokens expire after 60 minutes.
"Permission denied": The Lambda Authorizer generates IAM policies based on your JWT scopes. If you get a 403, your scopes don't grant access to that agent. Check your JWT scopes:
echo $JWT | cut -d. -f2 | base64 -d | jq .scopeThen verify the Permissions table maps those scopes to the agent you're trying to access. Note: Authorizer results are cached for 5 minutes, so permission changes may take time to take effect.
Agent not found: Verify the agent is registered and status is "active":
aws dynamodb get-item \
--table-name <agent-registry-table> \
--key '{"agentId": {"S": "test-agent"}}'Backend connection fails: Check CloudWatch logs for the Proxy Lambda. Verify OAuth credentials are correct.
/terraform - Infrastructure as Code
/modules
/dynamodb - Tables
/cognito - Auth
/ecr - Container registry for proxy Lambda
/lambda-functions - All Lambdas
/api-gateway - REST API with streaming support
/vpc - VPC with private subnets and security groups (private deployment)
/vpc-endpoints - VPC endpoints for AWS services (standalone, supports BYOVPC)
/s3-vectors - S3 Vectors bucket and index for semantic search
/src/lambdas
/authorizer - JWT validation
/registry - Agent discovery
/search - Semantic agent discovery via S3 Vectors
/proxy - DEPRECATED: kept for unit tests only (not deployed)
/proxy_container - A2A routing with streaming (FastAPI + Lambda Web Adapter)
/admin - Agent management
/shared - Common utilities
/tests
/unit - Unit tests
/property - Property-based tests
/scripts - Helper scripts
/diagrams - Architecture diagrams
This gateway is designed as a reference implementation. For production deployments, review the following security considerations:
The gateway operates on a trust-after-authentication model. Once a backend agent is registered and OAuth credentials are validated, the gateway trusts all responses from that backend without content validation. This means:
- Responses from backend agents are proxied directly to clients without inspection
- A compromised or malicious backend could return harmful content
- Production recommendation: Implement an approval workflow for agent registration. Admins should review backend agents before registration, ideally integrated with CI/CD pipelines for agent deployment.
Per-user, per-agent rate limiting is configured via the Permissions table. Add requestsPerMinute for a default limit and agentLimits for per-agent overrides:
aws dynamodb put-item --table-name <permissions-table> --item '{
"scope": {"S": "billing:read"},
"allowedAgents": {"L": [{"S": "cheap-agent"}, {"S": "expensive-agent"}]},
"requestsPerMinute": {"N": "100"},
"agentLimits": {"M": {"expensive-agent": {"N": "10"}}}
}'In this example, cheap-agent gets 100/min (default) and expensive-agent gets 10/min (override). When a user has multiple scopes, the highest limit for each agent applies. Scopes without any rate limit config have unlimited access. Exceeding the limit returns HTTP 429 with retryAfterSeconds.
CORS is configured with Access-Control-Allow-Origin: '*' for ease of development. This allows any origin to make requests to the API.
- Production recommendation: Restrict CORS to specific trusted origins.
The Lambda Authorizer caches results for 5 minutes for performance. This means:
- Permission revocations may take up to 5 minutes to take effect
- Newly granted permissions may also have a delay
- The cache TTL is configurable in
terraform/modules/api-gateway/main.tf
The gateway proxies A2A messages without modification.
- Backend agents are responsible for implementing prompt injection defenses
- The gateway provides access control to agents, not within agents
- Consider integrating guardrails at the backend agent level
API Gateway REST API has a default 29-second integration timeout. This gateway configures 300 seconds, but you must request a quota increase from AWS Support first. Go to AWS Service Quotas console and request an increase for "Amazon API Gateway - REST API integration timeout".
When private deployment is enabled, Lambda functions are attached to a VPC with security groups that restrict egress to HTTPS (port 443) only.
When the gateway creates the VPC (Option A): The gateway automatically creates both security groups and configures the prefix list egress rules for DynamoDB and S3 Gateway endpoints.
When you bring your own VPC (Option B): You are responsible for configuring security group rules. The Lambda security group must include egress rules for both:
- The VPC CIDR (for Interface VPC endpoints like Secrets Manager, CloudWatch Logs, etc.)
- DynamoDB and S3 prefix lists (for Gateway VPC endpoints, which route to public IP ranges outside the VPC CIDR)
Without the prefix list egress rules, Lambda functions will timeout when attempting to reach DynamoDB or S3 through Gateway endpoints. This is a common VPC networking pitfall — Gateway endpoints use route tables (not ENIs inside the VPC), so their destination IPs fall outside the VPC CIDR block. Alternatively, allowing egress to 0.0.0.0/0 on port 443 covers both cases.
VPC Block Public Access (BPA) is an account-level setting that blocks traffic through internet gateways across all VPCs in a region. This gateway does not enable BPA because:
-
BPA is account-scoped, not per-VPC — enabling it could impact other workloads in the account
-
The private deployment already has no internet gateway, so BPA provides no additional protection for this gateway
-
Production recommendation: Enable VPC BPA at the account or organization level as a defense-in-depth measure. Use the AWS Console (VPC → Settings → Block Public Access) or the
aws ec2 modify-vpc-block-public-access-optionsCLI command. If other workloads in the account require internet access, use BPA exclusions for those specific VPCs.
Ready-to-deploy sample agents are included to test the full gateway flow end-to-end. The example deploys two A2A-compliant agents (Weather and Calculator) to Amazon Bedrock AgentCore Runtime with Cognito OAuth authentication, all managed by Terraform.
See the full walkthrough: AgentCore A2A Example Agents
cd terraform
terraform destroyNote: You may need to manually delete secrets from Secrets Manager if they're not fully deleted.
MIT
