<?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>Pvc · XB Field Notes</title><link>https://www.xbcatm.com/tags/pvc/</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/pvc/index.xml" rel="self" type="application/rss+xml"/><item><title>A PVC gives a Pod durable storage beyond the container</title><link>https://www.xbcatm.com/posts/kubernetes-pvc-storage-lifecycle/</link><pubDate>Fri, 13 Aug 2027 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/kubernetes-pvc-storage-lifecycle/</guid><description>A PersistentVolumeClaim binds to a backing volume and stays across Pod restarts, so state survives a rescheduled workload.</description><content:encoded>&lt;p&gt;A container is ephemeral by design, so a PersistentVolumeClaim is the handle that pins real storage to it even when the Pod is recreated. The split is deliberate: a PersistentVolume declares capacity that exists, a PersistentVolumeClaim requests capacity that is needed, and a controller matches one to the other. With a StorageClass the claim can create its own volume through a provisioner, so nobody has to pre-build the storage.&lt;/p&gt;
&lt;p&gt;The part that bites is that claim and volume have separate lifetimes governed by separate rules. A finalizer decides when the claim is allowed to disappear, and a reclaim policy decides what happens to the volume afterwards. Get the deletion order wrong and you get a claim stuck in &lt;code&gt;Terminating&lt;/code&gt;; get the policy wrong and you get either data you meant to keep or a bill for storage nobody is using.&lt;/p&gt;
&lt;h2 id="the-claim-the-volume-and-the-binding"&gt;The claim, the volume, and the binding&lt;/h2&gt;
&lt;p&gt;Binding is a conjunction, not a best match. A volume is a candidate only if its capacity is at least the request, its access modes include the requested mode, its &lt;code&gt;storageClassName&lt;/code&gt; matches, and no other claim owns it. A claim that satisfies nothing stays &lt;code&gt;Pending&lt;/code&gt; with an event naming the requirement that failed, which is more useful than a generic failure; the usual causes are an access mode the class cannot deliver and a class name that does not exist.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;accessModes&lt;/code&gt; has no default; you must choose one, and &lt;code&gt;ReadWriteOnce&lt;/code&gt; is what most single-node block provisioners actually deliver. It is also single-node, so a StatefulSet with replicas on different nodes cannot share one claim. When a class supports dynamic provisioning, &lt;code&gt;volumeBindingMode&lt;/code&gt; sets the timing: &lt;code&gt;Immediate&lt;/code&gt; provisions the volume the moment the claim appears, which can place a topology-constrained volume in a zone with no node to use it, while &lt;code&gt;WaitForFirstConsumer&lt;/code&gt; keeps the claim &lt;code&gt;Pending&lt;/code&gt; until a pod is scheduled and then provisions in that pod&amp;rsquo;s zone.&lt;/p&gt;
&lt;h2 id="the-deletion-order-and-the-finalizer"&gt;The deletion order and the finalizer&lt;/h2&gt;
&lt;p&gt;The order is pod, then claim, then volume, and the protection is implemented with finalizers. Deleting a claim while a pod still mounts it does not delete anything: the claim gets a deletion timestamp, flips to &lt;code&gt;Terminating&lt;/code&gt;, and stops there because &lt;code&gt;kubernetes.io/pvc-protection&lt;/code&gt; is still in its finalizer list. The controller removes that finalizer only once no pod references the claim, so the &amp;ldquo;Terminating forever&amp;rdquo; PVC is not a stuck controller but a claim something still uses.&lt;/p&gt;
&lt;p&gt;The same protection exists in the other direction. A &lt;code&gt;PersistentVolume&lt;/code&gt; carries &lt;code&gt;kubernetes.io/pv-protection&lt;/code&gt;, so deleting a volume that is bound to a claim is postponed until it is no longer bound. That ordering makes an accidental &lt;code&gt;kubectl delete pv&lt;/code&gt; recoverable, and explains why a volume can seem to ignore a delete for as long as its claim exists.&lt;/p&gt;
&lt;h2 id="reclaim-policy-delete-or-retain"&gt;Reclaim policy: Delete or Retain&lt;/h2&gt;
&lt;p&gt;When the claim is finally gone the volume is released, and the reclaim policy takes over. &lt;code&gt;Delete&lt;/code&gt; removes the PV object and the external storage asset with it. &lt;code&gt;Retain&lt;/code&gt; keeps the PV object, marks it &lt;code&gt;Released&lt;/code&gt;, and deliberately does not make it available for a new claim, because the previous claimant&amp;rsquo;s data is still on it — reclamation becomes a manual sequence: delete the PV, clean the storage asset, delete the asset.&lt;/p&gt;
&lt;p&gt;Dynamically provisioned volumes inherit the policy from their StorageClass, whose default is &lt;code&gt;Delete&lt;/code&gt;. That default is right for scratch and wrong for a database, and it is invisible: nothing in the PVC or pod spec mentions the policy, so the decision lives in a cluster-scoped object nobody reviewing the workload ever sees.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;The classic failure is the claim that will not die. Someone deletes a PVC, sees &lt;code&gt;Terminating&lt;/code&gt;, and waits — while a pod somewhere still mounts it, usually a StatefulSet that was never scaled down or a pod stuck on an unreachable node. Nothing errors, nothing times out, and the PVC blocks the storage behind it. Find the pod rather than deleting the finalizer by hand; removing it while a pod still writes is how you detach a volume under a running process.&lt;/p&gt;
&lt;p&gt;The second failure is the Retain orphan. The claim is gone, the pods are gone, the namespace may be gone, and a &lt;code&gt;Released&lt;/code&gt; PV plus its storage asset sit there indefinitely. Nothing is in use and nothing is in error, so no alert fires and the volume does not appear in any workload&amp;rsquo;s list. The inverse mistake is just as quiet: a class whose default &lt;code&gt;Delete&lt;/code&gt; policy was never overridden, discovered only when someone tries to recover data that the reclaim already removed.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Look at both sides of the binding, and at what holds the claim:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;kubectl get pvc -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,STATUS:.status.phase,VOLUME:.spec.volumeName,CLASS:.spec.storageClassName
kubectl get pv -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,POLICY:.spec.persistentVolumeReclaimPolicy,CLAIM:.spec.claimRef.name
kubectl get pvc data -o jsonpath=&amp;#39;{.metadata.finalizers}{&amp;#34;\n&amp;#34;}&amp;#39;
kubectl get pods -A -o json | jq -r &amp;#39;.items[] | select(.spec.volumes[]?.persistentVolumeClaim.claimName==&amp;#34;data&amp;#34;) | &amp;#34;\(.metadata.namespace)/\(.metadata.name)&amp;#34;&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A claim in &lt;code&gt;Terminating&lt;/code&gt; whose finalizers still contain &lt;code&gt;kubernetes.io/pvc-protection&lt;/code&gt; is being held by a pod, and the fourth command names it. On the volume side, a PV stuck in &lt;code&gt;Released&lt;/code&gt; with a &lt;code&gt;Retain&lt;/code&gt; policy is an orphan: nothing references it, nothing will reuse it, and only manual reclamation clears it.&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/concepts/storage/persistent-volumes/"&gt;Persistent Volumes&lt;/a&gt;, binding, access modes, reclaim policies, &lt;code&gt;Released&lt;/code&gt; state and the pvc/pv protection finalizers, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/concepts/storage/storage-classes/"&gt;Storage Classes&lt;/a&gt;, dynamic provisioning, &lt;code&gt;reclaimPolicy&lt;/code&gt;, and &lt;code&gt;volumeBindingMode&lt;/code&gt;, checked 2026-09-16.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>