Introduction
Some software should run once on every eligible node rather than a fixed replica count. Log collectors, monitoring agents, and networking components commonly use DaemonSets for this node-oriented placement.
You will label the prepared node, deploy a node reporter, trace its ownership, change node eligibility, and perform a rolling update. The single-node lab makes the placement rule easy to observe while preserving the same controller behavior used by larger clusters.
The key question throughout this lab is not “how many replicas should run?” but “which nodes are eligible?” A DaemonSet continually answers that question and creates one Pod on each matching node. In this environment there is one node, so every eligibility change produces a clear one-to-zero or zero-to-one result.
Mark an Eligible Node
In this step, you will add a purpose-specific label to the node. Labels describe node attributes that controllers can select.
Begin by viewing the node and its existing labels. The output is wide, so do not try to memorize it; you only need to recognize that labels are key-value facts attached to the Node object.
cd /home/labex/project/daemonset-workloads
kubectl get nodes --show-labels
Add the new label. The --overwrite option is unnecessary here because setup removed any old value, but kubectl label will tell you if a conflicting value already exists:
kubectl label node labex-v135 practice-tier=operations
kubectl get node labex-v135 -L practice-tier
-L practice-tier adds that label as a table column. The node now advertises itself as eligible for the practice operations agent.
The second command gives you a focused result instead of the very long --show-labels line:
NAME STATUS ROLES VERSION PRACTICE-TIER
labex-v135 Ready control-plane v1.35.5 operations
PRACTICE-TIER=operations is the evidence you need. The label does not run anything by itself; it becomes meaningful when a workload selects it in the next step.
Deploy a DaemonSet
In this step, you will create a DaemonSet that targets nodes carrying the new label.
Read the manifest as three connected decisions before typing it. The selector and Pod label both use app: node-reporter, allowing the controller to recognize its Pods. nodeSelector then limits eligibility to nodes labeled practice-tier=operations. Finally, the Downward API copies the selected Pod's spec.nodeName into the container as NODE_NAME.
The loop keeps the demo agent alive. A real DaemonSet might run a log collector or monitoring agent instead.
cat <<'EOF' > node-reporter.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-reporter
namespace: practice-daemon
spec:
selector:
matchLabels:
app: node-reporter
template:
metadata:
labels:
app: node-reporter
spec:
nodeSelector:
practice-tier: operations
containers:
- name: reporter
image: busybox:1.36
imagePullPolicy: IfNotPresent
command: ["sh", "-c", "while true; do echo node=${NODE_NAME} version=v1; sleep 15; done"]
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
EOF
Create the namespace, apply the file, and wait for the DaemonSet rollout:
kubectl create namespace practice-daemon
kubectl apply -f node-reporter.yaml
kubectl rollout status daemonset/node-reporter -n practice-daemon --timeout=60s
kubectl get daemonset,pods -n practice-daemon -o wide
The downward API fieldRef exposes the scheduled node name without hard-coding it. On this one-node cluster, desired, current, and ready counts should all be one.
The Pod name contains a generated suffix, but the NODE column should be labex-v135. That is direct evidence that the eligibility rule and scheduling result agree.
On the one-node test cluster, the important columns looked like this:
NAME DESIRED CURRENT READY NODE SELECTOR
node-reporter 1 1 1 practice-tier=operations
NAME READY STATUS NODE
node-reporter-... 1/1 Running labex-v135
Read the first row from left to right: one node matches the selector, one Pod exists, and that Pod is ready. On a larger cluster, these counts would grow with the number of eligible nodes.
Observe Eligibility and Ownership
In this step, you will prove that node eligibility, not a replicas field, controls the DaemonSet Pod count.
First capture a healthy baseline. The custom columns connect the generated Pod back to the DaemonSet, and the logs prove that the Downward API supplied the node name:
kubectl get pods -n practice-daemon -l app=node-reporter -o custom-columns='POD:.metadata.name,NODE:.spec.nodeName,OWNER:.metadata.ownerReferences[0].kind,READY:.status.containerStatuses[0].ready'
kubectl logs -n practice-daemon -l app=node-reporter --tail=3
The ownership table and log should resemble:
POD NODE OWNER READY
node-reporter-... labex-v135 DaemonSet true
node=labex-v135 version=v1
The OWNER field proves this is a controller-managed Pod. The log proves that NODE_NAME came from the Pod's actual spec.nodeName, not from a hard-coded value.
Temporarily remove the label and watch the Pod disappear:
kubectl label node labex-v135 practice-tier-
kubectl get daemonset node-reporter -n practice-daemon
kubectl wait --for=delete pod -n practice-daemon -l app=node-reporter --timeout=60s
A trailing - removes a label key. Restore eligibility and wait for the controller to create a replacement Pod:
After removal, the DaemonSet briefly reports the following stable relationship:
NAME DESIRED CURRENT READY
node-reporter 0 0 0
This is a healthy response to changed eligibility, not a failure. No node currently matches the DaemonSet's nodeSelector, so the correct desired count is zero.
kubectl label node labex-v135 practice-tier=operations
kubectl rollout status daemonset/node-reporter -n practice-daemon --timeout=60s
Compare the Pod name before and after the label change. The replacement has a new Pod identity, while the DaemonSet name and desired behavior remain the same. This is reconciliation in action.
Roll Out a DaemonSet Update
In this step, you will update the saved manifest and observe the DaemonSet replace the node agent. Keeping the file and the live object aligned makes the desired state reproducible.
First create a working copy with cp. Then use the same focused text-editing pattern introduced in Kubernetes for Beginners: sed -i 's/old/new/' file replaces matching text directly in the file, while grep -n prints the matching line with its line number so you can inspect the result before applying it.
cp node-reporter.yaml node-reporter-v2.yaml
sed -i 's/version=v1/version=v2/' node-reporter-v2.yaml
grep -n 'version=' node-reporter-v2.yaml
kubectl apply -f node-reporter-v2.yaml
kubectl rollout status daemonset/node-reporter -n practice-daemon --timeout=60s
kubectl rollout history daemonset/node-reporter -n practice-daemon
kubectl logs -n practice-daemon -l app=node-reporter --tail=3
The copied file preserves the original v1 example for comparison. The one substitution changes only the version text inside the container command; applying the updated file changes the Pod template, which creates a new controller revision and replacement Pod. The logs should now contain version=v2.
rollout history confirms that Kubernetes recorded a new revision. DaemonSets therefore support controlled template updates much like Deployments, but the replacement is organized around eligible nodes instead of a replica count.
The tested output included two revisions and the new log message:
REVISION CHANGE-CAUSE
1 <none>
2 <none>
node=labex-v135 version=v2
CHANGE-CAUSE=<none> only means no human-readable change annotation was recorded. Revision 2 plus version=v2 in the replacement Pod's log are the useful proofs that the rollout happened.
Summary
You used node labels to control DaemonSet eligibility, observed one-Pod-per-eligible-node reconciliation, traced controller ownership, and rolled out a node-agent update. You can now contrast DaemonSets with replica-oriented Deployments and completion-oriented Jobs.


