StatefulSet and DaemonSet Solutions
SummaryStep-by-step solutions for Exercises 2 and 3: deploying...
Step-by-step solutions for Exercises 2 and 3: deploying...
Step-by-step solutions for Exercises 2 and 3: deploying a 3-replica StatefulSet with headless Service, verifying stable Pod naming, per-Pod PVCs, DNS resolution, and data persistence across Pod restarts; creating a DaemonSet with nodeSelector, verifying node-scoped scheduling, and observing dynamic Pod creation and removal as labels change.
StatefulSet and DaemonSet Solutions
Solution: Exercise 2 — StatefulSet with Headless Service
This exercise tests the core StatefulSet workflow: headless Service creation, StatefulSet deployment with volumeClaimTemplates, stable Pod naming verification, per-Pod PVC verification, data persistence across Pod deletion, and DNS resolution.
Step 1: Create the Namespace
kubectl create namespace stateful-exercise
namespace/stateful-exercise created
Step 2: Create the Headless Service
# web-headless-svc.yaml
apiVersion: v1
kind: Service
metadata:
name: web-headless
namespace: stateful-exercise
labels:
app: web
spec:
clusterIP: None
selector:
app: web
ports:
- port: 80
name: web
kubectl apply -f web-headless-svc.yaml
service/web-headless created
Verify the headless Service:
kubectl get svc web-headless -n stateful-exercise
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web-headless ClusterIP None <none> 80/TCP 5s
The CLUSTER-IP column shows None — this confirms the Service is headless. A headless Service does not allocate a virtual IP; instead, DNS queries for the Service name return the individual Pod IP addresses.
Step 3: Create the StatefulSet
# web-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
namespace: stateful-exercise
spec:
serviceName: web-headless
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
name: web
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: html
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Mi
kubectl apply -f web-statefulset.yaml
statefulset.apps/web created
Key configuration details:
- serviceName: web-headless — links the StatefulSet to the headless Service. This must match the Service’s
metadata.nameexactly. Without this, Pod DNS records will not be created. - volumeClaimTemplates[].metadata.name: html — matches the
volumeMounts[].namein the container spec. The resulting PVCs are namedhtml-web-0,html-web-1,html-web-2. - accessModes: ReadWriteOnce — each PVC is mounted by a single Pod. This is the correct mode for per-Pod storage.
Common error: If serviceName doesn’t match an existing headless Service, the StatefulSet is created but Pod DNS records won’t work. Kubernetes does not validate that the referenced Service exists at creation time.
Step 4: Verify Pod Names
Wait for all Pods to be Running:
kubectl get pods -n stateful-exercise -w
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 30s
web-1 1/1 Running 0 20s
web-2 1/1 Running 0 10s
Pods are named web-0, web-1, web-2 — ordinal names derived from the StatefulSet name. Notice the creation order: web-0 was created first, became Ready, then web-1 was created, and finally web-2. This is the default OrderedReady pod management policy.
Step 5: Verify Per-Pod PVCs
kubectl get pvc -n stateful-exercise
NAME STATUS VOLUME CAPACITY ACCESS MODES AGE
html-web-0 Bound pvc-a1b2c3d4-e5f6-7890-abcd-ef1234567890 100Mi RWO 45s
html-web-1 Bound pvc-b2c3d4e5-f6a7-8901-bcde-f12345678901 100Mi RWO 35s
html-web-2 Bound pvc-c3d4e5f6-a7b8-9012-cdef-012345678912 100Mi RWO 25s
Each Pod has its own PVC, named <volumeClaimTemplate-name>-<pod-name>. All PVCs are Bound, meaning storage has been provisioned.
Common error: If PVCs are stuck in Pending, you likely don’t have a default StorageClass configured. In a Kind cluster, the standard StorageClass is provisioned by default. Check with kubectl get storageclass. If no default exists, add storageClassName to the volumeClaimTemplate spec pointing to an available StorageClass.
Step 6: Write Data and Verify Persistence
Write a unique file to each Pod’s volume:
kubectl exec web-0 -n stateful-exercise -- sh -c 'echo "pod-0-data" > /usr/share/nginx/html/index.html'
kubectl exec web-1 -n stateful-exercise -- sh -c 'echo "pod-1-data" > /usr/share/nginx/html/index.html'
kubectl exec web-2 -n stateful-exercise -- sh -c 'echo "pod-2-data" > /usr/share/nginx/html/index.html'
Verify the data is in place:
kubectl exec web-0 -n stateful-exercise -- cat /usr/share/nginx/html/index.html
pod-0-data
Now delete web-0 and verify the replacement Pod retains the data:
kubectl delete pod web-0 -n stateful-exercise
pod "web-0" deleted
Wait for the replacement Pod:
kubectl get pods -n stateful-exercise -w
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 5s # New Pod, same name
web-1 1/1 Running 0 3m
web-2 1/1 Running 0 3m
The replacement Pod is named web-0 — the same name as the original. Check the data:
kubectl exec web-0 -n stateful-exercise -- cat /usr/share/nginx/html/index.html
pod-0-data
The data persisted. The replacement Pod web-0 mounted the same PVC html-web-0, which still contains the file written by the original Pod. This is the core StatefulSet guarantee: stable identity is coupled with stable storage.
Step 7: Verify Stable DNS Resolution
Launch a temporary Pod to test DNS:
kubectl run dns-test --rm -it --image=busybox:1.36 -n stateful-exercise -- nslookup web-0.web-headless
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-0.web-headless
Address 1: 10.244.0.15 web-0.web-headless.stateful-exercise.svc.cluster.local
The DNS entry web-0.web-headless resolves to the Pod’s IP address. The fully qualified domain name is web-0.web-headless.stateful-exercise.svc.cluster.local.
Test connectivity to a specific Pod by its DNS name:
kubectl run curl-test --rm -it --image=curlimages/curl -n stateful-exercise -- curl -s web-1.web-headless
pod-1-data
The curl request reached web-1 specifically (not a random Pod), and returned the unique data written to that Pod’s volume. This demonstrates both stable DNS and stable storage working together.
Common error: If DNS resolution fails with NXDOMAIN, verify that serviceName in the StatefulSet matches the headless Service name. Also ensure the test Pod is in the same namespace as the StatefulSet (cross-namespace resolution requires the full FQDN).
Cleanup
kubectl delete statefulset web -n stateful-exercise
kubectl delete pvc html-web-0 html-web-1 html-web-2 -n stateful-exercise
kubectl delete svc web-headless -n stateful-exercise
kubectl delete namespace stateful-exercise
Deleting the StatefulSet does not delete the PVCs. You must delete them explicitly. This is by design — accidental StatefulSet deletion should not destroy persistent data.
Solution: Exercise 3 — DaemonSet with Node Selection
This exercise tests DaemonSet creation with nodeSelector, dynamic Pod scheduling when labels change, and Pod removal when labels are removed.
Step 1: Create the Namespace
kubectl create namespace daemon-exercise
namespace/daemon-exercise created
Step 2: Identify and Label a Node
List your nodes:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
kind-control-plane Ready control-plane 10d v1.30.0
kind-worker Ready <none> 10d v1.30.0
kind-worker2 Ready <none> 10d v1.30.0
Label one worker node:
kubectl label node kind-worker disk=ssd
node/kind-worker labeled
Verify the label:
kubectl get nodes --show-labels | grep disk=ssd
kind-worker Ready <none> 10d v1.30.0 ...,disk=ssd,...
Step 3: Create the DaemonSet
# node-monitor-ds.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-monitor
namespace: daemon-exercise
labels:
app: node-monitor
spec:
selector:
matchLabels:
app: node-monitor
template:
metadata:
labels:
app: node-monitor
spec:
nodeSelector:
disk: ssd
containers:
- name: monitor
image: busybox:1.36
command: ["sh", "-c", "while true; do echo $(hostname) $(date); sleep 60; done"]
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mi
kubectl apply -f node-monitor-ds.yaml
daemonset.apps/node-monitor created
Step 4: Verify the DaemonSet Runs Only on Labeled Nodes
kubectl get daemonset node-monitor -n daemon-exercise
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
node-monitor 1 1 1 1 1 disk=ssd 10s
DESIRED is 1 because only one node has the disk=ssd label. The NODE SELECTOR column confirms the targeting.
Verify which node the Pod is running on:
kubectl get pods -n daemon-exercise -o wide
NAME READY STATUS RESTARTS AGE IP NODE
node-monitor-xk7m2 1/1 Running 0 15s 10.244.1.12 kind-worker
The Pod is running on kind-worker — the node we labeled with disk=ssd. No Pod is running on kind-worker2 or kind-control-plane.
Check the Pod logs to verify it’s functioning:
kubectl logs -n daemon-exercise -l app=node-monitor
node-monitor-xk7m2 Sun Feb 15 10:45:00 UTC 2026
Step 5: Label a Second Node
kubectl label node kind-worker2 disk=ssd
node/kind-worker2 labeled
Wait a few seconds, then check the DaemonSet:
kubectl get daemonset node-monitor -n daemon-exercise
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
node-monitor 2 2 2 2 2 disk=ssd 2m
DESIRED increased from 1 to 2. A new Pod was automatically scheduled on kind-worker2.
Verify both Pods:
kubectl get pods -n daemon-exercise -o wide
NAME READY STATUS RESTARTS AGE IP NODE
node-monitor-xk7m2 1/1 Running 0 2m 10.244.1.12 kind-worker
node-monitor-ab3k8 1/1 Running 0 10s 10.244.2.8 kind-worker2
Two Pods, one per labeled node. The DaemonSet controller detected the new matching node and scheduled a Pod automatically.
Step 6: Remove a Label and Verify Pod Removal
Remove the disk=ssd label from kind-worker:
kubectl label node kind-worker disk-
node/kind-worker unlabeled
The trailing - removes the label. Wait a few seconds:
kubectl get daemonset node-monitor -n daemon-exercise
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
node-monitor 1 1 1 1 1 disk=ssd 3m
DESIRED dropped back to 1. The Pod on kind-worker was terminated.
Verify:
kubectl get pods -n daemon-exercise -o wide
NAME READY STATUS RESTARTS AGE IP NODE
node-monitor-ab3k8 1/1 Running 0 75s 10.244.2.8 kind-worker2
Only the Pod on kind-worker2 remains. The DaemonSet controller continuously reconciles the desired state (one Pod per matching node) with the actual state. When kind-worker lost its disk=ssd label, it no longer matched the nodeSelector, and the Pod was removed.
Cleanup
kubectl label node kind-worker2 disk-
kubectl delete namespace daemon-exercise
Troubleshooting Reference
| Symptom | Cause | Fix |
|---|---|---|
StatefulSet Pods stuck in Pending | No default StorageClass or insufficient storage | Check kubectl get storageclass and ensure a default exists |
StatefulSet Pod web-1 not starting | web-0 not Ready yet (OrderedReady policy) | Wait for web-0 to be Running and Ready |
DNS lookup returns NXDOMAIN | serviceName mismatch or wrong namespace | Verify serviceName matches the headless Service name |
| PVC still exists after deleting StatefulSet | By design — PVCs are never auto-deleted | Delete PVCs manually: kubectl delete pvc <name> |
| DaemonSet shows DESIRED=0 | No nodes match the nodeSelector | Check node labels with kubectl get nodes --show-labels |
| DaemonSet Pod not on control plane | Control plane has NoSchedule taint | Add toleration for node-role.kubernetes.io/control-plane |
DaemonSet Pod in CrashLoopBackOff | Container command error | Check kubectl logs <pod> — verify the command syntax |
DaemonSet Pod in Pending | Insufficient resources on matching nodes | Lower resource requests in the DaemonSet spec |