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.

graph TD subgraph cp["Control plane (the brain — usually 1-3+ machines for HA)"] api["API server<br/>(the only thing anything talks to)"] etcd["etcd<br/>(stores all cluster state)"] sched["Scheduler<br/>(decides which node runs a new Pod)"] cm["Controller manager<br/>(runs reconciliation loops)"] api --- etcd sched --> api cm --> api end subgraph n1["Worker node 1"] kubelet1["kubelet<br/>(talks to API server, runs Pods)"] proxy1["kube-proxy<br/>(programs networking rules)"] runtime1["container runtime<br/>(containerd)"] kubelet1 --> runtime1 end subgraph n2["Worker node 2"] kubelet2["kubelet"] proxy2["kube-proxy"] runtime2["container runtime<br/>(containerd)"] kubelet2 --> runtime2 end api <-->|"watch / report status"| kubelet1 api <-->|"watch / report status"| kubelet2

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 through etcd.
  • etcd — a distributed key-value store holding the entire cluster state: every object's desired spec and last-known status. If etcd is 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:

graph LR A["1. Read desired state<br/>(spec, from etcd)"] --> B["2. Observe actual state<br/>(what's really running)"] B --> C{"Do they match?"} C -->|"no"| D["3. Take action to close the gap<br/>(create/delete/update)"] D --> B C -->|"yes"| B

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.

graph TD D["Deployment<br/>(desired: 3 replicas, this container image)"] --> RS["ReplicaSet<br/>(enforces: exactly 3 Pods matching this spec exist)"] RS --> P1["Pod 1"] RS --> P2["Pod 2"] RS --> P3["Pod 3"]
  • 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 undo can 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.