Migrate Ingress to Gateway API
CKA prep • ingress2gateway, mapping Ingress rules to HTTPRoute, step-by-step cutover, gotchas
Key terms
| Term | Meaning |
|---|---|
| ingress2gateway | Official tool that converts Ingress YAML to Gateway API YAML |
| Gateway | Replaces the controller's single entry point (the LB + listeners) |
| HTTPRoute | Replaces the Ingress rules (host/path -> Service) |
| ReferenceGrant | Permits cross-namespace backend references |
| Coexistence | Running Ingress and Gateway API side by side during cutover |
| TLS Terminate | The Gateway listener mode replacing Ingress spec.tls |
| Provider | The ingress2gateway flag selecting your controller's annotations |
Problem & solution
You already run Ingress in production, but want Gateway API's richer routing and annotation-free portability. Rewriting every Ingress by hand is slow and risky. You need a mechanical mapping and a safe, reversible cutover that keeps traffic flowing the whole time.
Solution: Convert existing Ingress objects to Gateway + HTTPRoute with ingress2gateway, run both stacks in parallel behind DNS, validate the new path, then shift traffic and retire Ingress.
The analogy
Your port runs one old main gate that every truck squeezes through, and you want a new modular gate where the port owns the gateway and each tenant owns their own posted route. You do not tear down the old gate on day one: you build the new gate beside it, copy each route card across one at a time, and wave a few trucks through to prove it works before redirecting everyone. In Kubernetes the old gate is your Ingress, the new modular gate is the Gateway, each copied route card is an HTTPRoute, and both run side by side until you flip the signs and retire the old gate.
What maps to what
Step 0 — prerequisites
Before converting anything, make sure the cluster can actually serve Gateway API objects: install the CRDs and a controller, then confirm a GatewayClass is present.
# install Gateway API CRDs + a controller (e.g. NGINX Gateway Fabric, Envoy Gateway)
kubectl apply -f \
https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/standard-install.yaml
kubectl get gatewayclass # confirm a class exists and is Accepted
Step 1 — convert with ingress2gateway
The tool reads Ingress (and some provider annotations) and emits Gateway + HTTPRoute YAML. Always review the output; not every annotation has an equivalent.
# install (Go) and convert from the live cluster
go install github.com/kubernetes-sigs/ingress2gateway@latest
ingress2gateway print --providers ingress-nginx > gateway-api.yaml
# or convert from a file instead of the cluster
ingress2gateway print --input-file ingress.yaml --providers ingress-nginx \
> gateway-api.yaml
Step 2 — review the generated resources
A typical Ingress like this:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
spec:
ingressClassName: nginx
tls:
- hosts: ["shop.example.com"]
secretName: shop-tls
rules:
- host: shop.example.com
http:
paths:
- path: /cart
pathType: Prefix
backend:
service:
name: cart
port: { number: 80 }
becomes a Gateway plus an HTTPRoute:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: shop-gateway
spec:
gatewayClassName: nginx
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: "shop.example.com"
tls:
mode: Terminate
certificateRefs:
- name: shop-tls
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop
spec:
parentRefs:
- name: shop-gateway
hostnames:
- "shop.example.com"
rules:
- matches:
- path: { type: PathPrefix, value: /cart }
backendRefs:
- name: cart
port: 80
Step 3 — apply and validate side by side
Keep the old Ingress live; apply the new Gateway/HTTPRoute and test it directly against the new entry point before touching DNS.
kubectl apply -f gateway-api.yaml
kubectl get gateway shop-gateway # wait for PROGRAMMED=True
kubectl get httproute shop -o yaml | grep -A5 conditions # Accepted=True
# resolve to the Gateway's address and test without DNS
GW=$(kubectl get gateway shop-gateway -o jsonpath='{.status.addresses[0].value}')
curl -k --resolve shop.example.com:443:$GW https://shop.example.com/cart
Step 4 — cut over traffic, then retire Ingress
With the new path proven, shift traffic at the edge and keep the old Ingress as an instant rollback until you are confident, then delete it.
1. point DNS (or the external LB) at the Gateway address
2. watch metrics/logs on the Gateway path for errors + latency
3. keep the Ingress as instant rollback for one TTL window
4. once stable, delete the Ingress object (and its controller if unused)
kubectl delete ingress shop # only after the Gateway path is proven
End-to-end: the migration flow
End-to-end example: convert a live Ingress and cut over safely
A complete, reversible migration: start from a real two-path Ingress, convert it
with ingress2gateway, apply the generated Gateway + HTTPRoute alongside the old
Ingress, validate parity with curl --resolve, shift DNS, then decommission.
- The starting Ingress in production (
storenamespace):
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
namespace: store
spec:
ingressClassName: nginx
tls:
- hosts: ["shop.example.com"]
secretName: shop-tls
rules:
- host: shop.example.com
http:
paths:
- path: /cart
pathType: Prefix
backend:
service: { name: cart, port: { number: 80 } }
- path: /checkout
pathType: Prefix
backend:
service: { name: checkout, port: { number: 80 } }
- Install the Gateway API CRDs/controller, then convert the live Ingress:
kubectl apply -f \
https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/standard-install.yaml
kubectl get gatewayclass # confirm a class exists and is Accepted
go install github.com/kubernetes-sigs/ingress2gateway@latest
ingress2gateway print --providers ingress-nginx -n store > gateway-api.yaml
- Review the generated YAML; it produces a Gateway plus an HTTPRoute:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: shop-gateway
namespace: store
spec:
gatewayClassName: nginx
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: "shop.example.com"
tls:
mode: Terminate
certificateRefs:
- name: shop-tls
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop
namespace: store
spec:
parentRefs:
- name: shop-gateway
hostnames:
- "shop.example.com"
rules:
- matches:
- path: { type: PathPrefix, value: /cart }
backendRefs:
- { name: cart, port: 80 }
- matches:
- path: { type: PathPrefix, value: /checkout }
backendRefs:
- { name: checkout, port: 80 }
- Apply the new stack while the Ingress stays live; wait for healthy status:
kubectl apply -f gateway-api.yaml
kubectl -n store get gateway shop-gateway \
-o jsonpath='{.status.conditions[?(@.type=="Programmed")].status}{"\n"}' # True
kubectl -n store get httproute shop \
-o jsonpath='{.status.parents[0].conditions[?(@.type=="Accepted")].status}{"\n"}' # True
- Validate parity against the Gateway address without touching DNS:
GW=$(kubectl -n store get gateway shop-gateway -o jsonpath='{.status.addresses[0].value}')
curl -k --resolve shop.example.com:443:$GW https://shop.example.com/cart
curl -k --resolve shop.example.com:443:$GW https://shop.example.com/checkout
# expected: same responses the Ingress serves today
- Cut over DNS to the Gateway, monitor, then decommission the Ingress:
# 1) point DNS / external LB at $GW
# 2) watch logs + latency on the Gateway path for one TTL window
kubectl -n store delete ingress shop # only after the Gateway path is proven
Common pitfalls
These are the mistakes that bite most often during a cutover, each paired with how to avoid it.
- annotations dropped silently -> rewrites, rate-limits, auth need manual mapping
- cutover before validation -> always test the Gateway path before DNS change
- cross-namespace backend -> add a ReferenceGrant or the route is rejected
- TLS Secret in another ns -> certificateRefs cross-namespace needs a grant
- Gateway not Programmed -> GatewayClass/controller missing or misconfigured
- deleted Ingress too early -> keep it one TTL as instant rollback
- pathType Exact vs Prefix -> verify match types survived the conversion
Key takeaways
- Migration is mechanical: Ingress -> Gateway (entry/TLS) + HTTPRoute (rules).
- ingress2gateway automates the bulk; always review the output by hand.
- Provider annotations rarely map 1:1 — re-express them as spec fields/filters.
- Coexist: keep Ingress live and validate the Gateway path before cutover.
- Cut over at DNS/LB, keep Ingress one TTL as rollback, then delete it.
- Confirm Programmed/Accepted conditions before trusting the new path.
Checklist
- [ ] Installed Gateway API CRDs + a controller with a usable GatewayClass
- [ ] Ran
ingress2gateway printagainst an existing Ingress - [ ] Reviewed and hand-fixed any unmapped annotations
- [ ] Applied the Gateway + HTTPRoute and saw Programmed/Accepted = True
- [ ] Tested the new path with
curl --resolvewhile Ingress stayed live - [ ] Cut over DNS, monitored, then retired the Ingress