Scenario 06 · gRPC routing
gRPC is method-level RPC over HTTP/2, and its status rides in trailers.
Forcing it through a path-prefix HTTPRoute throws away method granularity and
risks an ingress that downgrades HTTP/2 or mangles trailers. Native GRPCRoute
routes by service and method, splits traffic per method, and keeps HTTP/2 and
grpc-status intact end to end.
Architecture
A call to pay.v1.Payments/Charge is an HTTP/2 POST to
/pay.v1.Payments/Charge. GRPCRoute matches it by exact service + method and
weight-splits it — while Refund stays entirely on stable. Trailers, including
grpc-status, pass through untouched.
The setup
A single GRPCRoute carries two rules: send 10% of the Charge method to a
canary backend, and keep Refund at 100% on stable. No URL-path guesswork — the match is
the gRPC service and method:
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
name: payments
namespace: default
spec:
parentRefs:
- name: zeno-gateway
hostnames:
- pay.api.internal
rules:
# 10% of Charge → canary, matched by exact service + method
- matches:
- method:
type: Exact
service: pay.v1.Payments
method: Charge
backendRefs:
- name: payments-stable
port: 50051
weight: 90
- name: payments-canary
port: 50051
weight: 10
# Refund stays 100% on stable
- matches:
- method:
type: Exact
service: pay.v1.Payments
method: Refund
backendRefs:
- name: payments-stable
port: 50051
The backends are plain gRPC Services on HTTP/2 (h2c or TLS). ZenoIngress speaks HTTP/2
to them — no protocol downgrade — so streaming, multiplexing, and trailers work end to end.
What happens
pay.v1.Payments/Charge — on the wire,
an HTTP/2 POST to path /pay.v1.Payments/Charge.
Charge calls is
dispatched to payments-canary, the rest to payments-stable — and
Refund is untouched, because the split is per method.
grpc-status, grpc-message — pass through unmodified.
trailers preserved
Accepted / ResolvedRefs status onto the GRPCRoute,
exactly like an HTTPRoute — so kubectl shows whether it attached and its backends resolved.
#![forbid(unsafe_code)]. memory-safe
Outcome
Native GRPCRoute on par with HTTPRoute — method-granular matching and weighted canaries, HTTP/2 and
gRPC trailers preserved end to end, and full Accepted/ResolvedRefs status —
all in a memory-safe data plane. No URL-prefix guesswork, no mangled trailers.
Honest status: GRPCRoute is implemented and tested end to end through the proxy. The upstream Gateway API GRPCRoute conformance suite has not yet been run here — we'll publish those rows when they're green.
Run this yourself
Apply the route, then call the methods with grpcurl and watch which method gets split:
$ # call Charge a few times — ~1 in 10 lands on the canary
$ grpcurl -d '{"amount":1200}' pay.api.internal:443 pay.v1.Payments/Charge
{ "servedBy": "payments-stable", "ok": true }
$ grpcurl -d '{"amount":1200}' pay.api.internal:443 pay.v1.Payments/Charge
{ "servedBy": "payments-canary", "ok": true } # the 10%
$ # Refund is never split — always stable
$ grpcurl -d '{"id":"r_91"}' pay.api.internal:443 pay.v1.Payments/Refund
{ "servedBy": "payments-stable", "ok": true }
$ # gRPC status rides in HTTP/2 trailers — preserved end to end
$ grpcurl -v -d '{"id":"r_91"}' pay.api.internal:443 pay.v1.Payments/Refund 2>&1 | grep -i grpc-status
trailer: grpc-status: 0
$ # the route reports status, same as HTTPRoute
$ kubectl get grpcroute payments -o jsonpath='{.status.parents[0].conditions[*].type}'
Accepted ResolvedRefs
Native GRPCRoute, HTTP/2 end to end, in one memory-safe binary. Free for one production cluster.
Deploy Free