CHAPTER 04Part II · Fennel, year 3

Say what you want, not what to do: desired state and the reconcile loop

A year on, Fennel runs on managed Kubernetes. When the same machine dies at 3 a.m., nobody has to wake up. This chapter explains how that works: Fennel writes down what it wants running, and many small loops keep the cluster matching that record. It also covers what those loops can’t do.

Read
23 MIN
For
TECHNICAL MANAGERS
Chapter
04 / 15
Checks
09

§ 01 Year 3: the same 3 a.m.

Cast

Sam
Founding engineer. On call for everything until this year.
Jordan
Co-founder, now managing the engineers: that’s you, in this story.
Ari
Platform lead, hired this year to move Fennel onto Kubernetes.
I didn’t read the last chapter. What happened?
  • In Chapter 1, Fennel ran on one server that Sam had built by hand. A full disk took the site down for two hours, and rebuilding the server from Sam’s notes failed.
  • In Chapter 2, Fennel started packing each piece of its software, with everything it needs to run, into an image. A running copy of an image is a container.
  • In Chapter 3, a year later, Fennel’s app was four services (storefront, orders, payments and dispatch) running as containers on four machines, with Sam’s spreadsheet saying what ran where. One Saturday at 03:04, machine 03 died with orders and a dispatch copy on it. Customers couldn’t order until Sam, woken by a page, started both on other machines and told storefront where orders had gone.
  • Software that does that kind of work all the time is an orchestrator. Kubernetes is one.
RETRIEVECheck 01 / 09

A year ago, at 03:04, machine 03 died with orders and a dispatch copy on it. Which jobs did Sam have to do before customers could order again?

It’s now Year 3, and Fennel runs on managed Kubernetes from its cloud provider. The provider runs Kubernetes’ own machinery, while Fennel’s services run on four machines, numbered 01 to 04 as before. Kubernetes calls each machine in a cluster a node. Ari, the platform lead Fennel hired this year, led the move.

One Saturday at 03:04, machine 03 dies again. Kubernetes knows it as node 03 now, and just as a year ago, it was running orders and a dispatch copy. This time nobody’s phone rings. Sam sleeps through it and reads about it over breakfast.

On Monday, Jordan asks Ari how Kubernetes knew what to do.

“Nobody told it what to do. We told it what we want.”

Ari

The rest of this chapter explains what Ari means, and what that sentence doesn’t promise.

§ 02 Say what you want, not what to do

When a machine dies, there are two ways to write down how Fennel should recover. Fig. 01 compares both for the same incident. On the left is the step-by-step task list Sam worked through after machine 03 died last year. On the right is a goal that says how many copies of each service must keep running.

Fig. 01A task list and a goal

says what to do, step by step

Sam’s task list · after 03:04, year 2

  1. Start orders on machine 04.
  2. Start the lost dispatch copy on machine 02.
  3. Change storefront’s settings: orders is on 04.

says what should be running

Fennel’s goal · written for the cluster

storefront
3 copies
orders
1 copy
payments
2 copies
dispatch
3 copies
Fig. 01Two ways to plan the recovery from the same failure: a list of steps, and a goal.
COMPARECheck 02 / 09

This time it’s node 04 that dies at 3 a.m., not node 03. Which of the two documents in Fig. 01 still says the right thing to do, unchanged?

Sam’s list spells out what to do, step by step, and engineers call that style imperative. Fennel’s goal describes what should be running and leaves the steps to the system, which makes it declarative. What a declarative goal describes is called the cluster’s desired state.

New term · 01 / 04

desired state

A written description of what you want running, such as “storefront: 3 copies”. Kubernetes keeps working to make the cluster match it.

declarative: say what you want · imperative: say what to do

You met it in Fig. 01: Fennel’s goal.

The difference matters most on a night nobody planned for. A task list is only right for the failure its author imagined. A goal doesn’t mention machines, so it stays true whichever one fails. It still shows what’s missing, and whatever acts on the goal can work out the steps for itself.

ANALOGYYou know this from managing people. Suppose you give a capable owner a goal, such as “every order out by 6 p.m.”, instead of a task list like “call the supplier, then rebook the van”. The owner keeps an eye on that one number, notices when it slips, and fixes it without being told how.

WHERE THIS ANALOGY BREAKSThe owners inside Kubernetes have no judgement. They don’t ask why copies keep dying, and they don’t push back when a goal can’t be met: if there’s no room anywhere, a new copy simply waits. They never escalate, and they will restart a broken app forever. They also act only on the latest goal, ignoring any history of earlier instructions. In a person, all of that would be a problem. In software, it means you always know what it will do next, which is to close the gap between the goal and what’s running.

What if the goal changes twice in quick succession?

Say someone changes storefront’s goal from 2 copies to 5 and then, a moment later, to 3. If the loop hasn’t looked in between, it never acts on 5 at all, because it compares the latest goal with what’s running and heads straight for 3. If it had already started heading for 5, it simply turns around. Either way it ends at 3, since there’s no queue of instructions to work through. Engineers call this behaviour “level-triggered”.

Fennel writes its goals in files called manifests. Fig. 02 shows a shortened version of the manifest for storefront. Its line replicas: 3 is the desired state: three copies. Teams usually keep manifests in version control, so that a change to a goal can be reviewed before it goes in, and leaves a history behind.

Fig. 02A manifest · storefront
kind: ReplicaSet

the kind of record: a set of identical copies

metadata:
name: storefront
spec:
replicas: 3

the desired state: three copies

template:

what each copy is

# … labels and other
# details left out
spec:
containers:
- name: storefront
image: storefront:v38

the image to run (Chapter 2)

Fig. 02A manifest describes what should exist, without saying how to get there. This one is shortened, and real ones have a few more lines. Engineers usually write a Deployment, which wraps this and adds version updates; Chapter 6 shows how.

Chapter 3 ended on a question: what if the record were the thing the machines act on, instead of a note about them? With Kubernetes, that’s how it works. The cluster keeps its own record of what’s wanted and what’s running, and every part of Kubernetes works from that record.

§ 03 Pods: what Kubernetes counts

There’s one more word to learn before Ari applies the storefront manifest. Kubernetes doesn’t count or place containers directly. Instead, it wraps each copy’s container in a Pod, and Pods are what Kubernetes counts, places and replaces. Most Pods hold just one container, which is why people often use “Pod” and “container” interchangeably in conversation. Sometimes a small helper container runs alongside it in the same Pod.

Fig. 03A Pod
storefront

Pod1 address

one container: the usual case

apphelper

Pod1 address, shared by both

sometimes a small helper runs alongside

Fig. 03Kubernetes counts, places and replaces Pods. The containers in one Pod always run together, on the same node, sharing one address.

New term · 02 / 04

Pod

A wrapper that Kubernetes puts around one container, or around a few that must run together. Kubernetes counts, places and replaces Pods rather than containers.

Chapter 2’s container, wrapped. The name comes from a pod of whales, or a pea pod.

§ 04 Moving day: what “apply” does

Fennel didn’t move to Kubernetes all at once. Ari’s team moved its services onto the cluster one at a time, and by the morning of moving day, orders, payments and dispatch were already running there. Moving day is the day the last service, storefront, moves too. Ari has written its manifest, the one in Fig. 02, and all that’s left is to hand it to Kubernetes.

Two parts of Kubernetes matter for what happens next. The first is the cluster’s record. It holds every goal Fennel has declared, such as “orders: 1 copy”, together with what Kubernetes knows is actually there: which Pods exist, and which node each one is on. The second is the API server, the program through which everything reads and changes that record. When Ari applies a manifest, the command sends it to the API server. The loops read the record through the API server and write their results back the same way. And on every node, a small program called the node agent regularly reports to the API server what is running on its node, so the record stays up to date.

Kubernetes was already busy before Ari applied anything. Several loops, each a small program that looks after one kind of thing, run all the time, and each one repeats the same three steps: it observes part of the cluster, compares what it sees with what the record says is wanted, and acts if the two differ. On the morning of moving day, orders, payments and dispatch were running exactly as declared, so every comparison came out even and the loops had nothing to do. Lane A shows what changes when storefront arrives.

  1. PREDICTCheck 03 / 09

    Ari applies the storefront manifest, and Kubernetes answers created in under a second. At that moment, what is new in the cluster?

  2. Answer the question above to see what happens

  3. Answer the question above to see what happens

  4. Answer the question above to see what happens

  5. Answer the question above to see what happens

Every loop in lane A followed the three steps described at the start of this section: observe, compare with what’s wanted, act to close any gap, and then start again. That pattern is called a reconcile loop, and it isn’t special to moving day, because almost everything Kubernetes does is built from it. A controller is one implementation of the pattern: a part of Kubernetes that runs a reconcile loop for one kind of thing. The ReplicaSet loop, for example, is the controller for ReplicaSets. Engineers call one pass of the loop “reconciling”. The scheduler and the node agent work the same way, though engineers don’t usually call them controllers.

New term · 03 / 04

reconcile loop · controller

A loop that compares what the record says is wanted with what it observes, acts to close the gap, and then repeats, forever. A controller runs one such loop for one kind of thing.

the loop lens: which loop · wants · sees · does

You watched three of them on moving day.

The pattern is the same for every loop, but each loop fills it in differently, and filling it in is the quickest way to understand any one of them. Ask four questions: which loop is it, what does it want, what does it see, and what does it do about a gap? Together, these questions make up the loop lens. At the fourth step of lane A, for example, the loop was the scheduler. It wants every Pod to have a node; it saw three Pods with none; and it wrote a node into each one. Each new part of Kubernetes in the chapters ahead is another loop, so the loop lens will explain those too.

ANALOGYThe name fits. Kubernetes is Greek for helmsman, or pilot. A helmsman doesn’t plan for every wave; they hold a heading and correct the wheel whenever the ship drifts off it. Engineers often shorten the name to K8s, which is a K, then the number of letters in between, then an s.

WHERE THIS ANALOGY BREAKSA ship has one helmsman, but a cluster has many loops, each holding its own small heading, and no captain gives them orders. A helmsman also reads the weather, whereas a loop only compares what’s wanted with what’s there.

§ 05 Saturday, 03:04, again

  1. PREDICTCheck 04 / 09

    Node 03 has just stopped, and nobody will be woken. With Kubernetes’ default settings, how long until customers can place orders again?

  2. Answer the question above to see what happens

  3. Answer the question above to see what happens

  4. Answer the question above to see what happens

  5. Answer the question above to see what happens

  6. Answer the question above to see what happens

Chapter 1 left Jordan with a question to ask about any machine Fennel depends on:

“If this box vanished tonight, how long until we’re back, and who has to be awake?”

Chapter 1

In Year 3, the answer is roughly six or seven minutes with default settings, sometimes less, and nobody has to be awake.

Those minutes still cost something. Fennel runs orders as a single copy, so it was down the whole time: when its one Pod stopped, there was no other copy to take orders while the loops replaced it. A service running as one copy is what Chapter 1 called a single point of failure. The loops restore what’s been declared, but they can’t prevent the gap while they do it. That makes this a question worth asking Ari: “Which of our services run as a single copy?”

Can it be faster than six minutes?

It can be. In a cloud, Kubernetes asks the provider whether a silent machine still exists, and if the provider says it’s gone, its Pods are cleaned up without waiting the full five minutes. Engineers can also shorten or lengthen the five-minute wait for each app. The numbers in Fennel’s story are the defaults. In every case, the old Pod records stay around, marked for deletion, until Kubernetes knows the machine is gone, but the ReplicaSet loop stops counting them as soon as they’re marked. The wait exists because at first a network glitch looks the same as a dead machine, and replacing Pods too eagerly could leave two copies running where only one was wanted.

What about machine 03 itself?

Kubernetes replaced the Pods, but not the machine. Until a machine is added back, the cluster has three nodes and less spare room. That room is the subject of Chapter 5.

Fig. 04 looks at each loop you’ve watched through the loop lens, and shows which of Sam’s jobs it took over.

Fig. 04Which loop: wants, sees, does
LoopWantsSeesDoesSam’s job, now a loop’s
Updating and scaling are handled by loops too, in Chapters 6 and 8.
Fig. 04Each of Sam’s 3 a.m. jobs now belongs to a loop, and each loop does only its own job.
EXPLAINCheck 05 / 09

Jordan asks Ari: “When node 03 died, what worked out that orders should go to node 04?” Which answer is accurate?

No part of Kubernetes made a plan for 03:04. The node loop didn’t know what a ReplicaSet was, the ReplicaSet loop didn’t know why its Pods had gone, and the scheduler didn’t know there had been a failure. Each one read the record, did its one job and wrote the result back, and the next loop picked up the work from there. That’s how the work gets split up without a single conductor.

Chapter 3 called Kubernetes an orchestrator because of the jobs it does, and that’s still true. Kubernetes’ own documentation, though, warns about what the word suggests. It describes orchestration as following a fixed script, “first do A, then B, then C”, and says that Kubernetes instead “comprises a set of independent, composable control processes”. It adds: “Centralized control is also not required.” So Chapter 3 used the word for what gets done, while the documentation is talking about how it gets done.

There’s a practical reason for building it this way. The same documentation prefers “simple controllers rather than one, monolithic set of control loops”, because controllers can fail. When the loops are separate, one of them can fail without stopping the rest.

But engineers talk about “the controller manager”

They do. Kubernetes’ built-in controllers are shipped together as one program, which keeps them simple to run. Inside that program, each controller is still a separate loop watching its own kind of thing, much like separate teams sharing one office building. The scheduler and the node agents run as separate programs.

Nothing at 03:04 needed a release, or anyone applying anything, because the loops had been running all along.

§ 06 Replaced, not repaired

PREDICTCheck 06 / 09

Since 03:10, an orders Pod has been running on node 04. Is it the Pod that was on node 03?

Ari’s dashboard shows the difference. On Friday it listed orders-5c8x on node 03, and since 03:10 it has listed orders-q2rw on node 04. Kubernetes never moves a Pod. It replaces it with a new one, made from the same template, which has a new name and a new address and keeps nothing from inside the old one.

This is why Kubernetes treats Pods as disposable. People rarely create Pods by hand; a controller makes them, and each one lasts until it finishes its work, is removed, or loses its node. The new address is also the reason storefront needed another loop, Chapter 7’s, to find the new orders Pod.

What “disposable” asks of the app

Engineers will recognise this idea from the twelve-factor app, a widely used checklist for building software, which calls it “disposability”: an app’s processes “can be started or stopped at a moment’s notice”. For a service running on Kubernetes, that comes down to four things:

  • Keep nothing important inside the Pod. Fennel’s orders live in its database, outside the app, as Chapter 2 explained; where that database should run is the subject of Chapter 10.
  • Cope with being stopped or replaced at any moment.
  • Start in seconds rather than minutes, because a slow start lengthens the gap every time a Pod is replaced.
  • Finish any work in progress when asked to stop. By default, Kubernetes gives a Pod 30 seconds to do that.

Kubernetes can’t do these for you; they’re up to the app. They’re the questions to ask whenever one of your services moves onto the cluster.

Self-healing, honestly

You’ll hear what happened at 03:04 called self-healing. After last year’s 3 a.m. pages, it’s tempting to hear that as “Kubernetes makes our app reliable”. What the loops actually do is restore the declared state: they restart containers that crash, and they replace the Pods of nodes that die. There are several things they can’t do:

  • Fix a bug. An app that crashes as it starts is restarted, crashes again, and is restarted again, forever. §08 shows an example.
  • Find room that isn’t there. If no node has room for a new Pod, the Pod waits, unplaced, until room appears. Chapter 5 gives this state a name.
  • Bring back what was inside a Pod. Anything written inside a Pod is lost with it; keeping data safe is the subject of Chapter 10.
  • Keep a single copy up. As at 03:04, a service with only one copy is down while that copy is replaced.

In other words, Kubernetes restores the number of copies that are running. Whether the app itself is healthy is still your engineers’ responsibility.

§ 07 Who runs what

You’ve now met every part of a cluster that a manager needs to know by name. Fig. 05 brings them together and shows who runs each part.

Fig. 05Control plane and nodes
Fig. 05Every part talks only to the API server, and through it to the record. The provider runs everything in the top band, but what goes into the record is always yours.

You met the API server in §04. Everything that reads or writes the record goes through it, whether that’s a person applying a manifest, one of the loops, or a node agent reporting in. The node agent on each node is what engineers call the kubelet; it starts whatever has been assigned to its node and reports back. Together, the API server, the record behind it, and the loops that work on that record, apart from the node agents, make up the control plane.

New term · 04 / 04

control plane

The API server, the cluster’s record behind it, and the loops that work on it. On a managed service, your provider runs it.

You met each part in §04 and §05.

On a managed service, the provider runs the whole control plane in every mode, and carries out its upgrades to new Kubernetes versions. Deciding when to upgrade, and checking that Fennel’s apps still work on the new version, is still partly Fennel’s job; that work is the subject of Chapter 14.

Who looks after the nodes depends on the mode. In each big provider’s standard mode, the customer does, which at Fennel means Ari’s team. In the fully managed modes, the provider does. Either way, Fennel pays for the computing its Pods run on, which in most modes means paying for the machines themselves. Some things are always Fennel’s responsibility: what goes into the record, its images and code, its data, who may change what, and watching and responding to alerts.

Without a managed service, Fennel’s own team would also have to run the control plane, including keeping the record safe.

What is the cluster’s record, exactly?

Engineers often call it etcd, after the software usually used to store it, although some providers store it differently. Only the API server reads and writes it directly; every loop goes through the API server.

What if the control plane itself goes down?

Pods that are already running keep running, because each part carries on with what it was last told. But no Pod is replaced or placed until the control plane is back. What providers promise about this is covered in Chapter 14.

APPLYCheck 07 / 09

Fennel’s cluster runs on a managed service, in the provider’s standard mode. Which of these is still Fennel’s job?

§ 08 The conversation

Fennel’s newest service, notifications, sends customers texts and emails about their orders. If it runs late for a while, that’s annoying but not a disaster. On Wednesday, a setting it needs goes missing.

PREDICTCheck 08 / 09

Every time a notifications container starts, it crashes within seconds, because a setting it needs is missing. With default settings, what does Kubernetes do over the next hour?

On engineers’ dashboards, this state shows up as CrashLoopBackOff. An example in the Kubernetes documentation shows a Pod in this state that has restarted 200 times over two days and nine hours.

APPLYCheck 09 / 09 · think, then reveal

Ari says: “notifications keeps crash-looping, but Kubernetes keeps restarting it.” What would you ask? Think of one or two questions, then show the answer.

In short

  • With Kubernetes, you write down what you want, the desired state, in manifests. Applying a manifest records that intention; it doesn’t run anything itself.
  • Many small loops each compare one part of the record with what they observe, and act to close any gap, over and over. None of them plans for the others; work passes between them through the record.
  • Kubernetes runs Pods, and Pods are disposable. When a node dies, its Pods are replaced by new ones within minutes with default settings, and nobody has to be awake. The app has to cope with that, and the loops can’t fix an app that is broken.
  • Your provider runs the control plane. What goes into the record, and whether your apps survive being replaced, stays your responsibility.

From here on, whenever Kubernetes does something, the loop lens is the first thing to reach for: which loop is acting, what does it want, what does it see, and what does it do about the gap? In the next chapter, Morgan, the co-founder who runs Fennel’s money, opens the first cloud bill since the move and finds it has gone up. That story starts with the loop that picked node 04 for orders.

End of chapter 04

Next: Who runs where

Chapter 05 · 25 min Who runs where: scheduling, resources and cost Fennel’s first cloud bill since moving onto Kubernetes has gone up, a Pod wouldn’t start on Friday and a service kept restarting on Tuesday. All three come back to two numbers each team writes down for its services: what a Pod reserves and the most it may use. Start chapter 05 →

Course

chapters
15
running
05
in review
02
pending
08
  1. Part IBefore Kubernetes03/03 running

    1. Ch 0117 minOne app, one serverRunning
    2. Ch 0218 min“Works on my machine”containersRunning
    3. Ch 0317 minMany containers, many machinesthe orchestration problemRunning
  2. Part IIThe core model02/05 running · 02 in review

    1. Ch 0423 minSay what you want, not what to dodesired state and the reconcile loopYou are here
    2. Ch 0525 minWho runs wherescheduling, resources and costRunning
    3. Ch 0627 minShipping without downtimerollouts and rollbacksUnder review
    4. Ch 0724 minFinding each otherservices and trafficUnder review
    5. Ch 08The Friday dinner rushautoscalingPending
  3. Part IIIThe hard parts00/04 running

    1. Ch 09Configuration and secretsPending
    2. Ch 10The database problemstatePending
    3. Ch 11Many teams, one clusternamespaces, permissions and quotasPending
    4. Ch 12Seeing insideincidents in a self-healing systemPending
  4. Part IVOwning it00/02 running

    1. Ch 13Teaching the cluster new tricksextensions and the ecosystemPending
    2. Ch 14What all this costsmanaged Kubernetes, platform teams and when not to use itPending
  5. CapstoneMixed practice00/01 running

    1. Ch 15Fennel’s hard weekPending
Fig. 00The whole course: 15 chapters in 4 parts and a capstone. Raised tiles are running; ones marked Under review are written and readable, still being checked; dashed ones are Pending, planned but not written yet.