all_lessons/kubernetes/00lesson 1 / 14

Part I — The engine and the unit

The One Idea: Declare, Then Reconcile

This track derives Kubernetes from nothing. There is no prior lesson to repair — we start where every operator starts: you have a binary, you want it always running for the world, and your only tools are SSH, a startup command, and your own vigilance. That works for exactly one box on a calm afternoon. This lesson follows that manual baseline until it breaks under scale, node death, and rollouts, and shows that the only repair that survives all three is to stop typing commands and instead declare the state you want — then hand it to a loop that never stops checking. That loop is the whole machine. Every noun in the next thirteen lessons (container, Pod, controller, Service, scheduler) is just something this loop turns out to need.

The previous step left this broken
There is no previous step — this is lesson 1. So the thing we are repairing is the manual-ops baseline: you SSH into a server, run ./server &, and to keep it alive you add a restart cron and a runbook titled "what to do at 3am when it's down." That baseline has no model of desired state — only a human re-issuing commands. The moment the human is asleep, the box dies, traffic triples, or you need to ship v2, the gap between "what you wanted" and "what is actually running" goes unwatched, because nothing in the system is responsible for watching it.
Linear position
Forced by: a binary you need always-on for the world, where the only baseline is a human issuing commands — which does not survive scale, node death, or rollout.
New capability: declarative desired state driven by a level-triggered, idempotent control loop (observe → diff → act → repeat forever) that continuously drives actual state toward desired. This loop is Kubernetes.
Forces next: the loop must reconcile "a running app," but a bare OS process is too slippery to declare and reconcile reproducibly — you need a portable, isolated, frozen unit. That is the container (lesson 01).
The plan
Five moves. (1) Watch the manual baseline fail three concrete ways. (2) Separate the two ways to ask for something — imperative (the steps) vs declarative (the result) — and show why only declarative survives. (3) Build the control loop that makes a declaration true: observe, diff, act, repeat. (4) Pin down the two properties that make it self-heal — level-triggered and idempotent — and contrast them with edge-triggered. (5) Drive the reconciliation-loop widget yourself: set a desired count, kill Pods at random, watch it converge regardless.

1 · Where the manual baseline breaks

Start with the smallest honest setup. One server, one binary, one command: you SSH in and run ./server. To survive a crash you wrap it in a restart loop or a cron job. To survive your own forgetfulness you write a runbook. This is real, it ships products, and it holds — until it meets the three forces every production system eventually meets.

scale → one server becomes 50. "SSH in and start it" is now a 50-line for-loop you run by hand, and you can never be sure all 50 are actually up right now without checking all 50.
node death → a machine's disk fails at 3am. The process on it is simply gone. Your cron restarts processes on boxes that still boot; it has nothing to say about a box that is dead. The work that lived there evaporates and no one is woken.
rollout → you ship v2. Now you must stop v1 and start v2 on every box in some safe order, and if v2 crashes on contact you must reverse all of it by hand, from memory, under pressure.

The common root cause is not laziness — it is that the only place "I want 50 healthy copies of v2" exists is in your head. The system stores no desired state. It stores a pile of side effects from commands you happened to run. Nothing is continuously responsible for the gap between intent and reality, so the gap is only ever closed when a human notices and types. Humans sleep; reality does not. Every failure above is the same failure: intent is not represented anywhere a machine can act on.

2 · Two ways to ask: imperative vs declarative

There are exactly two ways to ask a computer for something, and the difference is the hinge of this entire track.

Imperative means you specify the steps: "start a process, then start another, then open this port." You are the planner; the machine just executes. The catch is that the steps only describe a transition from one specific assumed state to another. start the server assumes the server is not running. Run it twice and you may get two servers, or an error, or a port clash — the command does not know what already exists. The plan is only valid for the world you imagined when you typed it.

Declarative means you specify the result: "there should be 3 healthy copies of v2 serving on port 8080." You say nothing about how to get there, what already exists, or what just broke. You hand over a description of the end state and make it someone else's job to compute and apply whatever steps close the gap from wherever reality currently sits.

Imperative ("do these steps")Declarative ("reach this state")
What you giveA sequence of commandsA description of the desired end state
AssumesA specific starting stateNothing — it reads actual state first
Run it twiceMay double-apply or error (port clash, two copies)Same result — already-correct means do nothing
A Pod dies after you ran itNothing reacts; your command is long goneThe gap reappears next check and is re-closed
Who holds the intentYou (in your head / your shell history)The system (a stored, queryable object)

Declarative wins for one structural reason: a declaration is durable and re-checkable. "Steps I ran" are spent the instant they finish; "the state I want" is a standing fact the system can compare against reality again and again, forever. The whole leap is: store the intent, then build something that never stops comparing.

3 · The control loop that makes a declaration true

A declaration on its own is inert — a wish written on a card. Something has to act on it, and it can't be a one-shot script (a script runs once, then the world drifts and no one is watching). It must be a loop that runs forever. This is the control loop, and it is borrowed directly from control engineering — the same idea as a thermostat, which is the cleanest mental model: you set 21°C (desired), it reads the room (actual), and if they differ it acts (heat or cool), then it checks again, forever. It never assumes the room stayed at 21°C just because it once was.

1observe → read the actual state of the world: how many copies are running right now, and are they healthy?
2diff → compare actual against desired. Desired 3, actual 2 → the gap is "+1". Desired 3, actual 3 → the gap is empty.
3act → take the smallest step that shrinks the gap: create the one missing copy, or delete one surplus copy. If the gap is empty, do nothing.
4repeat → wait a moment, then go back to step 1. Forever. The loop is never "done"; reality keeps moving, so the loop keeps watching.

This is the entire engine. A program running this loop, watching one kind of desired state, is called a controller. Kubernetes is, at heart, a stored pile of declarations plus a swarm of controllers each running this loop. Strip away every other word you have heard about it and this is what is left. The loop is Kubernetes; every later noun is something the loop needs: it needs a durable place to keep the declarations and the observed truth (the control plane, lesson 03), a clean unit to count and create (the container and Pod, lessons 01–02), a way to find "its" copies among everyone else's (labels and selectors, lesson 04), and so on. None of those nouns is the point. The loop is the point.

      desired state (stored)            actual state (the world)
      "3 healthy copies of v2"           2 running, 1 just died on a dead node
              |                                   |
              v                                   v
        +---------------------- CONTROLLER ----------------------+
        |  observe actual  ->  diff vs desired  ->  act to close  |
        |        ^                                       |        |
        |        +--------------- repeat <---------------+        |
        +--------------------------------------------------------+
                              (forever)

4 · Level-triggered and idempotent: why it self-heals

Two properties turn that loop from "a thing that runs" into "a thing that heals." Both are easy to state and easy to underrate.

Level-triggered vs edge-triggered. An edge-triggered system reacts to events — the transitions, the "edges": "a Pod died" fires a handler that creates one. That sounds fine until you ask what happens if the event is missed: the handler was busy, the controller had just restarted, three Pods died in the same instant and only one event got through. Then the system is permanently wrong and nothing ever re-fires to fix it, because the edge is in the past. A level-triggered system instead reacts to the current level — the standing value of the world right now: "desired is 3, I currently observe 1, therefore create 2." It does not care how the world got to 1, how many events it missed, or whether it has ever seen this situation before. It re-derives the correct action from scratch on every single pass. Miss every event you like; the next observe-diff-act pass still computes "+2" and fixes it. That is why Kubernetes self-heals: it is level-triggered, so it is structurally incapable of "missing" a failure — it only ever asks "what is true now, and what closes the gap," never "what just happened."

Idempotent. Each pass must be safe to run any number of times with no extra effect once the goal is met. "Ensure 3 copies exist" run on a world that already has 3 copies does nothing — not "start 3 more." Idempotence is what lets the loop run every few seconds forever without piling up duplicates; it is the property that makes "reach this state" repeatable where "run this step" was not (§2). Level-triggered tells the loop what to compute (the gap from the current level); idempotent guarantees that computing it repeatedly is harmless. Together they mean: it is always safe to ask "are we there yet?" and act on the answer.

Edge-triggered
Acts on transitions ("a Pod died"). A missed or dropped event leaves the system permanently wrong — nothing re-fires. Fast, but fragile to gaps.
Level-triggered
Acts on the current level ("desired 3, observed 1 → +2"). Re-derives the right action every pass from reality alone, so missed events are irrelevant. This is why it self-heals.
Idempotent
A pass on an already-correct world does nothing. Safe to repeat forever — no duplicates, no drift from re-running. What makes "reach this state" repeatable.
Convergence
Each pass shrinks the gap; once the gap is zero the loop is at rest but still watching. The actual state is continuously driven toward — and held at — the desired state.

5 · Drive the loop yourself

The widget below is the engine in miniature. The slider sets desired replicas (the declaration). The "kill random Pods" button injects failure (node death, crashes — pick any number at once). Each tick the loop does one observe → diff → act pass: it counts the live Pods, compares to desired, and creates or removes to close the gap. Watch two things. First, it always converges — kill 1 or kill all 8, the next passes still recompute the gap and rebuild exactly to desired, because it is level-triggered and never relies on "catching" the deaths. Second, when actual already equals desired, a tick does nothing — that is idempotence. There is no command you can issue here; you can only change the desired number and perturb reality. Closing the gap is the loop's job, forever.

Reconciliation loop — declare a count, then break reality
Set desired replicas, then mash kill random Pods. The loop ticks on its own: green = running, yellow = being created this tick. It converges no matter how many you kill at once (level-triggered), and a tick on an already-correct cluster does nothing (idempotent). Toggle "pause loop" to see actual state drift away from desired with nothing watching — exactly the manual baseline from §1.
Desired
Actual (running)
Gap this tick
State

Notice what the loop never asks: "how did we get to 1 running?" It does not know and does not care. It reads the level, computes the gap, takes one step, and will look again next tick. That indifference to history is the whole source of its robustness.

Failure modes & checklist

Failure modes

  • Thinking imperatively inside a declarative system. Manually kubectl delete-ing a Pod the controller owns and expecting it to stay gone. The loop observes the gap and recreates it. Signal: "I keep deleting this Pod and it keeps coming back" — you fought the loop instead of editing the desired state.
  • Treating a one-shot script as reconciliation. A deploy script that runs once at release time has no loop behind it, so drift after it finishes goes unwatched. Signal: state is correct right after a deploy and silently wrong an hour later, with nothing reacting.
  • Assuming edge-triggered delivery. Designing logic that fires only on a "Pod died" event and breaks when several die at once or an event is dropped. Signal: after a multi-node outage some replicas never come back, because the missing events were never re-derived.
  • Non-idempotent actions. An "act" step that adds a copy instead of ensuring N exist, so repeated passes pile up duplicates. Signal: replica count climbs past desired over time with no scale-up requested.

Checklist

  • Write the result, not the steps. Describe the desired end state ("3 healthy v2 copies on :8080"); never script the transitions.
  • Make every action idempotent. "Ensure N exist," not "create one" — so re-running a pass on a correct world is a no-op.
  • React to the level, not the event. Compute the gap from current observed state every pass; never depend on catching a transition.
  • Assume the loop runs forever. If you want a different reality, change the declaration — do not issue a command and walk away.
  • Expect self-healing, and let it. Don't hand-patch what the loop owns; fix the desired state and let convergence do the rest.

Checkpoint exercise

Try it
Open the widget. Set desired to 6 and let it settle (actual 6, gap 0, idempotent — a tick changes nothing). Now kill all of them at once with one click, and predict the gap the loop will compute on its next pass before it recovers — then watch it rebuild to exactly 6. Next, tick the "pause loop" box and kill 3: actual drops to 3 and stays there, gap unclosed, because nothing is observing — this is precisely the §1 manual baseline (intent lives nowhere a machine acts on it). Unpause and watch it converge in one or two passes. Finally, in one sentence each: explain why an edge-triggered design that listened for "Pod died" events would have failed the "kill all at once" test, and why the recover step had to be idempotent.

Where this points next

The loop is now in place: declare desired state, observe actual, diff, act, repeat forever, level-triggered and idempotent — and it self-heals. But it has been counting and creating a vague thing we keep calling "a copy" or "a Pod." That hand-wave is the new crack. To reliably create and compare "a running app," the loop needs a unit that is identical everywhere it lands, can't trample its neighbors' libraries, CPU, or files, and can be frozen and shipped so that what you reconcile is exactly what you tested. A bare OS process is none of those things — it inherits the host's libraries (dependency hell, "works on my machine"), shares its CPU and memory (the noisy neighbor), and leaves no reproducible artifact. So the very first thing the loop forces us to build is that unit. That is lesson 01, The Container: Boxing a Process.

Takeaway
Kubernetes is one idea applied relentlessly: stop issuing commands and instead declare the desired state, then let a control loop — observe → diff actual vs desired → act → repeat forever — continuously drive the actual state toward it. The manual baseline (SSH in, start it, a restart cron, a 3am runbook) fails under scale, node death, and rollout for one shared reason: intent lives only in a human's head, so nothing is continuously responsible for the gap between wanted and real. Imperative commands are spent the moment they run and assume a fixed starting world; a declaration is a durable, re-checkable fact, which is the only thing a forever-loop can act on. The loop self-heals because it is level-triggered (it re-derives the correct action from the current observed level every pass, so missed or simultaneous failures are irrelevant — it never relies on catching an event) and idempotent (a pass on an already-correct world does nothing, so it is safe to run forever). This loop is Kubernetes; every later primitive — container, Pod, control plane, controllers, Services, scheduler — is just something the loop turns out to need. The next thing it needs is a clean unit to reconcile, which forces the container.

Interview prompts