Node maintenance: drain like you mean it
The situation
A kernel patch is due on one worker. Which one:
kubectl -n kubelings get configmap maintenance-target -o jsonpath='{.data.node}'
kubectl -n kubelings get pods -o wide
The target runs two tenants: the node-agent DaemonSet pod and the
orders-batch pod with scratch data on an emptyDir. In M2.11
(pdb-blocks-drain) a drain hanging was the problem to fix. This time
the drain is the job — done in the right order, with eyes open at each
refusal.
┌─────────┐
│ cordon │
│ │
└─────────┘
│
no new pods
│
▼
┌────────┐
│ drain │
│ │
└────────┘
│
patch, reboot
│
▼
┌───────────┐
│ uncordon │
│ │
└───────────┘
Your task
Read the target node into a variable first:
NODE=$(kubectl -n kubelings get cm maintenance-target -o jsonpath='{.data.node}')
- Cordon — stop new arrivals before evicting current tenants:
kubectl cordon $NODE kubectl get node $NODE # Ready,SchedulingDisabled
Cordon only flipsspec.unschedulable— nothing is evicted. Running a bare cordon during an incident is a scalpel: the node keeps serving, the scheduler just stops adding to it. - Drain — and read both refusals before flagging past them:
kubectl drain $NODE # error: cannot delete DaemonSet-managed Pods … node-agent-… # error: cannot delete Pods with local storage … orders-batch-…- The DaemonSet pod can't be rehomed — its whole job is "one per
node", and its controller would recreate it immediately.
--ignore-daemonsetsmeans leave it, I understand. - The
emptyDirdies with the pod — Kubernetes refuses to silently destroy data.--delete-emptydir-datais you signing for the loss.
kubectl drain $NODE --ignore-daemonsets --delete-emptydir-data kubectl -n kubelings get pods -o wide # orders-batch reborn elsewhere - The DaemonSet pod can't be rehomed — its whole job is "one per
node", and its controller would recreate it immediately.
- Maintain — the pretend patch. In this drill: label the node as the
maintenance record:
kubectl label node $NODE kubelings.dev/maintenance-done=true - Uncordon — reopen for business:
kubectl uncordon $NODE
Hint
orders-batch won't move back after uncordon — evicted pods are
replaced, not remembered; the replacement stays where the scheduler put
it. That asymmetry is normal. Rebalancing (if you cared) is a
rollout restart away — or a descheduler's job in real fleets.
Solution
Fix
NODE=$(kubectl -n kubelings get cm maintenance-target -o jsonpath='{.data.node}')
kubectl cordon $NODE
kubectl drain $NODE --ignore-daemonsets --delete-emptydir-data
kubectl label node $NODE kubelings.dev/maintenance-done=true
kubectl uncordon $NODE
Why the flags exist (and what they're really asking)
Drain is deliberately obstinate — each refusal is a data-loss or availability question aimed at you:
| Refusal | The question | Your answer |
|---|---|---|
| DaemonSet pods | "this can't move — proceed around it?" | --ignore-daemonsets (agent tolerates the taint and keeps running through maintenance — usually exactly right for log/metric shippers) |
| emptyDir pods | "this data dies — accept?" | --delete-emptydir-data (scratch: yes; anything that hurts to lose had no business in emptyDir — M3's storage lessons) |
| PDB at its limit | "this eviction breaks an availability promise — wait?" | drain waits, and M2.11 is the story of waiting forever |
Under the hood drain uses the eviction API (respects PDBs), not bare deletes — that's why a PDB can stall it and why it's the polite tool.
Prevention / takeaway
- The cycle is always cordon → drain → work → uncordon. Cordon-first means no pod gets scheduled onto a node that's about to go down mid-incident.
- Evicted pods don't come home after uncordon; expect the post-maintenance imbalance and let it be (or restart rollouts deliberately).
- This exact cycle, node by node, is the inner loop of the cluster upgrade runbook (M8.7) — there it's automated by kubeadm/managed pools, but the refusals and flags are the same ones.
- Real fleets record maintenance in systems, not labels — but "leave a
queryable trail" (
kubectl get nodes -l kubelings.dev/maintenance-done) is the habit this drill plants.
- Next lesson
- SLOs: the burn-rate alert that never fired