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 control plane at the top: etcd connected only to kube-apiserver, kube-scheduler and kube-controller-manager connected to kube-apiserver. Below, three worker nodes, each with a kubelet, kube-proxy, containerd and pods; every kubelet watches and writes the API server, and the kubelet starts containers through containerd
Every arrow ends at the API server: the control plane decides, the nodes run, and nobody calls anybody else directly.

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

ComponentJobTalks to
kube-apiserverREST API for every object; checks and stores them; streams changes to watchersetcd, and everyone else talks to it
etcdKey-value store holding every object, e.g. /registry/pods/default/web-6f9c-k2x8ponly the API server
kube-schedulerPicks a node for each pod that has noneAPI server
kube-controller-managerRuns 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):

WhoWatchesWrites
you (kubectl)—Deployment web: replicas 3, image web:1.0
Deployment controllerDeployments, ReplicaSetsReplicaSet web-6f9c: replicas 3
ReplicaSet controllerReplicaSets, Pods3 Pods, no nodeName
schedulerPods with no nodeNameBinding → spec.nodeName
kubeletPods on its nodePod 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.

The ReplicaSet controller loop: a watch event, compare spec with status, make one change such as create or delete a pod, write it to the API server and wait. Three causes feed the same loop: scaling 3 to 5 creates 2 pods, a deleted pod creates 1 with a new name and IP, and node-3 dying evicts its pod so 1 is created elsewhere
Scaling, a deleted pod and a dead node look the same to the controller: the number it has differs from the number it wants.

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.

replicasmaxSurgemaxUnavailableat mostat least Ready
3ceil(0.75) = 1floor(0.75) = 04 pods3
4115 pods3
5ceil(1.25) = 2floor(1.25) = 17 pods4

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.