On March 24, 2026, kubernetes/ingress-nginx — the most widely used ingress controller in the ecosystem — officially reached End-of-Life. The repo is frozen: no bugfixes, no security patches, no responses on issues. If it’s still handling traffic in your cluster, the question isn’t “should we migrate” — it’s “how long until the first unpatched vulnerability.” Gateway API isn’t an alternative syntax for Ingress; it’s the official replacement built into Kubernetes itself, and SIG Network has already shipped a migration tool. Here’s how to move over without a fire drill.
Table of contents
Open Table of contents
Why Ingress hit a wall
Ingress was specified back in 2015 as a minimal common denominator: one resource, host/path matching, a TLS block — and that’s it. Real controllers hit the ceiling of that spec almost immediately: canary releases, rate limiting, rewrite rules, mTLS between services — none of it exists in the API. Every controller plugged the gaps with nginx.ingress.kubernetes.io/...-style annotations, and the end result wasn’t a portable API but a pile of dialects. A manifest written for ingress-nginx wouldn’t run on Traefik or HAProxy without rewriting half the annotations.
The second problem is roles. In a real organization, the platform team owns the load balancer and TLS certificates, while a service team owns only its own routes. Ingress doesn’t distinguish between the two: it’s one YAML file, so either the developer gets rights over the whole load balancer, or the platform team becomes a bottleneck, manually reviewing every PR that adds a new path.
What Gateway API changes
Gateway API isn’t a patch on top of Ingress — it’s a different resource model, designed from the start around roles and typed fields.
- Role separation.
GatewayClassandGatewayare the platform engineer’s territory: which load balancer implementation, which ports, which TLS.HTTPRouteis the service team’s territory: its own host/path rules, its own RBAC, no access to the rest of the infrastructure. - Typed routes instead of annotations. Header-based routing, weighted canaries, path rewrites — these are fields in the
HTTPRouteschema, not free-text annotations tied to one vendor. The same manifest moves between Envoy Gateway, Cilium, and a cloud controller without rewriting. - Extensibility through types, not hacks.
HTTPRoutefor HTTP,GRPCRoutefor gRPC,TCPRoute/TLSRoutefor L4 — handled through a dedicated typed resource with its own schema, not annotations. - Cross-namespace routing built in. A
ReferenceGrantexplicitly allows anHTTPRoutein one namespace to reference aServicein another — something Ingress solved through nonstandard, controller-specific flags. - Policy attachment instead of annotations on the resource. Rate limiting, retry policies, mTLS between services live in separate CRDs (
BackendTrafficPolicy,ClientTrafficPolicy, and similar — the exact names vary by implementation) that “attach” to aGatewayorHTTPRouteviatargetRef, instead of cluttering the route itself with vendor syntax.
The resource model: GatewayClass → Gateway → HTTPRoute
Three resources, each solving its own layer of the problem.
GatewayClass is a cluster-scoped resource that declares an available implementation (Envoy Gateway, NGINX Gateway Fabric, a cloud LB) — roughly the way a StorageClass declares an available disk type. The platform team, or the implementation’s own operator during install, creates it.
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: envoy-gateway
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
Gateway is a concrete entry point instance: ports, protocols, TLS certificates, and a restriction on which namespaces are allowed to attach routes to it. Also platform team territory.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: public-gateway
namespace: infra
spec:
gatewayClassName: envoy-gateway
listeners:
- name: https
protocol: HTTPS
port: 443
tls:
certificateRefs:
- name: wildcard-tls
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels: { gateway-access: "allowed" }
HTTPRoute holds one service’s routes, lives in the team’s own namespace, and references a Gateway through parentRefs. This is also where the weighted canary split lives — no annotation needed.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: checkout
namespace: checkout-team
spec:
parentRefs:
- name: public-gateway
namespace: infra
hostnames: ["checkout.example.com"]
rules:
- matches:
- path: { type: PathPrefix, value: /api }
backendRefs:
- name: checkout-stable
port: 80
weight: 90
- name: checkout-canary
port: 80
weight: 10
ingress2gateway: conversion without a manual rewrite
SIG Network shipped ingress2gateway 1.0 specifically for the wave of migrations off EOL ingress-nginx. The tool reads a cluster’s existing Ingress resources (including a fair chunk of common annotations) and generates the equivalent Gateway/HTTPRoute pair.
# install the CLI
go install sigs.k8s.io/ingress2gateway@latest
# read the current Ingress resources from the cluster and print generated manifests
ingress2gateway print --namespace checkout-team > gateway-generated.yaml
# same, but scoped to a specific annotation provider (e.g. nginx)
ingress2gateway print --providers=ingress-nginx --namespace checkout-team
The generated YAML is a draft, not a final result: check the parentRefs (does the name and namespace match the Gateway you already created), confirm TLS actually got pulled from the right Gateway listener instead of coming out empty, and run it through kubectl apply --dry-run=server to catch schema errors before applying for real.
Picking an implementation
Gateway API is a spec, not a running controller — you need an implementation that supports it.
- Envoy Gateway — a CNCF project built on Envoy proxy, full Gateway API support, a solid standalone replacement for ingress-nginx with no ties to the rest of your stack.
- NGINX Gateway Fabric — F5/NGINX’s official implementation, minimizes migration shock for teams already used to nginx semantics (rewrite, rate limiting).
- Cilium — if the CNI is already Cilium, Gateway API turns on as part of the existing eBPF dataplane, no separate load balancer, lowest overhead.
- Cloud implementations — GKE Gateway controller, AWS Load Balancer Controller (Gateway API support), Azure Application Gateway for Containers — a fit if you want a managed L7 with no control plane of your own.
The deciding factor is almost always not “which implementation is technically stronger” but “what’s already running in the cluster.” If the CNI is Cilium, bolting on a separate L7 load balancer just for Gateway API is an extra component in the dataplane; if the infrastructure is fully cloud-hosted and L7 is already delegated to a managed LB, it’s more natural to turn on Gateway API support there rather than stand up Envoy Gateway alongside it. A standalone install (Envoy Gateway, NGINX Gateway Fabric) makes sense where you need control over a specific version and aren’t tied to one cloud.
Comparison: Ingress vs. Gateway API
| Ingress | Gateway API | |
|---|---|---|
| Roles | One resource, all-or-nothing access | GatewayClass/Gateway — platform, HTTPRoute — team |
| Canary/weight routing | Vendor annotations, not portable | weight field in the backendRefs schema |
| Portability across controllers | Low — every vendor’s annotations differ | High — a shared typed schema |
| Protocols | HTTP/HTTPS only | HTTP, gRPC, TCP, TLS — separate route types |
| Cross-namespace references | Nonstandard controller flags | ReferenceGrant, explicit opt-in |
| Project status | ingress-nginx: EOL since March 2026 | Actively developed, GA since v1.0 |
What it takes to migrate
An order of operations that doesn’t break production traffic along the way:
- Install the Gateway API CRDs —
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.3.0/standard-install.yaml. - Install one implementation — Envoy Gateway via its Helm chart, for example, and create a
GatewayClass+Gatewayfor a new public entry point. - Run the existing
Ingressresources throughingress2gateway, per namespace, to get a draftHTTPRoute. - Review every generated
HTTPRouteby hand — especially annotations with no 1:1 equivalent (rate limiting, custom headers, Lua/nginx.confsnippets) — those need porting through an implementation-specificExtensionRefor rewriting as separate logic. - Run
GatewayandHTTPRoutealongside the oldIngress, sending a small slice of traffic to the new entry point via a DNS weight or a separate test hostname — don’t switch offIngressright away. - Ramp the canary weight in
HTTPRoutegradually from 10% to 100% while watching metrics, and only then remove the oldIngressresources.
Migration traps
- Annotations with no direct equivalent. Custom
nginx.confsnippets, Lua scripts, controller-specific headers —ingress2gatewaydoesn’t carry these over because there’s nothing to carry over to: no equivalent exists in the spec. These stay on the manual-review list and often need an implementation-specificExtensionRef. - TLS secrets not where the
Gatewayexpects them. In Ingress, the TLS secret usually lives in the same namespace as the resource itself. AGatewayis more often created in an infra namespace, and the secret needs to be explicitly copied there or wired up via aReferenceGrant— otherwise theGatewaysits stuck atProgrammed: Falsewith no obvious error in the logs. - Weight-based canary isn’t the same as header- or cookie-based canary. A plain
weightsplit inbackendRefscovers the basic case, but sticky sessions or routing a specific user requiresmatchesrules withheaders/cookie, which need to be layered on top of the generated draft by hand. - Multiple Ingress controllers in one cluster mean multiple
GatewayClassresources. If the cluster historically ran ingress-nginx alongside, say, a separate internal controller, don’t try to collapse everything into oneGatewayat once — migrate one traffic class at a time. - RBAC on
HTTPRoutenever got configured. The whole point of role separation disappears if every team still hascluster-adminand editsGatewaydirectly instead of its ownHTTPRoute. Before calling the migration done, lock downRole/RoleBindingso a service team can only editHTTPRoutein its own namespace, whileGatewaystays in the platform team’s hands.
How to verify it works
Three quick checks after applying the manifests. The Gateway’s own status: kubectl get gateway public-gateway -n infra -o jsonpath='{.status.conditions}' should show Programmed: True and Accepted: True. The specific route’s status: kubectl get httproute checkout -n checkout-team -o jsonpath='{.status.parents}' confirms the Gateway actually accepted this HTTPRoute, not just that the resource exists unattached. And live traffic: curl -H "Host: checkout.example.com" https://<gateway-ip>/api/health run a few times in a row should show the split between checkout-stable and checkout-canary tracking the configured weights.
Bottom line
ingress-nginx won’t break overnight — it’ll keep running right up until the first vulnerability that no longer gets a patch. That makes migration less a matter of taste and more a matter of timing: the sooner you start shifting traffic to Gateway API alongside the old Ingress, the smaller the odds of a security incident landing on dead code in production. ingress2gateway handles most of the routine conversion, but manually checking every nonstandard rule is a required step, not an optional one.