Kubernetes Fundamentals¶
What problem this actually solves¶
Docker runs containers on one host. The moment an application needs to run across multiple machines — for capacity, for redundancy, for rolling out an update without downtime — something has to decide which machine runs which container, restart containers that die, move containers off a machine that fails, and route traffic to whichever containers are currently healthy. Kubernetes is that something: a system that takes a declared desired state ("run 3 replicas of this container, exposed on this port") and continuously works to make the actual state of a cluster of machines match it — not a one-time action, but an ongoing loop that keeps correcting drift for as long as the cluster runs.
Architecture: control plane vs. nodes¶
A Kubernetes cluster is split into two kinds of machines with fundamentally different jobs.
Control plane:
- API server — the single entry point for everything.
kubectl, every controller, every kubelet — all of them only ever talk to the API server, never directly to each other. It validates requests and reads/writes cluster state throughetcd. - etcd — a distributed key-value store holding the entire cluster
state: every object's desired spec and last-known status. If
etcdis lost with no backup, the cluster's memory of what it's supposed to be running is lost with it — this is the single most critical thing to back up in a self-managed cluster. - Scheduler — watches for Pods that exist (in
etcd, via the API server) but haven't been assigned to a node yet, and picks a node for each one based on resource requests, constraints, and current node load. - Controller manager — runs the reconciliation loops (below) for built-in resource types: the Deployment controller, the ReplicaSet controller, the Node controller, and others, each watching its own slice of cluster state and correcting drift.
Worker nodes:
- kubelet — the agent on every node that talks to the API server, is told "these Pods should be running on you," and makes that actually happen by talking to the local container runtime. It also reports the node's and Pods' health back up.
- kube-proxy — programs the node's networking rules (iptables or IPVS) so that traffic sent to a Service's virtual IP gets routed to one of the actual Pod IPs behind it — the mechanism behind Services (covered in a follow-up page).
- Container runtime (containerd, or another CRI-compatible runtime) — actually creates and runs containers, using the same underlying Linux primitives (namespaces, cgroups) described in Docker Fundamentals. Kubernetes doesn't reinvent containers — it orchestrates the same kind of container Docker runs, just across many machines instead of one.
The reconciliation loop: the core idea behind everything¶
Almost every Kubernetes controller follows the exact same pattern:
A Deployment's spec says replicas: 3. The Deployment controller
continuously checks: are there actually 3 healthy Pods matching this
Deployment's label selector right now? If a node crashes and takes one
Pod down with it, the controller observes only 2 — and creates a
replacement, without anyone telling it to. If someone manually deletes a
Pod with kubectl delete pod, the same loop notices the mismatch within
seconds and creates a new one to restore the count to 3. This is why
directly editing or deleting a Pod created by a Deployment doesn't
"fix" anything for long — the controller doesn't know or care that a
human intervened; it only knows the current count doesn't match the
desired count, and corrects it.
The Pod: why not just "a container"¶
A Pod — not a container — is Kubernetes's smallest deployable unit.
A Pod is one or more containers that are always scheduled together, on
the same node, sharing the same network namespace and IPC namespace
(but each container still gets its own filesystem/mount namespace,
unless volumes are explicitly shared). Containers inside one Pod can
reach each other over localhost, exactly as if they were two processes
on the same machine — because from the network namespace's perspective,
that's exactly what they are.
This matters for the sidecar pattern: a logging agent, or a proxy
that handles TLS termination, running as a second container in the same
Pod as the main application container, sharing its network namespace so
it can intercept or read from localhost without any special
networking configuration. Most Pods, though, run a single container —
multi-container Pods are the exception, used specifically when two
processes genuinely need this tight, same-machine, shared-namespace
coupling, not as a general multi-process convenience.
From Pod to Deployment: the layers that make this usable¶
A bare Pod, created directly, has no self-healing: if its node dies, the Pod is simply gone — nothing recreates it, because nothing is watching it and reconciling. This is why Pods are almost never created directly in practice.
- A ReplicaSet watches for Pods matching a label selector and ensures exactly N of them exist — the reconciliation loop from above, applied specifically to "how many Pods."
- A Deployment manages ReplicaSets, and adds rolling-update
behavior on top: changing a Deployment's container image creates a
new ReplicaSet at the new version, scales it up gradually while
scaling the old ReplicaSet down, and keeps the old ReplicaSet around
(scaled to 0) so
kubectl rollout undocan roll back by simply scaling the old one back up and the new one down.
A minimal working example:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: my-web-app:1.0
ports:
- containerPort: 8080
kubectl apply -f deployment.yaml
What actually happens, tracing it through the architecture above: apply
sends this YAML to the API server, which validates it and writes the
Deployment object into etcd. The Deployment controller (in the
controller manager) notices a Deployment exists with no matching
ReplicaSet, and creates one. The ReplicaSet controller notices it should
have 3 Pods but has 0, and creates 3 Pod objects — still unscheduled.
The scheduler notices 3 unscheduled Pods, picks a node for each
based on available resources, and writes that assignment back through
the API server. Each node's kubelet, watching for Pods assigned to
it, sees a new Pod, and instructs the local container runtime to
actually pull the image and start the container. Every step in this
chain is a separate, independent watch-and-react loop — nothing here is
one big synchronous function call, which is exactly why the same loops
also handle a node dying, a Pod crashing, or a rollout update, using the
identical mechanism instead of special-cased recovery code.
Common pitfall¶
Editing a running Pod's spec directly with kubectl edit pod ..., or
deleting it expecting the change to "stick," ignores that a
Deployment-managed Pod is owned by a ReplicaSet actively enforcing its
spec — a direct edit is either rejected for immutable fields, or
promptly overwritten/replaced the next reconciliation cycle. The correct
edit target is almost always the Deployment (kubectl edit deployment
..., or re-applying an updated YAML file) — changes there flow down
through ReplicaSet replacement and rolling update, which is the whole
point of not managing Pods by hand.