AI EngineeringZero to ProductionHome·About·What’s new·Contact
Kubernetes Orchestration · Part 3

Config, health & autoscaling

A Deployment that runs isn't a Deployment that's production-ready. This lesson is the hardening layer: liveness / readiness / startup probes so the cluster knows when a Pod is healthy and when to send it traffic; resource requests & limits so the scheduler can pack nodes and one Pod can't starve the rest; rolling updates & rollbacks for zero-downtime deploys; the HorizontalPodAutoscaler to add replicas under load; and a PodDisruptionBudget so maintenance never takes you below quorum.

⏱️ ~90 min☸️ Reliability🎯 Intermediate→Advanced
🌱 What changes hereSame app as K2, now made resilient. Every YAML block needs a cluster to see behavior — minikube/kind locally or EKS (K5) — and terminal output is illustrative. Autoscaling also needs a metrics-server installed. As always, verify probe/HPA field names and the current autoscaling API version against the Kubernetes docs.

Learning objectives

  • Configure liveness, readiness, and startup probes and say what each one gates.
  • Set resource requests (for scheduling) and limits (for capping), and explain QoS.
  • Perform a zero-downtime rolling update and a rollback.
  • Add a HorizontalPodAutoscaler that scales on CPU (and understand custom metrics).
  • Protect availability during maintenance with a PodDisruptionBudget.
📋 Before you startThis page builds on: Pods, Deployments & Services. Jumping in? Skim those first — or open the recap.
30-second recap
  • Pods, Deployments & Services: A Pod runs containers, a Deployment keeps N identical Pods alive and rolls out updates, a Service gives them one stable address.

1 · The three probes — and what each gates

Here's the outage nobody forgets. Your app is fine — a little slow to boot because it warms a model cache — but Kubernetes decides it's dead, kills it, and restarts it. It's slow to boot again, so Kubernetes kills it again. You've built a machine that murders your app on a loop, and the app was never actually broken. That whole disaster comes from getting probes wrong, which is why we start here.

Think of probes as the questions a hospital nurse asks a patient, not a single "are you OK?" A nurse doesn't collapse three very different checks into one. "Are you conscious?" (if not, we intervene) is a different question from "Are you ready to be discharged and see visitors?" (if not, we just keep you resting) — and both differ from "Have you woken up from the anaesthetic yet?" (don't judge the other two until they have). Kubernetes has exactly these three questions, and each triggers a different response. The single most common beginner mistake is treating them as interchangeable — copy-pasting the same endpoint into all three — which is precisely how a briefly-slow database turns into a restart storm.

Why does this matter more than it looks? Because a probe isn't passive monitoring — it's a control loop that takes action. Answer the wrong question and Kubernetes takes the wrong action: it pulls a healthy Pod out of rotation, or restarts one that just needed a moment. The three probes are readiness (should you get traffic?), liveness (should you be restarted?), and startup (has boot finished, so hold off the other two?). Readiness gates traffic — failing it quietly removes the Pod from the load balancer with no restart. Liveness gates restarts — failing it kills and recreates the container. Startup is the referee that suspends the first two until a slow-booting app has finished coming up. Now the precise version:

Kubernetes can't read your app's mind; you tell it health via probes. The three do different jobs and confusing them causes real outages:

Pod / container readiness — serve now? liveness — wedged? startup — booted? fail → drop from LB fail → restart holds off the other two Three questions, three different actions. Failing readiness only pulls the Pod from traffic; failing liveness restarts the container; startup suspends both until a slow boot finishes — which is why copy-pasting one endpoint into all three causes outages.
🗺️ How to read this diagram

Read top-to-bottom: the one Pod is checked by three probes (middle row), and each probe's failure triggers a different action (bottom row).

  • The top box is the single container. The three blue arrows show the same Pod being asked three separate questions.
  • The middle row is the three probes: readiness ("can I serve?"), liveness ("am I wedged?"), and startup ("have I booted?").
  • The bottom row is the payoff — the action each failure causes: readiness failing quietly drops you from the load balancer (green, no restart); liveness failing restarts the container (red); startup doesn't punish at all — it just holds off the other two while boot runs.

In short: the three probes are not interchangeable because each failure takes a different action — mixing them is how a brief slowdown becomes a restart storm.

ProbeQuestion it answersWhat failing it does
readiness"Can I serve traffic right now?"Removed from Service endpoints (no restart)
liveness"Am I alive, or wedged?"Container is restarted
startup"Have I finished booting?"Holds off the other probes until boot is done
Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
YAML · all three probes on a container (needs a cluster)
probes.yaml        readinessProbe:            # gates traffic: pull from LB until ready
          httpGet: { path: /healthz/ready, port: 8000 }
          periodSeconds: 5
        livenessProbe:             # gates restart: kill+restart if wedged
          httpGet: { path: /healthz/live, port: 8000 }
          periodSeconds: 10
          failureThreshold: 3
        startupProbe:              # slow starters: don't let liveness kill boot
          httpGet: { path: /healthz/live, port: 8000 }
          failureThreshold: 30
          periodSeconds: 5         # allows up to ~150s to start
The classic probe outagePointing liveness at a slow or dependency-touching endpoint. If liveness fails because your database is briefly slow, Kubernetes restarts the Pod — which doesn't fix the database and can cascade into a restart storm. Rule: liveness = am I wedged (cheap, in-process); readiness = can I serve (may check dependencies); use a startup probe for slow boots so liveness doesn't kill a Pod mid-startup.

2 · Resource requests and limits

This is where cloud bills quietly explode — or where apps mysteriously die at 3 a.m. Set these two numbers too high and you pay for reserved capacity nobody uses; set them too low and Kubernetes packs your Pods onto a node that can't actually feed them, and they start getting throttled or killed under load. Almost every "why is our cluster so expensive?" and every "why does this Pod keep restarting with no error in the logs?" conversation traces back to requests and limits.

Here's an analogy that makes the two numbers click. Booking a restaurant, you tell them "party of four" — that's your request: the restaurant reserves a table for four and won't give it away, even if only two of you show up early. Separately, the fire marshal posts a limit: this room holds at most twenty people, full stop. The request is a guarantee to you (this much is set aside); the limit is a wall around you (you may never exceed it). They are answering completely different questions — "how much do I plan to use?" versus "what's the absolute ceiling?" — and beginners constantly conflate them.

The subtlety that bites hardest is that CPU and memory behave differently when you cross the limit, because one is compressible and the other isn't. CPU is like clock time: if you want more than your cap, Kubernetes simply hands you less — you get throttled, slowed down, but nobody dies. Memory is like physical shelf space: once it's full, you can't be "slowed down" into using less, so the kernel does the only thing it can and OOM-kills the container outright. That asymmetry is the whole reason the guidance differs — you can be a little aggressive with CPU limits (worst case: slow), but a too-low memory limit is a loaded gun (worst case: dead). And the request has a second job people forget: it's the only thing the scheduler looks at when deciding which node a Pod fits on. Get the request wrong and Pods land on nodes that are already overcommitted. Now the precise version:

Two numbers per resource, and they mean different things. The request is what the scheduler reserves to place the Pod (and what other Pods can't use). The limit is the hard cap while running. Getting these right is most of good cluster economics.

one container's resource use request (reserved) ← scheduler packs on this ← limit (hard ceiling) burst room crossing the limit CPU (compressible) → throttled (slowed) memory (incompressible) → OOM-killed (dies) request == limit → Guaranteed QoS Request is a floor, limit is a ceiling. The scheduler only reserves the request; between request and limit is burst room. Cross the limit and CPU is merely throttled while memory is killed outright — which is why memory limits need more care.
🗺️ How to read this diagram

Read the left panel bottom-to-top as a stack of capacity, then the right panel top-to-bottom for what a breach costs.

  • The green band at the bottom is the request — the amount the scheduler sets aside and bin-packs onto a node; other Pods can't claim it.
  • The space above it up to the dashed top line is burst room, capped by the limit (the ceiling).
  • The right panel is the payoff: crossing the limit is harmless-ish for CPU (throttled, green→amber) but fatal for memory (OOM-killed, red). Setting request equal to limit earns the highest-priority Guaranteed QoS class.

In short: size the request from typical use so nodes pack well, and set the memory limit above observed peak — because a too-low memory limit kills the container, it doesn't just slow it.

YAML · requests and limits (needs a cluster)
resources.yaml        resources:
          requests:            # scheduler reserves this; used for bin-packing
            cpu: "250m"        # 0.25 of a core
            memory: "256Mi"
          limits:              # hard cap at runtime
            cpu: "1"           # throttled above 1 core
            memory: "512Mi"    # OOM-killed above 512Mi
BehaviorCPU (compressible)Memory (incompressible)
Over the limitThrottled (slowed, not killed)OOM-killed — the container dies
Set request onlyCan burst above request if node has roomSame — no cap
No request/limit"BestEffort" — first to be evictedFirst to be evicted under pressure
Requests vs limits, the mental modelRequest = a promise to the scheduler ("reserve this much for me"); limit = a wall ("never exceed this"). Set requests from observed typical use so nodes pack efficiently; set memory limits from observed peak plus headroom, because exceeding a memory limit means the container is killed, not slowed. Setting request == limit gives the highest-priority (Guaranteed) QoS class.

3 · Rolling updates and rollbacks

Deploying a new version used to mean a maintenance window and a held breath. You'd take the app down, swap the code, bring it back up, and pray it came back healthy — with users staring at an error page the whole time. Kubernetes makes that ritual obsolete: it can swap versions while the app keeps serving, and if the new one is broken it stops and backs out on its own. That capability is the rolling update, and it's a big part of why teams deploy ten times a day instead of once a quarter.

Picture repainting a long hallway that people are still walking through. You don't close the whole hallway — you rope off one section, paint it, let it dry and reopen it, then move to the next section. At every moment most of the hallway is open, so foot traffic never stops. A rolling update does exactly this with your Pods: it brings up a Pod on the new version, waits until that Pod is actually ready to serve, and only then retires one old Pod — shifting your replicas from all-old to all-new a slice at a time. The number of Pods answering requests barely dips, so users notice nothing.

The load-bearing detail — and the one beginners skip — is the phrase "waits until it's actually ready." That readiness check from §1 is what makes the whole thing safe: a new Pod is added to the load balancer only after it passes readiness, and an old Pod is removed only after the new one is in. The common mistake is shipping a new version with a broken or missing readiness probe. Then Kubernetes thinks each new Pod is ready the instant it starts, tears down the old ones, and cheerfully replaces a working app with a broken one at full speed. Wire readiness correctly and the opposite happens: a bad version can't pass readiness, so the rollout stalls with the old Pods still serving — a failed deploy instead of an outage. And because the previous ReplicaSet is kept around, undoing a bad rollout is a single command, not a frantic redeploy. Now the precise version:

Changing the image on a Deployment triggers a rolling update by default: new Pods come up and pass readiness before old ones are removed, so traffic never drops. If it goes wrong, rollback is one command because the old ReplicaSet is still there.

v1 ×3 v1 ×2+ v2 ×1 v1 ×1+ v2 ×2 v2 ×3
🗺️ How to read this diagram

Read the four boxes left-to-right, following the blue arrows — they trace one Deployment moving from all-old to all-new a slice at a time.

  • The leftmost green box is the steady state before the deploy: three replicas all on v1 (v1 ×3).
  • The two amber boxes in the middle are the transitional mix — first v1 ×2 + v2 ×1, then v1 ×1 + v2 ×2; each blue arrow is one step where a new v2 Pod comes up and passes readiness before an old v1 Pod is retired, so the total serving count barely dips.
  • The rightmost green box is the payoff: all three replicas are now on v2 (v2 ×3) — the swap finished with no downtime.

In short: a rolling update shifts replicas from the old version to the new one one Pod at a time, so traffic never drops during the deploy.

shell · roll out, watch, and roll back (needs a cluster)
rollout.shkubectl set image deploy/web app=ghcr.io/example/web:1.3.0
kubectl rollout status deploy/web      # blocks until healthy or fails
kubectl rollout history deploy/web     # revisions you can return to
kubectl rollout undo deploy/web        # instant rollback to previous revision
kubectl rollout undo deploy/web --to-revision=2
deployment "web" successfully rolled out
Readiness gates the rolloutA rolling update only removes an old Pod once a new one is Ready. If your new version fails its readiness probe, the rollout stalls instead of taking down the app — the old Pods keep serving. That's the safety net; wire readiness correctly and a bad deploy can't cause an outage on its own.

4 · HorizontalPodAutoscaler

Traffic isn't flat, but a fixed replica count is. Provision for your busiest hour and you burn money all night running idle Pods; provision for the average and you fall over the moment a spike hits. Sizing by hand means either overpaying or getting paged — and you can't sit there watching a dashboard and typing kubectl scale at 2 a.m. anyway. The HorizontalPodAutoscaler is the robot that watches the dashboard for you and adds or removes Pods to match demand.

The mental model is a thermostat, and it's worth holding onto because it explains the HPA's whole personality. You set a target ("keep the room at 20°C"), the thermostat reads the actual temperature, and it acts to close the gap — more heat when it's cold, less when it's warm. The HPA does the same with a metric: you say "keep average CPU at 60%," it reads the current average across your Pods, and it computes how many replicas would bring the reading back to target. Too hot (CPU above 60%) → add Pods; comfortably cool → remove some. It's a feedback loop, not a fixed rule.

Two things about that loop trip people up. First, it scales up fast and down slow on purpose: adding capacity late means dropped requests, but removing it too eagerly causes flapping — a jittery add-remove-add cycle that's worse than doing nothing — so the HPA is deliberately cautious about scaling in. Second, and this is the mistake almost everyone makes once: "60% CPU" means 60% of the CPU request you set in §2, not 60% of a whole core. The request is the denominator. If you never set a CPU request, there's no denominator, the HPA can't compute a percentage, and it just sits there inert while your app melts — no error, no scaling, nothing. The HPA also needs a metrics source feeding it numbers (metrics-server for CPU and memory, or an adapter for custom metrics like queue depth). Note too that the autoscaling API and its metric options evolve across Kubernetes versions, so verify the current shape against the docs rather than trusting a snippet's age. Now the precise version:

The HPA adjusts a Deployment's replica count to hit a target metric — classically CPU. It needs a metrics source (metrics-server for CPU/memory, or an adapter for custom metrics). It scales up quickly under load and down conservatively to avoid flapping.

read metric avg CPU across Pods compare to target = 60% of request set replica count min 2 … max 10 loop repeats — up fast, down slow (anti-flap) hot → +Pods cool → −Pods The HPA is a thermostat. It reads average CPU, compares it to the target (60% of the CPU request), and nudges the replica count to close the gap — scaling up fast but down slowly so it doesn't flap.
🗺️ How to read this diagram

Follow the blue arrows left-to-right through one cycle, then the arrow curving back proves it's a repeating loop.

  • The first box reads the live metric — average CPU across the Pods.
  • The middle box compares it to the target; note "60%" is 60% of the CPU request you set in §2 — that request is the denominator, so with no request there's nothing to divide by and the HPA sits inert.
  • The third box writes a new replica count, clamped between min and max. Hot means add Pods (green), cool means remove some (amber).
  • The curved arrow back to the start is the payoff: it's a feedback loop that runs continuously, deliberately scaling up fast and down slow to avoid flapping.

In short: the HPA closes the gap between actual and target CPU by adjusting replicas — but only if a CPU request exists to measure utilization against.

YAML · an HPA targeting 60% CPU, 2–10 replicas (needs a cluster + metrics-server)
hpa.yamlapiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60   # % of the CPU *request*
HPA needs requests set, and CPU is % of the request"60% CPU" means 60% of the container's CPU request, not of a whole core — so the HPA does nothing useful if you didn't set a CPU request (K3 §2). Also, the autoscaling/v2 API and available metric types evolve — verify the current API version and options against the Kubernetes docs. For bursty, spiky traffic (like LLM inference, K7), CPU may be the wrong signal — consider queue depth or a custom metric.

5 · PodDisruptionBudget

The outage you cause yourself is the most embarrassing kind. You have three replicas spread across three nodes — plenty of redundancy — and then a routine node upgrade drains all three nodes at once because the cluster was in a hurry. For a few seconds you have zero Pods serving, and the pager goes off during scheduled maintenance you thought was safe. Your replicas were fine; the timing of the maintenance wasn't. A PodDisruptionBudget exists to stop precisely this.

The key idea is that not all Pod deaths are the same, and you can only govern one kind. Think of a 24-hour shop with a staff of three. Some absences you simply can't plan for — someone calls in sick, a bus breaks down; that's involuntary disruption (a node crashes), and no policy prevents it. But scheduled things — sending people on training, closing for a deep-clean — those are voluntary, and for those a sensible manager sets a floor: "never send more than one person off at a time; at least two must be on the floor." A PodDisruptionBudget is that floor, applied to voluntary disruptions like node drains, cluster autoscaler scale-downs, and rolling node upgrades.

What makes it work is that it doesn't stop maintenance — it makes maintenance wait its turn. When something tries to voluntarily evict a Pod, Kubernetes checks the budget first: if evicting now would drop you below minAvailable, the eviction blocks until enough Pods are healthy again, then proceeds one at a time. The mistake to avoid is expecting a PDB to save you from a hard node crash — it can't, because a crash never asks permission. Involuntary loss is what spreading replicas across nodes and wiring readiness are for; the PDB is strictly a brake on the planned disruptions you'd otherwise let run too fast. (PDB semantics also shift between versions, so confirm the current behaviour against the docs.) Now the precise version:

There are two kinds of disruption: involuntary (a node crashes — you can't prevent it) and voluntary (you drain a node for an upgrade). A PodDisruptionBudget constrains the voluntary kind so routine maintenance can't take you below a safe number of Pods.

drain / scale-down voluntary eviction PDB check ≥ minAvailable: 2? floor holds → allow evict one, proceed would breach → block wait until Ready again node crash involuntary — bypasses A PDB is a brake on voluntary disruptions. Each drain or scale-down asks the budget first: if evicting would drop below minAvailable, the eviction waits its turn. A hard node crash never asks — the PDB can't protect against it.
🗺️ How to read this diagram

Follow the top arrow left-to-right: a voluntary eviction hits the PDB check, which branches into two outcomes.

  • The left box is a voluntary disruption — a kubectl drain, an autoscaler scale-down, or a node upgrade.
  • The middle diamond-style box is the budget check: would evicting now still leave at least minAvailable Pods?
  • The upper-right (green) branch is "yes" — the eviction is allowed and proceeds one Pod at a time. The lower-right (amber) branch is "no" — the eviction is blocked and waits until a replacement is Ready, rather than losing availability.
  • The red box at the bottom-left is the payoff caveat: an involuntary crash never reaches the check at all, so a PDB is powerless against it — that's what replicas spread across nodes are for.

In short: a PDB makes planned maintenance wait its turn to respect a floor; it slows voluntary disruptions but cannot stop an involuntary node crash.

YAML · keep at least 2 web Pods available during maintenance (needs a cluster)
pdb.yamlapiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web
spec:
  minAvailable: 2          # or: maxUnavailable: 1
  selector:
    matchLabels:
      app: web
What a PDB does and doesn't doA PDB makes a voluntary eviction (e.g. kubectl drain, a cluster autoscaler scale-down, a node upgrade) wait until it can respect the budget — it slows or blocks the disruption rather than losing availability. It does not protect against involuntary loss (a hard node crash); that's what replicas across nodes and readiness are for. Verify current PDB semantics against the Kubernetes docs.

✓ Checkpoint — you can move on when you can…

  • Say what each probe gates: readiness → traffic, liveness → restart, startup → boot window.
  • Explain the difference between a resource request and a limit, and what happens over each.
  • Perform a rolling update and roll it back with kubectl rollout.
  • Write an HPA on CPU and explain why it needs a CPU request set.
  • Explain what a PodDisruptionBudget protects and what it doesn't.
✓ Knowledge check

An app takes ~90 seconds to warm a model cache on boot. With a plain liveness probe (10s period, 3 failures) it enters CrashLoopBackOff and never starts. What's wrong and how do you fix it?

Show answer
The liveness probe starts checking immediately and fails during the long boot, so Kubernetes restarts the container before it ever finishes warming — an infinite crash loop. The fix is a startup probe that holds off liveness until boot completes: set a startup probe with a generous failureThreshold × periodSeconds window (e.g. 30 × 5s = ~150s). While the startup probe is still passing/pending, liveness and readiness are suspended, so the slow warm-up is allowed to finish; only after startup succeeds does liveness begin guarding against wedging.
✓ Knowledge check

You set an HPA to keep CPU at 60% but it never adds replicas even under obvious load. The Deployment has no resources.requests. Why is the HPA inert?

Show answer
CPU utilization for an HPA is measured as a percentage of each container's CPU request. With no request set, there's no denominator, so the HPA can't compute a utilization percentage and won't scale. Set a realistic resources.requests.cpu on the container; then "60%" becomes 60% of that request and the HPA can react. (Also confirm metrics-server is installed, or the HPA has no metrics at all.)

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Add readiness and liveness probesBeginner

Context: Probes are the first thing separating a demo Deployment from a real one.

Your task: Add a readiness and a liveness probe to a web container and state, in one line each, what failing each probe causes.

Requirements:

  • Readiness on a cheap /ready endpoint
  • Liveness on a cheap /live endpoint (no dependency calls)
  • State the effect of each failing

💡 Hint: Liveness must be cheap and in-process; readiness may check whether it can serve.

Show solution
        readinessProbe:
          httpGet: { path: /healthz/ready, port: 8000 }
          periodSeconds: 5
        livenessProbe:
          httpGet: { path: /healthz/live, port: 8000 }
          periodSeconds: 10
          failureThreshold: 3

Effects: a failing readiness probe removes the Pod from the Service's endpoints so it stops receiving traffic (no restart) — used to shed a temporarily-busy Pod. A failing liveness probe restarts the container — used to recover a wedged process. Keep liveness cheap and dependency-free so a slow database can't trigger needless restarts.

Exercise 2 · Set requests and limits from a profileIntermediate

Context: Right-sizing resources is most of cluster cost control.

Your task: Given an app that idles ~200m CPU / 180Mi and peaks 900m / 480Mi, choose requests and limits and justify each number.

Requirements:

  • Set the request from typical usage, the memory limit from peak + headroom
  • Explain why CPU over-limit and memory over-limit behave differently
  • Note what QoS class request==limit would give

💡 Hint: Memory over-limit kills; CPU over-limit only throttles — size memory more carefully.

Show solution
        resources:
          requests: { cpu: "250m", memory: "256Mi" }
          limits:   { cpu: "1",    memory: "640Mi" }

Reasoning: the request (250m / 256Mi) sits at or just above typical usage so the scheduler packs nodes efficiently and the Pod is guaranteed its baseline. The CPU limit (1 core) lets it burst for spikes; exceeding it only throttles the container, so a generous cap is safe. The memory limit (640Mi) is peak (480Mi) plus headroom, because exceeding a memory limit OOM-kills the container — you never want a normal peak to kill it. Setting request == limit on both would give the Guaranteed QoS class (least likely to be evicted), at the cost of no bursting.

Exercise 3 · Do a zero-downtime rollout and prove the safety netAdvanced

Context: A rollout that stalls safely on a bad version is the whole point of readiness-gated deploys.

Your task: Roll out a new image, then explain what happens if the new version fails its readiness probe, and how you'd recover.

Requirements:

  • Give the rollout and status commands
  • Explain why a failing readiness probe stalls rather than breaks the rollout
  • Give the rollback command

💡 Hint: Old Pods are only removed once new ones are Ready — so a never-Ready new version can't take the app down.

Show solution
kubectl set image deploy/web app=ghcr.io/example/web:1.3.0
kubectl rollout status deploy/web       # watch it progress or stall

If v1.3.0's readiness probe never passes: the rolling update creates a new Pod but won't mark it Ready, so it won't remove an old (working) Pod to make room beyond maxUnavailable. The rollout stalls — old Pods keep serving traffic, and rollout status reports it isn't progressing. The bad version never takes the app down on its own.

Recover: kubectl rollout undo deploy/web scales the previous ReplicaSet back up and the broken one down — an instant return to v1.2.0 because the old ReplicaSet was never deleted.

Exercise 4 · Add an HPA and reason about the metricExpert

Context: Choosing the right autoscaling signal is what separates a working HPA from a flapping one.

Your task: Add a CPU HPA to a web Deployment, then argue whether CPU is the right metric for a latency-sensitive, bursty API and what you'd use instead.

Requirements:

  • Write the HPA (2–10 replicas, 60% CPU)
  • Note the CPU-request prerequisite
  • Argue for/against CPU vs a custom metric for bursty inference-style load

💡 Hint: For request/response latency, in-flight requests or queue depth track user pain better than CPU.

Show solution
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: web }
spec:
  scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: web }
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource: { name: cpu, target: { type: Utilization, averageUtilization: 60 } }

Prerequisite: a CPU request must be set, since utilization is measured against it, plus a running metrics-server.

Is CPU the right signal? For a CPU-bound service, yes. But for a latency-sensitive, bursty API — or LLM inference (K7) — CPU can lag the actual pain: requests queue up before CPU saturates, so users see latency while the HPA sits idle. Better signals are in-flight requests, queue depth, or p95 latency, exposed as custom/external metrics via an adapter. Verify the current custom-metrics API against the docs before relying on it.

Exercise 5 · Protect availability with a PDB during a node upgradeProfessional

Context: Voluntary disruptions (node upgrades, autoscaler scale-down) will take your Pods down unless a budget constrains them.

Your task: Add a PodDisruptionBudget for a 3-replica service and explain exactly what it does — and doesn't do — during a rolling node upgrade.

Requirements:

  • Write a PDB keeping at least 2 of 3 available
  • Explain how it interacts with kubectl drain
  • State clearly what it does NOT protect against

💡 Hint: A PDB gates voluntary evictions only; involuntary loss still needs replicas across nodes.

Show solution
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: web }
spec:
  minAvailable: 2
  selector: { matchLabels: { app: web } }

During a node upgrade: the upgrade drains nodes one at a time. When a drain would evict a web Pod, the PDB checks whether at least 2 would remain available; if not, the eviction blocks and waits until a replacement Pod is Ready elsewhere. The upgrade proceeds only as fast as availability allows — so you never drop below 2 serving Pods.

What it does NOT do: a PDB is powerless against involuntary disruption — if a node crashes, both its Pods are gone regardless of the budget. Real resilience needs replicas spread across nodes/AZs (anti-affinity / topology spread) so no single node failure can breach your target. The PDB only governs the disruptions Kubernetes initiates.

Exercise 6 · Harden a service to a production reliability barIndustry scenario

Context: Representative scenario: a service passed review functionally but the SRE team blocks its prod promotion until it meets a reliability checklist.

Your task: Produce the full container spec and supporting objects that take a bare Deployment to the production bar: probes, resources, rollout strategy, HPA, and PDB, each with a one-line justification.

Requirements:

  • All three probe types where appropriate, correctly scoped
  • Requests and limits set from a profile, memory limit above peak
  • Rolling strategy with maxUnavailable: 0 for a critical service
  • HPA on an appropriate metric and a PDB
  • One line on what each control buys you

💡 Hint: Tie each control to a specific failure it prevents — that's how SRE reviews think.

Show solution

Container spec (probes + resources):

        readinessProbe:  { httpGet: { path: /healthz/ready, port: 8000 }, periodSeconds: 5 }
        livenessProbe:   { httpGet: { path: /healthz/live,  port: 8000 }, periodSeconds: 10, failureThreshold: 3 }
        startupProbe:    { httpGet: { path: /healthz/live,  port: 8000 }, failureThreshold: 30, periodSeconds: 5 }
        resources:
          requests: { cpu: "250m", memory: "256Mi" }
          limits:   { cpu: "1",    memory: "640Mi" }

Deployment strategy (critical service — never dip below capacity):

  strategy:
    type: RollingUpdate
    rollingUpdate: { maxUnavailable: 0, maxSurge: 1 }

HPA + PDB: a CPU (or custom-metric) HPA from minReplicas: 3 to 12, and a PDB with minAvailable: 2.

What each control buys: readiness → no traffic to a not-ready Pod; liveness → auto-recovery from wedging; startup → slow boots don't crash-loop; requests → the scheduler reserves capacity; memory limit above peak → no surprise OOM-kills; maxUnavailable: 0 → zero-downtime deploys; HPA → capacity follows load; PDB → maintenance can't breach quorum. Spread replicas across AZs (topology spread) so involuntary node loss can't either. Every line maps to a failure it prevents — which is exactly what the SRE checklist is asking for.

© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in