all_lessons/kubernetes/02lesson 3 / 14

Part I — The engine and the unit

The Pod and the Node: the Atom and the Hands

Lesson 01 gave us the container: an image (a frozen, layered filesystem plus a start command) run under namespaces for isolation and cgroups for limits — a unit that is identical everywhere and can't trample its neighbors. But it left two cracks. First, a real app is rarely one container: a web server needs a log shipper to tail its files and a proxy to terminate TLS — helpers that must share its network and disk and live and die with it, yet a container isolates each of those concerns into its own separate world. Second, a container spec is just inert text; nothing in lesson 01 actually pulls the image and runs it. This lesson derives the two primitives that close both cracks: the Pod (the atom the cluster schedules) and the Node (the machine, and the agent — the hands — that turn desired text into a running process).

The previous step left this broken
A single container is isolated too well: its log shipper sidecar can't reach its logs (separate filesystem) and a proxy can't sit on its port (separate network namespace), so cooperating helpers that should be one deployable thing are forced apart. And even one lone container is not something a cluster can place — it is desired-state data with no body. We need (a) a way to group a few containers so they share one network and disk and are scheduled as one, and (b) a physical machine with an agent that actually makes the spec real.
Linear position
Forced by: lesson 01's container isolates each process completely, so cooperating helpers can't share network/disk, and a spec alone never runs.
New capability: the Pod — the smallest schedulable unit: a group of containers sharing one network namespace (one IP, talk over localhost), IPC, and volumes — and the Node: a machine whose kubelet runs the lesson-00 reconcile loop for its assigned Pods against a real container runtime, reporting actual state upward.
Forces next: a Pod assignment and the truth the kubelet reports must live somewhere durable and consistent that every actor agrees on — lesson 03.
The plan
Five moves. (1) Show why "one container" is the wrong atom and derive the Pod as a shared-namespace group. (2) Explain the pause container — the trick that lets containers share one IP. (3) Name the sidecar pattern the Pod exists to enable. (4) Derive the Node: kubelet plus a container runtime via the CRI, plus kube-proxy. (5) Nail the key insight — a Pod spec is inert desired-state data; the kubelet is the hands — then map failure modes and the hand-off to the control plane.

1 · Why one container is the wrong atom

Take a concrete app: an HTTP server that writes structured logs to /var/log/app/. Two helpers naturally attach to it. A log shipper tails those files and forwards them to a central system. A proxy (sometimes called an ambassador) sits in front, terminating TLS on port 443 and forwarding plaintext to the server on port 8080. These three processes are not independent services — they are one logical unit. They must start together, stop together, land on the same machine, and the helpers must reach the main container's disk (the logs) and localhost port (8080).

But lesson 01's container does the opposite. Each container gets its own mount namespace (its own filesystem view) and its own network namespace (its own loopback, its own ports, its own IP). Run the shipper as a separate container and it sees an empty /var/log/app/; run the proxy separately and localhost:8080 from the proxy's view is the proxy itself, not the server. The isolation that made the container a clean unit now severs helpers that must cooperate. Scheduling them independently makes it worse: the scheduler could place the proxy on node A and the server on node B, and now "in front of" means a network hop instead of localhost.

The fix is not to weaken the container. It is to introduce a group that the cluster treats as one schedulable thing, inside which a chosen set of namespaces is shared rather than isolated. That group is the Pod — the smallest unit Kubernetes ever schedules. You never schedule a bare container; you schedule a Pod, and the Pod is placed on exactly one node, as a unit.

Shared: network
All containers in a Pod share one network namespace — so one Pod has one IP, and the containers reach each other over localhost on different ports. The proxy hits localhost:8080 and lands on the server. They must not collide on ports.
Shared: IPC + volumes
They share an IPC namespace (System V / POSIX shared memory) and any volumes the Pod declares. Mount the same emptyDir volume into server and shipper and the shipper finally sees the logs.
Isolated: still per-container
Each container keeps its own image, mount root (apart from shared volumes), PIDs (by default), and cgroup limits. You still get clean dependency boundaries and per-container CPU/memory ceilings — isolation where it helps.
One fate
A Pod is scheduled, lives, and is deleted as one unit. The kubelet co-locates and co-schedules its containers on a single node; you cannot split a Pod across two machines.

The rule to carry forward: a Pod ≠ a container. Most Pods hold exactly one main container, but the Pod is the boundary of co-location and the unit of an IP. The number that matters: one IP per Pod, not one IP per container. That has a concrete cost. If you ran our 3-container app as a Pod, the cluster spends one IP, not three; run 1,000 such Pods and you consume 1,000 cluster IPs total, not 3,000. It also fixes the accounting unit for resources: a Pod's CPU/memory request is the sum of its containers' requests (plus the negligible pause container), and the scheduler in lesson 10 places that one summed number, never the containers individually. One Pod, one IP, one placement decision, one fate.

2 · The pause container: how one IP is shared

Sharing a network namespace raises a lifecycle problem. If container A "owns" the namespace and the others join it, then when A crashes and restarts the namespace is destroyed and recreated — the Pod's IP would churn every time any container restarted, and the join order would matter. That is fragile. Kubernetes solves it with a tiny, near-invisible pause container (also called the sandbox or infra container).

When the kubelet creates a Pod it first starts the pause container — a few-kilobyte image whose only job is to pause() and hold the namespaces open. Every real container in the Pod then joins the pause container's network (and optionally IPC) namespace. The pause container almost never dies, so the namespace — and therefore the Pod IP — is stable across restarts of any application container. Your log shipper can crash and be recreated 50 times; the Pod keeps the same IP because the pause container, not the shipper, holds the network namespace.

create sandbox → kubelet starts the pause container; it acquires the Pod's network namespace and the CNI plugin assigns one IP
join → each app container is started with --net=container:pause (and shared IPC), so they all see the same loopback, ports, and IP
survive churn → an app container crashes → kubelet restarts just that container into the still-living namespace → Pod IP unchanged

This is why "containers in a Pod talk over localhost" is literally true: they are in the same network namespace, so 127.0.0.1 is shared. It is also why two containers in one Pod cannot both bind port 8080 — there is one port space, like two processes on one host.

Be precise about which namespaces are shared, because it is a deliberate choice, not all-or-nothing. By default a Pod's containers share the network and IPC namespaces (via the pause container) and any declared volumes, but each keeps its own PID namespace, mount root, and cgroup — so container A cannot kill a process inside container B, and B's crash and OOMKill (lesson 01) are scoped to B's own memory limit. You can opt into a shared PID namespace (shareProcessNamespace: true) when a sidecar genuinely needs to see the main process tree, but the default keeps the failure boundaries from lesson 01 intact inside the Pod while still letting helpers cooperate over localhost and shared disk. The Pod shares exactly what cooperation needs and isolates everything else.

3 · The patterns the Pod exists to enable

The Pod is not "containers, but plural for convenience." It exists specifically to support helper containers that augment a main container while sharing its fate. These are conventional patterns, not Kubernetes API objects — the shape comes entirely from shared namespaces and shared volumes.

PatternWhat the helper doesWhat it shares to do it
SidecarRuns alongside the main container for its whole life — e.g. a log shipper, a metrics exporter, a service-mesh proxy that handles all in/out traffic.Volumes (read the main container's log files) and/or network (intercept traffic on localhost).
Init containerRuns to completion before the main containers start — e.g. wait for a dependency, run a schema migration, fetch config into a shared volume.Volumes — writes setup into an emptyDir the main container then reads. Runs sequentially, must exit 0.
Ambassador / proxySits in front of (or beside) the app and brokers external connections — TLS termination, connection pooling, sharding.Network — the app talks to localhost and the ambassador handles the outside world.

The payoff: the main container stays simple and single-purpose (lesson 01's clean unit), while cross-cutting concerns live in separate, independently-built images that ride along in the same Pod. They start together, stop together, and are scheduled together — exactly the "one logical unit" §1 demanded. A modern note: native sidecar containers (a special init container with restartPolicy: Always, stable since Kubernetes 1.29) fixed the old ordering bugs, ensuring sidecars start before the app and shut down after it.

4 · The Node: the machine and the hands

A Pod is still just desired-state data — JSON the cluster has decided should run. Something physical must pull the images, start the pause container, wire the CNI, run the app containers under their cgroups, and keep watching. That something is the Node: a worker machine (a VM or bare metal) running three components.

kubelet
The per-node agent. It is handed the set of Pods assigned to its node and runs the lesson-00 reconcile loop for them: observe what is actually running, diff against the assigned Pod specs, act (create/restart/delete containers), repeat forever. It does not pick which node — it runs what it's told.
container runtime
The software that actually creates containers — typically containerd (or CRI-O). The kubelet talks to it over the CRI (Container Runtime Interface), a stable gRPC contract, so Kubernetes is not tied to one runtime. The runtime pulls images and asks the kernel for the namespaces and cgroups from lesson 01.
kube-proxy
A per-node network component that programs the node's packet rules (iptables or IPVS) so that the stable Service virtual IPs you'll meet in lesson 06 actually route to live Pods. It is part of the node's machinery; we only name it here.
CNI plugin
The Container Network Interface plugin the kubelet invokes when the pause container comes up: it assigns the Pod its IP and wires it onto the cluster network. Different plugins, same contract.

Concretely, when a Pod is assigned to a node, the kubelet drives this sequence against the CRI and CNI:

see the assignment → kubelet learns a new Pod is bound to its node (desired state for this node now includes one more Pod)
sandbox + network → tells the runtime to start the pause container; invokes the CNI plugin to assign the Pod IP
pull + run → runtime pulls each image (if not cached), then starts init containers in order, then the main/sidecar containers under their cgroup limits
watch + report → kubelet runs probes, restarts crashed containers per restartPolicy, and reports actual Pod status (Running/Ready/Failed) and node health back upward

That last step is the whole point. The kubelet does not just create things and forget them — it is a control loop. If a container exits, the kubelet recreates it (this is where lesson 00's loop physically lives, per Pod, per node). And it continuously publishes actual state: this Pod's containers are Running, this node has 14 GB of allocatable memory and 6 of 32 Pod slots free. The desired state flows down to the kubelet; the actual state flows up from it.

Those reported numbers are not decoration — they are the level the rest of the cluster reads. Work an example: a node advertises 32 GB of memory, but the kubelet subtracts what the OS and its own daemons need (call it 2 GB reserved), so it reports 30 GB allocatable — the real budget Pods can claim. The node also caps Pods it will run (the default is 110 per node). The kubelet keeps publishing both the capacity and what is currently used, because the scheduler in lesson 10 places new Pods purely from these reported levels: it never inspects a node directly, it reads the node's last-reported allocatable-minus-used. If the kubelet stops reporting (the node went silent), after a grace period the cluster marks the node NotReady and treats its Pods as gone — which is exactly the lesson-00 level-triggered reaction to a missing signal, now driven by a real heartbeat. The kubelet is both the hands (it runs the containers) and the eyes (its report is the actual state everyone else trusts).

5 · The key insight: inert data vs. the hands

Hold the two halves of this lesson side by side and the architecture of all of Kubernetes appears in miniature.

           DESIRED STATE (inert data)                 ACTUAL STATE (reported by the hands)
           --------------------------                 ------------------------------------
           Pod spec: "run these 3 containers,           kubelet: "pod web-7f is Running,
            share net+vol, on some node"                 3/3 containers up, node has 6 slots free"
                       |                                              ^
                       |  assigned to a node                          |  status reported up
                       v                                              |
   +-------------------------------------------------- NODE (worker machine) ------------------+
   |                                                                                            |
   |   kubelet  --- CRI (gRPC) --->  containerd (runtime)        kube-proxy     CNI plugin      |
   |   (the reconcile loop;          (pulls images, asks         (programs       (assigns       |
   |    runs assigned pods)           kernel for ns+cgroups)      Service rules)   pod IP)       |
   |        |                                                                                   |
   |        v                                                                                   |
   |   +--------------------------- POD (one IP: 10.1.4.7) -------------------------+            |
   |   |   [pause]      holds the shared network + IPC namespaces                  |            |
   |   |     ^   ^   ^                                                             |            |
   |   |     |   |   |   all join the same netns => talk over localhost            |            |
   |   |  [server]  [proxy sidecar]  [log-shipper sidecar]                         |            |
   |   |   :8080      :443->:8080      tails shared volume /var/log/app            |            |
   |   |     \________________ shared volume: emptyDir /var/log/app ____________/  |            |
   |   +-------------------------------------------------------------------------+              |
   +--------------------------------------------------------------------------------------------+

A Pod spec is inert desired-state data — a description, the same JSON whether or not anything runs it. The kubelet is the hands that make it real on a node, and the eyes that report what is actually true back up. This split — declared intent as data, an agent that reconciles it locally and reports up — is the lesson-00 loop made physical, and it is the template every higher-level controller in this track will copy. Three facts to lock in: a Pod is not a container (it is a group sharing one IP); there is exactly one IP per Pod (containers share it via the pause container over localhost); and a node is a worker machine whose kubelet runs assigned Pods and reports their truth.

Failure modes & checklist

Failure modes

  • Treating a Pod as one container. Putting unrelated services in one Pod "to save Pods," so they can't scale or restart independently and one crash takes the others' fate. Signal: you scale the web tier and your batch job scales with it; one container's OOMKill restarts a sibling that was fine.
  • Port collision inside a Pod. Two containers in one Pod both bind 8080. One fails to start because there is a single shared network namespace, not one per container. Signal: "address already in use" in the second container's logs at startup.
  • Expecting a stable Pod IP across reschedule. The pause container keeps the IP stable across container restarts, but if the whole Pod is deleted and recreated (node dies, rescheduled) it gets a new IP. Hardcoding a Pod IP breaks. Signal: clients fail after a node drain or Pod eviction; the IP they cached is gone.
  • Helper can't see the data. A log-shipper sidecar shows nothing because you forgot to mount the shared volume into both containers — separate mount namespaces mean separate filesystems. Signal: main container writes logs; sidecar's path is empty.
  • Believing the kubelet schedules. Assuming the per-node kubelet decides placement. It only runs Pods already assigned to its node and reports status; it does not choose nodes (that is the scheduler, lesson 10). Signal: confusion about why an unschedulable Pod sits Pending with no kubelet involved.

Checklist

  • One concern per Pod. Group containers only when they truly share fate, network, or disk (sidecar/init/ambassador) — otherwise give each its own Pod.
  • Address siblings via localhost and give each container a distinct port; never two binds on the same port in one Pod.
  • Share data via a declared volume mounted into every container that needs it; don't assume a shared filesystem just because they share a network.
  • Use init containers for one-shot setup that must finish before the app, and native sidecar (restartPolicy: Always init) containers for long-running helpers that must outlast startup.
  • Treat the Pod IP as ephemeral — stable across container restarts, not across reschedules. Reach Pods through the stable abstractions coming in lesson 06.
  • Remember the split: the Pod spec is data; the kubelet + runtime are the hands that realize it and report actual state up.

Checkpoint exercise

Try it
Design the three-container Pod from §1 on paper: an HTTP server on 8080, a TLS proxy sidecar on 443 forwarding to localhost:8080, and a log-shipper sidecar. (a) State exactly which namespaces/resources the three containers share and which stay isolated, and which container the pause container's namespace is holding. (b) Name the one volume you must declare and which containers mount it, so the shipper sees the server's logs. (c) The server container OOMKills and restarts: explain why the Pod's IP does not change. (d) The whole node dies and the Pod is recreated elsewhere: does the IP change, and which component pulls the images and starts the containers on the new node? Answer in five sentences.

Where this points next

We now have an atom (the Pod) and hands (the node's kubelet) to realize it. But a fault line is wide open. The kubelet on node A holds the desired specs for its Pods and reports actual status up — but up to where? Desired state (what an operator declared) and actual state (what every kubelet observes) both need a single, durable, consistent home. If two actors each keep their own copy, they disagree: one thinks Pod web-7f is on node A, another already rescheduled it to node B, and they fight. A node can also die and take its local view with it. Lesson 03, The Control Plane: One Source of Truth, derives the consistent store (etcd) and the single writer in front of it (the API server) that every kubelet, controller, and user agrees on — the place the desired Pod assignment lives and the actual status is recorded.

Takeaway
A container isolates a process too completely for cooperating helpers and is not itself schedulable, so Kubernetes introduces the Pod: the smallest schedulable unit, a group of containers that share one network namespace (one IP, talking over localhost), IPC, and declared volumes, while keeping per-container images and limits. A tiny pause container holds those shared namespaces so the Pod IP survives app-container restarts, and this is exactly what makes sidecar, init, and ambassador patterns possible. A Pod spec, though, is inert desired-state data; the Node — a worker machine running the kubelet (the per-node reconcile loop) plus a container runtime via the CRI, with kube-proxy and a CNI plugin — is the hands that pull images, run the containers, and report actual state back up. Pod ≠ container, one IP per Pod, node = worker machine: declared intent flows down to the kubelet, observed truth flows up — which forces the question of where that truth durably lives (lesson 03).

Interview prompts