48

Migrate Ingress to Gateway API

CKA prep • ingress2gateway, mapping Ingress rules to HTTPRoute, step-by-step cutover, gotchas

Key terms

TermMeaning
ingress2gatewayOfficial tool that converts Ingress YAML to Gateway API YAML
GatewayReplaces the controller's single entry point (the LB + listeners)
HTTPRouteReplaces the Ingress rules (host/path -> Service)
ReferenceGrantPermits cross-namespace backend references
CoexistenceRunning Ingress and Gateway API side by side during cutover
TLS TerminateThe Gateway listener mode replacing Ingress spec.tls
ProviderThe 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.

  1. The starting Ingress in production (store namespace):
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 } }
  1. 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
  1. 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 }
  1. 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
  1. 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
  1. 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 print against 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 --resolve while Ingress stayed live
  • [ ] Cut over DNS, monitored, then retired the Ingress