Node-level workloads in Kubernetes need a controller that follows nodes rather than replica counts. A DaemonSet creates one Pod on every eligible node, which fits log collectors, monitoring agents, storage helpers, and network components that must run close to the kubelet.
A DaemonSet still uses a normal Pod template, selector, labels, resource requests, and security settings. The selector must match the template labels, and a nodeSelector or node affinity narrows the eligible node set when the daemon should run only on selected workers.
For a disposable validation, two labeled worker nodes in a temporary namespace keep the DaemonSet away from unrelated workloads. Use existing production node labels instead of adding throwaway labels when the DaemonSet belongs in a live cluster, and leave production labels and namespaces in place when those objects should remain.
$ kubectl create namespace node-agents namespace/node-agents created
Use the real application or platform namespace instead of node-agents when deploying a production node agent.
Related: How to create a Kubernetes namespace
$ kubectl label node worker-a worker-b daemonset=log-agent node/worker-a labeled node/worker-b labeled
Replace worker-a and worker-b with node names from your cluster. If the nodes already have a stable pool or hardware label, use that existing label in the manifest instead of adding a temporary one.
apiVersion: apps/v1 kind: DaemonSet metadata: name: log-agent namespace: node-agents labels: app: log-agent spec: selector: matchLabels: app: log-agent template: metadata: labels: app: log-agent spec: nodeSelector: daemonset: log-agent containers: - name: log-agent image: busybox:1.36 command: - sh - -c - echo node-agent running on $NODE_NAME; sleep 3600 env: - name: NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName resources: requests: cpu: 10m memory: 16Mi securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: true runAsNonRoot: true runAsUser: 65534 seccompProfile: type: RuntimeDefault
The selector.matchLabels value must match the Pod template labels. The nodeSelector limits eligible nodes to the ones labeled daemonset=log-agent.
$ kubectl apply --dry-run=server -f log-agent-daemonset.yaml daemonset.apps/log-agent created (server dry run)
A server-side dry run asks the API server to validate the object without creating the DaemonSet pods.
$ kubectl apply -f log-agent-daemonset.yaml daemonset.apps/log-agent created
$ kubectl rollout status daemonset/log-agent --namespace node-agents --timeout=180s daemon set "log-agent" successfully rolled out
$ kubectl get daemonset log-agent --namespace node-agents -o wide NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE CONTAINERS IMAGES SELECTOR log-agent 2 2 2 2 2 daemonset=log-agent 13s log-agent busybox:1.36 app=log-agent
DESIRED, CURRENT, READY, and AVAILABLE should match the number of eligible nodes for the selector.
$ kubectl get pods --namespace node-agents -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES log-agent-hbn5c 1/1 Running 0 13s 10.244.2.2 worker-a <none> <none> log-agent-m7mgl 1/1 Running 0 13s 10.244.1.2 worker-b <none> <none>
One Running pod on each labeled node confirms the controller placed the daemon only on eligible nodes.
$ kubectl logs --namespace node-agents -l app=log-agent --prefix=true [pod/log-agent-hbn5c/log-agent] node-agent running on worker-a [pod/log-agent-m7mgl/log-agent] node-agent running on worker-b
The NODE_NAME environment variable comes from the Pod's spec.nodeName field, so the log line proves each container started on its assigned node.
Related: How to view Kubernetes pod logs
$ kubectl delete namespace node-agents namespace "node-agents" deleted
Run this cleanup against the temporary namespace only. Deleting a namespace removes all objects inside it.
$ kubectl label node worker-a worker-b daemonset- node/worker-a unlabeled node/worker-b unlabeled
Skip this cleanup when the label is a real node-pool, hardware, or placement label used by other workloads.