KubernetesXVIII · DaemonSetsDaemonSets
DaemonSet scheduling — nodeSelector, taints, and tolerations
What you'll learn
- Explain how the DaemonSet controller bypasses the normal scheduler while honouring nodeSelector and taints
- Configure nodeSelector and tolerations to control DaemonSet placement
- Distinguish control-plane taints from worker node placement
- Use the schedulerName field to delegate DaemonSet Pods to a custom scheduler
Prerequisites
Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16
A DaemonSet Pod is scheduled by the DaemonSet controller,
not the kube-scheduler. The controller iterates over all
nodes that match the DaemonSet’s nodeSelector and creates
one Pod per node. If a node has taints the Pod does not
tolerate, the Pod is not scheduled onto that node. This
lesson covers the configuration that makes a DaemonSet land
where it should — and where it shouldn’t.
The DaemonSet controller’s scheduling model
flowchart LR
DS[DaemonSet controller] --> N1["node-01<br/>worker"]
DS --> N2["node-02<br/>worker"]
DS --> N3["node-03<br/>control-plane"]
DS --> N4["node-04<br/>cordoned"]
N1 -->|selector matches<br/>tolerates taints| P1[Pod]
N2 -->|selector matches<br/>tolerates taints| P2[Pod]
N3 -->|selector matches<br/>tolerates taints?| P3{?}
N4 -->|cordoned| X[no Pod]
The controller picks a node, evaluates the Pod spec’s
nodeSelector and tolerations, and either creates the Pod
or skips. The nodeName field of the Pod is set to the
chosen node. The scheduler is bypassed.
nodeSelector
spec:
template:
spec:
nodeSelector:
node-role.kubernetes.io/worker: ""
disk: ssd
The Pod lands on nodes with all of these labels. The DaemonSet’s per-node count is the count of nodes with the selector match.
| Selector | Result |
|---|---|
| none | Every node |
node-role.kubernetes.io/worker: "" | Only worker nodes |
kubernetes.io/os: linux | Only Linux nodes |
topology.kubernetes.io/zone: us-east-1a | Only nodes in zone us-east-1a |
A common pattern: the log collector runs on worker nodes only; the metrics collector runs on every node including control-plane. Two DaemonSets, two selectors.
# Worker-only
spec:
template:
spec:
nodeSelector:
node-role.kubernetes.io/worker: ""
# Every node
spec:
template:
spec:
# no nodeSelector
Taints and tolerations
Control-plane nodes have the taint
node-role.kubernetes.io/control-plane:NoSchedule (or
node-role.kubernetes.io/master:NoSchedule on older
clusters). A Pod that does not tolerate this taint is not
scheduled onto control-plane nodes.
For a DaemonSet that must run on every node (e.g., a CNI agent), the Pod tolerates the control-plane taint:
spec:
template:
spec:
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
- key: node-role.kubernetes.io/master
operator: Exists
effect: NoSchedule
Or, more loosely, tolerate all taints:
spec:
template:
spec:
tolerations:
- operator: Exists
The operator: Exists with empty key matches every taint.
This is the safe default for agents that must run on every
node.
nodeAffinity
The DaemonSet’s Pod template supports nodeAffinity in
addition to nodeSelector. This is needed for more complex
matching:
spec:
template:
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: topology.kubernetes.io/zone
operator: In
values: ["us-east-1a", "us-east-1b"]
The Pod lands only on amd64 nodes in the listed zones.
DaemonSets for amd64-only images must filter for amd64; an
ARM cluster has nodes with kubernetes.io/arch: arm64, and
the wrong image on ARM is a crashloop.
schedulerName
By default, the DaemonSet controller bypasses the scheduler.
The schedulerName field on the Pod template can be set to
delegate to a custom scheduler:
spec:
template:
spec:
schedulerName: my-custom-scheduler
This is rare. Production DaemonSets use the default controller-based scheduling.
Cordoning and unschedulable nodes
A cordoned node (kubectl cordon or
--unschedulable=true in the node spec) is not schedulable
for normal Pods. The DaemonSet controller honours
unschedulable: true for new Pods but does not remove
existing DaemonSet Pods.
flowchart TB
A[kubectl cordon node-04] --> B["node-04 spec.unschedulable=true"]
B --> C[kube-scheduler ignores node-04]
B --> D[DaemonSet controller skips node-04 for new Pods]
B --> E[Existing DaemonSet Pod on node-04 still runs]
To remove a DaemonSet Pod from a cordoned node, the operator
must kubectl drain (which evicts the Pod) or
kubectl delete pod directly.
Inspecting node placement
kubectl get pods -l app=log-collector -n logging -o custom-columns=\
NAME:.metadata.name,NODE:.spec.nodeName,READY:.status.conditions[?(@.type=="Ready")].status
flowchart LR
A[Operator changes selector] --> B[DaemonSet controller]
B --> C{For each node}
C -->|selector matches| D{Node taints<br/>tolerated?}
D -->|yes| E[Create Pod]
D -->|no| F[Skip node]
C -->|selector does not match| F
kubectl get nodes --show-labels | grep node-role
# node-01 Ready control-plane
# node-02 Ready control-plane,worker
# node-03 Ready worker
A nodeSelector of node-role.kubernetes.io/worker: ""
matches node-02 and node-03 but not node-01.
Quiz
Knowledge check · 4 questions
Q1. Why does a DaemonSet Pod on a control-plane node need a toleration for the node-role.kubernetes.io/control-plane NoSchedule taint?
Q2. A cordoned node (kubectl cordon node-04) causes the DaemonSet controller to delete the existing DaemonSet Pod on that node.
Q3. Your DaemonSet is missing Pods on 2 of 10 nodes. The nodes have taints dedicated ml NoSchedule. Diagnose.
DaemonSet node-exporter with no tolerations. 8 of 10 nodes have Pods; 2 nodes (ml-01 and ml-02) with dedicated ml NoSchedule taint do not.
Q4. Explain the difference between a DaemonSet's nodeSelector and a toleration, and how they compose.
Passing score: 75%. Answers are checked in this browser.
Production discipline
- Tolerate the control-plane taints. Every DaemonSet
that needs to run on control-plane nodes must tolerate
node-role.kubernetes.io/control-plane:NoSchedule(and legacynode-role.kubernetes.io/master:NoSchedule). - Filter by architecture. A DaemonSet image built for
amd64 on an ARM node is a crashloop. Filter with
kubernetes.io/archinnodeAffinity. - Use
nodeSelectorfor clean placement rules. Avoidoperator: Existstolerations on DaemonSets that should not run on every node; the cluster-wide blast radius is real. - Treat cordoned nodes as a deliberate choice. A DaemonSet Pod stuck on a cordoned node is a leftover; drain or delete it.
- Audit the selector. A
nodeSelectorthat matches the wrong nodes — e.g., accidentally includingrole: experimental— produces a DaemonSet on every node the operator didn’t intend.
DaemonSet scheduling looks simple; the failure modes are silent. A DaemonSet that runs on 99 of 100 nodes is rarely caught without an explicit “count = node count” alert. Operators who run DaemonSets verify the count after every configuration change.