Skip to content

Instantly share code, notes, and snippets.

@rvennam
Created March 12, 2026 00:53
Show Gist options
  • Select an option

  • Save rvennam/c6a65d725b03f945804773772527108a to your computer and use it in GitHub Desktop.

Select an option

Save rvennam/c6a65d725b03f945804773772527108a to your computer and use it in GitHub Desktop.
AgentGateway: Route to Remote A2A Agent by Hostname

Route to a Remote A2A Agent by Hostname

Pre-requisites

This lab assumes that you have completed the setup in 001 and 002

Lab Objectives

  • Route to a remote A2A agent by hostname (not IP address) through AgentGateway
  • Preserve full A2A protocol detection, including "protocol":"a2a" and a2a.method in access logs
  • Handle port and path differences on the remote agent
  • Validate A2A connectivity and observability through the gateway

Overview

In the previous lab (032-remote-a2a), we routed to a remote A2A agent using a headless Service with an EndpointSlice pointing to an external IP address. That approach works when the remote agent has a known, static IP.

But what if the remote A2A agent is reachable by hostname — for example, agent.example.com:9090? You can't put a hostname in an EndpointSlice (which requires IPv4/IPv6 addresses), and the IP behind a hostname can change at any time — nobody would keep the EndpointSlice updated.

Solution: Self-Referencing Route

The trick is to route AgentGateway through itself. We create two routes:

  1. Client-facing route (/a2a) — routes to a Kubernetes Service with appProtocol: kgateway.dev/a2a that points back to the AgentGateway proxy pods. This triggers A2A protocol detection.
  2. Upstream route (/a2a-upstream) — routes to an AgentgatewayBackend with static that resolves the external hostname via DNS.
Client → AgentGateway (/a2a) → AgentGateway (/a2a-upstream) → Remote A2A Agent (hostname)
              ↑                        ↑
       Service with              AgentgatewayBackend
  appProtocol: kgateway.dev/a2a    static: hostname:port
    (A2A protocol detected)       (DNS resolution)

This requires no extra deployments — AgentGateway handles everything.

When to Use Each Approach

Remote agent address Approach A2A Protocol Detection
IP address (e.g., 10.0.1.50:9090) Headless Service + EndpointSlice (lab 032) Yes
Hostname (e.g., agent.example.com:9090) Self-referencing route (this lab) Yes

Step 1: Deploy the Mock A2A Agent

Deploy the same echo agent from the previous lab, exposed via a LoadBalancer:

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
EOF

Wait for the pod and LoadBalancer:

kubectl wait --for=condition=ready pod -l app=a2a-agent -n default --timeout=60s

Get the external IP and construct a hostname using nip.io (a wildcard DNS service that maps <IP>.nip.io back to the IP):

export A2A_AGENT_IP=$(kubectl get svc a2a-agent-lb -n default -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
export A2A_AGENT_HOST="${A2A_AGENT_IP}.nip.io"
echo "A2A Agent Hostname: $A2A_AGENT_HOST"

Note: In a real scenario, A2A_AGENT_HOST would be the actual hostname of your remote A2A agent (e.g., agent.example.com). We use nip.io here to simulate a hostname-based endpoint without needing custom DNS.

Verify Direct Connectivity by Hostname

curl -s http://$A2A_AGENT_HOST:9090/.well-known/agent.json | jq

You should see the Echo Agent card:

{
  "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"
    }
  ]
}

Step 2: Create the Upstream Backend

Create an AgentgatewayBackend with the static type pointing to the remote agent's hostname and port. This handles DNS resolution:

kubectl apply -f - <<EOF
apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayBackend
metadata:
  name: a2a-upstream
  namespace: agentgateway-system
spec:
  static:
    host: ${A2A_AGENT_HOST}
    port: 9090
EOF

Verify the backend is accepted:

kubectl get agentgatewaybackend -n agentgateway-system a2a-upstream

Step 3: Create the Self-Referencing Service

Create a Kubernetes Service that selects the AgentGateway proxy pods and sets appProtocol: kgateway.dev/a2a. This is what triggers A2A protocol detection when traffic flows through it:

kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: remote-a2a-agent
  namespace: agentgateway-system
spec:
  selector:
    app.kubernetes.io/name: agentgateway-proxy
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 8080
      appProtocol: kgateway.dev/a2a
EOF

Key point: This Service points to the same AgentGateway proxy pods that are already running — no extra deployments needed.

Step 4: Create the Routes

Create two HTTPRoutes:

  1. Upstream route — forwards /a2a-upstream to the static backend (external hostname)
  2. Client-facing route — forwards /a2a through the self-referencing Service (for A2A detection), rewriting the path to /a2a-upstream
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: a2a-upstream
  namespace: agentgateway-system
spec:
  parentRefs:
  - name: agentgateway-proxy
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /a2a-upstream
    filters:
    - type: URLRewrite
      urlRewrite:
        path:
          type: ReplacePrefixMatch
          replacePrefixMatch: /
    backendRefs:
      - group: agentgateway.dev
        kind: AgentgatewayBackend
        name: a2a-upstream
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: remote-a2a-hostname
  namespace: agentgateway-system
spec:
  parentRefs:
  - name: agentgateway-proxy
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /a2a
    filters:
    - type: URLRewrite
      urlRewrite:
        path:
          type: ReplacePrefixMatch
          replacePrefixMatch: /a2a-upstream
    backendRefs:
      - name: remote-a2a-agent
        port: 8080
EOF

How it works:

  • A request to /a2a hits the remote-a2a-hostname route, which rewrites the path to /a2a-upstream and sends it through the self-referencing Service (with appProtocol: kgateway.dev/a2a — triggering A2A detection)
  • AgentGateway receives the request again on /a2a-upstream, which matches the a2a-upstream route, rewrites the path to /, and forwards to the static backend (external hostname)

Handling a Path on the Remote Agent

If the remote A2A agent serves at a sub-path (e.g., agent.example.com:8080/myagent), adjust the replacePrefixMatch in the upstream route:

    filters:
    - type: URLRewrite
      urlRewrite:
        path:
          type: ReplacePrefixMatch
          replacePrefixMatch: /myagent

Get Gateway IP

export 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_IP

Step 5: Verify A2A Through the Gateway

Fetch the Agent Card

curl -s http://$GATEWAY_IP:8080/a2a/.well-known/agent.json | jq

You should see the Echo Agent card returned through the gateway.

Send a Task Through the Gateway

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": "hostname-test",
      "message": {
        "role": "user",
        "parts": [{"type": "text", "text": "hello via hostname"}]
      }
    }
  }' | jq

Expected response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "id": "hostname-test",
    "status": {
      "state": "completed",
      "message": {
        "role": "agent",
        "parts": [{"type": "text", "text": "on_send_task received: hello via hostname"}]
      }
    }
  }
}

Observability

View Access Logs

kubectl logs deploy/agentgateway-proxy -n agentgateway-system --all-containers --tail 5 | grep a2a

You should see two log entries per request — one for each hop through AgentGateway:

First hop (client-facing route) — with full A2A protocol detection:

{
  "route": "agentgateway-system/remote-a2a-hostname",
  "http.path": "/a2a",
  "http.status": 200,
  "protocol": "a2a",
  "a2a.method": "tasks/send"
}

Second hop (upstream route) — forwards to the external hostname:

{
  "route": "agentgateway-system/a2a-upstream",
  "endpoint": "agent.example.com:9090",
  "http.path": "/a2a-upstream",
  "http.status": 200,
  "protocol": "http"
}

The client-facing route has full A2A observability — "protocol":"a2a", a2a.method, and request/response body logging.

Cleanup

kubectl delete httproute -n agentgateway-system remote-a2a-hostname a2a-upstream
kubectl delete agentgatewaybackend -n agentgateway-system a2a-upstream
kubectl delete svc -n agentgateway-system remote-a2a-agent
kubectl delete svc -n default a2a-agent-lb
kubectl delete deployment -n default a2a-agent

Key Takeaways

  • To route to a remote A2A agent by hostname with full A2A protocol detection, use a self-referencing route — AgentGateway routes through itself via a Service with appProtocol: kgateway.dev/a2a, then forwards to the external hostname via an AgentgatewayBackend with static
  • No extra deployments are needed — AgentGateway handles both the A2A protocol detection and the hostname-based routing
  • The appProtocol: kgateway.dev/a2a annotation on the Kubernetes Service is what triggers A2A protocol detection — this enables "protocol":"a2a" in logs, a2a.method metadata, and agent card URL rewriting
  • The AgentgatewayBackend with static handles DNS resolution of the remote hostname, so the IP behind the hostname can change without any manual updates
  • This approach preserves all the same observability, security, and traffic management capabilities as routing to an IP-based remote A2A agent
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment