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-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 nodeMaps toWhat it does
client request to shop.boutique.example.comend-user HTTP trafficReaches the storefront through whichever entry point DNS points at
Ingress shopthe existing Ingress (ingress-nginx)The old single entry point being migrated
Gateway shop-gatewaythe new GatewayThe platform-owned replacement load balancer + listeners
HTTPRoute shopthe new HTTPRouteCarries 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 nodeMaps toWhat it does
IngressClass nginx -> GatewayClass nginxcontroller selectorNames the implementation that programs the data path
Ingress entry point -> Gateway listenerskind: GatewayBecomes the LB with explicit port/protocol/TLS listeners
spec.rules -> HTTPRoute matchesHTTPRoute.spec.rulesHost/path routing moves from Ingress rules into the route
backend.service -> backendRefsHTTPRoute.backendRefsPoints routing at the same Boutique Services (cartservice, ...)
spec.tls -> certificateRefs Terminatelisteners.tlsTLS termination moves onto the Gateway listener
nginx annotations -> spec fields/filtersGateway API filtersAnnotation 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 nodeMaps toWhat it does
Existing Ingress shopthe live Ingress (ingress-nginx)The production entry point being migrated
Install Gateway API CRDs and controllerkubectl apply standard-install.yamlMakes Gateway/HTTPRoute kinds servable
ingress2gateway printthe conversion CLIGenerates Gateway + HTTPRoute YAML from the Ingress
Review and fix unmapped nginx annotationsmanual editRe-expresses annotation behavior as spec fields
kubectl apply Gateway and HTTPRoutethe new stackCreates the parallel path while Ingress stays live
Programmed and Accepted Truestatus conditionsConfirms the Gateway/route are wired before cutover
Shift DNS or LB to Gatewayedge changeMoves live traffic onto the Gateway address
Delete Ingress shopkubectl delete ingressRetires 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.

  1. The starting Ingress in production (boutique namespace):
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 } }
  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 boutique > 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: 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 }
  1. 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
  1. 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
  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 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 nodeMaps toWhat it does
Live Ingress shopthe production IngressRoutes /cart and /checkout to Boutique Services over TLS
ingress2gateway printconversion CLIEmits the Gateway + HTTPRoute equivalent
kubectl apply Gateway and HTTPRoutethe new parallel stackRuns beside the still-live Ingress
Programmed and Accepted Truestatus conditionsGates progress until the new path is wired
curl --resolve both pathsparity testProves cartservice/checkoutservice respond via the Gateway
Shift DNS or LB to Gateway addressedge cutoverMoves real traffic onto the Gateway
kubectl delete ingress shopdecommissionRemoves 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 print against 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 --resolve while Ingress stayed live
  • [ ] Cut over DNS, monitored, then retired the Ingress