Challenge Details
Gateway API Learning Lab: From Zero to Hero
By Aleksandro Matejic · January 17, 2026

The ingress-nginx controller is reaching End-of-Life on March 26th, 2026. No more releases. No bug fixes. No security patches. The clock is ticking — and the Kubernetes community recommends something newer and better.
A few months ago, I got handed an assignment that made me a bit worried: migrate thousands of applications from Ingress to HTTPRoute and replace the soon-to-be outdated ingress-nginx controller to something else.
We didn't have simple redirects either — these were complex routing rules written in NGINX directives, the kind that had been accumulating for years. Custom annotations, rewrite rules, header manipulations, the works. No clean shortcut existed. I had to understand the Gateway API deeply enough to translate all of it, one rule at a time.
In this 12-lesson (plus bonus KillerCoda lessons), hands-on course, you'll migrate a real Kubernetes application from Ingress-NGINX to the Kubernetes Gateway API with Traefik — the modern, role-aware, production-grade way to route traffic in Kubernetes. No fluff, no slides — just real commands, real manifests, and a real bookstore app running on your local cluster.
By the end of this course, you will:
- Understand why the Gateway API exists and what problems it solves over Ingress
- Understand the full resource model: GatewayClass, Gateway and HTTPRoute
- Deploy and configure Traefik as a Gateway API controller
- Migrate Ingress rules to HTTPRoutes step by step
- Implement advanced routing: path rewrites, header manipulation, traffic splitting, and rate limiting
- Terminate TLS with locally trusted certificates using mkcert
- Troubleshoot Gateway API issues common in production
- Diagnose common migration pitfalls like request buffering differences between nginx and Traefik
- Extend Traefik with custom Go plugins for request tracing and middleware
Who is this for? Kubernetes engineers who know their way around Deployments, Services, and Ingress — and are ready to level up before their ingress-nginx setup becomes a liability.
⏱️ Estimated time to complete: 6–8 hours across 12 self-paced lessons. Start today and be migration-ready before the deadline.
💡 This is an interactive, text-based course. We build our material this way because reading and doing is simply the most effective way to master engineering tools.
Video tutorials make it easy to drift off, lose focus, or fall into passive watching. In contrast, a text-first approach keeps you active: you control the pace, scan code without scrubbing timelines, build precise mental models, and immediately apply commands in hands-on environments
Lessons: 16
Time Limit: No time limit
Ready to start this course?
Create a free account to track your progress and earn points.
Course Lessons
1. Lesson 01: Understanding Gateway API Architecture Duration: ~15 minutes...
1 pts2. Lesson 02: Setting Up Local Kubernetes with k3d Duration: ~20 minutes...
1 pts| Option | Correct? | Explanation |
|---|---|---|
| A | ✅ Yes | Kubernetes only defines the Ingress API (the resource schema). It does not ship with a controller that implements it. You must install a controller such as ingress-nginx, Traefik, or HAProxy separately. Without a controller, creating an Ingress resource has no effect. |
| B | ✅ Yes | Traefik supports multiple Kubernetes providers. The kubernetesIngress provider watches Ingress resources; the kubernetesGateway provider watches Gateway API resources. Both can be enabled at the same time, allowing Traefik to handle both old and new routing models during a migration. |
| C | ❌ No | The Gateway API is a different resource model, not a rename. An Ingress resource has a flat structure with rules, host, paths, and backend nested together. A Gateway API setup requires three separate resources: a GatewayClass, a Gateway, and an HTTPRoute. The fields, structure, and concepts are different — migration requires rewriting the resources, not just changing the apiVersion. |
| D | ✅ Yes | A LoadBalancer Service instructs Kubernetes to provision an external endpoint for the controller. In cloud environments (EKS, GKE, AKS) this creates a real cloud load balancer. In a local k3d cluster, k3s's ServiceLB (formerly Klipper LB) handles this by assigning the host port mappings that were configured at cluster creation time. |
| E | ✅ Yes | Helm packages all the Kubernetes resources for an application into a chart and manages them as a named release. helm install creates the release; helm upgrade updates it; helm uninstall removes all resources belonging to it. This is why uninstalling ingress-nginx with helm uninstall removes the Deployment, Service, and IngressClass in one command. |
3. Lesson 03: Deploying the Bookstore Application Duration: ~20 minutes Folder:...
1 pts| Option | Correct? | Explanation |
|---|---|---|
| A | ✅ Yes | imagePullPolicy: Never is an explicit guarantee that Kubernetes will never contact a remote registry for this image. It relies entirely on the image being present in the node's local cache — which k3d image import places it in. |
| B | ❌ No | ClusterIP is the default Service type and is only accessible from inside the cluster. It has no external IP. The EXTERNAL-IP column shows <none>. To expose the app externally, an Ingress controller or Gateway API controller is needed (covered in later lessons). |
| C | ✅ Yes | The readinessProbe is specifically designed to prevent traffic from reaching Pods that are not yet ready. Unlike the livenessProbe (which kills and restarts the container), a failed readinessProbe only removes the Pod from the Service endpoints temporarily. |
| D | ✅ Yes | The Service port: 80 is what consumers connect to. The targetPort: 8000 is what the Service forwards to on the Pod. This port translation means the application team can change the internal port without affecting how other services call it. |
| E | ❌ No | kubectl port-forward is a temporary tunnel that runs as a foreground process in your terminal. It terminates when you press Ctrl+C or close the terminal. It is intended for debugging and testing, not as a persistent networking solution. |
4. Lesson 04: The Old Way — Ingress-NGINX Controller Duration: ~25 minutes...
1 pts| Option | Correct? | Explanation |
|---|---|---|
| A | ✅ Yes | ingressClassName links the Ingress resource to a specific controller via the IngressClass object. When ingress-nginx is installed, it registers an IngressClass named nginx. Without the field, the Ingress may be ignored by all controllers (depending on default class configuration). |
| B | ❌ No | Annotations are free-form key-value strings in the resource metadata. Kubernetes does not validate them — the API server accepts any annotation. A misspelled annotation key is silently ignored. Errors only surface at runtime when the controller fails to apply the configuration. |
| C | ✅ Yes | The Ingress API has no concept of weighted backends. Traffic splitting requires vendor-specific workarounds (e.g., a separate canary Ingress with nginx.ingress.kubernetes.io/canary: "true" for ingress-nginx). It is not a native, portable feature. |
| D | ✅ Yes | An Ingress resource can only route traffic to Services within the same namespace. Cross-namespace routing is not supported in the Ingress API. This is one of the limitations that the Gateway API addresses natively with ReferenceGrant. |
| E | ❌ No | Annotations are controller-specific. The nginx.ingress.kubernetes.io/ prefix annotations are only understood by the ingress-nginx controller. Traefik uses entirely different annotation keys and values. Switching controllers requires rewriting every annotation — this is the core portability problem that led to Gateway API. |
5. Lesson 05: Installing Traefik as Gateway API Controller Duration: ~25...
1 pts| Option | Correct? | Explanation |
|---|---|---|
| A | ✅ Yes | The controllerName is the binding mechanism between the GatewayClass and the controller. Traefik watches for GatewayClass resources with controllerName: traefik.io/gateway-controller and marks them as Accepted. If the name does not match, Traefik ignores that GatewayClass. |
| B | ❌ No | Gateway API CRDs are not built into Kubernetes — they are an extension that must be installed separately. Without installing the CRDs (e.g., via the standard-install.yaml file), the cluster does not recognise resource kinds like Gateway or HTTPRoute. |
| C | ✅ Yes | Without providers.kubernetesGateway.enabled: true, Traefik runs but completely ignores all Gateway API resources. This single Helm value is what activates the Gateway API provider — the mode in which Traefik watches for and acts on GatewayClass, Gateway, and HTTPRoute objects. |
| D | ✅ Yes | from: Same restricts route attachment to HTTPRoutes in the same namespace as the Gateway. Other options are All (any namespace) and Selector (namespaces matching a label selector). This is the cluster operator's enforcement mechanism for role separation. |
| E | ❌ No | A PROGRAMMED: True Gateway means Traefik has configured its listener infrastructure — it is ready to accept routes. But with no HTTPRoutes attached, there are no routing rules defined. Requests will receive a 404 or be dropped. Traffic only flows after HTTPRoutes are created and attached to the Gateway. |
6. Lesson 06: Your First HTTPRoute Duration: ~30 minutes folder:...
1 pts| Option | Correct? | Explanation |
|---|---|---|
| A | ✅ Yes | allowedRoutes.namespaces.from: Same is enforced by the Gateway controller. If an HTTPRoute in a different namespace tries to attach, the attachment is refused and the HTTPRoute status shows Accepted: False. This is how the cluster operator controls which teams can use a Gateway. |
| B | ✅ Yes | parentRefs is the attachment mechanism — it declares which Gateway the HTTPRoute wants to attach to. The attachment is only permitted if the Gateway's allowedRoutes configuration allows it. This is a bidirectional relationship: the HTTPRoute requests, the Gateway approves. |
| C | ✅ Yes | Header matching (headers) and method matching (method) are both defined in the HTTPRoute matches field in the core Gateway API specification. They are portable features that work with Traefik, Envoy Gateway, Istio, and any other conformant implementation — no annotations required. |
| D | ❌ No | A missing Service does not cause Accepted: False. Accepted refers to whether the Gateway accepted the route's attachment. A missing Service causes ResolvedRefs: False — a separate status condition that indicates the backendRef could not be resolved. Both are in the HTTPRoute status but represent different checks. |
| E | ✅ Yes | This is one of the core design benefits of the Gateway API. Because Gateway and HTTPRoute are distinct Kubernetes resource types, standard RBAC policies can grant developers permission to create/update HTTPRoutes in their namespace without giving them any access to Gateway or GatewayClass resources. With Ingress, this separation was impossible — it was all one resource. |
7. Lesson 07: Securing Routes with TLS Duration: ~25 minutesfolder:...
1 pts| Option | Correct? | Explanation |
|---|---|---|
| A | ✅ Yes | Without sectionName, an HTTPRoute attaches to all matching listeners on the Gateway. By specifying sectionName: https, the HTTPRoute attaches only to the HTTPS listener. This is critical for the redirect setup: the redirect route uses sectionName: http (port 80 only) and the main route uses sectionName: https (port 443 only). |
| B | ✅ Yes | Terminate is the standard TLS mode for edge termination. Traefik handles the TLS handshake and decryption. The connection from Traefik to the backend Service is plain HTTP inside the cluster network. The alternative mode, Passthrough, forwards encrypted traffic without decrypting it. |
| C | ❌ No | The TLS Secret must be in the same namespace as the Gateway, not the HTTPRoute. The Gateway's HTTPS listener references the Secret via certificateRefs, and the controller reads it from the Gateway's namespace. In the bookstore setup, both the Gateway and the bookstore-tls Secret are in the bookstore namespace. |
| D | ✅ Yes | A RequestRedirect rule returns an HTTP redirect response (301 or 302) directly to the client. The rule does not forward traffic anywhere — there is intentionally no backendRefs field. This is why it is used on the HTTP listener: the redirect is the response, not a proxy to a backend. |
| E | ❌ No | mkcert's CA is a local Certificate Authority. It is only trusted on the machine where mkcert -install was run. Browsers on other machines will show certificate warnings. mkcert is designed for local development only. Production deployments use a publicly-trusted CA such as Let's Encrypt (via cert-manager, covered in Lesson 10). |
8. Lesson 08: Advanced Routing Patterns Duration: ~30 minutesfolder:...
1 pts| Option | Correct? | Explanation |
|---|---|---|
| A | ❌ No | Traffic splitting with weights is probabilistic, not deterministic. Traefik randomly selects a backend on each request according to the weight ratio. Over a large number of requests the distribution approaches 90/10, but individual windows of 10 requests may not be exactly 9:1. There is no guaranteed round-robin sequence. |
| B | ✅ Yes | The ReferenceGrant is placed in the namespace that owns the target resource (the Service). This design is intentional: the target namespace controls who can access its resources. A team managing the monitoring namespace grants permission by creating a ReferenceGrant there — not the team doing the routing. |
| C | ✅ Yes | Traefik Middleware CRDs use apiVersion: traefik.io/v1alpha1. To connect a Middleware to an HTTPRoute rule, you add a filter of type: ExtensionRef with extensionRef.group: traefik.io, extensionRef.kind: Middleware, and extensionRef.name pointing to the Middleware resource name. |
| D | ✅ Yes | ReplacePrefixMatch only replaces the matched prefix portion of the path. The remainder is appended. So /shop/featured with prefix /shop → /api/v1/books produces /api/v1/books/featured at the backend. To replace the entire path regardless of suffix, use ReplaceFullPath instead. |
| E | ❌ No | IngressRoute is a Traefik-specific CRD that only works with the Traefik controller. It is not portable to other Gateway API implementations (Envoy Gateway, Istio, Cilium, etc.). The recommended approach for new installations and portability is HTTPRoute (the Gateway API standard). IngressRoute is valuable for specific Traefik-only use cases like ExternalName service routing or legacy migrations. |
9. Lesson 09: Completing the Migration Duration: ~20 minutes Overview You have...
1 pts| Option | Correct? | Explanation |
|---|---|---|
| A | ❌ No | helm uninstall removes only the resources that were created by that Helm release — the controller Deployment, Service, RBAC, ConfigMaps, and the IngressClass. It does not remove Ingress resources in application namespaces (like bookstore-ingress in the bookstore namespace), because those are not owned by the Helm release. They must be deleted manually (Step 4 of the migration). |
| B | ✅ Yes | Scaling to zero is the recommended first step precisely because it is reversible in seconds. If Traefik has a routing issue, kubectl scale ... --replicas=1 immediately restores ingress-nginx. This is much faster than a full Helm reinstall. The lesson follows this staged approach to minimise risk. |
| C | ❌ No | Once helm uninstall has run, the ingress-nginx Deployment no longer exists — you cannot scale a deleted Deployment back up. Rollback after uninstall requires a full helm install command to reinstall the controller. This is why the lesson separates "scale to zero" (reversible) from "helm uninstall" (permanent), and requires all tests to pass before the final uninstall. |
| D | ✅ Yes | k3s's ServiceLB allocates host ports on a first-come, first-served basis. Since ingress-nginx was installed first (Lesson 04), it owns ports 80 and 443. Scaling it to zero releases those ports, at which point k3s's ServiceLB can assign them to Traefik's LoadBalancer Service. |
| E | ❌ No | IngressClass objects are cluster-scoped, not namespace-scoped. Deleting the ingress-nginx namespace does not remove the IngressClass resource. It must be deleted separately with kubectl delete ingressclass nginx. The troubleshooting section of the lesson specifically calls this out as a step to verify. |
10. Lesson 10: Production Considerations Duration: ~20 minutes | Level:...
1 pts| Option | Correct? | Explanation |
|---|---|---|
| A | ✅ Yes | cert-manager's HTTP-01 Gateway API solver creates a temporary HTTPRoute on the existing Gateway to serve the ACME challenge at /.well-known/acme-challenge/<token>. This reuses the existing infrastructure and avoids the need for additional ports or a separate LoadBalancer for domain verification. |
| B | ✅ Yes | A PodDisruptionBudget limits the number of pods that can be voluntarily disrupted at once. With minAvailable: 1, Kubernetes will not drain a node if doing so would bring the Traefik pod count below 1. Combined with a minimum of 2 replicas (via HPA), this ensures continuous availability during planned maintenance. |
| C | ❌ No | The correct approach is the opposite: application developers should be granted permission to manage HTTPRoutes only within their own namespace — not cluster-wide. The RBAC example in the lesson creates a Role (namespace-scoped, not ClusterRole) that allows creating/updating HTTPRoutes in the bookstore namespace. Cluster-wide access would break the role separation model that Gateway API is designed to enforce. |
| D | ✅ Yes | Let's Encrypt production has a rate limit of 5 certificate issuances per registered domain per week. If your cert-manager configuration has a bug that causes repeated failed requests, you can quickly exhaust these limits. The staging environment has much higher limits and issues certificates that are not publicly trusted, making it ideal for testing configuration. |
| E | ✅ Yes | Traefik's JSON access log format emits one structured JSON object per request, containing fields like RequestMethod, RequestPath, DownstreamStatus, Duration, and the backend ServiceName. This format is directly ingestible by tools like Loki, Datadog, and the Elastic Stack without custom log parsing rules. |
| F | ✅ Yes | nginx defaults to proxy_request_buffering on, buffering the entire request body and forwarding it with Content-Length. Traefik streams request bodies as they arrive without buffering. WSGI-based backends (uWSGI) cannot handle chunked transfer encoding — wsgi.input.read() returns empty data without Content-Length. The buffering middleware reads the full body, then forwards it with Content-Length, replicating nginx’s default behavior. |
11. Lesson 11: Common Migration Pitfalls Duration: ~15 minutes | Level:...
1 pts| Option | Correct? | Explanation |
|---|---|---|
| A | ✅ Yes | Traefik’s buffering middleware (applied via ExtensionRef on the HTTPRoute) intercepts the request before it reaches the backend. It reads the entire body into a buffer — RAM for small bodies, disk for larger ones — then forwards it with a Content-Length header. This replicates the behavior nginx provided by default with proxy_request_buffering on. |
| B | ❌ No | This is a plausible-sounding but incorrect diagnosis. Traefik is not running out of memory — it is working correctly by design. The issue is that Traefik streams request bodies without holding them in memory, which is the opposite of a memory problem. Increasing resource limits would have no effect on this behavior. |
| C | ✅ Yes | This correctly identifies the root cause. nginx’s proxy_request_buffering on (the default) reads the entire request body, de-chunks it if needed, and forwards it with a Content-Length header. Traefik’s default streaming behavior preserves the client’s transfer encoding — if the client sends chunked, the backend receives chunked. |
| D | ❌ No | The requests are not malformed. Chunked transfer encoding is a valid part of HTTP/1.1 (RFC 9112). Both nginx and Traefik accept the requests successfully — the difference is in how they forward the body to the backend. The 200 OK responses confirm that Traefik accepted and proxied the request without error. |
| E | ✅ Yes | The WSGI specification (PEP 3333) predates widespread use of chunked transfer encoding and relies on CONTENT_LENGTH to determine how many bytes to read. uWSGI follows this spec strictly: without Content-Length, the internal content length is zero, and wsgi.input.read() returns an empty byte string. This is why the application sees zero-byte files — the HTTP request was valid, but the WSGI layer could not access the body. |
Correct answers: A, C, E
12. Lesson 12: Extending Traefik with Custom Plugins Duration: ~25 minutes |...
1 ptsAnswers
| Option | Correct? | Explanation |
|---|---|---|
| A | No | Traefik plugins are NOT compiled. They are interpreted at runtime by Yaegi. There is no go build step — Yaegi reads the Go source files directly and interprets them. This is why plugins can be loaded without rebuilding the Traefik Docker image. |
| B | Yes | The plugin name in spec.plugin.<name> must match the key under experimental.localPlugins.<name>. This mapping is how Traefik knows which plugin implementation to use when processing the Middleware CRD. A mismatch causes the middleware to be silently ignored. |
| C | Yes | Yaegi is a Go interpreter embedded in Traefik that executes plugin source code at runtime. Plugins are loaded as raw .go files at Traefik startup — no compilation, no binary artifacts. This is what makes the inlinePlugin approach possible. |
| D | Yes | The Helm chart's inlinePlugin type creates a ConfigMap from the source field contents (go.mod, .traefik.yml, main.go). This ConfigMap is mounted as a read-only volume at the specified mountPath in the Traefik pod. Traefik's local plugin loader reads from this path. |
| E | No | Yaegi can only interpret Go standard library packages. Third-party modules from github.com or other registries are NOT automatically downloaded or supported. Plugins must use only the Go standard library unless the dependency is vendored (copied) into the plugin source directory. |
Correct answers: B, C, D