CKA Practice: Migrate an Ingress to Gateway API
A ticket lands in your queue:
PLAT-2119: Migrate
weboff ingress-nginx The platform team is deprecating ingress-nginx. Move theweb-backendapp in namespacewebto the Gateway API. Clients keep callinghttps://web.k8s.local/with no downtime. Do not change the app.
The cluster is single-node K3s. Namespace web holds Deployment and Service web-backend. An Ingress named web serves https://web.k8s.local/. ingress-nginx terminates TLS with Secret web-tls.
Why migrate: Kubernetes native Ingress has no standard fields for traffic splitting, header matching, or cross-namespace routing. Each controller filled the gap with its own annotations, so manifests are not portable. The Gateway API replaces Ingress and is part of the current CKA (Certified Kubernetes Administrator) curriculum. It splits the config by owner: a Gateway holds the listener, port, and certificate. An HTTPRoute holds hostnames and path rules.
The plan:
- Create the new path on a staging hostname:
gateway.web.k8s.local. - Verify it serves traffic.
- Move the production hostname to it.
- Delete the Ingress.
Both controllers run at the same time during the migration. This is normal. ingress-nginx keeps the Ingress working while NGINX Gateway Fabric handles the Gateway resources.
Constraints:
- Do not modify the Deployment or the Service.
- Reuse Secret
web-tls. Do not create new certificates.
Pre-flight
Note: the init scripts take about 30 to 40 seconds to set up the cluster. Please wait for them to finish before you start.
Inspect what you inherited:
kubectl get ingress web -n web -o yaml
Note that one resource holds the hostname, the TLS config, and the routing.
Check the certificate. A cutover needs a certificate that covers every hostname you plan to serve:
kubectl get secret web-tls -n web -o jsonpath='{.data.tls\.crt}' \
| base64 -d | openssl x509 -noout -ext subjectAltName
Step 1: Create the Gateway
In namespace web, create a Gateway named web-gateway:
- It uses the GatewayClass that the cluster's controller provides.
- It has one listener named
https: HTTPS on port 443 for hostnamegateway.web.k8s.local, terminating TLS with Secretweb-tls.
Gateway manifest shape
Find the class with kubectl get gatewayclass. Explore the fields with kubectl explain gateway.spec.listeners --recursive. TLS settings live under listeners[].tls: a mode and a list of certificate refs.
Step 2: Create the HTTPRoute
The Gateway listens, but nothing routes yet. In namespace web, create an HTTPRoute named web-route. It must attach to web-gateway, accept hostname gateway.web.k8s.local, and route every path to Service web-backend.
HTTPRoute vs Ingress
An HTTPRoute attaches to a Gateway through spec.parentRefs. There is no ingressClassName here. Map the Ingress fields you read during pre-flight: rule host becomes spec.hostnames, http.paths becomes rules[].matches, backend.service becomes rules[].backendRefs. Route hostnames must intersect the listener hostname, or the route does not attach.
Step 3: Verify the staging path
NGINX Gateway Fabric now reconciles your resources. The Gateway reaches Programmed, and the controller creates a data plane Deployment and Service named web-gateway-nginx in namespace web. This takes about a minute.
Then verify like a client would: resolve gateway.web.k8s.local to the data plane Service's ClusterIP and request / over HTTPS. Expect 200. Also confirm the old path still serves. Both paths running at once is the migration window.
Verification and debugging
curl --resolve pins a hostname to an IP for one request:
GW_IP=$(kubectl get svc -n web web-gateway-nginx -o jsonpath='{.spec.clusterIP}')
curl -k --resolve "gateway.web.k8s.local:443:${GW_IP}" https://gateway.web.k8s.local/
If GW_IP is empty, the Service does not exist yet. Check kubectl describe gateway web-gateway -n web for status conditions, and status.parents[].conditions in the HTTPRoute. A route that is not Accepted usually has a wrong parentRefs name or non-matching hostnames. The old path uses the ingress-nginx-controller Service ClusterIP the same way.
Step 4: Move the production hostname
Clients call web.k8s.local. Extend web-gateway and web-route so https://web.k8s.local/ answers through the Gateway as well. Listener names are unique within a Gateway. Reuse the same certificate.
Verify the same way as Step 3, now for web.k8s.local. In production this step is a DNS change. The --resolve flag simulates it.
Editing live resources
kubectl edit or re-applying an updated manifest both work. listeners is an array: append a second HTTPS listener with a new name (for example https-prod) for web.k8s.local, and add the hostname to the route's spec.hostnames. After the edit, status.listeners[].attachedRoutes on the Gateway should show 1 for the new listener.
Step 5: Delete the Ingress
kubectl delete ingress web -n web
The check passes only when the Ingress is gone and web.k8s.local still returns 200 through the Gateway.
What you practiced
- Mapping an Ingress onto Gateway API resources (a CKA exam objective)
- The expand, verify, cut over, contract pattern for live migrations
- Verifying each stage with
curl --resolveinstead of trusting status fields
References
- Gateway API and the full spec
- Migrating from Ingress, the upstream guide for this cutover
- Kubernetes docs: Gateway API, Ingress
- NGINX Gateway Fabric: provides the GatewayClass. Init pins v2.4.2 with Gateway API v1.4.1 CRDs.
- ingress-nginx: bare-metal manifest for the legacy path