This lab assumes that you have completed the setup in 001 and 002
- Deploy a mock A2A agent and expose it as a remote service
- Route to the remote A2A agent through AgentGateway
- Validate A2A connectivity through the gateway
- Observe A2A traffic in access logs
In this lab, we'll route to a remote Agent-to-Agent (A2A) server through AgentGateway. While we deploy the mock agent on the same cluster for simplicity, we expose it via a LoadBalancer and route to it by external IP — simulating a remote A2A server running outside the cluster.
Agent-to-Agent (A2A) is an open protocol developed by Google that enables communication and interoperability between agentic applications. A2A defines a common language that enables agents to discover each other's capabilities, negotiate interaction modalities, and securely collaborate on tasks — regardless of the framework or vendor they are built on.
Routing A2A traffic through AgentGateway provides:
- Centralized Observability: View metrics, logs, and traces for all agent-to-agent interactions
- Security Policies: Add authentication, authorization, and rate limiting to agent communication
- Unified Access: Single gateway endpoint for multiple agents, MCP servers, and LLM providers
- Traffic Management: Apply retry policies, timeouts, and other traffic controls
Deploy a simple echo agent that implements the A2A protocol. We expose it via a LoadBalancer to simulate a remote agent:
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: a2a-agent
namespace: default
labels:
app: a2a-agent
spec:
selector:
matchLabels:
app: a2a-agent
template:
metadata:
labels:
app: a2a-agent
spec:
containers:
- name: a2a-agent
image: gcr.io/solo-public/docs/test-a2a-agent:latest
ports:
- containerPort: 9090
---
apiVersion: v1
kind: Service
metadata:
name: a2a-agent-lb
namespace: default
spec:
selector:
app: a2a-agent
type: LoadBalancer
ports:
- protocol: TCP
port: 9090
targetPort: 9090
EOFWait for the pod to be ready and the LoadBalancer IP to be assigned:
kubectl wait --for=condition=ready pod -l app=a2a-agent -n default --timeout=60sGet the external IP of the A2A agent:
export A2A_AGENT_IP=$(kubectl get svc a2a-agent-lb -n default -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo "A2A Agent IP: $A2A_AGENT_IP"Before routing through AgentGateway, verify the agent works directly:
# Check the agent card
curl -s http://$A2A_AGENT_IP:9090/.well-known/agent.json | jqYou should see the agent card describing the Echo Agent and its capabilities:
{
"name": "Echo Agent",
"description": "This agent echos the input given",
"url": "http://0.0.0.0:9090/",
"version": "0.1.0",
"capabilities": {
"streaming": true,
"pushNotifications": false,
"stateTransitionHistory": false
},
"skills": [
{
"id": "my-project-echo-skill",
"name": "Echo Tool",
"description": "Echos the input given"
}
]
}Send a test task directly:
curl -s -X POST http://$A2A_AGENT_IP:9090/ \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tasks/send",
"params": {
"id": "direct-test",
"message": {
"role": "user",
"parts": [{"type": "text", "text": "hello direct"}]
}
}
}' | jqExpected response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"id": "direct-test",
"status": {
"state": "completed",
"message": {
"role": "agent",
"parts": [{"type": "text", "text": "on_send_task received: hello direct"}]
}
}
}
}Since there is no AgentgatewayBackend type for A2A, we use a headless Kubernetes Service with an Endpoints resource pointing to the remote agent's external IP. The key is setting appProtocol: kgateway.dev/a2a on the Service — this tells AgentGateway to handle the traffic using the A2A protocol.
kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
name: remote-a2a-agent
namespace: agentgateway-system
spec:
clusterIP: None
ports:
- protocol: TCP
port: 9090
targetPort: 9090
appProtocol: kgateway.dev/a2a
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: remote-a2a-agent-1
namespace: agentgateway-system
labels:
kubernetes.io/service-name: remote-a2a-agent
addressType: IPv4
ports:
- port: 9090
protocol: TCP
endpoints:
- addresses:
- "${A2A_AGENT_IP}"
EOFKey Configuration Details:
clusterIP: None— Headless service since we provide our own endpointsappProtocol: kgateway.dev/a2a— Tells AgentGateway to use A2A protocol handlingEndpointSlice— Points to the remote agent's external IP address
Create an HTTPRoute with a /a2a path prefix. We use a URLRewrite filter to strip the prefix, since the A2A agent expects requests at the root path:
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: remote-a2a
namespace: agentgateway-system
spec:
parentRefs:
- name: agentgateway-proxy
rules:
- matches:
- path:
type: PathPrefix
value: /a2a
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /
backendRefs:
- name: remote-a2a-agent
port: 9090
EOFexport GATEWAY_IP=$(kubectl get svc -n agentgateway-system --selector=gateway.networking.k8s.io/gateway-name=agentgateway-proxy -o jsonpath='{.items[*].status.loadBalancer.ingress[0].ip}{.items[*].status.loadBalancer.ingress[0].hostname}')
echo $GATEWAY_IPcurl -s http://$GATEWAY_IP:8080/a2a/.well-known/agent.json | jqYou should see the Echo Agent card returned through the gateway. Notice that AgentGateway automatically rewrites the agent's url field to reflect the gateway address.
curl -s -X POST http://$GATEWAY_IP:8080/a2a \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tasks/send",
"params": {
"id": "gateway-test",
"message": {
"role": "user",
"parts": [{"type": "text", "text": "hello through the gateway"}]
}
}
}' | jqExpected response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"id": "gateway-test",
"status": {
"state": "completed",
"message": {
"role": "agent",
"parts": [{"type": "text", "text": "on_send_task received: hello through the gateway"}]
}
}
}
}AgentGateway automatically logs A2A requests with protocol-specific metadata:
kubectl logs deploy/agentgateway-proxy -n agentgateway-system --tail 5 | grep a2aYou should see log entries with "protocol":"a2a" and A2A-specific fields like a2a.method:
{
"level": "info",
"scope": "request",
"route": "agentgateway-system/remote-a2a",
"http.method": "POST",
"http.path": "/a2a",
"http.status": 200,
"protocol": "a2a",
"a2a.method": "tasks/send"
}If you set up the monitoring stack in lab 002, you can view A2A request metrics and traces in the Grafana dashboard:
- Port-forward to Grafana:
kubectl port-forward svc/grafana-prometheus -n monitoring 3000:3000-
Open http://localhost:3000 and navigate to Dashboards > AgentGateway Dashboard
-
Navigate to Home > Explore and select Tempo to view distributed traces for A2A requests
kubectl delete httproute -n agentgateway-system remote-a2a
kubectl delete endpointslice -n agentgateway-system remote-a2a-agent-1
kubectl delete svc -n agentgateway-system remote-a2a-agent
kubectl delete svc -n default a2a-agent-lb
kubectl delete deployment -n default a2a-agent- AgentGateway supports A2A protocol routing using the
appProtocol: kgateway.dev/a2aannotation on Kubernetes Services - Remote A2A agents can be reached by creating a headless Service with an EndpointSlice pointing to the external IP
- A
URLRewritefilter is needed when using path-prefix routing, since most A2A agents expect requests at the root path - AgentGateway automatically detects the A2A protocol and provides protocol-aware logging with A2A-specific metadata
- The same observability, security, and traffic management policies available for LLM and MCP traffic also apply to A2A traffic