The Anatomy of Traffic Management: Dissecting HTTP Routing in Envoy Gateway

Overview

The HTTPRoute resource allows users to configure HTTP routing by matching HTTP traffic and forwarding it to Kubernetes backends. Currently, the only supported backend supported by Envoy Gateway is a Service resource. This task shows how to route traffic based on host, header, and path fields and forward the traffic to different Kubernetes Services.

Diagram

Diagram Kubernetes Resources
Diagram Kubernetes Resources

Install the HTTP routing example resources:

# This file contains the base resources that the docs/user/HTTP_ROUTING.md guide relies on.
# This includes a GatewayClass, Gateway, Services and Deployments that are used as backends
# for routing traffic.
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
  name: example-gateway-class
  labels:
    example: http-routing
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: example-gateway
  labels:
    example: http-routing
spec:
  gatewayClassName: example-gateway-class
  listeners:
    - name: http
      protocol: HTTP
      port: 80
---
apiVersion: v1
kind: Service
metadata:
  name: example-svc
  labels:
    example: http-routing
spec:
  ports:
    - name: http
      port: 8080
      targetPort: 3000
  selector:
    app: example-backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: example-backend
  labels:
    app: example-backend
    example: http-routing
spec:
  replicas: 1
  selector:
    matchLabels:
      app: example-backend
  template:
    metadata:
      labels:
        app: example-backend
    spec:
      containers:
        - name: example-backend
          image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          resources:
            requests:
              cpu: 10m
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: example-route
  labels:
    example: http-routing
spec:
  parentRefs:
    - name: example-gateway
  hostnames:
    - "example.com"
  rules:
    - backendRefs:
        - name: example-svc
          port: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: foo-svc
  labels:
    example: http-routing
spec:
  ports:
    - name: http
      port: 8080
      targetPort: 3000
  selector:
    app: foo-backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: foo-backend
  labels:
    app: foo-backend
    example: http-routing
spec:
  replicas: 1
  selector:
    matchLabels:
      app: foo-backend
  template:
    metadata:
      labels:
        app: foo-backend
    spec:
      containers:
        - name: foo-backend
          image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          resources:
            requests:
              cpu: 10m
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: foo-route
  labels:
    example: http-routing
spec:
  parentRefs:
    - name: example-gateway
  hostnames:
    - "foo.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /login
      backendRefs:
        - name: foo-svc
          port: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: bar-svc
  labels:
    example: http-routing
spec:
  ports:
    - name: http
      port: 8080
      targetPort: 3000
  selector:
    app: bar-backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: bar-backend
  labels:
    app: bar-backend
    example: http-routing
spec:
  replicas: 1
  selector:
    matchLabels:
      app: bar-backend
  template:
    metadata:
      labels:
        app: bar-backend
    spec:
      containers:
        - name: bar-backend
          image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          resources:
            requests:
              cpu: 10m
---
apiVersion: v1
kind: Service
metadata:
  name: bar-canary-svc
  labels:
    example: http-routing
spec:
  ports:
    - name: http
      port: 8080
      targetPort: 3000
  selector:
    app: bar-canary-backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: bar-canary-backend
  labels:
    app: bar-canary-backend
    example: http-routing
spec:
  replicas: 1
  selector:
    matchLabels:
      app: bar-canary-backend
  template:
    metadata:
      labels:
        app: bar-canary-backend
    spec:
      containers:
        - name: bar-canary-backend
          image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          resources:
            requests:
              cpu: 10m
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: bar-route
  labels:
    example: http-routing
spec:
  parentRefs:
    - name: example-gateway
  hostnames:
    - "bar.example.com"
  rules:
    - matches:
        - headers:
            - type: Exact
              name: env
              value: canary
      backendRefs:
        - name: bar-canary-svc
          port: 8080
    - backendRefs:
        - name: bar-svc
          port: 8080

Verification

Check the status of the GatewayClass:

kubectl get gc --selector=example=http-routing

The status should reflect Accepted=True, indicating Envoy Gateway is managing the GatewayClass.

A Gateway represents configuration of infrastructure. When a Gateway is created, Envoy proxy infrastructure is provisioned or configured by Envoy Gateway. The gatewayClassName defines the name of a GatewayClass used by this Gateway. Check the status of the Gateway:

kubectl get gateways --selector=example=http-routing

The status should reflect “Ready=True”, indicating the Envoy proxy infrastructure has been provisioned. The status also provides the address of the Gateway. This address is used later to test connectivity to proxied backend services.

Testing the Configuration

Without Load Balancer

Get the name of the Envoy service created the by the example Gateway:

export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=example-gateway -o jsonpath='{.items[0].metadata.name}')

Port forward to the Envoy service:

kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 8888:80 &

Test HTTP Routing to the example.com app through Envoy proxy

curl -vvv --header "Host: example.com" "http://localhost:8888/"

Output:

* Host localhost:8888 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8888...
* Connected to localhost (::1) port 8888
> GET / HTTP/1.1
> Host: example.com
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
Handling connection for 8888
< HTTP/1.1 200 OK
< content-type: application/json
< x-content-type-options: nosniff
< date: Tue, 18 Nov 2025 14:02:13 GMT
< content-length: 468
<
{
 "path": "/",
 "host": "example.com",
 "method": "GET",
 "proto": "HTTP/1.1",
 "headers": {
  "Accept": [
   "*/*"
  ],
  "User-Agent": [
   "curl/8.7.1"
  ],
  "X-Envoy-External-Address": [
   "127.0.0.1"
  ],
  "X-Forwarded-For": [
   "10.10.0.33"
  ],
  "X-Forwarded-Proto": [
   "http"
  ],
  "X-Request-Id": [
   "b32842e4-ac2b-47f1-a843-021febe79fa2"
  ]
 },
 "namespace": "default",
 "ingress": "",
 "service": "",
 "pod": "example-backend-54988c6bfd-2kl7x"
* Connection #0 to host localhost left intact
}

A 200 status code should be returned and the body should include "pod": "example-backend-*" indicating the traffic was routed to the example backend service. If you change the hostname to a hostname not represented in any of the HTTPRoutes, e.g. www.example.com, the HTTP traffic will not be routed and a 404 should be returned.

curl -vvv --header "Host: www.example.com" "http://localhost:8888/"
* Host localhost:8888 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8888...
* Connected to localhost (::1) port 8888
> GET / HTTP/1.1
> Host: www.example.com
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
Handling connection for 8888
< HTTP/1.1 404 Not Found
< date: Tue, 18 Nov 2025 14:17:52 GMT
< content-length: 0
<
* Connection #0 to host localhost left intact

This is because the HTTPRoute you applied before defines example.com not www.example.com.

Test HTTP Routing to the foo.example.com app /login through Envoy proxy

curl -vvv --header "Host: foo.example.com" "http://localhost:8888/login"

Output:

* Host localhost:8888 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8888...
* Connected to localhost (::1) port 8888
> GET /login HTTP/1.1
> Host: foo.example.com
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
Handling connection for 8888
< HTTP/1.1 200 OK
< content-type: application/json
< x-content-type-options: nosniff
< date: Tue, 18 Nov 2025 14:24:38 GMT
< content-length: 473
<
{
 "path": "/login",
 "host": "foo.example.com",
 "method": "GET",
 "proto": "HTTP/1.1",
 "headers": {
  "Accept": [
   "*/*"
  ],
  "User-Agent": [
   "curl/8.7.1"
  ],
  "X-Envoy-External-Address": [
   "127.0.0.1"
  ],
  "X-Forwarded-For": [
   "10.10.0.33"
  ],
  "X-Forwarded-Proto": [
   "http"
  ],
  "X-Request-Id": [
   "1a02d909-a2e1-4563-ba5b-7d9495f7372f"
  ]
 },
 "namespace": "default",
 "ingress": "",
 "service": "",
 "pod": "foo-backend-758cc5c78b-gb9ld"
* Connection #0 to host localhost left intact
}

A 200 status code should be returned and the body should include “pod”: “foo-backend-*” indicating the traffic was routed to the foo backend service. Traffic to any other paths that do not begin with /login will not be matched by this HTTPRoute. Test this by removing /login from the request.

Test HTTP routing to the bar-canary-svc backend by adding the env: canary header to the request.

curl -vvv --header "Host: bar.example.com" --header "env: canary" "http://localhost:8888/"

Output:

* Host localhost:8888 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8888...
* Connected to localhost (::1) port 8888
> GET / HTTP/1.1
> Host: bar.example.com
> User-Agent: curl/8.7.1
> Accept: */*
> env: canary
>
* Request completely sent off
Handling connection for 8888
< HTTP/1.1 200 OK
< content-type: application/json
< x-content-type-options: nosniff
< date: Tue, 18 Nov 2025 14:28:09 GMT
< content-length: 502
<
{
 "path": "/",
 "host": "bar.example.com",
 "method": "GET",
 "proto": "HTTP/1.1",
 "headers": {
  "Accept": [
   "*/*"
  ],
  "Env": [
   "canary"
  ],
  "User-Agent": [
   "curl/8.7.1"
  ],
  "X-Envoy-External-Address": [
   "127.0.0.1"
  ],
  "X-Forwarded-For": [
   "10.10.0.33"
  ],
  "X-Forwarded-Proto": [
   "http"
  ],
  "X-Request-Id": [
   "132234dd-eeb5-417c-8abd-97db06ab4d21"
  ]
 },
 "namespace": "default",
 "ingress": "",
 "service": "",
 "pod": "bar-canary-backend-56c4c6b46-zm6sd"
* Connection #0 to host localhost left intact
}

A 200 status code should be returned and the body should include “pod”: “bar-canary-backend-*” indicating the traffic was routed to the foo backend service.

Manage Canary Traffic

We could also manage the traffic by percentage to the specific resource (e.g., Pod) Update the HTTP Route for bar-route, which becomes:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: bar-route
  labels:
    example: http-routing
spec:
  parentRefs:
    - name: example-gateway
  hostnames:
    - "bar.example.com"
  rules:
    - backendRefs:
        - name: bar-canary-svc
          port: 8080
          weight: 90 # <--- 90% of the total traffic will be redirected to the bar-canary-svc
        - name: bar-svc
          port: 8080
          weight: 10 #  <--- 10% of the total traffic will be redirected to the bar-svc

Simulate the Traffic

You could create a sample script to check which service the traffic will be redirected to

#!/bin/bash

HOST="bar.example.com"
TARGET_URL="http://localhost:8888/"
REQUEST_COUNT=5
CANARY_PREFIX="canary"

echo "--- Starting 90/10 Canary Weight Test ---"
echo "Sending $REQUEST_COUNT requests to Host: $HOST (via $TARGET_URL)"
echo "------------------------------------------"

CANARY_COUNT=0
STABLE_COUNT=0
UNMATCHED_COUNT=0

for i in $(seq 1 $REQUEST_COUNT); do
    # Send request and extract the pod name using the user-defined pipeline
    # -sS suppresses progress but shows error if failed
    POD_NAME=$(curl -sS --header "Host: $HOST" "$TARGET_URL" | grep pod | awk -F '"' '{print $4}')

    # Check if the extracted pod name contains the defined canary prefix
    if [[ "$POD_NAME" == *"$CANARY_PREFIX"* ]]; then
        echo "Request $i: Routed to CANARY pod ($POD_NAME)"
        CANARY_COUNT=$((CANARY_COUNT + 1))
    elif [[ -n "$POD_NAME" ]]; then
        # If POD_NAME is not empty and does not contain 'canary', assume it is stable
        echo "Request $i: Routed to STABLE pod ($POD_NAME)"
        STABLE_COUNT=$((STABLE_COUNT + 1))
    else
        echo "Request $i: FAILED to get a valid pod name from the response."
        UNMATCHED_COUNT=$((UNMATCHED_COUNT + 1))
    fi
done

echo "------------------------------------------"
echo "FINAL RESULTS (90/10 Split Demonstration):"
echo "STABLE Pod Count: $STABLE_COUNT requests"
echo "CANARY Pod Count: $CANARY_COUNT requests"
echo "Failed/Unmatched Count: $UNMATCHED_COUNT requests"
echo ""
echo "Note: For a small number of requests (like $REQUEST_COUNT), deviations from the 90/10 ratio are expected due to probability."

Run the script

./test-canary.sh

Output:

--- Starting 90/10 Canary Weight Test ---
Sending 5 requests to Host: bar.example.com (via http://localhost:8888/)
------------------------------------------
Request 1: Routed to CANARY pod (bar-canary-backend-56c4c6b46-zm6sd)
Request 2: Routed to CANARY pod (bar-canary-backend-56c4c6b46-zm6sd)
Request 3: Routed to CANARY pod (bar-canary-backend-56c4c6b46-zm6sd)
Request 4: Routed to STABLE pod (bar-backend-7dcd687b74-hmfhz)
Request 5: Routed to CANARY pod (bar-canary-backend-56c4c6b46-zm6sd)
------------------------------------------
FINAL RESULTS (90/10 Split Demonstration):
STABLE Pod Count: 1 requests
CANARY Pod Count: 4 requests
Failed/Unmatched Count: 0 requests

Note: For a small number of requests (like 5), deviations from the 90/10 ratio are expected due to probability.