Controllers: the reconcile loop
Almost every feature you have used so far, from Deployments and ReplicaSets to Services and Jobs, is made by a controller: a small program that keeps looking at what you asked for, looks at what really exists, and fixes the difference. Once you see this loop, all of Kubernetes makes sense, and you can even write your own.
A thermostat for your cluster
A thermostat does not care why the room is cold: someone opened a window, the sun went down, whatever. It only compares two numbers, "wanted 21°" and "now 18°", and turns the heating on until they match. Then it keeps watching.
A Kubernetes controller is exactly that, for objects. You write the wanted state in YAML; the controller makes reality match, forever.
Observe, compare, act
You saw this loop in chapter 4 as "desired vs current". Try it once more, thinking "controller" this time: crash Pods, change the number, and watch the controller close the gap.
Controllers you already use
The controller manager (chapter 44) is a bundle of dozens of these loops. A few you know:
| Controller | Watches | Makes sure that |
|---|---|---|
| Deployment | Deployments | The right ReplicaSet exists, and old ones scale down (chapter 10) |
| ReplicaSet | ReplicaSets, Pods | Exactly N matching Pods exist (chapter 9) |
| EndpointSlice | Services, Pods | Each Service lists the IPs of its ready Pods (chapter 11) |
| Job | Jobs | Pods run until the task has succeeded (chapter 36) |
| Node | Nodes | Pods on a dead node are marked for replacement |
| Namespace | Namespaces | Deleting a namespace deletes everything inside it |
Even the scheduler and the kubelet follow the same idea: watch for Pods, and make the world match them.
Watching, not polling
Asking the API server "anything new?" every second would be wasteful. Instead a controller opens a watch: a long-lived connection where the API server pushes every change as it happens.
$ kubectl get configmaps -n blog --watch
NAME DATA AGE
app-config 3 2d
app-config 3 2d ← someone just edited it
app-config 4 2d ← and again
Real controllers also keep a local copy (a cache) of everything they watch, so they can compare without asking the API server again and again.
Look at the state, not the event
This is the one idea that makes controllers robust. Compare two ways to handle "a Pod was deleted":
on "Pod deleted": create 1 Pod
on "Pod created": do nothingSounds fine, until the controller restarts and misses an event, or two events arrive at once, or one is delivered twice. Now you have 2 Pods, or 4, and nothing ever corrects it.
on any change (or every few minutes anyway):
wanted = spec.replicas # 3
have = count(matching Pods) # whatever is true right now
create or delete (wanted - have) PodsEvents only mean "something changed, go look". The controller then looks at the whole current state and decides. Missed or repeated events do not matter; the next check fixes everything. Engineers call this level-triggered (look at the level) instead of edge-triggered (react to the change).
Owners and garbage collection
When a controller creates an object, it writes its own name into the new object's ownerReferences. That is how Kubernetes knows what belongs to what:
$ kubectl get pod posts-7d9f8c6b5d-4kq2p -o yaml
metadata:
ownerReferences:
- apiVersion: apps/v1
kind: ReplicaSet
name: posts-7d9f8c6b5d
controller: true
Deployment posts └── ReplicaSet posts-7d9f8c6b5d (owner: Deployment posts) ├── Pod …-4kq2p (owner: ReplicaSet) └── Pod …-m7x4t (owner: ReplicaSet)A family tree, written in owner references.
Deployment posts ✗ deletedYou delete only the top.
garbage collector: ReplicaSet posts-7d9f8c6b5d has no owner any more → delete garbage collector: Pods …-4kq2p, …-m7x4t have no owner any more → deleteThe garbage collector, another controller, removes everything whose owner is gone. You never clean up children by hand.
Write a tiny controller
A controller is just a program that watches and acts. Here is a real, working one in a few lines of shell. The goal: when a ConfigMap labelled restart-on-change=true changes, restart every Deployment that uses it. This fixes the "env vars never update" problem from chapter 18.
#!/bin/sh
# config-watcher: restart Deployments when their ConfigMap changes
kubectl get configmaps -n blog -l restart-on-change=true \
--watch-only -o jsonpath='{.metadata.name}{"\n"}' |
while read -r cm; do
for d in $(kubectl get deploy -n blog \
-l uses-config="$cm" -o name); do
kubectl rollout restart "$d" -n blog
echo "ConfigMap $cm changed → restarted $d"
done
done
Observe
A watch on ConfigMaps with a label. --watch-only prints one line each time one of them changes. The label is the "input" that tells our controller which ConfigMaps to care about.
Compare / find
Find the Deployments that declared, with a label uses-config=<name>, that they depend on this ConfigMap. Labels and annotations are the usual way to configure a controller.
Act
Restart them, so new Pods read the new values. Restarting twice by accident does no harm, so the action is safe to repeat.
To run it in the cluster: put the script in a container, give it a ServiceAccount with a small Role (watch ConfigMaps; get and patch Deployments, see chapter 43), and run one replica as a Deployment. Real controllers are usually written in Go (client-go, controller-runtime), but libraries exist for most languages, including Node (@kubernetes/client-node) and Python.
Rules for good controllers
Which of these does a well-behaved controller do?
- Safe to run its action twice (idempotent)
Loops repeat. "Make sure 3 exist" is safe twice; "create 1 more" is not.
- Remembers events in memory and acts only on those
A restart loses the memory. Always re-read the real state.
- Writes what it did into the object's
statusspecis what the user wants,statusis what the controller reports. That is howkubectl getshows READY 3/3. - Two controllers both managing the same field of the same object
They fight forever, each undoing the other. One owner per thing.
- Runs one active copy at a time
Two copies would act twice. Use one replica, or leader election (chapter 39).
Recap
- A controller is a loop: observe, compare with the desired state, act. Then repeat.
- Deployments, ReplicaSets, Services, Jobs and namespaces all work through controllers.
- Controllers watch the API server instead of polling, and keep a local cache.
- They look at the whole current state, not single events, so missed events do no harm.
- Owner references link children to parents; the garbage collector removes orphans.
- You can write your own: a watch, a comparison, an idempotent action, and a small RBAC role.
Quick check
replicas from 3 to 5. A minute later it is back to 3. Who did that, and why?Show answer
The Deployment controller. The ReplicaSet is owned by a Deployment that says 3. The controller compared the two, saw 5, and set it back. Change the Deployment instead.
Controllers: common questions
What is a Kubernetes controller?
A program that watches objects through the API server, compares the desired state with the current state, and acts to close the difference, in an endless loop.