Migrate an Ingress to the Gateway API
Migrate the existing Ingress web in namespace web-app to the new Gateway API. A GatewayClass named nginx is already installed.
Live lab access
This task description is public. Its live lab requires full access; six other CKA labs are available in Free Practice with AI Lab Coach.
Create an account to continue →
Explore the six free labs with AI Lab Coach →
Read the brief on any device. Use a desktop or laptop for the terminal lab. Nothing starts until you choose Start.
The task
Migrate the existing Ingress web in namespace web-app to the new Gateway API.
A GatewayClass named nginx is already installed. Use API version v1 (the older v1beta1 is also accepted on this environment).
Task 1: Create a Gateway named web-gateway with:
- hostname:
gateway.web.k8s.local - TLS termination using secret
web-tls - GatewayClass:
nginx
Task 2: Create an HTTPRoute named web-route with:
- hostname:
gateway.web.k8s.local - path prefix
/→ serviceweb-service:80 - parentRef: the
web-gateway
Original Prepium guide · Written for Kubernetes v1.35 and checked against the official documentation on 6 October 2026. Use a disposable practice cluster. Gateway API CRDs are not built into Kubernetes; you need them plus a controller that provides a GatewayClass (kubectl get gatewayclass must list one).
Gateway API in three objects: class, Gateway listeners and HTTPRoute, with TLS termination
An Ingress packs the controller choice, hostnames, certificates and paths into one object. Gateway API splits them by role: a GatewayClass names the controller, a Gateway declares listeners (port, protocol, hostname, certificates), and an HTTPRoute attaches to a listener and says which paths go to which Services. All three are served as gateway.networking.k8s.io/v1. Translating an Ingress is mostly a matter of moving each field to the right object.
Ingress field Gateway API home
spec.ingressClassName ──→ Gateway spec.gatewayClassName
spec.tls[].secretName ──→ Gateway listener tls.certificateRefs
spec.rules[].host ──→ listener hostname + HTTPRoute hostnames
paths[].path / pathType ──→ HTTPRoute rules[].matches[].path
backend.service name/port ──→ HTTPRoute rules[].backendRefs
1. Prepare backends and a certificate
kubectl create namespace prepium-tutorial
kubectl -n prepium-tutorial create deployment docs-web --image=nginx:1.27-alpine
kubectl -n prepium-tutorial create deployment status-web --image=nginx:1.27-alpine
kubectl -n prepium-tutorial expose deployment docs-web --port=8081 --target-port=80
kubectl -n prepium-tutorial expose deployment status-web --port=8082 --target-port=80
openssl req -x509 -newkey rsa:2048 -noenc -days 7 -keyout pages.key -out pages.crt \
-subj "/CN=pages.prepium.test" -addext "subjectAltName=DNS:pages.prepium.test"
kubectl -n prepium-tutorial create secret tls pages-cert --cert=pages.crt --key=pages.key
kubectl create secret tls stores the pair under the keys tls.crt and tls.key (on OpenSSL older than 3.0, use -nodes instead of -noenc). Keep the Secret in the Gateway's namespace; a reference to another namespace is rejected unless a ReferenceGrant there allows it.
2. Declare the Gateway and the route
Save this as gateway-edge.yaml. It defines two listeners on the same hostname and one HTTPRoute that attaches only to the HTTPS listener through sectionName.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: edge
namespace: prepium-tutorial
spec:
gatewayClassName: GATEWAY_CLASS # replaced by sed with a real class name
listeners:
- name: plain
protocol: HTTP
port: 80
hostname: pages.prepium.test
- name: secure
protocol: HTTPS
port: 443
hostname: pages.prepium.test
tls:
mode: Terminate # the default, written out for clarity
certificateRefs:
- kind: Secret
name: pages-cert
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: pages
namespace: prepium-tutorial
spec:
parentRefs:
- name: edge
sectionName: secure # attach to the HTTPS listener only
hostnames:
- pages.prepium.test
rules:
- matches:
- path:
type: PathPrefix
value: /status
backendRefs:
- name: status-web
port: 8082
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: docs-web
port: 8081
GWC=$(kubectl get gatewayclass -o jsonpath='{.items[0].metadata.name}')
sed "s/GATEWAY_CLASS/$GWC/" gateway-edge.yaml | kubectl apply -f -
On an HTTPS listener tls.mode defaults to Terminate: the Gateway decrypts and the backends receive plain HTTP. In an HTTPRoute, a port on a Service backendRef is required and means the Service port, not the container port. When paths overlap, the longest matching prefix wins, so /status reaches status-web and everything else falls through to /.
3. Read status before sending traffic
kubectl -n prepium-tutorial get gateway edge
kubectl -n prepium-tutorial describe gateway edge
kubectl -n prepium-tutorial get httproute pages -o jsonpath='{range .status.parents[*].conditions[*]}{.type}={.status} {.reason}{"\n"}{end}'
The Gateway should report Accepted and Programmed, and each listener should show ResolvedRefs True; a missing Secret appears there as reason InvalidCertificateRef. Each parent entry on the route carries Accepted and ResolvedRefs; a wrong Service name or listener name shows up as a False condition long before a request fails.
4. Test with the hostname, not the IP
GW=$(kubectl -n prepium-tutorial get gateway edge -o jsonpath='{.status.addresses[0].value}')
curl -sk --resolve pages.prepium.test:443:$GW https://pages.prepium.test/
curl -sk --resolve pages.prepium.test:443:$GW https://pages.prepium.test/status
--resolve makes curl send the right SNI and Host header to the Gateway address, and -k accepts the self-signed certificate. Whether that address is reachable from your shell depends on the implementation and your network.
Common mistakes
- Route hostname that does not overlap the listener hostname. The route is not attached; look for
Accepted=Falsewith reasonNoMatchingListenerHostname. sectionNamethat names no listener. The route reportsAccepted=Falsewith reasonNoMatchingParent.- Putting the certificate on the HTTPRoute. TLS settings belong to the Gateway listener.
- Creating the route in another namespace. Listeners accept routes only from their own namespace by default (
allowedRoutes.namespaces.from: Same), so the reason isNotAllowedByListeners.
| Example check | Evidence |
|---|---|
| Gateway uses the intended class | spec.gatewayClassName matches an installed GatewayClass; Accepted and Programmed are True |
| TLS is terminated at the listener | HTTPS listener whose certificateRefs names an existing TLS Secret in the same namespace |
| Route is attached | parentRefs names the Gateway; route conditions Accepted and ResolvedRefs are True |
| Routing works end to end | HTTPS request with the right Host returns each backend for its path |
Cleanup: kubectl delete namespace prepium-tutorial and rm pages.key pages.crt. References: Migrating from Ingress, TLS configuration, API specification. Next, try Ingress path routing or TLS protocol versions.
Video walkthrough: Gateway API Migration on YouTube. It shows the full solution, so try the task first.
What this tests
Expose workloads with services and ingress, and restrict traffic with network policies. On the CKA exam, Services & Networking tasks are graded purely on what you build in the cluster - not multiple choice - so the only way to get faster is to do them on a real cluster against a clock.
Practice it for real
The public description lets you study the task before signing up. Full access includes every live lab. Six selected CKA scenarios are included in Free Practice, with five shared sessions each week. You solve the task in a real terminal, hit validate, and a programmatic checker scores exactly what you got right and wrong (with partial credit). You can open the solution while practising, then retry and use the failed checks to improve your score.