Challenge, Medium,  on  KubernetesNetworking

CKA Practice: Migrate an Ingress to Gateway API

A ticket lands in your queue:

PLAT-2119: Migrate web off ingress-nginx The platform team is deprecating ingress-nginx. Move the web-backend app in namespace web to the Gateway API. Clients keep calling https://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:

  1. Create the new path on a staging hostname: gateway.web.k8s.local.
  2. Verify it serves traffic.
  3. Move the production hostname to it.
  4. 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 hostname gateway.web.k8s.local, terminating TLS with Secret web-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 --resolve instead of trusting status fields

References