Gateway API vs. Ingress: The Migration Every Kubernetes Team Now Owes
Sources
- Kubernetes Blog — Ingress NGINX Retirement Statement (Jan 2026)
- Kubernetes Contributors — Ingress NGINX Retirement: What You Need to Know (Nov 2025)
- Gateway API v1.6 Release — TCPRoute and UDPRoute Graduate to Standard (Aug 2026)
- Gateway API v1.6.2 Release Notes (Sept 2026)
- Cilium 1.20 Release Blog — Gateway API ExternalAuth, TCPRoute/UDPRoute (Sept 2026)
- Ingress2Gateway 1.0 Release Announcement (Mar 2026)
- Gateway API — Migrating from Ingress (SIG-Network)
- Kubernetes Blog — Ingress NGINX CVE-2025-1974 (Mar 2025)
Ingress NGINX was, for a decade, the default answer to "how do we get traffic into this cluster." On March 19, 2026, the project shipped its final releases — controller v1.15.1 and v1.14.5 — and the repositories went read-only. No more bug fixes, no more security patches, nothing. If you are still running it, every ingress-nginx pod in your fleet is now frozen code on a hostile internet, and the Kubernetes Steering and Security Response Committees were blunt about it: "choosing to remain with Ingress NGINX after its retirement leaves you exposed to attack."
This guide is not a "Gateway API is nicer" opinion piece. It is the migration plan you owe your platform: what actually changed in the API, which implementation to pick, the annotation conversion table that will consume most of your engineering time, the CRD upgrade traps that can eat your routes, and a staged cutover that keeps revenue traffic safe. Everything here targets Gateway API v1.6.2 (the current patch line, Sept 3, 2026) on Kubernetes 1.37, with verified manifests — nothing is a sketch.
One framing note before the detail: if you already run Cilium as your CNI, the migration calculus changed this month. Cilium 1.20 (September 14, 2026) jumped from Gateway API v1.4 support to v1.6.1, adding ExternalAuth, CORS filters, ListenerSets, and GA TCPRoute/UDPRoute — with the release blog explicitly telling ingress-nginx refugees: "now is the time to let the CNI you already run take on that traffic management too." That option belongs in your evaluation alongside the usual controller candidates, and it gets a full section below.
TL;DR
- Migrate now if: you run ingress-nginx anywhere in the fleet. The project is retired; the last controller release was v1.15.1 on March 19, 2026. Since then, any new CVE in its codebase ships with no patch. The Steering Committee statement gives the risk profile plainly: 50% of cloud native environments relied on it, and it was maintained by one or two volunteers.
- Controller picks: Cilium (v1.20+) if it is already your CNI — Gateway API is a Helm flag away; NGINX Gateway Fabric 2.7.2 for the closest thing to a like-for-like NGINX replacement; Envoy Gateway (v1.9.1) when you want Envoy underneath; GKE's built-in gke-l7-* GatewayClasses if you are all-in on GCP; VPC Lattice's controller on EKS.
- Biggest migration trap: annotation conversion is where the effort lives —
configuration-snippet,proxy-body-size,rate-limit, and regex path behaviors do not translate. Budget review time for every ingress that carries them; the rest convert mechanically withingress2gateway. - Biggest operational trap: CRD channel discipline. Install the Standard channel (v1.6.1+ line), pin the version, and never install Experimental over Standard — the
safe-upgradesValidatingAdmissionPolicy shipped in v1.5 exists precisely because downgrades and channel mixes silently corrupt route visibility. And verify your controller actually implements the routes you apply: GKE's built-in controller supports HTTPRoute only — TCPRoute, UDPRoute, and TLSRoute objects are ignored. - Feature win: Gateway API v1.6 gives you L4 routing (TCPRoute/UDPRoute GA), ExternalAuth, CORS, ListenerSets, BackendTLSPolicy for upstream TLS validation — all standardized, all portable across implementations, none of which the frozen Ingress API will ever have.
What Actually Changed, and When
The migration deadline was not a surprise sprung on anyone. The timeline, verified from the primary announcements:
| Date | Event | What it means for you |
|---|---|---|
| Mar 24, 2025 | IngressNightmare (CVE-2025-1974, CVSS 9.8) disclosed — unauthenticated RCE in the ingress-nginx controller pod | The dress rehearsal. Five CVEs in one disclosure, all fixed because maintainers still existed. Post-retirement, the same class of bug ships unfixed forever. |
| Nov 12, 2025 | SIG Network and the Security Response Committee announce ingress-nginx retirement: best-effort maintenance until March 2026 | The writing on the wall, in committee-official language. |
| Jan 29, 2026 | Steering + Security Response joint statement: "We cannot overstate the severity of this situation" | Two months of runway left. Datadog research cited: ~50% of cloud native environments run ingress-nginx. |
| Mar 19, 2026 | Final releases: controller v1.15.1 / v1.14.5 / v1.13.9 + matching charts. Repositories read-only. | Frozen code. Anything discovered after this date in the controller, admission webhook, or chart is unpatchable upstream. |
| Mar 20, 2026 | ingress2gateway 1.0 released — the SIG-Network migration assistant hits stable | The conversion tool got a 1.0 the day after retirement. Use it as a starting point, not a finish line. |
| Jun 30 / Aug 3, 2026 | Gateway API v1.6.0: TCPRoute and UDPRoute graduate to Standard (v1); experimental resources move to the gateway.networking.x-k8s.io group with an X prefix |
The API now covers raw L4 — the last "we still need Service type LoadBalancer for that" argument is gone. |
| Sept 3, 2026 | Gateway API v1.6.2 patch release | The current stable line to pin your CRDs to. |
| Sept 14, 2026 | Cilium 1.20: Gateway API v1.4 → v1.6.1, ExternalAuth, CORS filters, ListenerSets, TCPRoute/UDPRoute, stable MCS API | The "let the CNI do it" option became fully credible this month. |
The strategic picture underneath the timeline: the Ingress API is frozen (it has had no new features since it went GA in v1.19 of Kubernetes), and the entire ecosystem's innovation moved to Gateway API. The v1.6 API group split — experimental resources living in gateway.networking.x-k8s.io with an X prefix (XBackend, XMesh) until they graduate — is the project's way of making that boundary machine-visible. Your cluster's routing layer is now a versioned product you track, not a set of annotations you accumulate.
Ingress vs. Gateway API: The Structural Differences That Bite
Most "Ingress vs Gateway API" comparisons stop at "Gateway API is more expressive." True and useless. The differences that bite during a real migration are structural:
| Dimension | Ingress (frozen) | Gateway API (v1.6) | Migration consequence |
|---|---|---|---|
| API shape | One object: host + path → service. Everything else is annotation. | Separate GatewayClass (infra vendor), Gateway (listener/LB instance), Route objects (per-app, per-protocol). |
N ingresses become 1 Gateway + N Routes. Cross-team ownership boundaries change: infra owns Gateways, app teams own Routes. |
| Vendor-specific behavior | ~200 ingress-nginx annotations; config lives in a ConfigMap + snippets | Standardized fields for most of it (CORS, timeouts, retries experimental, header manipulation); extension points for the rest | ingress2gateway converts standard fields mechanically; the annotation long tail is your manual work queue. |
| Protocols | HTTP/HTTPS + TLS passthrough (annotation-flavored) | HTTPRoute, GRPCRoute, TLSRoute (v1), TCPRoute (v1), UDPRoute (v1) — all Standard channel in v1.6 | Your databases, brokers, DNS, and game servers can now ride the same Gateway model instead of ad-hoc LoadBalancer services. |
| Cross-namespace references | Implicit and frequently accidental (any ingress can route to any service) | Explicit via ReferenceGrant (v1 in 1.5); cross-namespace backend refs are rejected without one |
Audit every cross-namespace route in advance. This is a security feature you must configure deliberately, and it will block migrations done blind. |
| RBAC granularity | One object type to rule them all | Infra role manages Gateways; app role manages Routes in their namespace | Finally a clean platform-team/app-team split — the core original selling point. |
Architecture: Same Cluster, New Traffic Path
The mental model shift, in one diagram — note where policy enforcement moves and where the app team's YAML ends up:
INGRESS ERA GATEWAY API ERA
─────────────────────────────── ───────────────────────────────
Internet Internet
│ │
▼ ▼
Cloud LB (static) Cloud LB (per Gateway)
│ │
▼ ▼
ingress-nginx Deployment GatewayClass (cilium / ngf / eg /
│ nginx.conf from: gke-l7 / lattice) ← infra team
│ Ingress objects + │
│ ~50 annotations/object + Gateway (listeners, TLS,
│ ConfigMap + snippets ← platform certRefs) ← infra team
▼ │
Service → Pods ▼
HTTPRoute / TCPRoute / UDPRoute
│ host+path match, filters,
│ weights ← app team
▼
Service → Pods
ownership: ONE platform-owned object ownership: infra owns Gateway;
consumed by everyone app teams own Routes;
ReferenceGrant governs the
boundary between them
Two things to internalize before writing a single manifest, because they drive the migration plan:
- The Gateway is shared infrastructure again — deliberately. One Gateway (or a few, per environment) fronts many Routes from many namespaces. You are not creating one Gateway per app unless you want N load balancers. Design your Gateway layout (per-cluster? per-env? internal + external?) before converting anything.
- Route attachment is where apps meet infra. A Route references a Gateway via
parentRefs; if the Route's namespace is not allowed by the listener'sallowedRoutes, it silently does not attach. Test attachment early — "route created but nothing listens" is the most common first-week failure.
Choosing Your Implementation: The 2026 Field
Gateway API is a specification; you still must pick a data plane. The credible options as of September 2026, with the verification steps you should run regardless of which you pick:
| Implementation | GatewayClass / controllerName | Current line | Pick it when | Watch out |
|---|---|---|---|---|
| Cilium (CNI-embedded) | cilium / io.cilium/gateway-controller |
1.20.x (chart 1.20.2), Gateway API v1.6.1 | Cilium is already your CNI; you want ingress without a new dataplane to babysit; you value network-policy integration at the edge | Requires kube-proxy replacement + L7 proxy (TPROXY); client-IP handling differs from classic controllers (XFF trusted-hops model); source-IP passthrough nuances documented thoroughly in Cilium docs |
| NGINX Gateway Fabric (F5) | nginx / gateway.nginx.org/nginx-gateway-controller |
2.7.2 | You want NGINX semantics to survive the migration — the natural ingress-nginx heir; TCP/UDP support, rate limiting, session persistence shipped in 2.4.0+ | It is a rewrite, not a config translation: snippets and ConfigMap behaviors do not carry over; budget real review time |
| Envoy Gateway | eg / gateway.envoyproxy.io/gatewayclass-controller |
v1.9.1 | You standardized on Envoy for mesh/API-gateway reasons; you want the richest extension surface (BackendTrafficPolicy etc.) with Envoy as a single runtime | More policy surface to learn; check conformance-profile coverage against your must-have list before committing |
| GKE Gateway controller (Google-hosted) | gke-l7-global-external-managed etc. / networking.gke.io/gateway |
GKE 1.35+ installs Gateway API 1.5.0 CRDs | GCP-only fleet; you want Google-managed, out-of-band controller reconciliation into Cloud Load Balancing | HTTPRoute only — TCPRoute/UDPRoute/TLSRoute unsupported; Standard channel only; requires VPC-native clusters, HttpLoadBalancing add-on; proxy-only subnet for internal LBs |
| AWS Gateway API Controller (VPC Lattice) | amazon-vpc-lattice / gateway.networking.k8s.aws/gateway-api-controller |
Active project (aws-application-networking-k8s) | EKS-heavy shop that wants Lattice service networks as the routing layer | Lattice is a separate AWS service with its own pricing; controller maturity trails the NGINX/Envoy/Cilium options — pilot before fleet-wide use |
Decision heuristic: if Cilium is your CNI, start there — gatewayAPI.enabled=true is one Helm flag on an existing chart, and the per-node Envoy you would otherwise deploy as a new controller is already running. If NGINX config fluency is concentrated in your platform team's muscle memory, NGINX Gateway Fabric minimizes retraining. If neither applies, run a two-week bake-off between Envoy Gateway and your incumbent alternative — with your own top-20 ingresses as the test corpus, not vendor demos.
Step 1 — Install the CRDs (Correctly, Once)
Gateway API CRDs are not shipped by Kubernetes — you (or your cloud provider) own installing and upgrading them. This is the single most common source of Gateway API outages, so it gets the paranoid treatment:
# Pin Gateway API CRDs to the v1.6.2 standard channel (current stable line, Sept 3, 2026).
# --server-side is required: the experimental channel CRDs exceed the client-side apply limit.
kubectl apply --server-side -f \
https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.2/standard-install.yaml
# Verify what landed: v1 CRDs for the routes you care about
kubectl get crd | grep gateway.networking.k8s.io
# gateways, gatewayclasses, httproutes, grpcroutes, tlsroutes,
# tcproutes, udproutes, backendtlspolicies, referencegrants, listenersets
# Never do this:
# kubectl apply -f .../experimental-install.yaml ← over a Standard install
# The safe-upgrades ValidatingAdmissionPolicy (shipped in v1.5) exists to block
# channel mixes and pre-1.5 downgrades — it fails closed for a reason.
Why the paranoia: the v1.5→v1.6 line moved graduated resources to v1 and deprecated their v1alpha2 forms. If you install a Standard channel that lacks a version your stored objects use, the API server will still serve your objects in the versions it knows — but your controller may see nothing, and tools like ingress2gateway will generate for the channel they find. Version drift here is a silent data-visibility outage, not a loud failure. The Cilium upgrade guide's Gateway API v1.6.1 + TLSRoute warning is representative of the whole class: read the CRD notes before every bump.
Step 2 — Deploy the Controller (Reference Manifests)
Pick exactly one controllerName per GatewayClass — that is the spec's contract. Reference installs for the three leading self-managed options:
# ── Option A: Cilium (if it's your CNI) ─────────────────────────────
# Gateway API CRDs (v1.6.1+, Standard channel) must be installed FIRST.
helm upgrade cilium cilium/cilium --version 1.20.2 \
--namespace kube-system \
--reuse-values \
--set kubeProxyReplacement=true \
--set gatewayAPI.enabled=true
kubectl -n kube-system rollout restart deployment/cilium-operator
kubectl -n kube-system rollout restart ds/cilium
# Chart creates GatewayClass "cilium" (controllerName: io.cilium/gateway-controller)
# ── Option B: NGINX Gateway Fabric 2.7.2 ────────────────────────────
helm install ngf oci://ghcr.io/nginx/charts/nginx-gateway-fabric \
--create-namespace -n nginx-gateway
kubectl wait --timeout=5m -n nginx-gateway deployment/ngf-nginx-gateway-fabric \
--for=condition=Available
# Deploys GatewayClass "nginx" (controllerName: gateway.nginx.org/nginx-gateway-controller)
# ── Option C: Envoy Gateway v1.9.1 ──────────────────────────────────
helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.9.1 \
-n envoy-gateway-system --create-namespace
kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-gateway \
--for=condition=Available
# Quickstart GatewayClass is "eg" (controllerName: gateway.envoyproxy.io/gatewayclass-controller)
# ── Option D: GKE built-in ──────────────────────────────────────────
gcloud container clusters update CLUSTER_NAME \
--location=CLUSTER_LOCATION \
--gateway-api=standard
# Up to 45 min to reconcile; then gke-l7-* GatewayClasses appear automatically.
Sanity check after install, before converting anything:
kubectl get gatewayclass
# NAME CONTROLLER ACCEPTED AGE
# cilium io.cilium/gateway-controller True 2m
# Check the "supportedFeatures" field if you need to know exactly what your
# controller implements (shipped in v1.4, Standard) — it is the spec's
# machine-readable conformance claim, and your migration checklist should
# reconcile against it, not against vendor marketing.
Step 3 — Convert the Ingresses (The Actual Work)
ingress2gateway (SIG-Network, v1.2.0 current, 1.0 shipped March 20, 2026 — the day after retirement) is the standard converter. It emits warnings for everything it cannot fully translate; those warnings are your work queue:
# Install
brew install ingress2gateway
# or: go install github.com/kubernetes-sigs/ingress2gateway@v1.2.0
# Inventory first: what are we actually migrating?
kubectl get ingress -A -o custom-columns='NS:.metadata.namespace,NAME:.metadata.name,CLASS:.spec.ingressClassName,ANNOTATIONS:.metadata.annotations' | head -30
# Convert the whole cluster, ingress-nginx flavor
ingress2gateway print --providers=ingress-nginx --all-namespaces > gwapi-all.yaml 2> i2g-warnings.log
# Read the warnings. This is the migration's critical path:
grep -c "warning" i2g-warnings.log
cat i2g-warnings.log
# Expect hits for: configuration-snippet, proxy-body-size, rate-limit,
# proxy-read/send-timeout, regex path behaviors, URL normalization
What the tool does mechanically, verified against its documented conversion table:
| Ingress field / annotation | Gateway API destination | Fidelity |
|---|---|---|
spec.ingressClassName |
Gateway.spec.gatewayClassName |
Direct |
rules[].host |
Listener hostname + Route hostnames |
Direct (distinct hosts → distinct listeners/routes) |
tls[].hosts + secretName |
HTTPS listener, tls.mode: Terminate, certificateRefs |
Direct |
paths[] with pathType: Prefix / Exact |
Route matches[].path.type PathPrefix / Exact |
Direct |
nginx.ingress.kubernetes.io/rewrite-target |
URLRewrite filter |
Direct |
ssl-redirect / force-ssl-redirect |
RequestRedirect filter (scheme https) |
Direct (verify the port-80 listener and redirect route get created) |
enable-cors / cors-* |
CORS filter (Standard since v1.5, GEP-1767) |
Direct for common fields |
whitelist-source-range |
Route matches on source CIDR — or keep it at the cloud LB / NetworkPolicy layer |
Partial — implementation-specific policies fill the gap |
proxy-body-size |
None in core Gateway API — implementation-specific policy (BackendTrafficPolicy etc.) | Manual — check your implementation's body-size default, which now applies to your traffic |
rate-limit annotation |
Implementation-specific policy (e.g. NGF RateLimitPolicy, EG BackendTrafficPolicy) | Manual |
configuration-snippet / server-snippet |
No equivalent — deliberately. The snippet escape hatch does not exist in Gateway API | Manual, case-by-case — the single biggest migration cost item. Each snippet must be re-expressed as filters/policies or moved into the app |
Regex paths (nginx implementationSpecific) |
Core HTTPRoute RegularExpression match type |
Partial — ingress-nginx regexes were case-insensitive prefix matches by default; ingress2gateway flags the (?i) artifacts, and most teams want to normalize to case-sensitive here |
proxy-read-timeout / proxy-send-timeout |
timeouts on the route rule (BackendTrafficPolicy-adjacent; exact surface varies) |
Partial — tool makes a best-effort 10s default; review per-route |
The honest sizing answer: a plain host+path+TLS ingress converts in minutes. Every ingress carrying configuration-snippet, custom rate limiting, or subtle timeout behavior needs an engineer. Budget accordingly — a 500-ingress fleet with 40 snippet users is a quarter-long program with the snippets as the critical path, not the YAML conversion.
Step 4 — The Cutover (Zero-Downtime, Boring On Purpose)
The proven pattern: run both stacks in parallel, switch at DNS/load-balancer level, keep rollback one step away, delete the old world only after burn-in. Boring is the goal:
# 0. Prereqs (done above): CRDs pinned, controller up, GatewayClass ACCEPTED
# 1. Create the shared Gateway (infra team). One Gateway fronts many Routes.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: edge
namespace: edge-system
spec:
gatewayClassName: cilium # or nginx / eg / gke-l7-global-external-managed
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All
- name: https
protocol: HTTPS
port: 443
hostname: "*.example.com"
tls:
mode: Terminate
certificateRefs:
- name: example-wildcard # cert-manager can target Gateways directly
allowedRoutes:
namespaces:
from: All
# kubectl apply -f gateway.yaml
# Wait for PROGRAMMED status — the LB is real only when this is True:
kubectl get gateway -n edge-system edge -o jsonpath='{.status.conditions[?(@.type=="Programmed")].status}'
# 2. Attach your first converted Route — one app, one namespace
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop-frontend
namespace: shop
spec:
parentRefs:
- name: edge
namespace: edge-system
hostnames: ["shop.example.com"]
rules:
- matches:
- path: { type: PathPrefix, value: / }
filters:
- type: RequestRedirect # http→https, replaces ssl-redirect
requestRedirect: { scheme: https, port: 443 }
- matches:
- path: { type: PathPrefix, value: / }
backendRefs:
- name: shop-frontend
port: 80
# NOTE: the redirect rule and the serve rule are separate rules on the SAME route
# (http listener hosts the redirect; https listener serves). Review ingress2gateway's
# output for the split it generates — do not hand-roll the http listener away.
# 3. Smoke test through the new path, Host header first:
GW_IP=$(kubectl get gateway -n edge-system edge -o jsonpath='{.status.addresses[0].value}')
curl -sv -H "Host: shop.example.com" "http://$GW_IP/" | head -5
# Expect: 301 to https. Then against 443 with --resolve:
curl -sv --resolve shop.example.com:443:$GW_IP https://shop.example.com/ -o /dev/null -w '%{http_code}\n'
# 4. Cutover per-hostname at DNS (or per-backend at the cloud LB):
# TTL dropped to 60s a day earlier; watch 5xx/latency dashboards per host.
# 5. Burn-in (1-2 weeks typical), then delete Ingress resources and uninstall
# the retired controller. Keep the Ingress objects in git history — they are
# your audit trail for what the annotations meant.
Cross-namespace wiring: the Route above lives in shop and attaches to a Gateway in edge-system. That works because the listener's allowedRoutes permits all namespaces — but if the Route's backend Service lives in a third namespace, or your policy tightens allowedRoutes, you need a ReferenceGrant (v1) making the reference explicit. Same when Gateways reference certificates across namespaces. Audit first, migrate second — this is the #1 cause of "route accepted but 500s everywhere" tickets in the first week.
Post-Migration: The Gateway API v1.6 Features You Should Actually Use
Migrating off retired software is necessary. These are the reasons the new world is worth the effort beyond survival:
Standardized filters that used to be annotations
- TCPRoute / UDPRoute (GA, v1): the database/broker/DNS/game-server ports that used to need raw
LoadBalancerservices now attach to the same Gateway as HTTP routes, each on its own listener, sharing one LB address. - ExternalAuth (GEP-1494): per-route authentication outsourced to an authorizer service (oauth2-proxy, Authelia, OPA, your IDP) before traffic reaches the app. Cilium 1.20 implemented it; NGF and Envoy Gateway have their own policy flavors. This replaces a decade of
auth-url/auth-snippetannotation hacks. - CORS filter (Standard since v1.5): no more
enable-corsannotation drift. Schema follows upstream CORS semantics —allowCredentialsis strictly boolean since v1.4's breaking change; review legacy annotations for stringy values. - BackendTLSPolicy (v1): gateway→backend TLS with certificate validation — the answer to "is the TLS between my edge and my pods actually verified?" that ingress-nginx answered with a shrug.
- ListenerSets (Standard in v1.5): additional listeners on a Gateway without redefining the main spec — useful for multi-tenant edges.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: demo-gateway
namespace: default
spec:
gatewayClassName: cilium
listeners:
- name: web
protocol: HTTP
port: 80
allowedRoutes:
kinds:
- kind: HTTPRoute
- name: mongo
protocol: TCP
port: 27017
allowedRoutes:
kinds:
- kind: TCPRoute
- name: dns
protocol: UDP
port: 53
allowedRoutes:
kinds:
- kind: UDPRoute
---
apiVersion: gateway.networking.k8s.io/v1
kind: TCPRoute
metadata:
name: mongo
namespace: default
spec:
parentRefs:
- name: demo-gateway
sectionName: mongo
rules:
- backendRefs:
- name: pacman-mongo
port: 27017
---
apiVersion: gateway.networking.k8s.io/v1
kind: UDPRoute
metadata:
name: dns
namespace: default
spec:
parentRefs:
- name: demo-gateway
sectionName: dns
rules:
- backendRefs:
- name: coredns
port: 53
Operational Costs Nobody Puts in the Migration Deck
The migration itself is bounded. The ongoing costs are where the honest comparison lives:
| Cost area | What to expect |
|---|---|
| CRD lifecycle ownership | You now track Gateway API releases (v1.5 → v1.6 → …) as a product. Pin versions, read release notes before every bump, keep the Standard channel discipline. Managed providers (GKE) do this for you — that is part of what you are buying. |
| Annotation archaeology | The one-time cost everyone underestimates: configuration-snippet users need case-by-case redesign. Count yours before promising a timeline: kubectl get ingress -A -o json | jq '[.items[] | select(.metadata.annotations["nginx.ingress.kubernetes.io/configuration-snippet"] != null)] | length'. |
| Learning curve & on-call | New failure modes to learn: routes not attaching (allowedRoutes/ReferenceGrant), silent policy gaps, per-implementation conformance deltas. Expect a month of on-call ramp; run both stacks' dashboards side by side during burn-in. |
| Per-implementation surprises | Cilium: TPROXY prerequisites and client-IP/XFF semantics differ from ingress-nginx — read the "How Cilium Ingress and Gateway API differ" doc before assuming parity. NGF: it's a rewrite, not a lift. GKE: HTTPRoute-only is a hard ceiling if you have TCP/UDP/TLS passthrough needs. Envoy Gateway: policy surface breadth is a feature and a review burden. |
| Dual-stack running costs | During migration you pay for two data planes. Fine. The trap is staying there: dual-running beyond a quarter turns into an unmaintainable snowflake edge. Set a deletion deadline for the old stack when you start. |
Who Should Skip This Migration (Honestly)
Nobody who runs ingress-nginx gets to skip entirely — retirement removed that option. But the pace and target differ:
- You are on a managed alternative already (GKE Ingress/Gateway, AWS Load Balancer Controller, AKS App Routing): you were never on the retired project; ignore this guide except as architecture reference.
- Your ingresses are a handful of plain host+path rules: this is a one-sprint migration. Do it now, use ingress2gateway, review the warnings, cutover, done.
- You are mid-platform-build, standardizing on Cilium anyway: skip the separate-controller debate; enable
gatewayAPI.enabled=trueas part of the Cilium rollout. One dataplane, one upgrade cadence. - Fleet scale, heavy annotation/snippet heritage: you cannot skip, but you can sequence: migrate the plain ingresses first (fast wins, tooling handles them), triage snippet users into a parallel workstream with per-app owners, and negotiate the calendar with security — every month past March 2026 is a month of unpatchable edge code.
The Bottom Line
Ingress NGINX retired on March 19, 2026 and is not coming back. Gateway API v1.6 is a genuinely better API — role-oriented, protocol-complete to L4, portable across implementations — but the migration is real engineering, not a find-and-replace. The work splits cleanly: CRD discipline and controller choice are platform-team decisions with a right answer you can verify; annotation conversion is per-app archaeology where the snippets live; the cutover is boring if you let it be. Do the inventory this week. Every day on retired edge code is unpriced risk, and the industry-wide migration wave means migration expertise will be scarce exactly when you need it.
References & Further Reading
- Ingress NGINX: Statement from the Kubernetes Steering and Security Response Committees — the retirement edict, with the 50%-of-environments Datadog figure.
- Ingress NGINX Retirement: What You Need to Know — SIG Network's detailed FAQ: what keeps working, what stops, and what "no more releases of any kind" means.
- Gateway API v1.6: TCPRoute and UDPRoute Graduate to Standard — the L4 graduation and the experimental API-group split, from the release blog.
- Gateway API v1.6.2 Release Notes — the CRD line this guide pins to.
- Cilium 1.20 Release Blog — Gateway API v1.4→v1.6 jump, ExternalAuth, TCPRoute/UDPRoute support, and the ingress-nginx migration pitch.
- Announcing Ingress2Gateway 1.0 — the migration assistant's own guidance on what translates, what warns, and what needs manual work.
- Migrating from Ingress — Gateway API docs — the canonical feature-mapping walkthrough.
- Ingress-nginx CVE-2025-1974 — the 2025 IngressNightmare disclosure post; why "retired means unpatchable" is not theoretical.