# Envoy Gateway: route behavior, backend switch and rollback This fixture tests an HTTPRoute through a real Envoy data plane in a **fresh disposable kind cluster**. It does not install ingress-nginx or claim to convert its annotations. No public load balancer, cloud account, DNS change or production resource is involved. Recorded environment: kind **0.33.0**, Kubernetes **1.36.4**, Helm **4.3.0**, Envoy Gateway chart/controller **1.9.2**, Envoy Proxy **1.39.2**, and the chart's Gateway API **1.6.1 experimental-channel bundle**. The HTTPRoute and Gateway resources use the stable `v1` API. Kubernetes 1.36 is in Envoy Gateway 1.9's [published compatibility matrix](https://gateway.envoyproxy.io/news/releases/matrix/); the earlier Helm/Collector fixture's Kubernetes 1.37 cluster was not reused. The included files are: - `values.yaml`: pinned controller and default proxy image references. - `gateway.yaml`: explicit EnvoyProxy configuration, a ClusterIP-only proxy Service, GatewayClass and Gateway. - `backends.yaml`: harmless blue and green HTTP backends using a pinned BusyBox image. - `route-blue.yaml`: root/prefix traffic to blue, with a more-specific status path to green. - `route-green.yaml`: the same HTTPRoute with root/prefix traffic switched to green. - `observed-results.json`: actual commands, route conditions, requests, image identities and cleanup. ## Reproduce only in a new cluster The chart installs cluster-scoped CRDs and policy resources. Do not use this experiment as an installation procedure for an existing shared cluster. Its hardcoded namespace and GatewayClass names are intentional; adapt them only after reviewing every manifest. Use verified binaries and explicit paths. These are Bash commands from the example directory: ```bash fixture_name=kubedex-envoy-fixture-20261009 fixture_config="$PWD/envoy-kubeconfig" fixture_context="kind-$fixture_name" kind_bin=/absolute/path/to/kind-v0.33.0 helm4=/absolute/path/to/helm-v4.3.0 "$kind_bin" create cluster --name "$fixture_name" --kubeconfig "$fixture_config" --image kindest/node:v1.36.4@sha256:099e049362a1526b2db71494e1947aae99bd16290d7c895f2b7ea312e3cbfaed --wait 120s k=(kubectl --kubeconfig "$fixture_config" --context "$fixture_context") h=(--kubeconfig "$fixture_config" --kube-context "$fixture_context" --namespace kubedex-envoy-system) "${k[@]}" get nodes "${k[@]}" create namespace kubedex-gateway-fixture "${k[@]}" create namespace kubedex-envoy-system "$helm4" pull oci://docker.io/envoyproxy/gateway-helm --version v1.9.2 ``` The tested OCI chart digest was `sha256:be034275b55deeddd6b7bc1f4da6eeb02b8efc418a4a29e2b1977851b24b1e63`. The downloaded archive `gateway-helm-v1.9.2.tgz` had SHA-256 `1079cad009e0885f6e10e5f712257d8e5fdaf911d2ceb3fa1b78632c8f29bbf9`. Verify it before installation: ```bash "$helm4" install eg ./gateway-helm-v1.9.2.tgz "${h[@]}" --values values.yaml --wait --timeout 300s "${k[@]}" apply -f backends.yaml "${k[@]}" rollout status deployment/blue -n kubedex-gateway-fixture --timeout=120s "${k[@]}" rollout status deployment/green -n kubedex-gateway-fixture --timeout=120s "${k[@]}" apply -f gateway.yaml "${k[@]}" apply -f route-blue.yaml "${k[@]}" wait gateway/local-gateway -n kubedex-gateway-fixture --for=condition=Programmed --timeout=180s "${k[@]}" get httproute fixture-route -n kubedex-gateway-fixture -o yaml ``` Verify `Accepted=True` and `ResolvedRefs=True` for the HTTPRoute's current generation. The automated fixture made this check after every route change. Also wait for the controller-created proxy Deployment to become ready. Find its Service and Deployment using the owning-gateway label: ```bash "${k[@]}" get service,deployment -n kubedex-envoy-system -l gateway.envoyproxy.io/owning-gateway-name=local-gateway ``` In a separate terminal, port-forward the listed **proxy Service**, not the control-plane Service: ```bash "${k[@]}" -n kubedex-envoy-system port-forward service/ 18080:8080 --address 127.0.0.1 ``` The placeholder above must be replaced with the returned Service name. The automatic runner discovered it and verified `type: ClusterIP`. No external address is provisioned. ## Request matrix and switch Send requests with the declared hostname while connecting only to localhost: ```bash curl --include --header 'Host: fixture.example.test' http://127.0.0.1:18080/ curl --include --header 'Host: fixture.example.test' http://127.0.0.1:18080/app curl --include --header 'Host: fixture.example.test' http://127.0.0.1:18080/app/ curl --include --header 'Host: fixture.example.test' http://127.0.0.1:18080/app/status curl --include --header 'Host: fixture.example.test' http://127.0.0.1:18080/application curl --include --header 'Host: other.example.test' http://127.0.0.1:18080/ ``` The first route produced these observations: | Host/path | Status | Body | What it checks | |---|---|---|---| | declared host `/` | 200 | `blue` | Exact root route | | declared host `/app` | 200 | `blue` | Prefix match and prefix rewrite | | declared host `/app/` | 200 | `blue` | Trailing slash in that prefix | | declared host `/app/status` | 200 | `green` | More-specific exact match with full-path rewrite | | declared host `/application` | 404 | empty | Prefix path-element boundary | | other host `/` | 404 | empty | Hostname mismatch | Switch the same route resource to green, check its current-generation status, and repeat `/`, `/app` and `/app/status`. All three should serve `green`. Then restore the blue route and repeat; `/` and `/app` should serve `blue`, while `/app/status` remains `green`: ```bash "${k[@]}" apply -f route-green.yaml "${k[@]}" get httproute fixture-route -n kubedex-gateway-fixture -o yaml # Run the three requests after current-generation acceptance. "${k[@]}" apply -f route-blue.yaml "${k[@]}" get httproute fixture-route -n kubedex-gateway-fixture -o yaml # Run the three requests again after current-generation acceptance. ``` The final automated run passed all **12** requests and verified the controller/programmed-route conditions and declared image digests. Its bounded response wait is an acceptance mechanism, not a latency benchmark. HTTPRoute rollback here means restoring this known routing declaration; it does not reverse application data or a DNS cutover. ## Image configuration detail The custom EnvoyProxy in `gateway.yaml` explicitly pins its data-plane image. Setting only the chart's default proxy image did not establish that image for this custom proxy in the first experiment. The final run therefore checked the **declared pod image** and recorded its runtime image identity, rather than assuming Helm values had controlled the resulting data plane. Preserve that check when modifying the example. ## Cleanup and limits Stop port-forwarding, then remove only the named cluster created for this experiment: ```bash "$kind_bin" delete cluster --name "$fixture_name" --kubeconfig "$fixture_config" ``` The final test deleted that cluster, including its test CRDs and GatewayClass. No other cluster or namespace was changed. Not tested: ingress-nginx translation, annotation equivalence, TLS/certificates, authentication, rate limits, client-IP trust, cross-namespace authorization, gRPC, WebSockets, long-lived requests, cloud load balancers, DNS, controller upgrades, capacity or production cutover. These remain separate request-matrix and recovery tasks in the migration guide.