Shadow Deployment with Gateway API Request Mirroring
Scenario
The checkout API runs in the checkout namespace. A new version sits beside the stable one and receives nothing:
| Deployment | Version | Role |
|---|---|---|
checkout-stable | v1 | serves production traffic |
checkout-shadow | v2 | new version, a rewritten pricing engine |
Both listen on port 8080, and both print one line per request they handle:
07:41:12 checkout-stable v1 handled GET /api/checkout id=- host=checkout.acme-retail.io from=10.42.0.9
A Gateway named edge-gateway is already provisioned on 192.168.1.240, listening on port 80 for checkout.acme-retail.io, and /etc/hosts on cplane-01 resolves that name. No HTTPRoute exists yet, so the domain answers nothing so far.
Task
The team wants v2 exercised under real production load before it ever answers a customer. That is a shadow deployment: every live request is duplicated to checkout-shadow, and the copy's response is discarded.
- Create an HTTPRoute named
checkout-routein thecheckoutnamespace, attached toedge-gatewayand servingcheckout.acme-retail.io. - Route
/api/checkouttocheckout-stableon port8080. - Send a copy of every one of those requests to
checkout-shadowon port8080. A request to the domain is then answered byv1and logged by both Pods. - Send ten tagged requests to the domain:
for i in $(seq 1 10); do curl -s -H "X-Request-Id: shadow-$i" \ http://checkout.acme-retail.io/api/checkout done - Save the shadow Pod logs to
/home/laborant/shadow.logs:kubectl logs deployment/checkout-shadow -n checkout > /home/laborant/shadow.logs
The file has to show all ten requests,shadow-1throughshadow-10, handled byv2even though no client ever saw av2response.

The client is always answered by v1. v2 receives an identical copy and its response goes nowhere.
checkout-shadow must not become a second backend. If a client can ever receive a v2 answer, this is a canary release rather than a shadow deployment, and v2 is no longer free to fail.
Hint 1 | Where duplication lives in the API
Duplicating traffic is not a second backend and not a weight. It is a filter, and filters attach to a rule beside its backendRefs.
kubectl explain httproute.spec.rules.filters
kubectl explain httproute.spec.rules.filters.requestMirror
The list of filter types is short. One of them describes sending a copy elsewhere.
Hint 2 | The shape of a rule
A rule carries what to match, what to do with the request, and where to send it:
rules:
- matches:
- path:
type: PathPrefix
value: <path>
filters:
- type: <filter-type>
<filterField>:
backendRef:
name: <service-name>
port: <service-port>
backendRefs:
- name: <service-name>
port: <service-port>
The copied destination sits inside the filter. The real destination stays in backendRefs.
See: HTTPRoute, Request mirroring
Hint 3 | Reading the result
A working mirror looks like nothing changed from the client side, so check the route first and the logs second:
kubectl describe httproute checkout-route -n checkout
Accepted: True and ResolvedRefs: True mean the route attached and every Service it names, including the mirrored one, resolved. A mirror pointing at a Service that does not exist fails here.
Then send a few tagged requests and grep both log streams for the same id:
kubectl logs deployment/checkout-shadow -n checkout --tail=50 | grep 'id=shadow-'
The same id in both Pods is the proof that one client request became two.