Seeking Wisdom · AI/ML Engineering · Investing · Books · Projects · About ·

Kubernetes by Example · Part 5

DaemonSets

Three replicas gave you two pods on one machine and none on another. When the whole point is to be there, you need a different question.

assumes Parts 1–4~16 minutesneeds a 3-node cluster

The symptom

You add the decoder to airwaves. It needs to be on every machine, because tuners hand it audio through shared memory and shared memory does not travel.

Three nodes, so you write a Deployment with three replicas. It comes up green: 3/3 ready.

And a third of your tuners are broken. Not crashing — just sitting there, waiting for a decoder that isn't on their machine.

$ kubectl get pods -o wide -l app=decoder
NAME              READY   STATUS    NODE
decoder-x7k2p     1/1     Running   node-1
decoder-m4n8q     1/1     Running   node-1
decoder-p2w9s     1/1     Running   node-2

Two on node-1. One on node-2. Nothing at all on node-3.

The Deployment did precisely what you asked. You asked for three pods. You wanted one per node, and those are not the same request — there is no way to express the second one with a replica count.

Counting nodes instead of pods

Everything so far has been counted in replicas: a number you choose, pods the scheduler places wherever they fit. For most workloads that's exactly right — you care how much capacity exists, not where it sits.

Some workloads invert that completely. They don't have a sensible number; they have a place they must be.

node-1 4 CPU node-2 4 CPU node-3 4 CPU · has a GPU Deployment · replicas: 3 — placed by count scribe pod 1 scribe pod 2 scribe pod 3 nothing here at all DaemonSet — placed by node decoder one per node decoder one per node decoder one per node a node joins → a pod appears, unprompted nodeSelector: gpu=true → these two are excluded node tainted → needs a toleration to land serves only what is on its own node

Three questions

Answer yes to any of these and you want a DaemonSet. Answer no to all three and you want a Deployment.

1. Does it need to exist wherever work might land? 2. Does it serve only clients on its own node? 3. Does it own a node-local resource — a device, a GPU, a shared memory region?

The commands

Reading a DaemonSet

Click any piece of the command.

Taints and tolerations

Click any piece of the command.

Updating one, and watching it go

Click any piece of the command.

Build it

A multi-node cluster, then the decoder as a DaemonSet. If you've been running a single-node kind cluster, this is the point where you need more than one machine.

A cluster with three nodes

kind delete cluster --name airwaves
kind create cluster --name airwaves --config manifests/kind-3node.yaml
kubectl get nodes

The decoder, everywhere

kubectl apply -f manifests/decoder.yaml
kubectl get ds decoder
kubectl get pods -o wide -l app=decoder

DESIRED is 3 and you never typed a 3. It was computed from the cluster. One pod per node, including the one the Deployment skipped.

Confirm the spread

kubectl get pods -o wide -l app=decoder --no-headers \
  | awk '{print $7}' | sort | uniq -c

One per node, every node. That command is worth keeping — it's how you notice the machine that quietly has none.

Break it

1 · Make a node refuse it

kubectl taint nodes airwaves-worker2 maintenance=true:NoExecute

kubectl get pods -o wide -l app=decoder
kubectl get ds decoder

The pod on that node is evicted immediately — that's NoExecute rather than NoSchedule — and DESIRED drops from 3 to 2. The DaemonSet has not failed. It has correctly concluded that this node no longer wants it.

Now give it permission:

kubectl patch ds decoder --type=merge -p='
spec:
  template:
    spec:
      tolerations:
        - key: maintenance
          operator: Exists
          effect: NoExecute'

kubectl get pods -o wide -l app=decoder

Back to three. The taint is still there — you didn't remove the node's objection, you gave the pod an exemption from it.

Put it back

kubectl taint nodes airwaves-worker2 maintenance=true:NoExecute-

2 · Narrow it to nothing

kubectl patch ds decoder --type=merge \
  -p='{"spec":{"template":{"spec":{"nodeSelector":{"gpu":"true"}}}}}'

kubectl get ds decoder

DESIRED 0, and every decoder pod gone. No error anywhere, because nothing is wrong — you asked for one per GPU node and there are no GPU nodes.

This is the DaemonSet failure mode worth recognising on sight. DESIRED 0 means your selector matched no nodes, and the fix is always on the node side:

kubectl get nodes --show-labels
kubectl label node airwaves-worker gpu=true
kubectl get ds decoder

DESIRED climbs to 1 the moment the label exists. Nothing was restarted, nothing was reapplied — the controller simply ran its query again and found a different answer.

Put it back: remove the nodeSelector patch and the label when you're done experimenting.

What follows from this

Because……this is what happens
There is no replica countYou cannot scale a DaemonSet. The only way to have more is to have more nodes.
DESIRED is computedDESIRED 0 means no node matched — look at node labels, not at the DaemonSet.
Nodes are tracked continuouslyScaling a node pool needs no change here. New nodes get the pod automatically.
There is no surgeUpdates go node by node, old pod gone before new pod starts. Slow on purpose.
One pod serves one nodeIts failure is bounded by node size — and bigger nodes mean a bigger blast radius.
Taints repel, tolerations permitA toleration lets a pod land somewhere; it never attracts it there.

Cheat sheet

Part 5 · daemonsets

• A Deployment counts pods. A DaemonSet counts nodes. Three replicas is not three machines.

• No replicas field exists. DESIRED is derived from how many nodes match.

• A node joins, a pod appears. No edit, no reapply — the reconcile loop over nodes.

• nodeSelector narrows which nodes qualify. DESIRED 0 means nothing matched, not that something broke.

• Nodes taint to repel; pods tolerate to be allowed. NoExecute also evicts what's already there.

• No surge on update — old pod must go before the new one starts. maxUnavailable counts nodes.

• Use one when locality is a requirement: node-local resources, local-only clients, must-be-everywhere agents.

• Don't use one just for even spreading — topologySpreadConstraints on a Deployment does that and keeps your count.

• A DaemonSet pod is a single point of failure for its node, and there is no replica count to raise.

• rollout restart ds/x respects pacing; delete pod -l does not.