Wat een DaemonSet is
Een DaemonSet is een workload-controller (apiVersion: apps/v1, kind: DaemonSet) die zorgt dat er een kopie van een pod draait op elke node die aan de selector voldoet. Komt er een node bij, dan maakt de controller er een pod op. Verdwijnt een node, dan wordt de pod opgeruimd. Je zet nooit een replica-aantal; het aantal is gewoon het aantal matchende nodes.
Daar volgen drie garanties uit:
- Eén pod per matchende node. Niet twee, niet nul. De controller bewaakt die invariant.
- Schaalt automatisch mee met het cluster. Node erbij, pod erbij. Node draineren, pod gaat mee.
- Plaatsing op node-niveau. Elke pod is van moment van aanmaken vastgepind aan een specifieke node, dus heeft toegang tot het bestandssysteem, netwerk en de devices van die node als je die mount.
Dat is het hele werk van een DaemonSet. Hij verdeelt geen load. Hij rolt niet tussen nodes. Hij draait niet meerdere replicas per node. Het is een "één per machine"-controller, en de hele API is rond dat ene idee gebouwd.
Veelvoorkomende use cases
Een DaemonSet is het juiste antwoord als de waarde van de workload zit in het op elke node draaien, niet in horizontaal opschalen. De patronen die passen:
- Logcollectors. Fluent Bit als DaemonSet leest
/var/log/containers/*.logvan elke node en stuurt het naar centrale opslag. Mist één node een pod, dan zijn de logs van die node weg. - Monitoring-agents. Prometheus node-exporter, Datadog, New Relic en de OpenTelemetry Collector in agent-mode draaien allemaal als DaemonSet zodat de metrics van elke node lokaal worden opgehaald.
- CNI-plugins. Cilium, Calico en Flannel installeren hun data plane op elke node via een DaemonSet. Pods krijgen pas netwerk op een node als de CNI-agent daar geland is.
- Storagedrivers. CSI node-plugins draaien als DaemonSet zodat elke pod die een volume mount kan praten met een lokale agent die de mount-syscall afhandelt. De CSI controller-plugin is daarentegen een Deployment: die draait centraal.
- Security- en compliance-scanners. Falco, Trivy Operator node-collector en de meeste runtime-security-tools hebben een proces nodig op elke node dat syscalls of kernel-events bekijkt.
- Ingress data plane op een dedicated nodepool. Sommige teams pinnen ingress-controller-pods aan een specifieke nodepool met een DaemonSet plus nodeSelector, zodat elke ingress-node precies één ingress-pod krijgt. (Vaker zie je een Deployment op gelabelde nodes, maar het DaemonSet-patroon is prima als je een strikte 1-op-1 wilt.)
De rode draad: elke node heeft werk te doen. Het werk is niet "handel 1/N van het verkeer af" maar "wees de agent voor deze specifieke machine."
Anatomie van een DaemonSet-manifest
Een minimale DaemonSet-spec heeft hetzelfde skelet als een Deployment, minus het replica-aantal:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter
namespace: monitoring
spec:
selector:
matchLabels:
app: node-exporter
template:
metadata:
labels:
app: node-exporter
spec:
hostNetwork: true # bind aan de network namespace van de node
hostPID: true # zie alle PIDs op de node
containers:
- name: node-exporter
image: quay.io/prometheus/node-exporter:v1.8.2
args:
- --path.rootfs=/host
ports:
- name: metrics
containerPort: 9100
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 256Mi
volumeMounts:
- name: rootfs
mountPath: /host
readOnly: true
volumes:
- name: rootfs
hostPath:
path: /
De velden die er het meeste toe doen:
spec.selectoris verplicht en immutable. Zodra een DaemonSet is aangemaakt, kun je niet meer veranderen welke pods hij beheert. Wil je de selector wijzigen, dan verwijder en hermaak je de DaemonSet.spec.templateis de pod-spec die op elke geselecteerde node wordt uitgerold. De labels inspec.template.metadata.labelsmoeten overeenkomen metspec.selector.matchLabels.spec.template.spec.restartPolicymoetAlwayszijn (de default). DaemonSets ondersteunen geenNeverofOnFailure.hostNetwork,hostPID,hostPath-volumes kom je in DaemonSets veel vaker tegen dan in gewone workloads, omdat de hele taak van de agent draait om het inspecteren of aanpassen van de node.
De DaemonSet-controller schrijft daarnaast een spec.affinity.nodeAffinity regel in elke aangemaakte pod die hem vastpint aan de bedoelde node, en zet tolerations voor de controller-injected taints die hieronder besproken worden. Die velden zie je terug op de pods, ook als je ze niet zelf in de template hebt gezet.
Een subset van nodes selecteren met nodeSelector en affinity
Standaard zet een DaemonSet één pod op elke node. Wil je alleen een subset, gebruik dan spec.template.spec.nodeSelector voor het simpele geval:
spec:
template:
spec:
nodeSelector:
disktype: ssd
De DaemonSet draait nu alleen op nodes met label disktype=ssd. De controller blijft precies één pod per matchende node beheren, en nul pods op de rest.
Voor rijkere selectie (multi-zone, "een van deze instance types," uitsluitlogica) gebruik je nodeAffinity. Een typisch patroon voor een GPU-monitoring-agent:
spec:
template:
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: nvidia.com/gpu.present
operator: In
values:
- "true"
De affinity-regels in een DaemonSet combineren met de regels die de controller injecteert. Jij schrijft de "wat voor soort nodes"-helft. De controller schrijft de "deze specifieke node"-helft. Beide moeten matchen voor er een pod wordt aangemaakt.
Zoek je een opfrisser over het verschil tussen selectors en affinity, kijk dan naar taints, tolerations en node affinity.
Tolerations en de controlplane-vraag
De DaemonSet-controller injecteert automatisch tolerations voor de bekende node-conditie-taints, zodat DaemonSet-pods blijven draaien op nodes met problemen. De geinjecteerde set:
| Toleration key | Effect |
|---|---|
node.kubernetes.io/not-ready |
NoExecute |
node.kubernetes.io/unreachable |
NoExecute |
node.kubernetes.io/disk-pressure |
NoSchedule |
node.kubernetes.io/memory-pressure |
NoSchedule |
node.kubernetes.io/pid-pressure |
NoSchedule |
node.kubernetes.io/unschedulable |
NoSchedule |
node.kubernetes.io/network-unavailable |
NoSchedule (alleen bij hostNetwork: true) |
Dit is waarom een logcollector op een node met disk pressure gewoon doordraait. Het hele idee is dat de agent moet rapporteren over die kapotte staat, niet erdoor weggewerkt worden.
Wat de controller niet injecteert is een toleration voor node-role.kubernetes.io/control-plane:NoSchedule. Die taint wordt op control-plane-nodes gezet tijdens cluster-bootstrap (kubeadm doet het expliciet; bij managed control planes zie je het meestal niet eens). Een DaemonSet die ook op control-plane-nodes moet draaien moet de taint zelf tolereren:
spec:
template:
spec:
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
Of je dat wilt hangt af van wat de agent doet. Een logcollector of monitoring-agent heeft meestal wel control-plane logs en metrics nodig, dus die toleration is terecht. Een workload-specifieke agent (zoals een database-backuphelper) heeft niets op de control plane te zoeken.
Hetzelfde geldt voor elke eigen taint die je platformteam op dedicated nodepools heeft gezet (GPU, spot, tenant-isolatie). Die kent de DaemonSet-controller niet, dus voeg je de tolerations expliciet toe.
Resource requests en limits doen er ook bij een DaemonSet toe
Een hardnekkig misverstand: DaemonSets zouden vrijgesteld zijn van de gewone resource-discipline omdat het "infrastructuur" is. Niet dus. Elke DaemonSet-pod gebruikt capaciteit die anders naar applicatie-workloads had kunnen gaan. Maal dat met het aantal nodes, en een slordige DaemonSet eet zonder dat iemand het merkt 1-2 GB geheugen en een hele CPU per node op.
Twee gevolgen:
- Zet realistische requests, zodat de scheduler de agent meerekent. Heeft
node-exportergeen requests en plant er een zware pod op de node, dan kan de agent onder memory pressure ge-evict worden. Dan breekt de "één per node"-garantie stilletjes. - Zet limits om worst-case gedrag te begrenzen. Een logcollector die op elke node onbegrensd geheugen gaat alloceren wordt een cluster-wijd incident, niet een per-node-probleempje.
Een 50-node cluster met een DaemonSet die per pod 100m CPU gebruikt, draait continu op 5 volle CPUs aan capaciteit. Dat is geen afrondingsfout en hoort gewoon in je capaciteitsplanning te staan. De volledige mechaniek vind je in Kubernetes resource requests en limits.
Rolling updates: maxUnavailable en maxSurge
DaemonSets ondersteunen twee waarden voor updateStrategy.type:
- RollingUpdate is de default en wat je in vrijwel elk geval wil. Pods worden node-voor-node vervangen als de template wijzigt.
- OnDelete is een handmatige strategie: de controller raakt bestaande pods niet aan als je de template aanpast. Nieuwe pods adopteren de nieuwe template pas als je ze zelf verwijdert.
RollingUpdate werd de default toen DaemonSet in Kubernetes 1.9 naar apps/v1 GA ging. Daarvoor bestond de rolling-update-strategie wel (sinds 1.6), maar OnDelete was de default, en dat heeft een hoop teams tijdens cluster-upgrades verrast.
De rolling update wordt geconfigureerd onder spec.updateStrategy.rollingUpdate:
spec:
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # default
maxSurge: 0 # default
maxUnavailable is hoeveel pods er tijdens een rollout cluster-breed tegelijk onbeschikbaar mogen zijn. De default is 1. Zet hem hoger (of op een percentage als 25%) om sneller te rollen op grote clusters, ten koste van een grotere blast radius als de nieuwe versie kapot is.
maxSurge is de nieuwere optie, stable sinds Kubernetes 1.25. Hiermee mag de controller tijdens een rollout tijdelijk twee pods op dezelfde node draaien, zodat de nieuwe pod op is voordat de oude weg is. De default is 0 (het historische gedrag). Zet hem op 1 of een klein percentage als je echt zero-downtime rollouts wil op een systeem dat geen gat kan velen.
Drie dingen om te weten over maxSurge:
- Het kan niet samen met
hostPort. Twee pods op dezelfde node zouden botsen op de hostpoort. De Kubernetes API weigert die combinatie. maxSurgeenmaxUnavailablemogen niet allebei 0 zijn. Minstens één moet niet-nul zijn, anders kan een rollout geen voortgang boeken.- Surgen verbruikt nodecapaciteit voor de duur van de rollout. Op volle nodes komt de nieuwe pod soms Pending tot de oude verdwijnt, wat het voordeel weer wegneemt.
Voor het achterliggende scheduling-verhaal (waarom DaemonSets vroeger door de controller werden gepland en nu via de default scheduler gaan): de ScheduleDaemonSetPods feature gate ging alpha in 1.11, beta-by-default in 1.12, GA in 1.17, en de gate werd in 1.18 verwijderd. Moderne clusters lopen altijd via de kube-scheduler, en daarom respecteren DaemonSet-pods nu priority, preemption en resource-fit op precies dezelfde manier als andere pods.
Pod Security Standards voor privileged DaemonSets
Veel DaemonSets zijn op de een of andere manier privileged: ze mounten hostPath, zetten hostNetwork: true, voegen capabilities toe of draaien als root. Dat botst met de Pod Security Standards, die Kubernetes als ingebouwd admission-policy meelevert.
De drie niveaus:
- Restricted is het strengst. Geen host-namespaces, geen privileged containers, geen
hostPath. Een typische CNI-agent of node-exporter draait simpelweg niet onder Restricted. - Baseline voorkomt bekende privilege escalations maar laat genoeg ruimte dat sommige monitoring-agents er nog onder werken.
- Privileged is unrestricted. Systeem-DaemonSets met host-toegang draaien hier meestal.
Het juiste patroon: zet systeem-DaemonSets in hun eigen namespace (kube-system, monitoring, cilium-system) met label pod-security.kubernetes.io/enforce: privileged, en hou applicatie-namespaces op baseline of restricted. Zo blijft het privileged gedrag netjes ingekapseld waar het hoort, en kunnen applicatieteams niet per ongeluk privileged pods aanmaken. Voor het volledige plaatje zie Kubernetes Pod Security Standards.
Een DaemonSet debuggen (waarom heeft een node geen pod)
De meest voorkomende DaemonSet-bug is "ik verwachtte een pod op elke node, maar worker-3 heeft er geen." De diagnose-loop:
# Hoeveel pods zijn desired vs scheduled vs ready
kubectl get daemonset <naam> -n <namespace>
# Welke nodes hebben pods
kubectl get pods -n <namespace> -l app=<naam> -o wide
# Waarom mist deze node er een
kubectl describe daemonset <naam> -n <namespace>
De kubectl describe-output toont events van de controller, met meldingen als nodes are available: 1 node(s) didn't match Pod's node affinity/selector of 1 node(s) had untolerated taint. Match de melding met een van de vier hoofdoorzaken:
- De node heeft een taint die de DaemonSet niet tolereert. Een eigen taint op een GPU-pool, een tenant-isolatie-taint, of een control-plane-taint terwijl de DaemonSet hem niet tolereert. Voeg de toleration toe.
- Het nodelabel matcht de
nodeSelectorofnodeAffinityniet. Verifieer metkubectl get nodes --show-labelsen vergelijk met de selector. Label de node, of maak de selector minder strikt. - De node zit vol. Heeft de DaemonSet resource requests en is er geen ruimte, dan blijft de pod Pending. Kijk naar
0/N nodes are available: ... Insufficient cpu/memory. De fix is meestal een andere workload van de node halen, niet de requests van de agent te laag zetten ten opzichte van wat hij nodig heeft. - Een
hostPort-conflict. Binden twee DaemonSets op hostpoort 9100, dan wint er per node maar een. De andere blijft Pending metnode(s) didn't have free ports.
De pod Pending-troubleshooting-gids gaat dieper in op de scheduler-kant van de diagnose, en dezelfde technieken werken voor DaemonSet-pods, want die lopen ook gewoon via de default scheduler.
Wat een DaemonSet NIET is
Dit is de sectie waar de meeste teams baat bij hebben. De vier misverstanden die ik in productie tegenkom:
Een DaemonSet is niet "voor monitoring." Hij is voor elke workload waarvan de unit van replicatie de node is, niet de request. Monitoring is het meest voorkomende geval, maar storagedrivers, CNI-agents, security-scanners en per-node admission-helpers zijn even valide. Andersom: een "metrics-aggregator" als DaemonSet bouwen omdat monitoring daar past, is fout. Aggregators horen Deployments te zijn, want die handelen verkeer af, geen nodes.
Een DaemonSet slaat de control plane niet automatisch over. Hij slaat control-plane-nodes alleen over omdat die nodes een node-role.kubernetes.io/control-plane:NoSchedule-taint hebben die de DaemonSet-controller niet automatisch tolereert. Voeg je die toleration expliciet toe, dan draait de DaemonSet er gewoon. Dus "DaemonSets raken control-plane-nodes nooit aan" klopt aan beide kanten niet: het is opt-out per conventie, niet per ontwerp.
Een DaemonSet is niet vrijgesteld van resource-bookkeeping. De pods gebruiken CPU, geheugen en ephemeral storage net als alle andere. De scheduler past dezelfde fit-logica toe. Een DaemonSet zonder resource requests krijgt gewoon QoS-class BestEffort en is het eerste dat onder nodepressure ge-evict wordt, wat de "één per node"-belofte stilletjes breekt.
DaemonSet-pods schalen niet onafhankelijk. Je zet geen replicas. Je zet geen Horizontal Pod Autoscaler op een DaemonSet (de API weigert dat). Het pod-aantal is het aantal matchende nodes, en de enige manier om een DaemonSet te "schalen" is matchende nodes toevoegen of verwijderen. Wil je per-node-aantal flexibiliteit, dan wil je eigenlijk een Deployment met pod anti-affinity of topology spread constraints, geen DaemonSet.
Verder lezen
- Voor de volledige mechaniek van node-selectie en tolerations zoals die geldt voor elke workload (DaemonSet, Deployment, StatefulSet) zie taints, tolerations en node affinity.
- Voor een compleet uitgewerkt voorbeeld van een productiewaardige DaemonSet loopt de cluster-logging-tutorial met Fluent Bit door RBAC, host-path mounts, control-plane tolerations en resource-limits in context.
- Voor het contrast dat het ontwerp van een DaemonSet helderder maakt, behandelt het StatefulSets-artikel de andere "niet-Deployment" workload-controller en waar de identiteitsgaranties daarvan op hun plek vallen.