<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Drain · XB Field Notes</title><link>https://www.xbcatm.com/tags/drain/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://www.xbcatm.com/tags/drain/index.xml" rel="self" type="application/rss+xml"/><item><title>Cordon and drain take a node out safely</title><link>https://www.xbcatm.com/posts/kubernetes-cordon-drain-node/</link><pubDate>Thu, 03 Feb 2028 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/kubernetes-cordon-drain-node/</guid><description>Cordon stops new pods from landing, then drain evicts existing ones, so a node can go offline without losing work.</description><content:encoded>&lt;p&gt;Cordon and drain are two actions people usually say in one breath. Cordon sets &lt;code&gt;spec.unschedulable&lt;/code&gt; on the node so the scheduler stops choosing it. Drain evicts what is already running there. Cordon alone is instant and reversible; drain is a loop that talks to the eviction API once per pod.&lt;/p&gt;
&lt;p&gt;The tension is that drain is a client-side procedure with a safety rail: it refuses before it deletes anything, and every flag that gets past a refusal trades some of that safety away. Knowing which flag covers which refusal is the difference between a maintenance window and an outage.&lt;/p&gt;
&lt;h2 id="cordon-closes-the-door-drain-clears-the-room"&gt;Cordon closes the door, drain clears the room&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;kubectl cordon node-1&lt;/code&gt; patches &lt;code&gt;spec.unschedulable: true&lt;/code&gt;, and the scheduler filters that node out for new pods. Pods already running keep serving, which is why you cordon first: stop accepting new work before removing existing work. Cordoning is also what makes the drain meaningful: without it the scheduler would place replacements straight back onto the node you are emptying. &lt;code&gt;kubectl drain&lt;/code&gt; sets the same flag itself, so a separate cordon is a courtesy rather than a requirement. &lt;code&gt;kubectl uncordon&lt;/code&gt; is the same patch in reverse, and the scheduler starts using the node again immediately — confirm the kubelet is Ready first.&lt;/p&gt;
&lt;p&gt;Drain then evicts pods one at a time through the eviction API, falling back to a plain delete where the server does not. Eviction is a graceful termination: &lt;code&gt;terminationGracePeriodSeconds&lt;/code&gt; is honoured and drain waits for it. Grace is a window, not a guarantee: a pod that ignores SIGTERM is killed when it expires, so the pod&amp;rsquo;s own shutdown logic protects the work. Mirror pods are the exception — the kubelet manages them from a static manifest and they cannot be deleted through the API server at all, so drain skips them rather than failing on them.&lt;/p&gt;
&lt;h2 id="the-refusal-list-is-all-or-nothing"&gt;The refusal list is all or nothing&lt;/h2&gt;
&lt;p&gt;Drain refuses before it deletes anything, and one bad pod blocks every eviction on the node. The refusals map to three flags, and the binary names them: &lt;code&gt;cannot delete Pods that declare no controller (use --force to override)&lt;/code&gt; for a bare pod or one whose managing resource has gone missing; &lt;code&gt;Pods with local storage (use --delete-emptydir-data to override)&lt;/code&gt; for anything using &lt;code&gt;emptyDir&lt;/code&gt;; and &lt;code&gt;DaemonSet-managed Pods (use --ignore-daemonsets to ignore)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The DaemonSet case is the one where the flag does not change the outcome. Drain never deletes DaemonSet pods, with or without the flag, because the DaemonSet controller ignores the unschedulable taint and would recreate them immediately; &lt;code&gt;--ignore-daemonsets&lt;/code&gt; only silences the refusal. Two other kinds of pod reach a drained node regardless: anything tolerating &lt;code&gt;node.kubernetes.io/unschedulable&lt;/code&gt;, and anything that sets &lt;code&gt;nodeName&lt;/code&gt; directly, bypassing the scheduler. Pods tolerating that taint are usually DaemonSets, which is why the docs say not to tolerate it for anything else.&lt;/p&gt;
&lt;h2 id="the-budget-not-the-flags-decides-the-rest"&gt;The budget, not the flags, decides the rest&lt;/h2&gt;
&lt;p&gt;Once the refusals are cleared, drain starts submitting evictions and the PodDisruptionBudget has the last word. A PDB with &lt;code&gt;disruptionsAllowed&lt;/code&gt; at zero answers 429, and drain retries with a backoff until &lt;code&gt;--timeout&lt;/code&gt;, whose default of zero means forever. That retry loop is why a stalled drain looks like a hang, not a failure: drain is doing exactly what it was told. No error, no progress, just an &lt;code&gt;evicting pod&lt;/code&gt; line repeating.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--disable-eviction&lt;/code&gt; is the flag that bypasses budgets, by deleting pods directly instead of asking the eviction API. &lt;code&gt;--force&lt;/code&gt; does not do that; it only covers pods with no controller, the opposite of what people assume. The two flags are not interchangeable. The default unhealthy-pod policy, &lt;code&gt;IfHealthyBudget&lt;/code&gt;, also lets one &lt;code&gt;CrashLoopBackOff&lt;/code&gt; pod stall an entire drain until it becomes Ready.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;The drain that never returns. A PDB with &lt;code&gt;minAvailable&lt;/code&gt; equal to the replica count, or &lt;code&gt;maxUnavailable: 0&lt;/code&gt;, resolves to zero allowed disruptions, so the eviction is rejected forever and the terminal just sits there. Draining several nodes in parallel does not help either; they share the same budget. Diagnose with &lt;code&gt;kubectl get pdb -A&lt;/code&gt;: a PDB whose pods live on the node you are draining, showing ALLOWED DISRUPTIONS 0, is the culprit; the fix is a budget that permits one eviction, not a longer timeout.&lt;/p&gt;
&lt;p&gt;The drain that takes the workload down. Faced with a stalled drain, someone reaches for &lt;code&gt;--force&lt;/code&gt;, and the node empties — including the pods the budget was protecting. If that workload held quorum, the maintenance becomes the outage. The safe order is to fix the budget and wait, reserving &lt;code&gt;--force&lt;/code&gt; for a pod you have verified is genuinely stateless.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Confirm the node is closed and see what drain would refuse:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;kubectl get node node-1 -o jsonpath=&amp;#39;{.spec.unschedulable}{&amp;#34;\n&amp;#34;}&amp;#39;
kubectl drain node-1 --dry-run=client --ignore-daemonsets --delete-emptydir-data
kubectl get pods --field-selector spec.nodeName=node-1 -o custom-columns=NAME:.metadata.name,OWNER:.metadata.ownerReferences[0].kind
kubectl get pdb -A -o custom-columns=NAME:.metadata.name,MIN:.spec.minAvailable,MAX:.spec.maxUnavailable,ALLOWED:.status.disruptionsAllowed
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first prints &lt;code&gt;true&lt;/code&gt; once the node is cordoned. The dry run lists the pods drain would evict and the ones it refuses by name. In the third command an empty &lt;code&gt;OWNER&lt;/code&gt; column means &lt;code&gt;--force&lt;/code&gt; is required for that pod, and in the fourth an &lt;code&gt;ALLOWED&lt;/code&gt; of 0 on a PDB whose pods are on this node means the drain will wait rather than fail.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/tasks/administer-cluster/safely-drain-node/"&gt;Safely Drain a Node&lt;/a&gt;, the drain workflow and its interaction with PodDisruptionBudgets, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/reference/kubectl/generated/kubectl_drain/"&gt;kubectl drain reference&lt;/a&gt;, the exact refusal conditions and flags, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/disruptions/"&gt;Pod disruptions&lt;/a&gt;, voluntary versus involuntary disruption and eviction retries, checked 2026-09-16.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>