/writing/envoy-gateway-migration
Moving from Ingress to Envoy Gateway
The Gateway API is usually explained as a better Ingress. That undersells it.
Ingress collapsed two different jobs into one resource: the
infrastructure-level concern of running a load balancer and terminating TLS,
and the application-level concern of routing a path to a service. Gateway API
splits those into Gateway and HTTPRoute, owned by different people.
That split is the reason to migrate. The routing improvements are real but secondary.
Where we started
Ingress-NGINX with a growing pile of annotations: TLS, rewrites, timeouts, affinity, and the occasional snippet that nobody wanted to own. Changes that touched shared ingress config required platform involvement even when the intent was “route this path to that service.” Ownership was muddy because the resource itself was muddy.
Why Envoy Gateway rather than the alternatives
We already ran Envoy in places and understood its config model. Envoy Gateway implements Gateway API without inventing a parallel control plane vocabulary, and the project was moving at a pace that matched what we needed. Contour and other implementations were fine on paper; existing Envoy familiarity tipped it. That is a legitimate engineering reason — the migration cost is mostly people, not YAML.
Running both during the cutover
We did not flip the cluster in one change. New routes landed on Gateway API
first; existing Ingress stayed serving until each service had an equivalent
HTTPRoute and we were happy with health checks and TLS. Cutover was
per-service: point DNS or the upstream load balancer at the Gateway listener,
watch, then remove the Ingress object. Rollback was the reverse — leave the
Ingress in place until the Gateway path was proven, not delete-and-hope.
What the annotations became
| Ingress annotation (typical) | Gateway API equivalent |
|---|---|
cert-manager.io/cluster-issuer |
Certificate on the Gateway / listener TLS |
nginx.ingress.kubernetes.io/rewrite-target |
URLRewrite filter on the HTTPRoute |
nginx.ingress.kubernetes.io/proxy-body-size |
Envoy proxy policy / connection limits |
nginx.ingress.kubernetes.io/affinity |
Session persistence policy (where supported) |
nginx.ingress.kubernetes.io/configuration-snippet |
Usually nowhere — rewrite as a first-class filter or admit you needed a custom Envoy config |
The last row is the migration tax. Snippets were an escape hatch that let teams paper over missing features. Gateway API pushes those into explicit policies or makes you admit the requirement was never supported properly.
What I’d tell someone starting this
- Treat ownership as the success metric. If app teams still open tickets to change a path, you migrated the API without migrating the model.
- Inventory every annotation before you write the first
HTTPRoute. The ones with no equivalent are your real scope. - Run both controllers until the last Ingress is gone. Parallel is boring and correct; a big-bang cutover is a weekend you do not get back.
