Chapter 45 of 47 · Under the hood

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

The reconcile loopA controller observes the current state through the API server, compares it with the desired state written in objects, and acts to close the difference, then repeats. 1 · Observewhat exists right now? 2 · Comparewanted 3 Pods, have 2 3 · Actcreate 1 Pod API serverdesired state (spec)+ current state (status) …and again, forever

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:

ControllerWatchesMakes sure that
DeploymentDeploymentsThe right ReplicaSet exists, and old ones scale down (chapter 10)
ReplicaSetReplicaSets, PodsExactly N matching Pods exist (chapter 9)
EndpointSliceServices, PodsEach Service lists the IPs of its ready Pods (chapter 11)
JobJobsPods run until the task has succeeded (chapter 36)
NodeNodesPods on a dead node are marked for replacement
NamespaceNamespacesDeleting 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 nothing

Sounds 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.

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
  1. Deployment posts
    └── ReplicaSet posts-7d9f8c6b5d     (owner: Deployment posts)
        ├── Pod …-4kq2p                 (owner: ReplicaSet)
        └── Pod …-m7x4t                 (owner: ReplicaSet)

    A family tree, written in owner references.

  2. Deployment posts   ✗ deleted

    You delete only the top.

  3. garbage collector: ReplicaSet posts-7d9f8c6b5d has no owner any more → delete
    garbage collector: Pods …-4kq2p, …-m7x4t have no owner any more → delete

    The 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.

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?

Recap

Quick check

You manually change a ReplicaSet's 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.