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-nginx in production fronting Google's
Online Boutique
(frontend, cartservice, checkoutservice), 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.
Graph legend — each Kubernetes node maps a port concept to the real Boutique migration:
| Graph node | Maps to | What it does |
|---|---|---|
| client request to shop.boutique.example.com | end-user HTTP traffic | Reaches the storefront through whichever entry point DNS points at |
| Ingress shop | the existing Ingress (ingress-nginx) | The old single entry point being migrated |
| Gateway shop-gateway | the new Gateway | The platform-owned replacement load balancer + listeners |
| HTTPRoute shop | the new HTTPRoute | Carries the cart/checkout routing rules off the Ingress |
What maps to what
Migrating is mostly a mechanical translation: every piece of an Ingress has a direct counterpart in Gateway API. The single Ingress entry point splits into a Gateway (the load balancer and its listeners) plus one or more HTTPRoutes (the host/path rules), the IngressClass becomes a GatewayClass, TLS settings move onto the Gateway listener, and controller-specific annotations become typed spec fields. The diagram below pairs each Ingress concept on the left with its Gateway API replacement on the right.
Graph legend — each arrow is one Ingress concept and its Gateway API replacement:
| Graph node | Maps to | What it does |
|---|---|---|
| IngressClass nginx -> GatewayClass nginx | controller selector | Names the implementation that programs the data path |
| Ingress entry point -> Gateway listeners | kind: Gateway | Becomes the LB with explicit port/protocol/TLS listeners |
| spec.rules -> HTTPRoute matches | HTTPRoute.spec.rules | Host/path routing moves from Ingress rules into the route |
| backend.service -> backendRefs | HTTPRoute.backendRefs | Points routing at the same Boutique Services (cartservice, ...) |
| spec.tls -> certificateRefs Terminate | listeners.tls | TLS termination moves onto the Gateway listener |
| nginx annotations -> spec fields/filters | Gateway API filters | Annotation behavior is re-expressed as typed spec fields |
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 Online Boutique Ingress like this:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
spec:
ingressClassName: nginx
tls:
- hosts: ["shop.boutique.example.com"]
secretName: boutique-tls
rules:
- host: shop.boutique.example.com
http:
paths:
- path: /cart
pathType: Prefix
backend:
service:
name: cartservice
port: { number: 7070 }
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.boutique.example.com"
tls:
mode: Terminate
certificateRefs:
- name: boutique-tls
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop
spec:
parentRefs:
- name: shop-gateway
hostnames:
- "shop.boutique.example.com"
rules:
- matches:
- path: { type: PathPrefix, value: /cart }
backendRefs:
- name: cartservice
port: 7070
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.boutique.example.com:443:$GW https://shop.boutique.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
Every node names the actual command or resource in the Boutique cutover.
Graph legend — each node is a real step in the ingress-nginx -> Gateway API migration:
| Graph node | Maps to | What it does |
|---|---|---|
| Existing Ingress shop | the live Ingress (ingress-nginx) | The production entry point being migrated |
| Install Gateway API CRDs and controller | kubectl apply standard-install.yaml | Makes Gateway/HTTPRoute kinds servable |
| ingress2gateway print | the conversion CLI | Generates Gateway + HTTPRoute YAML from the Ingress |
| Review and fix unmapped nginx annotations | manual edit | Re-expresses annotation behavior as spec fields |
| kubectl apply Gateway and HTTPRoute | the new stack | Creates the parallel path while Ingress stays live |
| Programmed and Accepted True | status conditions | Confirms the Gateway/route are wired before cutover |
| Shift DNS or LB to Gateway | edge change | Moves live traffic onto the Gateway address |
| Delete Ingress shop | kubectl delete ingress | Retires the old path after the rollback window |
End-to-end example: convert a live Boutique Ingress and cut over safely
A complete, reversible migration: start from a real two-path Online Boutique
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 (
boutiquenamespace):
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
namespace: boutique
spec:
ingressClassName: nginx
tls:
- hosts: ["shop.boutique.example.com"]
secretName: boutique-tls
rules:
- host: shop.boutique.example.com
http:
paths:
- path: /cart
pathType: Prefix
backend:
service: { name: cartservice, port: { number: 7070 } }
- path: /checkout
pathType: Prefix
backend:
service: { name: checkoutservice, port: { number: 5050 } }
- 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 boutique > 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: boutique
spec:
gatewayClassName: nginx
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: "shop.boutique.example.com"
tls:
mode: Terminate
certificateRefs:
- name: boutique-tls
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop
namespace: boutique
spec:
parentRefs:
- name: shop-gateway
hostnames:
- "shop.boutique.example.com"
rules:
- matches:
- path: { type: PathPrefix, value: /cart }
backendRefs:
- { name: cartservice, port: 7070 }
- matches:
- path: { type: PathPrefix, value: /checkout }
backendRefs:
- { name: checkoutservice, port: 5050 }
- Apply the new stack while the Ingress stays live; wait for healthy status:
kubectl apply -f gateway-api.yaml
kubectl -n boutique get gateway shop-gateway \
-o jsonpath='{.status.conditions[?(@.type=="Programmed")].status}{"\n"}' # True
kubectl -n boutique 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 boutique get gateway shop-gateway -o jsonpath='{.status.addresses[0].value}')
curl -k --resolve shop.boutique.example.com:443:$GW https://shop.boutique.example.com/cart
curl -k --resolve shop.boutique.example.com:443:$GW https://shop.boutique.example.com/checkout
# expected: same responses ingress-nginx 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 boutique delete ingress shop # only after the Gateway path is proven
Graph legend — each node is a concrete step from the Boutique walkthrough above:
| Graph node | Maps to | What it does |
|---|---|---|
| Live Ingress shop | the production Ingress | Routes /cart and /checkout to Boutique Services over TLS |
| ingress2gateway print | conversion CLI | Emits the Gateway + HTTPRoute equivalent |
| kubectl apply Gateway and HTTPRoute | the new parallel stack | Runs beside the still-live Ingress |
| Programmed and Accepted True | status conditions | Gates progress until the new path is wired |
| curl --resolve both paths | parity test | Proves cartservice/checkoutservice respond via the Gateway |
| Shift DNS or LB to Gateway address | edge cutover | Moves real traffic onto the Gateway |
| kubectl delete ingress shop | decommission | Removes the old Ingress after the rollback window |
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 the existing ingress-nginx 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