{"id":"l-dk-5-4","courseId":"docker-kubernetes","lang":"en","kind":"lesson","summary":"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.","html":"<p>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.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Getting HTTP traffic in</h2>\n<p>A <code dir=\"ltr\">LoadBalancer</code> 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.</p>\n<p>For years the answer was <strong>Ingress</strong>. 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 <strong>Gateway API</strong>, which is generally available as <code dir=\"ltr\">gateway.networking.k8s.io/v1</code>. 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.</p>\n<p>Gateway API splits the job by role:</p>\n<ul>\n<li><strong>GatewayClass</strong>: which implementation handles traffic. Set up once by whoever runs the cluster.</li>\n<li><strong>Gateway</strong>: an entry point with listeners, ports, hostnames and TLS certificates. Owned by the platform team.</li>\n<li><strong>HTTPRoute</strong>: one app’s routing rules, attached to a Gateway. Owned by the app team.</li>\n</ul>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 220\" role=\"img\" aria-labelledby=\"l-dk-5-4-t1\"><defs><marker id=\"arrow-l-dk-5-4-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-dk-5-4-t1\">GatewayClass is chosen by the cluster operator. The Gateway public in namespace infra terminates TLS on port 443. The HTTPRoute notes-api in namespace notes attaches to it and sends requests for notes.skylane.example to the notes-api Service, which forwards to Pods.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"80\" width=\"120\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"70\" y=\"106\" text-anchor=\"middle\">GatewayClass</text>\n  <text class=\"d-label-muted\" x=\"70\" y=\"126\" text-anchor=\"middle\">cluster ops</text>\n  <rect class=\"d-box-primary\" x=\"155\" y=\"80\" width=\"130\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"220\" y=\"106\" text-anchor=\"middle\">Gateway :443</text>\n  <text class=\"d-label-muted\" x=\"220\" y=\"126\" text-anchor=\"middle\">platform</text>\n  <rect class=\"d-box-accent\" x=\"310\" y=\"80\" width=\"140\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"380\" y=\"106\" text-anchor=\"middle\">HTTPRoute</text>\n  <text class=\"d-label-muted\" x=\"380\" y=\"126\" text-anchor=\"middle\">notes team</text>\n  <rect class=\"d-box-success\" x=\"475\" y=\"80\" width=\"100\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"525\" y=\"115\" text-anchor=\"middle\">Service</text>\n  <rect class=\"d-box-success\" x=\"600\" y=\"80\" width=\"90\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"645\" y=\"115\" text-anchor=\"middle\">Pods</text>\n  <path class=\"d-arrow\" d=\"M130 110 L153 110\" marker-end=\"url(#arrow-l-dk-5-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M285 110 L308 110\" marker-end=\"url(#arrow-l-dk-5-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M450 110 L473 110\" marker-end=\"url(#arrow-l-dk-5-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M575 110 L598 110\" marker-end=\"url(#arrow-l-dk-5-4-1)\"/>\n  <text class=\"d-code\" x=\"380\" y=\"175\" text-anchor=\"middle\">notes.skylane.example → notes-api:80</text>\n</svg><figcaption>Each team owns its layer, and routes attach to a shared Gateway</figcaption></figure>\n<p>The platform team’s Gateway, once per cluster:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">infra/gateway.yaml</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">apiVersion</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">gateway.networking.k8s.io/v1</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">kind</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">Gateway</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">metadata</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">public</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  namespace</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">infra</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">spec</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  gatewayClassName</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">skylane-lb</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">        # provided by your implementation</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  listeners</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">https</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      protocol</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">HTTPS</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      port</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">443</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      hostname</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"*.skylane.example\"</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      tls</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        mode</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">Terminate</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        certificateRefs</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">          - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">skylane-wildcard-tls</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      allowedRoutes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        namespaces</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">All</span></span></code></pre></div>\n<p>Your route, in the <code dir=\"ltr\">notes</code> namespace:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">k8s/httproute.yaml</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">apiVersion</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">gateway.networking.k8s.io/v1</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">kind</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">HTTPRoute</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">metadata</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">notes-api</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  namespace</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">notes</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">spec</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  parentRefs</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">public</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      namespace</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">infra</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  hostnames</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    - </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">notes.skylane.example</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  rules</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">matches</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">path</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">PathPrefix</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            value</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      backendRefs</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        - </span><span style=\"color:#116329;--shiki-dark:#7EE787\">name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">notes-api</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          port</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">80</span></span></code></pre></div>\n<p><code dir=\"ltr\">kubectl describe httproute notes-api</code> shows whether the Gateway accepted the route, under <code dir=\"ltr\">Status</code>. If you’re migrating existing Ingress objects, the <code dir=\"ltr\">ingress2gateway</code> tool converts them, including many ingress-nginx annotations.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">The debugging playbook</h2>\n<p>When notes-api misbehaves, resist guessing. Follow the same order every time, and let each step choose the next:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">kubectl</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> get</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> pods</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -l</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> app.kubernetes.io/name=notes-api</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">kubectl</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> describe</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> pod</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x3C;</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">po</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">d</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">></span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">kubectl</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> logs</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x3C;</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">po</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">d</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">></span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --previous</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">kubectl</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> events</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --for</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> pod/</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">po</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">d</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">></span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --types=Warning</span></span></code></pre></div>\n<p>The <code dir=\"ltr\">STATUS</code> column tells you where to look:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>Status</th>\n<th>What it means</th>\n<th>Where to look</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code dir=\"ltr\">Pending</code></td>\n<td>No node can take the Pod</td>\n<td><code dir=\"ltr\">describe</code>: events like <code dir=\"ltr\">Insufficient cpu</code> or unmatched node selectors</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">ImagePullBackOff</code></td>\n<td>The kubelet can’t pull the image</td>\n<td><code dir=\"ltr\">describe</code>: wrong tag, missing pull credentials, wrong architecture</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">CreateContainerConfigError</code></td>\n<td>A referenced ConfigMap or Secret key is missing</td>\n<td><code dir=\"ltr\">describe</code> names the key</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">CrashLoopBackOff</code></td>\n<td>The app starts and exits, repeatedly</td>\n<td><code dir=\"ltr\">logs --previous</code>, plus the exit code in <code dir=\"ltr\">describe</code> (137 = OOM or killed)</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">Running</code>, <code dir=\"ltr\">0/1</code> ready</td>\n<td>Readiness probe is failing</td>\n<td><code dir=\"ltr\">describe</code> shows probe failures; check what <code dir=\"ltr\">/readyz</code> depends on</td>\n</tr>\n</tbody>\n</table></div><p>If Pods are healthy but requests fail, work outwards: <code dir=\"ltr\">kubectl get endpointslices -l kubernetes.io/service-name=notes-api</code> (empty means selector or readiness), then the route’s status, then the Gateway.</p>\n<p>Distroless images have no shell, so <code dir=\"ltr\">kubectl exec</code> can’t help. Attach an <strong>ephemeral debug container</strong> instead. It joins the Pod’s network namespace, and with <code dir=\"ltr\">--target</code> it can see the app’s processes too:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">kubectl</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> debug</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -it</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x3C;</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">po</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">d</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">></span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --image=busybox:1.37</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --target=api</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># inside: nslookup notes-db, wget -qO- localhost:3000/readyz, ps</span></span></code></pre></div>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Deleting the evidence</span></p>\n<p>The reflex during an incident is <code dir=\"ltr\">kubectl delete pod</code> to “make it restart cleanly”. The replacement crashes the same way, and the previous container’s logs and events go with the old Pod. Read <code dir=\"ltr\">describe</code> and <code dir=\"ltr\">logs --previous</code> first. Restarting is for after you know why.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">The last piece: securityContext</h2>\n<p>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:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">k8s/deployment.yaml</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          securityContext</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            runAsNonRoot</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            allowPrivilegeEscalation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            readOnlyRootFilesystem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            capabilities</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">              drop</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"ALL\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span></code></pre></div>\n<p><code dir=\"ltr\">runAsNonRoot</code> 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 <code dir=\"ltr\">USER</code> is a name such as <code dir=\"ltr\">node</code> fails with <code dir=\"ltr\">CreateContainerConfigError</code>; write <code dir=\"ltr\">USER 1000</code> in the Dockerfile or add <code dir=\"ltr\">runAsUser: 1000</code> here. Give the app an <code dir=\"ltr\">emptyDir</code> volume at <code dir=\"ltr\">/tmp</code> if it needs scratch space under a read-only root.</p>\n<p>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.</p>\n","toc":[{"id":"sec-1","text":"Getting HTTP traffic in","level":2},{"id":"sec-2","text":"The debugging playbook","level":2},{"id":"sec-3","text":"The last piece: securityContext","level":2}],"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 <code dir=\"ltr\">kubectl describe</code> for events, then <code dir=\"ltr\">kubectl logs --previous</code> for crashes; the status tells you which tool to reach for.","<code dir=\"ltr\">kubectl debug --target</code> attaches 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."],"further":[{"title":"Gateway API","url":"https://kubernetes.io/docs/concepts/services-networking/gateway/"},{"title":"Debug Running Pods","url":"https://kubernetes.io/docs/tasks/debug/debug-application/debug-running-pod/"},{"title":"Configure a Security Context for a Pod or Container","url":"https://kubernetes.io/docs/tasks/configure-pod-container/security-context/"},{"title":"Ingress NGINX Retirement","url":"https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement/"}],"quiz":[{"q":"<p>Your platform team runs a shared Gateway in the <code dir=\"ltr\">infra</code> namespace. What do you, the notes-api team, create to expose <code dir=\"ltr\">notes.skylane.example</code>?</p>","options":[{"text":"A new GatewayClass for notes-api.","why":"GatewayClasses describe an implementation and are usually set up once per cluster by the platform team."},{"text":"An HTTPRoute in the <code dir=\"ltr\">notes</code> namespace that references the shared Gateway in <code dir=\"ltr\">parentRefs</code> and the <code dir=\"ltr\">notes-api</code> Service in <code dir=\"ltr\">backendRefs</code>.","why":"Correct. Routes belong to app teams; the Gateway decides (via <code dir=\"ltr\">allowedRoutes</code>) which namespaces may attach."},{"text":"A second Gateway listening on port 443.","why":"That duplicates the platform’s entry point and its TLS setup. Attach a route to the existing one."},{"text":"A LoadBalancer Service for notes-api.","why":"That bypasses the shared entry point and gives you a separate public address to manage."}],"answer":1},{"q":"<p>A Pod shows <code dir=\"ltr\">ImagePullBackOff</code>. Which is the most useful next step?</p>","options":[{"text":"<code dir=\"ltr\">kubectl logs &lt;pod&gt;</code>","why":"The container never started, so there are no logs to read."},{"text":"<code dir=\"ltr\">kubectl rollout restart deployment/notes-api</code>","why":"New Pods will fail to pull the same image the same way."},{"text":"<code dir=\"ltr\">kubectl debug -it &lt;pod&gt; --image=busybox:1.37</code>","why":"There’s no running container to attach to, and the problem is the pull, not the Pod’s runtime."},{"text":"<code dir=\"ltr\">kubectl describe pod &lt;pod&gt;</code> and read the pull error in the events: wrong tag, missing registry credentials, or no image for this architecture.","why":"Correct. The events state exactly why the kubelet couldn’t pull."}],"answer":3},{"q":"<p>notes-api is in <code dir=\"ltr\">CrashLoopBackOff</code>. <code dir=\"ltr\">kubectl logs &lt;pod&gt;</code> shows only a line or two of startup output. What should you run?</p>","options":[{"text":"<code dir=\"ltr\">kubectl logs &lt;pod&gt; --previous</code>, to read the output of the container instance that crashed.","why":"Correct. The current instance may have only just started. <code dir=\"ltr\">--previous</code> shows the last one’s output, including the error that killed it."},{"text":"<code dir=\"ltr\">kubectl delete pod &lt;pod&gt;</code>, so it starts fresh.","why":"The replacement runs the same image with the same config and crashes the same way, and you’ve lost the evidence."},{"text":"<code dir=\"ltr\">kubectl scale deployment/notes-api --replicas=0</code>","why":"That stops the crash loop by stopping the service, without telling you anything."},{"text":"<code dir=\"ltr\">kubectl get nodes</code>","why":"Node health rarely causes a crash loop in a single app; the app’s own output is the place to start."}],"answer":0},{"q":"<p>notes-api’s image is distroless, so <code dir=\"ltr\">kubectl exec -it &lt;pod&gt; -- sh</code> fails. How do you check DNS resolution from inside the Pod?</p>","options":[{"text":"Rebuild the image with a shell, deploy it, and try again.","why":"It changes the thing you’re debugging and takes a full release cycle."},{"text":"Use <code dir=\"ltr\">kubectl port-forward</code> and run <code dir=\"ltr\">nslookup</code> on your laptop.","why":"That resolves names with your laptop’s DNS, not the Pod’s."},{"text":"<code dir=\"ltr\">kubectl debug -it &lt;pod&gt; --image=busybox:1.37 --target=api</code>, then run <code dir=\"ltr\">nslookup notes-db</code> in the debug container.","why":"Correct. The ephemeral container shares the Pod’s network namespace (and, with <code dir=\"ltr\">--target</code>, its process view), bringing the tools the image lacks."},{"text":"Read <code dir=\"ltr\">/etc/resolv.conf</code> with <code dir=\"ltr\">kubectl logs</code>.","why":"Logs show what the process printed, not files inside the container."}],"answer":2}],"exercise":{"kind":"choice","multiple":true,"options":["crash-logs","pending-describe","pull-logs","notready-probe","crash-delete"],"correct":["crash-logs","pending-describe","notready-probe"],"title":"First move for each symptom","prompt":"<p>You’re on call for notes-api. For each symptom below, a teammate proposes a first move. Select <strong>every</strong> pairing where the first move is the right one.</p>\n","hints":["Ask of each: has a container actually run yet? If not, it has no logs.","Which moves gather evidence, and which ones destroy it?","Three pairings are right."],"explanation":"<p>A crash loop leaves its error in the previous container’s output, so <code dir=\"ltr\">logs --previous</code> comes first. A Pending Pod was never scheduled; <code dir=\"ltr\">describe</code> shows the scheduler’s reason. A Running Pod that isn’t ready is failing its readiness probe; <code dir=\"ltr\">describe</code> shows the failures and <code dir=\"ltr\">/readyz</code> tells you which dependency is down. An image pull failure has no logs because no container ran; read the pull error in <code dir=\"ltr\">describe</code>. Deleting a crashing Pod throws away its logs and events and the replacement crashes the same way.</p>\n","checkLabels":{},"optionText":{"crash-logs":{"text":"<code dir=\"ltr\">CrashLoopBackOff</code> → <code dir=\"ltr\">kubectl logs &lt;pod&gt; --previous</code>","why":"Correct. The crashed instance’s output usually contains the error that killed it."},"pending-describe":{"text":"<code dir=\"ltr\">Pending</code> for five minutes → <code dir=\"ltr\">kubectl describe pod &lt;pod&gt;</code> and read the scheduling events","why":"Correct. Events like <code dir=\"ltr\">Insufficient memory</code> or an unmatched node selector explain why no node accepted it."},"pull-logs":{"text":"<code dir=\"ltr\">ImagePullBackOff</code> → <code dir=\"ltr\">kubectl logs &lt;pod&gt;</code>","why":"The container never started, so there are no logs. <code dir=\"ltr\">describe</code> shows the pull error."},"notready-probe":{"text":"<code dir=\"ltr\">Running</code> but <code dir=\"ltr\">0/1</code> ready → <code dir=\"ltr\">kubectl describe pod &lt;pod&gt;</code> for probe failures, then check what <code dir=\"ltr\">/readyz</code> depends on","why":"Correct. Readiness failures are recorded as events, and the endpoint’s dependencies are usually the cause."},"crash-delete":{"text":"<code dir=\"ltr\">CrashLoopBackOff</code> → <code dir=\"ltr\">kubectl delete pod &lt;pod&gt;</code> so it restarts cleanly","why":"The replacement runs the same image and config and crashes the same way, and the old Pod’s evidence is gone."}}},"words":676,"readMinutes":3}