Part of the series Kubernetes networking
Ingress to Gateway API: A safe, step-by-step migration
Your Ingress objects still work, but the most popular controller behind them no longer gets security fixes. Understand the mapping from Ingress to Gateway API first. Then convert, test side by side, and switch traffic without downtime.
If your cluster uses Ingress NGINX, it is running software that no longer gets security fixes.
In November 2025, Kubernetes SIG Network announced that Ingress NGINX would get only best-effort maintenance until March 2026, and then nothing: no releases, no bug fixes, no security patches (Kubernetes blog). In January 2026, the Kubernetes Steering and Security Response Committees described it as critical infrastructure in about half of cloud native environments, and warned that staying on it leaves users exposed (statement).
Nothing breaks on the day support ends. Your Ingress objects keep routing traffic. That is exactly why this migration is easy to postpone and risky to ignore.
This guide explains the ideas first: what was retired, how Ingress maps to Gateway API, and which old behaviours quietly change. Then it walks through a migration you can practise: inventory, convert, test side by side, switch traffic, and clean up.
If Ingress or Gateway API is new to you, read how Ingress routes traffic and Gateway API explained first.
Part 1: Understand the concepts
1. What exactly was retired?
Three things have similar names. Only one of them was retired.
| Thing | What it is | Status |
|---|---|---|
| Ingress API | The Ingress resource type, networking.k8s.io/v1 |
Still part of Kubernetes. Frozen: no new features, but not removed (Google Open Source blog). |
| Ingress NGINX | One popular controller that implements Ingress, from kubernetes/ingress-nginx |
Retired after March 2026. |
| Gateway API | The newer routing API: GatewayClass, Gateway, HTTPRoute | The recommended successor (Kubernetes blog). |
A real-life comparison: the government still accepts the old paper form (Ingress API), but the office that processed most of those forms has closed (Ingress NGINX). You can find another office that accepts the old form, or switch to the new online system (Gateway API).
So there are two valid paths:
- Keep Ingress, change the controller. Move to another maintained Ingress controller. This is the smaller change, but you stay on a frozen API and must still translate NGINX-specific annotations.
- Move to Gateway API. This is the bigger change, but it is where new Kubernetes networking features are being added.
This guide covers path 2. The committees’ statement warns that none of the alternatives is a drop-in replacement, whichever path you choose (statement).
The NGINX Ingress Controller made by F5/NGINX (nginxinc/kubernetes-ingress) is a different project and was not part of this retirement. Check which one you run before deciding anything.
2. Why Gateway API is the successor
Ingress was designed for simple host-and-path routing. Anything beyond that, such as rewrites, header matching, or canary traffic, was added through controller-specific annotations. The same feature was written differently for every controller, so moving between controllers meant rewriting annotations.
Gateway API fixes this in three ways:
Features are part of the API. Redirects, rewrites, header matching, and weighted traffic splitting are normal fields, not annotations. The same YAML works on any conformant controller.
Responsibilities are split by role. One Ingress object mixed infrastructure and app settings. Gateway API separates them:
| Resource | Who usually owns it | Restaurant comparison |
|---|---|---|
| GatewayClass | Platform or cloud provider | The company that builds restaurant entrances |
| Gateway | Cluster operator | One entrance: its address, ports, and TLS certificate |
| HTTPRoute | App team | The sign inside that says which table each guest goes to |
Sharing is explicit. A Gateway decides which namespaces may attach routes to it. App teams manage their own routes without being able to edit the shared entrance.
3. How Ingress maps to Gateway API
Every part of an Ingress has a home in Gateway API:
| Ingress | Gateway API |
|---|---|
| Ingress controller | Gateway controller (an implementation of Gateway API) |
ingressClassName / IngressClass |
gatewayClassName / GatewayClass |
| The controller’s address | The Gateway and its listeners |
spec.tls (hosts and Secret) |
Gateway listeners[].tls.certificateRefs |
rules[].host |
HTTPRoute hostnames |
paths[].path with pathType: Prefix |
matches[].path with type: PathPrefix |
pathType: Exact |
type: Exact |
pathType: ImplementationSpecific |
No direct equivalent. Decide what you meant: prefix, exact, or regex. |
backend.service.name and port |
backendRefs[].name and port |
| Redirect and rewrite annotations | RequestRedirect and URLRewrite filters |
| Canary annotations | Several backendRefs with weight |
defaultBackend |
A catch-all HTTPRoute rule for / |
The key shift: one Ingress becomes at least one Gateway plus one HTTPRoute. The Gateway holds the entrance details, such as ports and TLS. The HTTPRoute holds the routing rules.
4. Hidden Ingress NGINX behaviours that change
This is the part most migrations get wrong. Ingress NGINX did several things you never configured. Conformant Gateway API controllers do not copy these defaults, so routes that worked can start returning 404.
The Kubernetes project documented five of these behaviours (before you migrate):
| Ingress NGINX behaviour | What happens after migration |
|---|---|
Regex paths match as case-insensitive prefixes. /[A-Z]{3} also matches /uuid. |
Many Gateway controllers match regex fully and case-sensitively, so /uuid returns 404. |
use-regex on one Ingress affects every path on that host, even in other Ingresses. |
Exact and PathPrefix matches are never silently treated as regex. |
rewrite-target silently turns on regex for the host. |
A URLRewrite filter does not change how paths match. |
A missing trailing slash is redirected. /my-path gets a 301 to /my-path/. |
No automatic redirect. /my-path returns 404 unless you add a redirect rule. |
URLs are normalised before matching, so /a/../uuid becomes /uuid. |
Normalisation differs between controllers, and Gateway API has no setting for it. |
Before migrating, ask: does any client depend on routing that works only because of these defaults? Test each important URL on the new setup, including odd ones such as missing trailing slashes or mixed case.
5. Migrate side by side, not all at once
Never delete the old Ingress first and hope the new setup works. Run both at the same time, then move traffic gradually:
1. Inventory What Ingresses, annotations, and controller do you have?
2. Prepare Install Gateway API CRDs and a Gateway controller
3. Convert Write Gateway + HTTPRoute (by hand or with ingress2gateway)
4. Test Gateway gets its OWN address; test it directly
5. Shift traffic Move DNS or load balancer weight step by step
6. Clean up Delete Ingresses, uninstall Ingress NGINX
During steps 4 and 5, both entrances work. Customers still use the old one until you move them, and if something breaks, rollback is just pointing DNS back.
This is the approach recommended with the official migration tool: deploy Gateway API next to the existing Ingress, shift traffic with weighted DNS or your load balancer, and only then remove the Ingress (Ingress2Gateway 1.0).
6. What ingress2gateway does, and what it does not
ingress2gateway is the official SIG Network tool that reads Ingress resources and prints the equivalent Gateway API YAML. Version 1.0, released in March 2026, supports more than 30 common Ingress NGINX annotations, each tested for behaviour against the real controller (Ingress2Gateway 1.0).
It is a migration assistant, not a one-click replacement. From the project’s own notes and from testing it:
| It does | It does not |
|---|---|
| Reads Ingresses from files, one namespace, or the whole cluster | Install a Gateway controller for you |
| Creates a Gateway, listeners, TLS references, and HTTPRoutes | Choose the right gatewayClassName. It copies your old Ingress class name. |
| Translates common annotations, such as SSL redirect, CORS, and rewrites | Translate everything. configuration-snippet and proxy-body-size are not translated. |
| Prints warnings for things it cannot match exactly | Guarantee identical behaviour for regex, timeouts, or URL normalisation |
| Supports providers including ingress-nginx, kong, traefik, istio, cilium, apisix, and gce | Copy annotations across unchanged (README) |
Always read its warnings and review the output line by line.
7. Doing it by hand matters too
In the CKA exam and in small clusters, you may not have ingress2gateway. You need to write the Gateway and HTTPRoute yourself from an existing Ingress. The mapping table in section 3 is the whole skill. Part 2 shows both the tool and the manual way.
Part 2: Commands and practice
The example is an online shop. The shop namespace has two Services, web on port 80 and api on port 8080, and a TLS Secret named shop-tls.
1. Inventory what you have
Find every Ingress and which class it uses:
kubectl get ingress -A
kubectl get ingressclass
Find which controller is running, and whether it is the retired one:
kubectl get pods -A -l app.kubernetes.io/name=ingress-nginx -o wide
kubectl get deploy -A -o wide | grep -i ingress
An image such as registry.k8s.io/ingress-nginx/controller means you run the retired Kubernetes project.
List every NGINX annotation in use, with counts. Each one is a feature you must translate or replace:
kubectl get ingress -A -o yaml | grep -o 'nginx.ingress.kubernetes.io/[a-z0-9-]*' | sort | uniq -c | sort -rn
Pay extra attention to use-regex, rewrite-target, configuration-snippet, server-snippet, proxy-body-size, and the timeout annotations. Those are the ones most likely to behave differently.
2. Install the Gateway API CRDs
Gateway API resources are CRDs, not built into Kubernetes. Check whether they exist:
kubectl get crd | grep gateway.networking.k8s.io
If not, install the standard channel from the Gateway API releases page. At the time of writing, the latest release line is v1.6 (v1.6 release):
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.1/standard-install.yaml
Many Gateway controllers install these CRDs for you. Check your controller’s documentation, so you do not install two different versions.
3. Install a Gateway controller
CRDs only teach Kubernetes the new object types. Something must implement them, just as an Ingress needed an Ingress controller.
Pick a conformant implementation from the official implementations list. Common choices include Envoy Gateway, Istio, Cilium, NGINX Gateway Fabric, Kong, Traefik, and kgateway. Follow that project’s install guide, then check that its GatewayClass is accepted:
kubectl get gatewayclass
NAME CONTROLLER ACCEPTED AGE
eg gateway.envoyproxy.io/gatewayclass-controller True 1m
Note the NAME. You will need it for gatewayClassName. The output above is an example; yours depends on the controller.
4. The Ingress you are migrating
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
namespace: shop
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts: [shop.example.com]
secretName: shop-tls
rules:
- host: shop.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 8080
In plain words: HTTPS on shop.example.com, with /api going to api and everything else to web. HTTP requests are redirected to HTTPS.
5a. Convert with ingress2gateway
Install it (README):
brew install ingress2gateway
# or
go install github.com/kubernetes-sigs/ingress2gateway@v1.0.0
Convert from a file, a namespace, or the whole cluster:
ingress2gateway print --providers=ingress-nginx --input-file ingress.yaml > gwapi.yaml
ingress2gateway print --providers=ingress-nginx --namespace shop > gwapi.yaml
ingress2gateway print --providers=ingress-nginx --all-namespaces > gwapi.yaml
Running the first command on the Ingress above with v1.0.0 printed this warning:
WARN Gateway API does not support configuring URL normalization (RFC 3986, Section 6).
Please check if this matters for your use case and consult implementation-specific details.
And this output (shortened):
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: nginx
namespace: shop
spec:
gatewayClassName: nginx # ← copied from ingressClassName. Change it!
listeners:
- name: shop-example-com-http # ← port 80 listener, added to match NGINX
hostname: shop.example.com
port: 80
protocol: HTTP
- name: shop-example-com-https
hostname: shop.example.com
port: 443
protocol: HTTPS
tls:
certificateRefs:
- kind: Secret
group: ""
name: shop-tls
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop-shop-example-com
namespace: shop
spec:
parentRefs:
- name: nginx
port: 443
hostnames: [shop.example.com]
rules:
- matches:
- path: { type: PathPrefix, value: / }
backendRefs:
- { name: web, port: 80 }
- matches:
- path: { type: PathPrefix, value: /api }
backendRefs:
- { name: api, port: 8080 }
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop-shop-example-com-http
namespace: shop
spec:
parentRefs:
- name: nginx
port: 80
hostnames: [shop.example.com]
rules:
- matches:
- path: { type: PathPrefix, value: / }
filters:
- type: RequestRedirect
requestRedirect: { scheme: https, statusCode: 308 }
# (a second identical redirect rule for /api is also generated)
Notice how the pieces line up with the mapping table:
spec.tlsbecame an HTTPS listener with acertificateRefsentry.- The
ssl-redirectannotation became a second HTTPRoute on port 80 with a RequestRedirect filter. - Each Ingress path became a rule with
PathPrefixandbackendRefs. - The Gateway is named
nginxand uses classnginx, both taken from the old Ingress class.
5b. Or write it by hand
For the exam, or when you want clean, minimal YAML, write the same thing yourself:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: shop-gateway
namespace: shop
spec:
gatewayClassName: eg # from: kubectl get gatewayclass
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: shop.example.com
tls:
mode: Terminate
certificateRefs:
- name: shop-tls
- name: http
protocol: HTTP
port: 80
hostname: shop.example.com
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop
namespace: shop
spec:
parentRefs:
- name: shop-gateway
sectionName: https # attach to the HTTPS listener only
hostnames: [shop.example.com]
rules:
- matches:
- path: { type: PathPrefix, value: /api }
backendRefs:
- { name: api, port: 8080 }
- matches:
- path: { type: PathPrefix, value: / }
backendRefs:
- { name: web, port: 80 }
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop-http-redirect
namespace: shop
spec:
parentRefs:
- name: shop-gateway
sectionName: http
hostnames: [shop.example.com]
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
Rule order does not decide the winner. Gateway API picks the most specific match, so /api/orders goes to api even if the / rule were listed first. A rule with no matches matches every path, which is why the redirect route needs none.
If you do not want to serve plain HTTP at all, delete the http listener and the redirect route.
kubectl explain works for these resources too, which helps when you cannot remember a field:
kubectl explain gateway.spec.listeners.tls
kubectl explain httproute.spec.rules.filters.requestRedirect
6. Review before applying
Go through the generated or hand-written YAML with this checklist:
gatewayClassNamematches a class fromkubectl get gatewayclass.- The TLS Secret is in the same namespace as the Gateway. If it is elsewhere, a ReferenceGrant is needed.
- If routes in other namespaces must attach, the listener’s
allowedRoutes.namespacesallows them. By default, only routes in the Gateway’s own namespace can attach. - Regex paths: remove the
(?i)prefix and.*suffix the tool adds unless you really want case-insensitive prefix matching (Ingress2Gateway 1.0). - Timeouts and body size limits: check your controller’s defaults (Ingress2Gateway 1.0).
- Every warning from ingress2gateway is either fixed or deliberately accepted.
7. Apply next to the Ingress and test directly
Apply the new resources. The old Ingress stays in place:
kubectl apply -f gwapi.yaml
Check that the Gateway is ready and has its own address:
kubectl get gateway -n shop
NAME CLASS ADDRESS PROGRAMMED AGE
shop-gateway eg 203.0.113.25 True 40s
Check that each route was accepted and its Services were found:
kubectl describe httproute shop -n shop
In the status section, look for both conditions to be True:
Type: Accepted Status: True
Type: ResolvedRefs Status: True
Now test the new address without changing DNS. --resolve tells curl to use a specific IP for the hostname:
GW=$(kubectl get gateway shop-gateway -n shop -o jsonpath='{.status.addresses[0].value}')
curl -sI --resolve shop.example.com:443:$GW https://shop.example.com/
curl -sI --resolve shop.example.com:443:$GW https://shop.example.com/api/health
curl -sI --resolve shop.example.com:80:$GW http://shop.example.com/
The first two should return your app’s normal response. The last should return a 301 with a location: https://... header. Add -k only if you are testing with a self-signed certificate.
Repeat these tests for your important URLs, including the edge cases from Part 1: missing trailing slashes, mixed-case paths, and regex paths.
8. Shift traffic gradually
Real users still reach the old Ingress. Move them in steps:
- Lower the DNS TTL for
shop.example.coma day ahead, so changes spread quickly. - Send a small share of traffic to the Gateway address, using weighted DNS records or your cloud load balancer.
- Watch error rates and latency on both paths.
- Increase the share step by step until the Gateway handles everything.
Rollback is simple while the Ingress still exists: point DNS back to the old address.
9. Clean up
Only after all traffic has moved and stayed healthy for a while:
kubectl delete ingress shop -n shop
helm uninstall ingress-nginx -n ingress-nginx # if it was installed with Helm
kubectl delete ingressclass nginx
Then raise the DNS TTL back to its normal value.
10. Troubleshoot
| Symptom | Likely cause | Check |
|---|---|---|
Gateway PROGRAMMED is False or has no address |
No controller for that class, or the class name is wrong | kubectl get gatewayclass, kubectl describe gateway |
Route Accepted: False |
Hostname does not match the listener, or the namespace is not allowed | allowedRoutes, listener hostname, sectionName |
ResolvedRefs: False |
Service name or port is wrong, or the Service is in another namespace without a ReferenceGrant | kubectl get svc -n shop |
| TLS handshake fails | Secret missing, in the wrong namespace, or wrong type | kubectl get secret shop-tls -n shop |
| 404 on a URL that worked before | One of the hidden NGINX behaviours: regex, trailing slash, or normalisation | Compare the old and new path rules |
| Request too large (413) | The new controller has a different body size limit | Your controller’s documentation |
Works with --resolve but not for users |
DNS still points to the old address | dig shop.example.com |
11. Migration checklist
- Listed every Ingress, IngressClass, and annotation in use
- Confirmed which controller you run, and whether it is the retired one
- Installed Gateway API CRDs (standard channel) and a conformant controller
- Converted each Ingress to a Gateway and HTTPRoutes, and reviewed every warning
- Fixed
gatewayClassName, TLS Secret location, and namespace permissions - Checked regex paths, trailing slashes, timeouts, and body size limits
- Tested the new address with
curl --resolvebefore touching DNS - Shifted traffic gradually, with a tested rollback
- Deleted the Ingresses and uninstalled Ingress NGINX only after the move
The roles stay small: GatewayClass picks the implementation. Gateway is the entrance: address, ports, TLS. HTTPRoute holds the routing rules. ingress2gateway drafts the YAML; you review it, test it side by side, and move traffic only when it is proven.
References
- Kubernetes SIG Network. (2025, November 11). Ingress NGINX Retirement: What You Need to Know. Kubernetes Blog.
- Kubernetes Steering and Security Response Committees. (2026, January 29). Ingress NGINX: Statement from the Kubernetes Steering and Security Response Committees. Kubernetes Blog.
- Google Open Source Blog. (2026). Posts on Ingress, including the note that the Ingress API is feature-frozen, not removed.
- Kubernetes Blog. (2026, February 27). Before You Migrate: Five Surprising Ingress-NGINX Behaviors You Need to Know.
- Modebadze, B., & Jin, S. (2026, March 20). Announcing Ingress2Gateway 1.0: Your Path to Gateway API. Kubernetes Blog.
- kubernetes-sigs. ingress2gateway README. GitHub.
- Kubernetes SIG Network. (2026). Gateway API v1.6: TCPRoute and UDPRoute Graduate to Standard. Kubernetes Blog.
- kubernetes-sigs. Gateway API releases and implementations.