{"id":"l-dk-5-2","courseId":"docker-kubernetes","lang":"en","kind":"lesson","summary":"Set CPU and memory requests and limits for notes-api from measurements, predict throttling, OOM kills and QoS classes, and scale replicas automatically with an autoscaling/v2 HorizontalPodAutoscaler.","html":"<p>The first time notes-api met real traffic on a shared cluster, two things happened within an hour. A batch job on the same node used all the memory and one notes-api Pod was evicted. Then a traffic spike pegged the CPU, latency went to seconds, and there were still exactly three Pods because nobody had told Kubernetes it could add more. Both failures came from the same gap: Kubernetes didn’t know what notes-api needed.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Requests and limits</h2>\n<p>Every container can declare two numbers per resource:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"0\"><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:#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\">api</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          image</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">ghcr.io/skylane/notes-api:1.4.0</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          resources</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            requests</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">              cpu</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">250m</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">              memory</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">256Mi</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            limits</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">              memory</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">512Mi</span></span></code></pre></div>\n<ul>\n<li>A <strong>request</strong> is what the scheduler reserves. A Pod is only placed on a node with enough unreserved capacity for all its requests. <code dir=\"ltr\">250m</code> is 250 millicores, a quarter of a CPU core.</li>\n<li>A <strong>limit</strong> is the ceiling the kernel enforces through cgroups, the mechanism you met in lesson 1.1.</li>\n</ul>\n<p>The two resources behave very differently when a container hits its limit:</p>\n<ul>\n<li><strong>CPU is compressible.</strong> Over the CPU limit, the container is throttled: it gets less CPU time and gets slower, but keeps running.</li>\n<li><strong>Memory isn’t.</strong> Over the memory limit, the kernel’s OOM killer ends the process. <code dir=\"ltr\">kubectl describe pod</code> shows <code dir=\"ltr\">OOMKilled</code> and exit code 137, and the container restarts.</li>\n</ul>\n<p>If you set a limit without a request, Kubernetes copies the limit into the request. A request larger than its limit is rejected outright.</p>\n<aside class=\"callout callout-why\" 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\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">Why this matters</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Why notes-api has no CPU limit</span></p>\n<p>This is contested, so here’s my reasoning. A CPU limit throttles the app even when the node has idle cores, which shows up as latency spikes nobody can explain. With a sensible CPU request, the scheduler already guarantees notes-api its share under contention, so we leave the CPU limit off and set a memory limit, because running out of memory hurts the whole node. Some organisations require CPU limits by policy; if yours does, set them generously and watch throttling metrics.</p>\n</aside>\n<p>For Node.js, give V8 a heap ceiling below the container’s memory limit, for example <code dir=\"ltr\">NODE_OPTIONS=--max-old-space-size=384</code> with a 512Mi limit. Then the garbage collector works hard as memory fills, instead of the kernel ending the process without warning.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Choosing the numbers</h2>\n<p>Don’t guess. Run notes-api under realistic load and measure:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"1\"><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\"> top</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></code></pre></div>\n<p><code dir=\"ltr\">kubectl top</code> needs metrics-server, which many managed clusters include or offer as an add-on; on kind you install it yourself. Set the CPU request near typical usage under normal load, and the memory request and limit with headroom above the peak you observed. Revisit the numbers after major releases. Requests that are too high waste money on idle reservations; too low, and Pods are packed onto nodes that can’t actually carry them.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">QoS classes and eviction</h2>\n<p>From requests and limits, Kubernetes derives a <strong>Quality of Service class</strong> for each Pod:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>Class</th>\n<th>Rule</th>\n<th>Evicted under node memory pressure</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Guaranteed</td>\n<td>Every container has CPU and memory requests equal to limits</td>\n<td>Last</td>\n</tr>\n<tr>\n<td>Burstable</td>\n<td>At least one request or limit set, but not Guaranteed</td>\n<td>After BestEffort, starting with Pods furthest over their requests</td>\n</tr>\n<tr>\n<td>BestEffort</td>\n<td>No requests or limits anywhere</td>\n<td>First</td>\n</tr>\n</tbody>\n</table></div><p>notes-api above is Burstable. The batch job that caused our eviction was BestEffort, so with requests in place it would have been evicted before notes-api. Running production workloads as BestEffort means volunteering them to go first.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Horizontal Pod Autoscaling</h2>\n<p>The HorizontalPodAutoscaler (HPA) adjusts a Deployment’s replica count from metrics. It’s another control loop: every 15 seconds or so it compares observed usage with a target and computes the replica count that would bring usage back to the target.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">k8s/hpa.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\">autoscaling/v2</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\">HorizontalPodAutoscaler</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\">  scaleTargetRef</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<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\">apps/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\">Deployment</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\">  minReplicas</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  maxReplicas</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">12</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  metrics</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\">type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">Resource</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      resource</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\">cpu</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        target</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\">Utilization</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          averageUtilization</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">70</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  behavior</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    scaleDown</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      stabilizationWindowSeconds</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">300</span></span></code></pre></div>\n<p><code dir=\"ltr\">averageUtilization: 70</code> means “keep average CPU usage at 70% of the CPU <strong>request</strong>”. With a 250m request, that’s about 175m per Pod. At six Pods averaging 350m, the HPA computes 6 × 350 / 175 = 12 and scales to the maximum. The <code dir=\"ltr\">behavior</code> block makes scale-down wait for five minutes of consistently lower load, so a brief lull doesn’t remove capacity you need a minute later. Five minutes is already the default; writing it down makes the choice visible and easy to tune.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 220\" role=\"img\" aria-labelledby=\"l-dk-5-2-t1\"><defs><marker id=\"arrow-l-dk-5-2-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-2-t1\">metrics-server collects CPU usage from the Pods. The HPA reads it, compares usage with 70 percent of requests, and updates the Deployment's replicas, which changes the number of Pods.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"80\" width=\"150\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"95\" y=\"115\" text-anchor=\"middle\">metrics-server</text>\n  <rect class=\"d-box-primary\" x=\"240\" y=\"80\" width=\"200\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"340\" y=\"106\" text-anchor=\"middle\">HPA</text>\n  <text class=\"d-code\" x=\"340\" y=\"128\" text-anchor=\"middle\">target 70% of request</text>\n  <rect class=\"d-box-accent\" x=\"510\" y=\"80\" width=\"170\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"595\" y=\"106\" text-anchor=\"middle\">Deployment</text>\n  <text class=\"d-code\" x=\"595\" y=\"128\" text-anchor=\"middle\">replicas: 3 → 6</text>\n  <path class=\"d-arrow\" d=\"M170 110 L238 110\" marker-end=\"url(#arrow-l-dk-5-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M440 110 L508 110\" marker-end=\"url(#arrow-l-dk-5-2-1)\"/>\n  <rect class=\"d-box-success\" x=\"300\" y=\"170\" width=\"250\" height=\"40\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"425\" y=\"195\" text-anchor=\"middle\">notes-api Pods</text>\n  <path class=\"d-arrow\" d=\"M595 140 L520 168\" marker-end=\"url(#arrow-l-dk-5-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M300 190 L95 142\" marker-end=\"url(#arrow-l-dk-5-2-1)\"/>\n  <text class=\"d-label-muted\" x=\"170\" y=\"185\" text-anchor=\"middle\">CPU usage</text>\n  <text class=\"d-label-muted\" x=\"350\" y=\"60\" text-anchor=\"middle\">every ~15 s</text>\n</svg><figcaption>The HPA is a control loop over the Deployment’s replica count</figcaption></figure>\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>Two owners for one field</span></p>\n<p>Once an HPA manages notes-api, delete <code dir=\"ltr\">replicas:</code> from <code dir=\"ltr\">k8s/deployment.yaml</code>. Otherwise every <code dir=\"ltr\">kubectl apply</code> sets the count back to the file’s value, perhaps 3 in the middle of a traffic peak at 9, and the HPA has to climb back up while users wait. One field, one owner.</p>\n</aside>\n<p>Watch it work with <code dir=\"ltr\">kubectl get hpa notes-api --watch</code> while you run a load test. If the targets column shows <code dir=\"ltr\">&lt;unknown&gt;</code>, either the containers have no CPU request or metrics-server isn’t running.</p>\n<p>CPU suits notes-api because its work is CPU-bound request handling. Services whose load shows up elsewhere, like queue depth or requests per second, can scale on custom or external metrics through the same <code dir=\"ltr\">metrics</code> list, with an adapter installed in the cluster.</p>\n<p>notes-api now has the resources it needs and grows with load. The next lesson changes the image safely: rolling updates, rollbacks and packaging the whole thing with Helm.</p>\n","toc":[{"id":"sec-1","text":"Requests and limits","level":2},{"id":"sec-2","text":"Choosing the numbers","level":2},{"id":"sec-3","text":"QoS classes and eviction","level":2},{"id":"sec-4","text":"Horizontal Pod Autoscaling","level":2}],"takeaways":["Requests are what the scheduler reserves for a container; limits are the ceiling enforced at run time.","Going over a CPU limit throttles the container; going over a memory limit gets it OOM-killed with exit code 137.","QoS class follows from requests and limits: Guaranteed (equal for every container), Burstable (some set), BestEffort (none), and BestEffort Pods are evicted first.","An HPA’s CPU utilization target is a percentage of the CPU <em>request</em>, so it can’t work on containers without one.","When an HPA manages a Deployment, remove <code dir=\"ltr\">replicas</code> from the Deployment manifest so <code dir=\"ltr\">kubectl apply</code> doesn’t fight the autoscaler."],"further":[{"title":"Resource Management for Pods and Containers","url":"https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/"},{"title":"Pod Quality of Service Classes","url":"https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/"},{"title":"Horizontal Pod Autoscaling","url":"https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/"}],"quiz":[{"q":"<p>A notes-api Pod restarts several times a day. <code dir=\"ltr\">kubectl describe pod</code> shows <code dir=\"ltr\">Reason: OOMKilled</code> and <code dir=\"ltr\">Exit Code: 137</code>. What happened?</p>","options":[{"text":"The CPU limit was too low, so the kernel killed the container.","why":"CPU over the limit is throttled (slowed down), never killed."},{"text":"The liveness probe failed and Kubernetes restarted the container.","why":"A liveness restart shows a probe failure in the events, not <code dir=\"ltr\">OOMKilled</code> as the reason."},{"text":"The container used more memory than its limit, and the kernel’s OOM killer ended it.","why":"Correct. Raise the memory limit to fit real usage, or find the leak; for Node.js, also cap the V8 heap below the limit."},{"text":"The node ran out of disk space for logs.","why":"Disk pressure leads to eviction with a different reason, not an OOM kill."}],"answer":2},{"q":"<p>An HPA targets 70% CPU utilization, and <code dir=\"ltr\">kubectl get hpa</code> shows <code dir=\"ltr\">cpu: &lt;unknown&gt;/70%</code>. What’s the most likely cause?</p>","options":[{"text":"The HPA uses <code dir=\"ltr\">autoscaling/v2</code>, which doesn’t support CPU.","why":"autoscaling/v2 supports CPU, memory, custom and external metrics."},{"text":"The notes-api containers have no CPU request, so utilization can’t be computed (or metrics-server isn’t installed).","why":"Correct. Utilization is usage divided by request. No request, no percentage. Also check that metrics-server is running."},{"text":"<code dir=\"ltr\">minReplicas</code> is higher than the current replica count.","why":"That makes the HPA scale up to the minimum; it doesn’t make metrics unknown."},{"text":"The Deployment has too many replicas for the HPA to manage.","why":"There’s no such limit; the HPA can manage any number within its bounds."}],"answer":1},{"q":"<p>notes-api sets <code dir=\"ltr\">requests: {cpu: 250m, memory: 256Mi}</code> and <code dir=\"ltr\">limits: {memory: 512Mi}</code>. Which QoS class does the Pod get?</p>","options":[{"text":"Guaranteed","why":"Guaranteed needs requests equal to limits for both CPU and memory in every container."},{"text":"BestEffort","why":"BestEffort means no requests or limits at all. This Pod has requests."},{"text":"Critical","why":"That’s not a QoS class. The three classes are Guaranteed, Burstable and BestEffort."},{"text":"Burstable","why":"Correct. It has requests, but they don’t equal limits for every resource, so it can burst above its requests."}],"answer":3},{"q":"<p>notes-api’s Deployment manifest says <code dir=\"ltr\">replicas: 3</code>, and an HPA has scaled it to 9 during a traffic peak. What happens when CI runs <code dir=\"ltr\">kubectl apply -f k8s/</code>?</p>","options":[{"text":"The Deployment drops to 3 replicas until the HPA scales it back up, causing a capacity dip in the middle of the peak.","why":"Correct. Apply sets the field from the file. Remove <code dir=\"ltr\">replicas</code> from the manifest once an HPA owns it."},{"text":"Nothing; the HPA locks the replicas field.","why":"The HPA writes the field, but it doesn’t lock it. Anything else that writes it wins until the next HPA decision."},{"text":"Apply fails with a conflict error.","why":"With ordinary client-side apply there’s no error; it overwrites silently, which is what makes this dangerous."},{"text":"The HPA’s <code dir=\"ltr\">minReplicas</code> changes to 3.","why":"Applying a Deployment never edits the HPA object."}],"answer":0}],"exercise":{"kind":"spot-bug","language":"yaml","code":"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: notes-api\nspec:\n  replicas: 3\n  # …selector and template labels as before\n  template:\n    spec:\n      containers:\n        - name: api\n          image: ghcr.io/skylane/notes-api:1.4.0\n          resources:\n            limits:\n              memory: 512Mi\n---\napiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: notes-api\nspec:\n  scaleTargetRef:\n    apiVersion: apps/v1\n    kind: Deployment\n    name: notes-api\n  minReplicas: 3\n  maxReplicas: 12\n  metrics:\n    - type: Resource\n      resource:\n        name: cpu\n        target:\n          type: Utilization\n          averageUtilization: 70","bugLines":[6,14],"title":"The autoscaler that never scales","prompt":"<p><code dir=\"ltr\">kubectl get hpa notes-api</code> shows <code dir=\"ltr\">cpu: &lt;unknown&gt;/70%</code> even though metrics-server is running, and on busy days the replica count drops to 3 every time CI deploys. Select the <strong>two</strong> lines responsible.</p>\n","hints":["CPU utilization is measured as a percentage of something. Is that something defined for this container?","Which field do both the HPA and every <code dir=\"ltr\">kubectl apply</code> write to?"],"explanation":"<p>Line 14: the container sets only a memory limit, so there’s no CPU request, and the HPA can’t compute utilization as a percentage of it. Add requests:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">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\">          resources</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            requests</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">              cpu</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">250m</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">              memory</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">256Mi</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">            limits</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">              memory</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">512Mi</span></span></code></pre></div>\n<p>Line 6: <code dir=\"ltr\">replicas: 3</code> in the manifest means every <code dir=\"ltr\">kubectl apply</code> resets the count, fighting the HPA. Remove the field once an HPA manages the Deployment; <code dir=\"ltr\">minReplicas</code> now holds the floor.</p>\n","checkLabels":{}},"words":826,"readMinutes":4}