Section 5 · Running in Production
Traffic In, Trouble Out: Gateway API and Debugging
22 min3 min readChoose wisely
Lesson at a glance
Route public HTTPS traffic to notes-api with Gateway API, harden its Pods with a securityContext, and diagnose broken Pods quickly with get, describe, logs, events and kubectl debug.
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
Two things stand between notes-api and real users. It isn’t reachable from the internet yet, and when something breaks at 3 a.m., you need to find out what within minutes. This lesson covers both, and finishes with the security settings that complete the production picture.
Getting HTTP traffic in
A LoadBalancer Service per app works for one service. With ten, you’re paying for ten load balancers, managing ten certificates, and routing by hostname or path is impossible. You want one shared entry point that routes HTTP requests to the right Service.
For years the answer was Ingress. It still works, but the Ingress API is frozen and gets no new features, and the widely used community ingress-nginx controller was retired in March 2026: no more releases or security fixes. The Kubernetes project points new work at Gateway API, which is generally available as gateway.networking.k8s.io/v1. It’s installed as an add-on (its CRDs plus an implementation, such as your cloud’s load balancer controller, Envoy Gateway, Cilium or Istio), and many managed clusters offer it out of the box.
Gateway API splits the job by role:
- GatewayClass: which implementation handles traffic. Set up once by whoever runs the cluster.
- Gateway: an entry point with listeners, ports, hostnames and TLS certificates. Owned by the platform team.
- HTTPRoute: one app’s routing rules, attached to a Gateway. Owned by the app team.
The platform team’s Gateway, once per cluster:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: public
namespace: infra
spec:
gatewayClassName: skylane-lb # provided by your implementation
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: "*.skylane.example"
tls:
mode: Terminate
certificateRefs:
- name: skylane-wildcard-tls
allowedRoutes:
namespaces:
from: AllYour route, in the notes namespace:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: notes-api
namespace: notes
spec:
parentRefs:
- name: public
namespace: infra
hostnames:
- notes.skylane.example
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: notes-api
port: 80kubectl describe httproute notes-api shows whether the Gateway accepted the route, under Status. If you’re migrating existing Ingress objects, the ingress2gateway tool converts them, including many ingress-nginx annotations.
The debugging playbook
When notes-api misbehaves, resist guessing. Follow the same order every time, and let each step choose the next:
kubectl get pods -l app.kubernetes.io/name=notes-api
kubectl describe pod <pod>
kubectl logs <pod> --previous
kubectl events --for pod/<pod> --types=WarningThe STATUS column tells you where to look:
| Status | What it means | Where to look |
|---|---|---|
Pending |
No node can take the Pod | describe: events like Insufficient cpu or unmatched node selectors |
ImagePullBackOff |
The kubelet can’t pull the image | describe: wrong tag, missing pull credentials, wrong architecture |
CreateContainerConfigError |
A referenced ConfigMap or Secret key is missing | describe names the key |
CrashLoopBackOff |
The app starts and exits, repeatedly | logs --previous, plus the exit code in describe (137 = OOM or killed) |
Running, 0/1 ready |
Readiness probe is failing | describe shows probe failures; check what /readyz depends on |
If Pods are healthy but requests fail, work outwards: kubectl get endpointslices -l kubernetes.io/service-name=notes-api (empty means selector or readiness), then the route’s status, then the Gateway.
Distroless images have no shell, so kubectl exec can’t help. Attach an ephemeral debug container instead. It joins the Pod’s network namespace, and with --target it can see the app’s processes too:
kubectl debug -it <pod> --image=busybox:1.37 --target=api
# inside: nslookup notes-db, wget -qO- localhost:3000/readyz, psThe last piece: securityContext
In lesson 2.2 you ran the image as non-root and promised to enforce it in the cluster. Here it is, on the notes-api container:
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]runAsNonRoot makes the kubelet refuse to start the container if the image would run as UID 0. It checks a numeric UID, so an image whose USER is a name such as node fails with CreateContainerConfigError; write USER 1000 in the Dockerfile or add runAsUser: 1000 here. Give the app an emptyDir volume at /tmp if it needs scratch space under a read-only root.
That completes notes-api’s path: a cached multi-stage build, a small non-root image pinned by digest and scanned, a Compose stack for development, and a Deployment with probes, resources, autoscaling, safe rollouts, a Gateway route and a hardened security context. Each of those settings is in the file because an incident taught someone it was needed. Keep that habit: when something breaks, ask which setting would have made it impossible, and add it.
Key takeaways
- Gateway API splits external traffic into GatewayClass (the implementation), Gateway (the entry point and TLS) and HTTPRoute (an app’s routing rules), so platform and app teams each own their part.
- Ingress still works but is frozen, and the community ingress-nginx controller was retired in March 2026; new setups should use Gateway API.
- Read Pod status first, then
kubectl describefor events, thenkubectl logs --previousfor crashes; the status tells you which tool to reach for. kubectl debug --targetattaches a temporary container with tools to a running Pod, which is how you inspect distroless images.- A production Pod runs as non-root with no privilege escalation, a read-only root filesystem and all capabilities dropped.