From nginx-ingress to GKE Gateway API: Automating Load Balancing and Certificates with cert-manager and Vault PKI
For a long time, the conversation about moving off nginx-ingress was a forward-looking one: a client needed per-route traffic splitting for a canary release, or multi-team ownership of a single load balancer, or TLS termination policy that didn’t live in a wall of annotations nobody wanted to touch. That conversation changed on November 12, 2025, when Kubernetes SIG Network and the Security Response Committee announced the retirement of ingress-nginx. That’s the reason this migration is on every client’s roadmap now, not just the ones chasing new features.
The primary driver: ingress-nginx is going away
The retirement announcement is worth reading in full, but the short version is that the project had been running on fumes for a while — by the maintainers’ own account, only one or two people were doing development work, on their own time, after hours and on weekends. At the same time, features that once looked like flexibility, like injecting arbitrary NGINX configuration through snippet annotations, are now understood as a real security liability rather than a convenience. That combination of thin maintenance and growing technical debt is what pushed SIG Network to retire the project rather than keep patching it indefinitely.
The timeline matters for planning: ingress-nginx gets best-effort maintenance through March 2026, after which the repository moves to kubernetes-retired and goes read-only. No more releases, no more bugfixes, and critically, no more security patches after that date. Existing installations keep running — nothing shuts off automatically — but a controller sitting on unpatched CVEs in front of production traffic isn’t a risk we let clients carry, so for anyone still running ingress-nginx, this migration has a real deadline attached to it now, not just a “someday” on the backlog.
The upside: this also future-proofs the platform
Retirement is the forcing function, but it’s not the only reason we’re glad to make this move. The core issue with the Ingress resource was never nginx specifically — it’s that Ingress was never designed for how teams actually run traffic today. Anything beyond basic host/path routing gets bolted on through controller-specific annotations, which means the moment you need weighted traffic splitting, header-based routing, or cross-namespace routing delegated to different teams, you’re either hand-rolling it in annotations or reaching for a service mesh you didn’t otherwise need.
The Gateway API, now GA and natively supported by GKE, models this properly: a Gateway owned by the platform team defines the load balancer and listeners, and HTTPRoute resources owned by individual application teams attach to it. That role separation is exactly what the retirement announcement points teams toward, and it’s what lets a single cluster genuinely support multi-tenant traffic management — each application team manages its own HTTPRoute objects without needing access to (or the ability to break) the shared Gateway. On GKE specifically, a Gateway provisions a Google Cloud load balancer directly — regional or global, internal or external — with none of the sidecar-proxy overhead a full mesh would add if all you actually need is smarter L7 routing.
That role separation also opens up an isolation model Ingress never gave us. Instead of one shared load balancer for the whole cluster, we can deploy a dedicated Gateway per tenant — each with its own IP, its own listener configuration, and its own certificate — so one tenant’s traffic and blast radius stay fully separate from another’s. The same pattern works a level down, per environment within a single application: a staging Gateway and a production Gateway, each configured independently, and an HTTPRoute simply sets parentRefs to point at whichever Gateway it belongs to. Doing this with Ingress meant standing up entirely separate ingress controllers; with Gateway API it’s just another Gateway object.
Letting the Gateway own certificates instead of every application
Certificates were already mostly automated before this migration, and it’s worth being precise about that since it’s not the part that was actually broken. With nginx-ingress, cert-manager watches Ingress resources for a cert-manager.io/cluster-issuer annotation, and for every hostname listed in the Ingress’s tls block, it creates a Certificate resource that drives a CertificateRequest, an ACME Order, and a Challenge against the issuer — typically Let’s Encrypt via an HTTP-01 challenge. Once issued, the certificate and key land in the Secret the Ingress references, and cert-manager re-evaluates every certificate well ahead of expiry — by default at two-thirds of its lifetime, so around day 60 of a 90-day Let’s Encrypt certificate — and quietly repeats the whole process before it ever gets close to expiring. For most of the nginx-ingress deployments we inherit, that renewal loop was already working; the manual-renewal horror stories tend to mean cert-manager was adopted late or inconsistently, not that per-hostname automation is somehow missing from Ingress.
What actually changes under Gateway API is the shape of that automation, not whether it exists. Under Ingress, cert-manager issues roughly one certificate per application — every team’s manifest carries its own issuer annotation and tls block, and every new hostname means a new Certificate resource and a new ACME challenge to complete. Under Gateway API, we instead provision a single wildcard certificate — say, *.apps.client.com — attached directly to the Gateway’s TLS listener, and every HTTPRoute that attaches to that Gateway inherits TLS termination from it automatically. Application teams stop managing certificates at all: they write an HTTPRoute, and the wildcard the platform team already provisioned covers it. That’s a real reduction in what an application deployment has to carry, not just a change in which controller happens to be watching.
Vault PKI still earns its place here, just in a narrower scope than before. For internal, service-to-service TLS where we want short-lived certificates issued per workload rather than a shared public wildcard, cert-manager’s Vault issuer requests certificates from Vault’s PKI secrets engine the same way it would from any ACME issuer, with Vault acting as an intermediate CA that chains back to infrastructure the client already trusts. Vault PKI makes issuing a 24-hour certificate exactly as cheap as issuing a 1-year one, so short-lived internal certificates are the default rather than the exception, and cert-manager handles the renewal cadence the same way it does for the public wildcard.
How the migration actually happens
Ingress and Gateway API can run side by side
The single fact that makes this migration low-risk is that nginx-ingress and GKE’s Gateway API controller are watching two entirely different resource types. nginx-ingress reconciles Ingress objects; GKE’s Gateway controller reconciles HTTPRoute objects attached to a Gateway. Neither controller knows or cares that the other exists. That means we’re never doing an in-place, all-or-nothing cutover of a live resource — we stand up the new Gateway/HTTPRoute stack entirely alongside the existing Ingress stack, on its own load balancer with its own IP, and let both serve traffic in parallel for as long as we need.
Mapping Ingress rules to HTTPRoute
The routing logic itself translates fairly mechanically. An Ingress rule’s host becomes an HTTPRoute hostnames entry, and each path/pathType combination becomes a matches entry with a backendRefs pointing at the same Kubernetes Service. Where it stops being a one-to-one translation is everything Ingress could only do through annotations: weighted traffic splitting becomes multiple backendRefs with a weight on each, header- or method-based routing becomes explicit matches conditions instead of a controller-specific rewrite annotation, and request/response header manipulation becomes an HTTPRoute filter instead of a snippet. In practice this step is where we usually find two or three annotations per service that were quietly load-bearing and undocumented — the migration is a good forcing function for surfacing those.
Moving annotations to GCPBackendPolicy
The other category of annotation — the ones that configured the load balancer itself rather than routing — doesn’t move to HTTPRoute at all. Custom backend timeouts, connection draining behavior, session affinity, Cloud Armor security policies, and Identity-Aware Proxy settings move onto a GCPBackendPolicy resource that targets the Service directly via targetRef, and GKE’s Gateway controller reconfigures the underlying Google Cloud load balancer automatically whenever the policy changes. It’s a cleaner split than the old annotation model: GCPBackendPolicy owns backend behavior, GCPGatewayPolicy owns frontend/listener concerns like SSL policy and whether the load balancer is regional or global, and HealthCheckPolicy owns probe configuration — three typed, validated resources standing in for what used to be a grab-bag of string annotations that only the ingress controller’s source code could fully explain.
Cutting over
Once an HTTPRoute and its backend policies are live and validated — usually against a test hostname first, then a canary slice of real traffic — cutting over is just a DNS change: point the service’s record at the Gateway API load balancer’s IP instead of nginx-ingress’s. Because the two stacks are fully independent, this is trivially reversible for the life of the DNS TTL; if something’s wrong, we point DNS back and debug without any pressure. Only after DNS has been migrated for every service and had time to fully propagate and stabilize do we decommission the now-unused Ingress resources, and only after that do we uninstall the nginx-ingress controller itself.
What clients get out of it
Beyond dodging the March 2026 end-of-support cliff, the real win is structural: certificates collapse from one-per-application to one wildcard per Gateway, so application teams stop carrying TLS configuration at all. Traffic management moves from controller-specific annotations to a portable standard API, and load balancer configuration moves from opaque annotations to typed, validated policy resources that reconfigure the load balancer safely on every change. The platform comes out the other side genuinely multi-tenant — every application team owns its own routes, tenants and environments can be isolated behind their own Gateway when they need to be, and none of it requires the keys to a shared load balancer.