Pod, Node, Scheduler: The Three Atoms of a Kubernetes Cluster
· tech
📑 Contents
- Pod: the smallest unit, and not a “container”
- Node: a machine
- Scheduler: decides which Node a Pod lands on
- The key mindset: Pods are short-lived
- Reflections
- Once “a Pod is not a container” clicks, everything flows
- Short-lived Pods are a feature, not a bug
- The Scheduler decides “where” for you, but you can step in
The previous post covered the soul of K8s: you declare a desired state, and the reconcile loop keeps pulling reality towards it. But that “I want 3” — 3 of what? Landing on which machine? Who decides where? This post makes the cluster’s three most basic atoms clear: Pod, Node, Scheduler.
Pod: the smallest unit, and not a “container”
The first counter-intuitive point: the smallest unit K8s schedules and scales isn’t a container; it’s a Pod. A Pod wraps one (or a few) containers so they live and die together and share network and storage:
Why the extra wrapper? Because some containers naturally belong together — a main service plus a sidecar that collects its logs or acts as a proxy; they need to share a network, be scheduled together, live and die together. But don’t overuse it: most Pods are one container. Just remember this: the container is “what runs”; the Pod is the unit K8s actually moves, replicates and schedules.
In YAML, a Pod with “app + sidecar sharing a scratch volume” looks like this — containers is an array (you can list several), and the disk defined under volumes can be mounted by both:
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: app # main container: your service
image: myrepo/web:1.0
volumeMounts:
- { name: shared, mountPath: /var/log/app }
- name: log-agent # sidecar: collects its logs on the side
image: fluent-bit:latest
volumeMounts:
- { name: shared, mountPath: /logs } # same disk mounted, so it can read what app wrote
volumes:
- name: shared
emptyDir: {} # scratch space shared by both containers (gone when the Pod goes)
You can see how the Pod’s two kinds of “sharing” land: both containers mount the same shared volume (so the sidecar can read the files the app writes), and being in one Pod they share one network (they reach each other on localhost). In practice, though, you rarely write a Pod by itself — you let a Deployment‘s template generate them; the standalone Pod here is just to show the structure clearly.
Node: a machine
A Node is a real machine (on the cloud, usually a VM). The previous post mentioned the cluster has two halves:
- Control Plane (the brain): decides what to run, where to place it, keeps correcting the gap.
- Worker Nodes (the workers): the machines that actually run your Pods. Each has a kubelet that looks after the Pods on that machine and reports status back to the brain.
So what “I want 3 Pods” actually looks like is those 3 Pods assigned to some Nodes and running there. And what decides “which Pod goes to which Node” is the third atom.
Scheduler: decides which Node a Pod lands on
When the reconcile loop needs a new Pod, that Pod starts out Pending (no Node yet). The Scheduler’s job is to pick a Node for it to land on:
The knobs behind filtering and scoring — requests/limits, node affinity, taints and tolerations — are the key to controlling “who runs where”; the advanced scheduling post in this series covers them properly. For now, know this: where a Pod lands isn’t random; the Scheduler computes it from resources and rules.
The key mindset: Pods are short-lived
One last idea you must build right now: Pods are disposable. When one dies, gets rescheduled, or gets replaced by a rolling update, the old Pod simply disappears and the reconcile loop starts a brand-new Pod in its place — and the new Pod has a new IP.
Which means: you should never remember a Pod’s IP, and never store state inside a Pod. Pods are cattle, not pets — when one breaks you replace it, you don’t nurse it. That leads to two later topics: if Pod IPs change, how do other services find them reliably? (→ Service, post 4); and if a Pod’s data goes with it, what about stateful things like databases? (→ Volumes and StatefulSets, post 6).
Reflections
Once “a Pod is not a container” clicks, everything flows
What I got stuck on longest when learning K8s was “if it runs containers, why the extra Pod?” Then it clicked: the Pod is K8s’s scheduling unit; the container is the execution unit. What K8s moves, replicates and places is the Pod; whether a Pod holds one container or several is a design choice about “do these things need to live and die together”. Ninety percent of the time it’s one Pod, one container — don’t cram in sidecars to show off. Get the “unit” straight and everything after lines up at once: Deployments manage Pods, Services point at Pods, scheduling schedules Pods.
Short-lived Pods are a feature, not a bug
“A Pod can vanish at any moment and its IP will change” sounds unsettling, but it’s actually the precondition for K8s self-healing, not a flaw. Precisely because Pods are treated as disposable, a broken one can be swapped painlessly. It’s the same thinking as the “executors are deleted when done” I kept repeating in the Spark and Spark on K8s posts — treat compute units as cattle, not pets. Once you accept that, you stop doing things like “remember a particular Pod” that are destined to hurt, and turn instead to the stable abstractions K8s gives you (Service, Volume).
The Scheduler decides “where” for you, but you can step in
The default Scheduler picks a node with room automatically, and most of the time you don’t need to care. But when you have needs like “this batch of Pods should run on high-memory machines” or “don’t squeeze in next to that noisy neighbour”, requests, affinity and taints are how you step in — in Airflow + Spark on K8s I used them to pin the Airflow core to stable nodes and throw Spark executors onto cheap spot nodes; same principle. Let the Scheduler place things automatically first, and only reach in when you actually need to — that’s my attitude to every advanced K8s feature: the defaults are enough; don’t use something just to use it.