Controllers & reconciliation
The loops that turn desired state into running pods.
You never actually tell Kubernetes to start a Pod. A Pod is the smallest thing the cluster runs, one or more containers that share a single network address, and there is no 'run this one right now' button anywhere in the system. You write down what you want to exist, and something else makes it true and keeps it true. That something is a controller, and the loop it runs is why a cluster quietly heals itself at 3am while you're asleep.
The loop that never ends
Think of a thermostat. You set it to 21 degrees. It reads the room, sees 19, switches on the heat, reads again, and keeps nudging until the room matches the number you asked for. It never stops checking. A controller is that same idea pointed at a cluster. You declare 'I want three copies of this web server running.' The controller reads the cluster, counts two, and creates one more. A node dies and takes a copy with it. The controller counts two again, and makes a third.
Two words carry the whole model. The spec is your declared wish, the desired state. The status is what is actually true right now, the observed state. Every controller has exactly one job: drag status toward spec, over and over, and never stop.
Here is the part that trips people up. A controller does not react to events one at a time, as in 'a Pod was deleted, therefore create a Pod.' It reacts to the current gap between spec and status, however that gap got there. Think of two night guards. One walks the whole building every round and fixes whatever looks wrong. The other sits by the alarm panel and only moves when a specific bell rings. Miss the bell and the second guard never knows it happened. The first one catches it on the next lap. Kubernetes controllers are the first guard, and engineers call this style level-triggered rather than edge-triggered. It is why controllers are so hard to wedge. Miss a notification, get a duplicate, crash halfway through an action, and none of it matters. Next time around the loop the controller re-reads reality and does whatever is needed. A dropped event never leaves the cluster stuck in a wrong state.
Controllers also don't hammer the API server (the front door to the cluster's database) with constant polling. Each one opens a watch, a long-lived subscription, and the API server pushes changes as they happen. Dozens of these controllers live inside a single process called the kube-controller-manager: one for ReplicaSets, one for Jobs, one for Nodes, one for endpoints, and on it goes. Each watches its own kind of object and reconciles it.
Watch it heal itself
Enough theory. Make a Deployment (a controller that keeps a set of identical Pods running) and watch the loop work.
kubectl create deployment web --image=nginx:1.27 --replicas=3kubectl get pods -l app=web
deployment.apps/web createdNAME READY STATUS RESTARTS AGEweb-6b7f8c9d4-4qk9t 1/1 Running 0 9sweb-6b7f8c9d4-hs2vp 1/1 Running 0 9sweb-6b7f8c9d4-zt7mb 1/1 Running 0 9s
Now delete one Pod by hand, the way a crash or a dead node would, and list again straight away.
kubectl delete pod web-6b7f8c9d4-4qk9tkubectl get pods -l app=web
pod "web-6b7f8c9d4-4qk9t" deletedNAME READY STATUS RESTARTS AGEweb-6b7f8c9d4-hs2vp 1/1 Running 0 51sweb-6b7f8c9d4-zt7mb 1/1 Running 0 51sweb-6b7f8c9d4-n8xql 0/1 ContainerCreating 0 2s
You didn't ask for that new Pod. You never do. The ReplicaSet controller saw its count drop from three to two and closed the gap. You can catch it in the act in the ReplicaSet's own event log, which names the exact controller that acted.
kubectl describe rs -l app=web | grep -A4 Events
Events:Type Reason Age From Message---- ------ ---- ---- -------Normal SuccessfulCreate 2s replicaset-controller Created pod: web-6b7f8c9d4-n8xql
How the controller knows which pods are its
When you create a Deployment you don't get Pods directly. The Deployment makes a ReplicaSet, and the ReplicaSet makes the Pods. Three layers. Each Pod carries a little note of parentage called an ownerReference that points back up to the ReplicaSet that made it. That note is how the controller knows which Pods to count as its own, and it is how a cascade delete finds everything to clean up when you remove the Deployment.
kubectl get pod web-6b7f8c9d4-n8xql \-o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'kubectl get rs -l app=web -o custom-columns=\NAME:.metadata.name,DESIRED:.spec.replicas,CURRENT:.status.replicas,READY:.status.readyReplicas
ReplicaSet/web-6b7f8c9d4NAME DESIRED CURRENT READYweb-6b7f8c9d4 3 3 3
DESIRED comes from spec, what you asked for. CURRENT and READY come from status, what the controller has actually achieved. When those numbers match, the loop is quiet. When they stay apart, a controller is either mid-fix or stuck, and that gap is the first thing to check when something feels off.
When the loops stop
Reconciliation has to run somewhere, and that somewhere can fail. If the kube-controller-manager stops, desired state quietly stops being enforced. Already-running Pods keep running, because the kubelet (the agent on each node that actually starts and watches containers) babysits them on its own, with no controller involved. So the cluster still looks healthy. But delete a Pod now and nothing replaces it. Kick off a rollout and it just hangs there. A node dies in the night and no one notices. The lights are on and nobody's reconciling.
So when self-healing seems broken, don't stare at the Pod. Look one level up. Is the controller-manager even running? Does the ReplicaSet show DESIRED 3 but CURRENT 2 and never catch up? Are there FailedCreate events complaining about a resource quota or a missing service account? The controller almost always tells you why it can't converge, right there in the events.
kubectl -n kube-system get pods -l component=kube-controller-managerkubectl -n kube-system get lease kube-controller-manager
NAME READY STATUS RESTARTS AGEkube-controller-manager-cp-1 1/1 Running 2 6dNAME HOLDER AGEkube-controller-manager cp-1_9f3c1a2b-7d4e-4c8a-b1e2 6d
Owner references and garbage collection explain why deleting a Deployment removes its ReplicaSets and pods. Break the owner link carefully or you orphan workloads.
If two controllers fight over the same object, you get thrash. Prefer one clear owner for each concern.
Status conditions are how controllers report progress. Read them before you restart random pods.
Try this
Create a Deployment, delete one pod, and watch the ReplicaSet recreate it. That correcting loop is reconciliation — desired state pulling reality back into line.
$ kubectl create deployment web --image=nginx:1.27 --replicas=3$ kubectl get pods -l app=web$ kubectl delete pod web-6b7f8c9d4-4qk9t$ kubectl get pods -l app=web$ kubectl describe rs -l app=web | grep -A4 Events$ kubectl get pod web-6b7f8c9d4-n8xql \-o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'$ kubectl get rs -l app=web -o custom-columns=\$ NAME:.metadata.name,DESIRED:.spec.replicas,CURRENT:.status.replicas,READY:.status.readyReplicas$ kubectl -n kube-system get pods -l component=kube-controller-manager$ kubectl -n kube-system get lease kube-controller-managerNAME READY STATUS RESTARTS AGEkube-controller-manager-cp-1 1/1 Running 2 6dNAME HOLDER AGEkube-controller-manager cp-1_9f3c1a2b-7d4e-4c8a-b1e2 6d
Takeaway
Controllers do not push once and forget. They watch, compare, and act forever. Your YAML is a thermostat setting, not a one-shot script.