Ingress-NGINX Retirement: A Step-by-Step Migration Playbook to Kubernetes Gateway API

Ingress-NGINX Retirement: A Step-by-Step Migration Playbook to Kubernetes Gateway API

Ingress-NGINX Retirement: A Step-by-Step Migration Playbook to Kubernetes Gateway API

If your cluster still runs the community ingress controller, you are now running software that nobody patches. The ingress-nginx retirement took effect in March 2026: the repository was archived on 24 March 2026, and the Kubernetes Steering and Security Response Committees were blunt that staying put “leaves you and your users vulnerable to attack.” Your existing pods keep serving traffic, which is exactly the trap. Nothing breaks on the day, so nothing forces the migration until a vulnerability lands with no fix behind it.

The replacement is not a drop-in. Gateway API splits one overloaded object into role-based resources, and the annotations you accumulated over years of tuning have no one-to-one home. Worse, ingress-nginx has quirks your applications may depend on without anyone knowing it.

This playbook gives you a defensible path. You will leave with an inventory method, a translation workflow built on ingress2gateway, a mapping of the common annotations, runnable YAML, and a cutover plan with a rollback that takes one DNS change.

What this covers: what the retirement means, how Gateway API differs, how to choose a controller, a phased migration with weighted DNS, the behavioral traps, and a pre-flight checklist.

Context and Background

On 11 November 2025 Kubernetes SIG Network and the Security Response Committee announced the retirement. The wording is precise: best-effort maintenance would continue until March 2026, after which there would be “no further releases, no bugfixes, and no updates to resolve any security vulnerabilities that may be discovered.” Existing deployments continue to function, and installation artifacts remain available. The project’s GitHub page confirms the archive date and tells new users not to deploy it.

The follow-up statement on 29 January 2026 added the human context. It cited internal Datadog research that about 50 percent of cloud native environments relied on the controller, and noted it had been maintained for years by one or two people in their free time. It also said the flexibility that made ingress-nginx popular, such as snippets and arbitrary configuration injection, had become a burden that could not be resolved even with new staffing. That is a design verdict, not just a staffing one.

Two clarifications prevent expensive mistakes. First, “Ingress-NGINX” (the Kubernetes community project) and “NGINX Ingress Controller” (F5’s product) are different projects that share a data plane technology; the retirement concerns only the first. Second, the Ingress API itself is not removed. It is frozen, and the Kubernetes team’s recommendation is Gateway API, with other Ingress controllers listed in the Kubernetes docs as a fallback if you must stay on Ingress.

Why does this hurt more than a typical deprecation? Because ingress-nginx grew by absorbing features through annotations. The Ingress resource only models hosts, paths, and a backend. Everything else, from rewrites to authentication to timeouts to custom NGINX configuration, lived in nginx.ingress.kubernetes.io/* annotations, a global ConfigMap, and snippets. A migration therefore means reverse-engineering behavior, not just converting YAML.

If you are still deciding between staying on Ingress with a different controller and moving to Gateway API, read our comparison of Kubernetes Gateway API versus Ingress in 2026 first. The short version used in this post: choose Gateway API unless a hard constraint stops you, because every new feature in the ecosystem is landing there. The official retirement notice is on the Kubernetes blog.

The same discipline applies to any infrastructure interface that changes under you. Our notes on migrating the AAS metamodel to the IDTA 26-01 release follow a similar pattern of inventory, translate, verify side by side, then cut over.

From Ingress to Gateway API: The Reference Architecture

Gateway API replaces the single Ingress object with three roles. A platform team owns the GatewayClass and Gateway (the listener, the address, the TLS certificates). Application teams own HTTPRoute objects that attach to a Gateway through parentRefs. Cross-namespace access is explicit, governed by the Gateway’s allowedRoutes and by ReferenceGrant, rather than implied by whoever can create an Ingress.

Ingress-NGINX retirement architecture comparing annotation-driven Ingress with role-based Gateway API resources

Figure 1: One annotated Ingress object and a global ConfigMap on the left, role-separated Gateway API resources and policy CRDs on the right.

The left half of the diagram shows why the old model was fragile. Behavior came from three places that nothing validated together: annotations on each Ingress, a controller-wide ConfigMap, and raw NGINX snippets. The right half moves routing semantics into typed, versioned fields on HTTPRoute, and pushes controller-specific concerns such as authentication and rate limiting into policy resources that attach to a Gateway or route.

The direct answer: what replaces what

An Ingress becomes a Gateway plus one or more HTTPRoutes. The ingressClassName becomes a gatewayClassName. Path and host rules become HTTPRoute matches and hostnames. Annotations become either standard route filters, standard route fields such as timeouts, or implementation-specific policy CRDs. The things with no equivalent, chiefly configuration snippets, must be redesigned rather than ported.

Standard channel versus experimental channel

Gateway API ships two release channels. The Standard channel holds GA-quality resources and fields. The Experimental channel holds features that can still change. Install the Standard channel CRDs unless you have a specific reason not to, and treat any feature that requires Experimental as a risk item in your plan.

Recent releases moved a lot of migration-relevant functionality into Standard. Gateway API v1.5, released on 27 February 2026, graduated six features: ListenerSet, TLSRoute, the HTTPRoute CORS filter, client certificate validation, certificate selection for Gateway TLS origination, and ReferenceGrant. Gateway API v1.6.0, released on 30 June 2026, promoted TCPRoute and UDPRoute to Standard at the v1 API version and moved experimental resources into a separate gateway.networking.x-k8s.io API group so they cannot be mistaken for stable ones.

Two upgrade notes from the v1.5 release notes matter in practice. If you installed Standard v1.5 over an earlier Experimental install, existing Experimental TLSRoutes stored as v1alpha2 or v1alpha3 stop being usable until migrated to v1. And the project adopted a release-train model, so expect predictable cadence, not feature-driven delays. Pin the CRD version in your GitOps repository and upgrade it deliberately.

What ListenerSet changes for multi-tenant clusters

Before ListenerSet, every listener lived on the Gateway object itself. That forced platform and application teams to coordinate edits on the same resource, and it capped a Gateway at 64 listeners. ListenerSet lets an application team define its own HTTPS listeners in its own namespace and attach them to a shared Gateway, as long as the Gateway’s allowedListeners permits it.

For a migration this is the cleanest replacement for the pattern where each team had its own Ingress with its own tls: block. You keep one load balancer and one Gateway, and each team owns its hostname and certificate reference. Note that the Gateway must still declare at least one valid listener of its own.

Why the data plane choice is separate from the API choice

With ingress-nginx, the API and the proxy were one product. With Gateway API they are decoupled, and that is the point. The same HTTPRoute can run on Envoy-based, NGINX-based, or eBPF-assisted data planes, subject to each implementation’s conformance. The practical consequence: you can change the controller later without rewriting your routes, provided you stayed inside the Standard channel and kept policy CRDs thin and isolated.

Choosing a Gateway API Controller

There is no single correct pick, but there is a correct process: choose on the features your inventory proves you need, then confirm the controller’s conformance status on the official Gateway API implementations page. That page lists, among others, Envoy Gateway, Cilium, Istio, Kgateway, Kong Operator, NGINX Gateway Fabric, and Traefik Proxy, each with its own maturity and conformance level. Read the entry for the exact release you plan to run, because support for individual route types and filters varies.

Option Data plane Why teams pick it Watch out for
Envoy Gateway Envoy Gateway API first; rich policy CRDs (BackendTrafficPolicy, ClientTrafficPolicy, SecurityPolicy) for rate limit, timeouts, external auth Different config model from NGINX; regex semantics are full-match
NGINX Gateway Fabric NGINX Same proxy family, so operators keep NGINX mental models; supports snippet-style escape hatches Escape hatches reintroduce the risk you are leaving; verify feature coverage per release
Istio (gateway only) Envoy Reuse if the mesh is already in place; can run purely as a Gateway API controller Control plane weight if you do not need the mesh
Cilium Envoy via eBPF-based CNI Fewer moving parts when Cilium is already your CNI Couples ingress lifecycle to CNI upgrades
Traefik Proxy Traefik Familiar to existing Traefik shops, supports Gateway API Check the supported route kinds and filters for your version
Kgateway / agentgateway Envoy / purpose-built Supported emitters in ingress2gateway for generating extension config Newer projects; confirm production references

Decision matrix. Treat the “why” column as selection heuristics from the project documentation and community practice, not benchmarks.

Three rules keep the choice reversible. First, prefer a controller that passes the Gateway API conformance suite for the Standard features you use. Second, isolate implementation-specific policy objects in their own directory in Git so a future swap has a bounded blast radius. Third, do not pick an escape hatch such as a snippets filter as your default migration path; it is the same pattern that sank ingress-nginx.

If you already operate a service mesh or a CNI with built-in Gateway API support, start there, because every additional control plane is another thing to patch. If you have none, a dedicated Gateway API controller with a Standard-channel-first design is the lowest-friction choice. Whichever you choose, run it in a dev cluster first and run the same behavior tests against it that you will run in production.

Deeper Analysis: The Migration Walk-through

The migration has eight steps, shown in Figure 2. The sequence is deliberate: the expensive discoveries happen in steps 1 to 5, where nothing touches production traffic.

Ingress-NGINX retirement migration workflow from inventory through ingress2gateway to weighted DNS cutover

Figure 2: Eight-step migration workflow. Steps 1 to 5 are risk discovery; steps 6 to 8 move traffic and remove the old controller.

Steps 6 to 8 are mechanical once 1 to 5 are done. The value of the eight-step shape is that the cutover itself becomes boring.

Step 1: Inventory what you actually use

First confirm you depend on the controller. The Steering Committee statement gives the check, which needs cluster-admin permissions:

kubectl get pods --all-namespaces --selector app.kubernetes.io/name=ingress-nginx

Then list every distinct annotation in use and how often. This tells you which mappings matter and which snippets will block you.

kubectl get ingress -A -o json \
  | jq -r '.items[] | .metadata.annotations // {} | keys[]' \
  | grep '^nginx.ingress.kubernetes.io/' \
  | sort | uniq -c | sort -rn

Also dump the controller ConfigMap, because global settings such as proxy-body-size, ssl-protocols, and use-forwarded-headers change behavior for every route and have no annotation to find:

kubectl -n ingress-nginx get configmap ingress-nginx-controller -o yaml

Flag every configuration-snippet, server-snippet, and auth-snippet. Those are your hard blockers, and you will triage them individually in step 4. The default namespace and ConfigMap name above are the common Helm defaults; adjust if yours differ.

Step 2: Install the CRDs and a controller

Install the Gateway API Standard channel CRDs at a pinned version, then your chosen controller per its documentation. Pin the version in Git. The exact install command differs by release, so use the release assets from the Gateway API repository instead of copying a URL from a blog post.

Create one Gateway in a shared namespace, matching the listeners your hosts need. Keep it on a new load balancer. You are going to run both stacks side by side, so the new Gateway must not take over the old controller’s address.

Step 3: Run ingress2gateway

SIG Network released ingress2gateway 1.0 on 20 March 2026. Before 1.0 it handled only three ingress-nginx annotations; 1.0 covers more than 30 common ones, such as CORS, backend TLS, regex matching, and path rewrite. Each supported annotation is backed by integration tests that start a real ingress-nginx controller and multiple Gateway API controllers, translate the Ingress, and compare runtime behavior such as routing, redirects, and rewrites. That is a stronger guarantee than checking YAML shape.

Install it with Go or Homebrew, then translate from files or directly from the cluster:

go install github.com/kubernetes-sigs/ingress2gateway@v1.0.0
# or: brew install ingress2gateway

# from manifests
ingress2gateway print --input-file my-manifest.yaml --providers=ingress-nginx > gwapi.yaml

# one namespace
ingress2gateway print --namespace my-api --providers=ingress-nginx > gwapi.yaml

# whole cluster
ingress2gateway print --providers=ingress-nginx --all-namespaces > gwapi.yaml

The tool also accepts an --emitter flag with agentgateway, envoy-gateway, or kgateway to generate implementation-specific extension resources for things standard Gateway API cannot express, such as body size. Treat ingress2gateway as an assistant, not a converter. It prints warnings for everything it could not translate exactly, and the warnings are the real output.

Step 4: Read the warnings, then fix the output

The official example takes an Ingress with proxy-body-size, use-regex, proxy-send-timeout, proxy-read-timeout, enable-cors, and a configuration-snippet, and shows what the tool does with each:

  • The configuration-snippet is not translated. You must find an implementation-specific equivalent or drop the behavior.
  • The regex path /users/(\d+) becomes (?i)/users/(\d+).*, because ingress-nginx regex is a case-insensitive prefix match. The tool’s own advice is that most teams will want an exact, case-sensitive match, which means deleting the leading (?i) and trailing .*.
  • The two timeout annotations of 1 second each became a 10 second request timeout, a best-effort translation. If the real requirement is 3 seconds, edit it to 3 seconds.
  • proxy-body-size has no Gateway API equivalent and was not translated; many implementations have sane defaults, and the emitters above can output extension config.
  • A port 80 listener and an HTTP-to-HTTPS redirect were added to mimic ingress-nginx defaults. Delete them if you do not want to serve plain HTTP.
  • A warning about URL normalization says Gateway API cannot configure it. Behavior differs by implementation and must be tested.

After fixing, the route in that example looks like this (headers trimmed with ... in the source; the CORS block is shortened here as well):

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-ingress-my-host-example-com
  namespace: my-ns
spec:
  hostnames:
  - my-host.example.com
  parentRefs:
  - name: nginx
    port: 443
  rules:
  - name: rule-0
    matches:
    - path:
        type: RegularExpression
        value: /users/(\d+)
    backendRefs:
    - name: website-service
      port: 80
    filters:
    - type: CORS
      cors:
        allowOrigins: ['*']
        allowMethods: [GET, PUT, POST, DELETE, PATCH, OPTIONS]
        allowCredentials: true
        maxAge: 1728000
    timeouts:
      request: 3s

This is adapted from the Kubernetes ingress2gateway 1.0 announcement; the full header list is omitted for brevity. Notice what the fix pass accomplished: it made implicit ingress-nginx behavior explicit and let a human decide whether to keep it. See the ingress2gateway 1.0 announcement for the complete output.

Mapping the Annotations You Actually Use

Most clusters lean on a small set of annotations. Figure 3 sorts them by whether standard Gateway API can express them or whether you need an implementation policy.

Ingress-NGINX retirement annotation mapping decision tree to HTTPRoute filters and policy CRDs

Figure 3: Where common ingress-nginx annotations land. Standard filters and fields on the left, implementation policies and redesigns for the rest.

ingress-nginx annotation Gateway API home Standard?
rewrite-target URLRewrite filter Yes
ssl-redirect / force-ssl-redirect RequestRedirect filter with scheme https Yes
enable-cors and CORS settings CORS filter on HTTPRoute Yes, Standard since v1.5
canary, canary-weight Weighted backendRefs Yes
canary-by-header Separate rule with a header match Yes
proxy-read-timeout, proxy-send-timeout HTTPRoute timeouts Yes, coarser
use-regex RegularExpression path match Yes, semantics differ
proxy-body-size Implementation policy or default No
auth-url, auth-signin Implementation external-auth policy No
limit-rps, limit-connections Implementation rate-limit policy No
configuration-snippet and friends None; redesign No

The official announcement names CORS, backend TLS, regex matching, and path rewrite among the 30-plus annotations ingress2gateway 1.0 handles; the remaining rows are my own read of the Standard channel, so run the tool against your manifests to see what it covers. Check your exact controller for policy names, because that is where implementations diverge.

Rewrite, redirect, and canary: the standard cases

A rewrite and a redirect are both filters. Here is a route that rewrites a prefix and forces HTTPS in the same hostname, expressed as two routes bound to different listeners:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api-rewrite
  namespace: shop
spec:
  parentRefs:
  - name: public-gw
    namespace: infra
    sectionName: https
  hostnames: ["api.example.com"]
  rules:
  - matches:
    - path: { type: PathPrefix, value: /v1/orders }
    filters:
    - type: URLRewrite
      urlRewrite:
        path:
          type: ReplacePrefixMatch
          replacePrefixMatch: /orders
    backendRefs:
    - name: orders
      port: 8080
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api-http-redirect
  namespace: shop
spec:
  parentRefs:
  - name: public-gw
    namespace: infra
    sectionName: http
  hostnames: ["api.example.com"]
  rules:
  - filters:
    - type: RequestRedirect
      requestRedirect:
        scheme: https
        statusCode: 301

A canary is the cleanest win in the whole migration. Weighted backendRefs replace three annotations and a second Ingress object:

rules:
- matches:
  - path: { type: PathPrefix, value: / }
  backendRefs:
  - name: web-stable
    port: 80
    weight: 95
  - name: web-canary
    port: 80
    weight: 5

Weights are relative proportions, so 95 and 5 give a 5 percent canary. A header-based canary becomes a separate rule that matches a header and sends all matching requests to the canary service. This is also the right pattern for your own pre-cutover testing: send a test header to the new stack while normal traffic stays on the old one.

Authentication and rate limiting: policy territory

Standard Gateway API has no filter for external authentication or rate limiting. Each implementation exposes these through its own policy resources. Envoy Gateway, for example, documents BackendTrafficPolicy (which includes rate limiting), SecurityPolicy (which includes external authorization), and ClientTrafficPolicy in its API reference.

An illustrative local rate limit for Envoy Gateway looks roughly like the following. Field names and API versions evolve, so verify against the documentation for the release you install before applying it:

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: orders-ratelimit
  namespace: shop
spec:
  targetRefs:
  - group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: api-rewrite
  rateLimit:
    type: Local
    local:
      rules:
      - limit:
          requests: 100
          unit: Second

Two caveats apply. A per-instance local limit is not the same as ingress-nginx’s per-replica limits scaled by your replica count, so recompute the numbers rather than copying them. And any auth-url flow must be re-tested end to end, including header propagation, failure modes when the auth service is down, and whether the new controller fails open or closed.

The things with no equivalent

Configuration snippets let teams inject raw NGINX directives, and that freedom is exactly why they cannot be translated. Triage each snippet into one of four buckets: it sets a header (use a RequestHeaderModifier or ResponseHeaderModifier filter), it implements a redirect or rewrite (use the filters above), it implements auth or limits (use a policy), or it is a one-off hack nobody remembers (delete it and watch the tests). Only the last bucket should surprise you.

Five Behaviors That Break Silent Migrations

A syntactically perfect translation can still cause an outage because ingress-nginx has defaults that Gateway API deliberately does not copy. The Kubernetes team documented five in “Before You Migrate” (27 February 2026). Every one of them produces a 404 or a changed redirect in production, and none produces an error in the translator.

First, regex matches are prefix-based and case-insensitive. A pattern like /[A-Z]{3} in ingress-nginx matches /uuid, while a full-match, case-sensitive gateway returns 404. Envoy-based implementations such as Istio, Envoy Gateway, and Kgateway do full case-sensitive matching. To preserve the old behavior you write (?i)/[a-z]{3}.*; to improve on it, you fix the pattern.

Second, use-regex applies to every path of that host across all ingress-nginx Ingress objects, not just the one carrying the annotation. A typo’d Exact path elsewhere can be silently rescued by a regex-enabled sibling. Gateway API will not rescue it, so the typo now returns 404.

Third, rewrite-target implies regex for the host. Adding that annotation to one Ingress quietly turns on regex matching, with all the side effects above, for every path under that hostname. Moving to the URLRewrite filter removes the side effect, which is correct but changes behavior for the other paths.

Fourth, a request missing a trailing slash is redirected with a 301 to the slash version when the Ingress path ends in /. Conformant Gateway API implementations do not create redirects silently, so you must add an explicit RequestRedirect rule if clients depend on it.

Fifth, ingress-nginx normalizes URLs per RFC 3986 section 6.2 before matching: it collapses . and .. segments and duplicate slashes. Istio, Envoy Gateway, and Kgateway normalize dot segments by default, but the behavior is implementation-specific and not configurable through standard Gateway API. Test hostile paths such as /ip/abc/../../uuid and ////uuid against the new stack.

A traffic-replay test beats a code review

Convert those five behaviors into a regression suite before cutover. Capture a day of access logs from the old controller, extract distinct method, host, and path combinations, and replay them against both stacks, comparing status code, redirect Location, and selected upstream. Differences are your migration bugs. Add your own odd paths on top, for example mixed-case, trailing-slash, and dot-segment variants.

# replay a path list against both stacks and diff status codes
while read -r host path; do
  old=$(curl -s -o /dev/null -w '%{http_code}' -H "Host: $host" "http://$OLD_IP$path")
  new=$(curl -s -o /dev/null -w '%{http_code}' -H "Host: $host" "http://$NEW_IP$path")
  [ "$old" != "$new" ] && echo "DIFF $host$path old=$old new=$new"
done < paths.txt

This is intentionally simple. Extend it to compare Location headers, and run it from inside the cluster network if your load balancers are not publicly reachable.

The Cutover: Side by Side With Weighted DNS

The Kubernetes team’s own advice for the cutover is to deploy the Gateway API configuration alongside the existing Ingress and shift traffic gradually using weighted DNS, a cloud load balancer, or your platform’s traffic-splitting features. Only after all traffic has moved do you delete the Ingress resources and uninstall the controller. This works because the two stacks use different load balancer addresses, so each can be tested and rolled back independently.

Ingress-NGINX retirement cutover sequence showing weighted DNS splitting clients between the old load balancer and the new Gateway

Figure 4: Weighted DNS sends a small share of resolutions to the new Gateway address. Rollback is a weight change, not a deploy.

An illustrative phased schedule

The schedule below is an illustrative plan, not a standard. The numbers are chosen so each stage is large enough to surface errors but small enough to bound damage. Suppose a service handles 2,000 requests per second at peak.

Phase New stack weight Approx. requests per second on new Minimum dwell Gate to proceed
0 0 percent, header-only testing test traffic only 1 day Replay suite shows zero unexplained diffs
1 1 percent 20 1 hour at peak Error rate within agreed margin of old stack
2 5 percent 100 4 hours Latency p99 within margin, no new 404 patterns
3 25 percent 500 1 day Includes a full daily traffic cycle
4 50 percent 1,000 1 day Capacity check on the new data plane
5 100 percent 2,000 1 week Old stack idle; keep as warm standby

Do not skip the capacity check at 50 percent. Many migrations pass at low weights and then discover that the new Gateway’s replica count, connection limits, or rate-limit policy were sized for a fraction of load.

DNS-specific gotchas

Weighted DNS is probabilistic and cached. Clients and resolvers that cache an answer keep hitting the same stack until the TTL expires, so a 5 percent weight does not mean 5 percent of requests at any instant, and rollback is not instantaneous. Lower the TTL to something short, such as 60 seconds, a day or more before you start, because the old TTL has to expire first.

Long-lived connections such as gRPC streams and WebSockets pin to whichever address they resolved, so their migration follows connection churn rather than your weights. If you can use a cloud load balancer with weighted target groups instead, you get request-level splitting and faster rollback, at the cost of adding a layer in front of both stacks. Pick based on whether your rollback time objective is seconds or minutes.

Finally, keep certificate issuance in mind. If cert-manager issues certificates using HTTP-01 challenges via the old Ingress, validate the equivalent path through the new Gateway before the old controller is removed, or renewals will fail weeks after cutover when nobody is watching.

Observability during the shift

Compare the two stacks on the same dashboard. At minimum track request rate, 4xx and 5xx rate by route, p50 and p99 latency, upstream connection errors, and TLS handshake failures. Tag metrics by stack so you can slice them. Define the abort condition before you start, for example “5xx rate on the new stack exceeds the old stack’s by more than the agreed margin for ten minutes,” and write it into the runbook so the on-call engineer does not have to improvise at 3 a.m.

If your platform uses Delta Lake or other data pipelines behind these services, the same side-by-side logic applies to upgrades; see our Delta Lake 4.4 versus 4.3 and Spark 4.2 migration notes for a parallel pattern, and our Aras Innovator R40 versus R38 on .NET 10 write-up for another example of staged cutovers in enterprise systems.

Trade-offs, Gotchas, and What Goes Wrong

The honest summary is that Gateway API is better designed and more work to adopt. The role split pays off in multi-team clusters and costs you in small ones where one person owns everything. Be explicit about which of those you are.

Policy portability is the weak point. Standard filters move between controllers. Authentication, rate limiting, body size, and advanced timeouts do not, because they live in implementation-specific CRDs. If you pick the richest controller and then use every extension, you have rebuilt a different lock-in. Keep a list of every non-standard resource and an owner for it.

Regex semantics differ across implementations. The Gateway API spec leaves RegularExpression matching implementation-specific, and the Kubernetes team explicitly says to check your implementation’s semantics. Prefer PathPrefix and Exact wherever you can, and reserve regex for cases that truly need it.

ingress2gateway is an assistant, not an oracle. The tool is honest about what it cannot do, and the integration tests give real confidence for covered annotations, but anything not covered is silently your responsibility. Read the warnings, treat unlisted annotations as untranslated, and do not assume a clean exit code means a faithful translation.

Cert and secret handling changes shape. Certificates referenced from a Gateway in another namespace need a ReferenceGrant, or a ListenerSet arrangement that places the secret reference where permissions allow it. Teams that migrate routes first and TLS second discover this at the worst moment.

Staying put is a real option only briefly. Existing ingress-nginx deployments keep working, but they now receive no fixes for newly discovered vulnerabilities, and the Steering Committee said alternatives are not drop-in replacements, so the work grows the longer you defer. A March 2025 critical vulnerability in the ingress-nginx admission controller, tracked as CVE-2025-1974, is the kind of event you can no longer count on a patch for.

Fallback to another Ingress controller is legitimate. If a deadline makes Gateway API infeasible, moving to a maintained Ingress controller buys time, though you still carry annotation-based configuration and a second migration later. If you choose this, record it as an explicit, dated risk, not a quiet decision.

Practical Recommendations

Treat this as a three-week project for a mid-sized platform, not a weekend. Start with inventory because it determines everything else, and let the number of snippets and auth annotations set your timeline. A cluster with fifty plain Ingress objects and no snippets is a short job. A cluster with forty teams, each with its own snippets, is an organizational change program.

Run migrations by namespace or by application, never as a single cluster-wide flip. Start with a low-risk internal service to prove the pipeline, then move to customer-facing traffic. Put the generated manifests through code review like any other change, with the ingress2gateway warnings pasted into the pull request description so reviewers see what was not translated.

Standardize early. Publish a platform template of one Gateway, a ListenerSet pattern for team hostnames, and a reference HTTPRoute so application teams are not each learning the API from scratch. Decide up front which policies you offer (auth, rate limit) and how teams request them.

Checklist:

  • [ ] Confirmed whether you run ingress-nginx using the documented pod selector
  • [ ] Counted annotations, ConfigMap overrides, and every snippet
  • [ ] Chosen a controller and checked its conformance for the features you use
  • [ ] Pinned Gateway API Standard CRDs by version in Git
  • [ ] Ran ingress2gateway and reviewed every warning
  • [ ] Rewrote regex paths to exact or prefix where possible
  • [ ] Added explicit redirects for trailing-slash and HTTP-to-HTTPS behavior
  • [ ] Replayed real paths against both stacks and resolved diffs
  • [ ] Lowered DNS TTLs ahead of cutover and defined abort criteria
  • [ ] Verified certificate renewal works through the new path
  • [ ] Documented every implementation-specific policy and its owner
  • [ ] Kept the old stack as warm standby for a week before removal

Frequently Asked Questions

What exactly happened with the ingress-nginx retirement?

Kubernetes SIG Network and the Security Response Committee announced on 11 November 2025 that best-effort maintenance would end in March 2026. After that there are no releases, bug fixes, or security patches. The GitHub repository was archived and made read-only on 24 March 2026. Existing deployments and installation artifacts continue to work, which is why many teams have not noticed yet.

Will my existing ingress-nginx deployment stop working?

No. Running controllers and published Helm charts and images remain available and keep serving traffic. The risk is silent: any vulnerability discovered after retirement will have no official fix. The Steering Committee statement warned that you might not know you are affected until you are compromised, so treat the running controller as an unpatched internet-facing component and plan accordingly.

Is Gateway API a drop-in replacement for Ingress?

No. The Kubernetes Steering Committee stated that none of the available alternatives are direct drop-in replacements. Gateway API uses separate Gateway and route resources, expresses many annotation behaviors as filters, and leaves features such as authentication and rate limiting to implementation policies. Plan engineering time for translation, testing, and a staged cutover.

Can ingress2gateway migrate everything automatically?

No. Version 1.0 supports over 30 common ingress-nginx annotations and tests them against real controllers, but it is explicitly a migration assistant. It warns about untranslatable configuration, such as configuration snippets, and some translations are best-effort, such as combining read and send timeouts. Always review the output and the warnings before applying anything.

Which Gateway API controller should I choose?

Choose based on your inventory and your existing stack. Envoy Gateway, Istio, Cilium, Kgateway, NGINX Gateway Fabric, Traefik Proxy, and others are listed on the official implementations page. Prefer one that is conformant for the Standard features you use, keep implementation-specific policies isolated, and test the same replay suite on your finalist in a dev cluster before deciding.

How do I roll out without downtime?

Deploy the Gateway and routes alongside the existing Ingress on a separate load balancer address, test with header-based requests, then shift traffic gradually with weighted DNS or a cloud load balancer, as the Kubernetes team recommends. Lower DNS TTLs in advance, define abort thresholds, and keep the old stack as standby. Rollback is then a weight change rather than a redeploy.

Further Reading

By Riju — about

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *