The idea: a database of desired state, and loops that chase it
A Kubernetes cluster has two halves. The control plane decides what should run. The worker nodes run it. You never tell a node "start this container". You write a desired state (for example "3 copies of web:1.0") into the API server. Then several independent programs each notice the part that concerns them and do one small step, until the real state matches.
The canvas shows the control plane at the top and three worker nodes below it. Every request goes into the kube-apiserver request log at the bottom. Yellow boxes are writes, purple boxes are watch notifications, and orange boxes are the kubelet talking to containerd on its own node.
The control plane
| Component | Job | Talks to |
|---|---|---|
kube-apiserver | REST API for every object; checks and stores them; streams changes to watchers | etcd, and everyone else talks to it |
etcd | Key-value store holding every object, e.g. /registry/pods/default/web-6f9c-k2x8p | only the API server |
kube-scheduler | Picks a node for each pod that has none | API server |
kube-controller-manager | Runs the controllers: Deployment, ReplicaSet, Node, Job, EndpointSlice, … | API server |
The worker nodes
Every node runs the same three programs. The kubelet watches for pods bound to its node and makes them real.
It asks the container runtime (here containerd) to start them through the CRI interface. It also
reports their status back and renews the node's Lease every 10 s as a heartbeat. kube-proxy
watches Services and EndpointSlices and writes the iptables rules that send Service traffic to pods. It plays no part in
this page's demos. See The Life of an HTTP Request in Kubernetes for it.
Everything goes through the API server
No component calls another. The scheduler does not tell the kubelet to start a pod. It writes a Binding to the API server,
and the kubelet, which watches the API server, sees it. Only the API server reads and writes etcd. Each write gets
the next etcd revision (the object's resourceVersion). A watcher that reconnects says "send me
everything after revision N", so it misses nothing. This hub design keeps validation, authentication and auditing in one
place, and it lets each component fail and restart on its own.
Controllers and the reconcile loop
A controller compares the spec (what is wanted) with the status (what is true) and
makes one change to close the gap. Then it waits for the next watch event. The controllers form a chain, and each creates
the next object down (Demo: Apply):
| Who | Watches | Writes |
|---|---|---|
| you (kubectl) | — | Deployment web: replicas 3, image web:1.0 |
| Deployment controller | Deployments, ReplicaSets | ReplicaSet web-6f9c: replicas 3 |
| ReplicaSet controller | ReplicaSets, Pods | 3 Pods, no nodeName |
| scheduler | Pods with no nodeName | Binding → spec.nodeName |
| kubelet | Pods on its node | Pod status: Running, Ready |
Because every controller just compares wanted with actual, the same code handles every cause. Scaling changes the wanted number (Demo: Scale 3 → 5 → 2). Deleting a pod changes the actual number, and the ReplicaSet makes a new one with a new name and IP (Demo: Self-healing). The deleted pod is not "restarted": pods are cattle, not pets.
What the scheduler does, and does not do
The scheduler filters out nodes that cannot take the pod: not Ready, tainted, full, or short of the requested
CPU and memory. Then it scores the rest. This page uses one rule, "fewest pods wins", which spreads the pods out.
It writes the winner into the pod as spec.nodeName and moves on. It never starts a container and never moves a running pod.
Rolling updates: maxSurge and maxUnavailable
Changing the pod template (kubectl set image) makes the Deployment controller create a new ReplicaSet.
It then moves replicas from the old one to the new one in small steps. Two limits apply. maxSurge is how many extra pods
may exist, and maxUnavailable is how many pods may be missing. Both default to 25 %: surge rounds up and unavailable rounds down.
| replicas | maxSurge | maxUnavailable | at most | at least Ready |
|---|---|---|---|---|
| 3 | ceil(0.75) = 1 | floor(0.75) = 0 | 4 pods | 3 |
| 4 | 1 | 1 | 5 pods | 3 |
| 5 | ceil(1.25) = 2 | floor(1.25) = 1 | 7 pods | 4 |
With 3 replicas, each round adds one new pod, waits until it is Ready, and only then removes one old pod
(Demo: Rolling update). The old ReplicaSet is kept at 0 replicas. kubectl rollout undo scales it back up,
and pressing Update Image a second time does the same here.
When a node dies
A cut-off node cannot say that it is gone. It just stops renewing its Lease. After node-monitor-grace-period
(50 s), the node controller marks it Ready=Unknown and adds the taint node.kubernetes.io/unreachable:NoExecute.
Every pod tolerates that taint for tolerationSeconds: 300 by default, in case the network comes back quickly. After that the pods
are evicted, and the ReplicaSet makes replacements on healthy nodes. The old pods stay Terminating. Only their kubelet can confirm
the containers stopped. When the node returns, it is empty: Kubernetes does not move pods back
(Demo: Node-3 dies). For the pod's side of this, see The Life of a Kubernetes Pod.
If the control plane goes down
Running pods keep running and keep serving traffic. The kubelets and containerd do not need the API server for containers that are already started, and kube-proxy's iptables rules stay in place. What stops is change. kubectl fails, nothing is scheduled, and a pod that crashes on a dead node is not replaced. That is why production clusters run 3 or 5 API servers and etcd members. etcd keeps its copies consistent with the Raft protocol (see Raft).
How etcd itself stores objects, numbers every change with a revision and streams watch events to informers: etcd: the Store Behind Kubernetes.
What the page leaves out
A highly available control plane (several API servers, 3 or 5 etcd members, leader election between schedulers and controller managers), authentication, RBAC and admission webhooks, the cloud-controller-manager, CoreDNS, DaemonSets, StatefulSets and Jobs, CPU and memory requests in the scheduler's filter, the status writes of the Deployment and ReplicaSet controllers, Lease writes (they do bump the etcd revision in a real cluster), and real timings: components react in milliseconds, not whole seconds.