skip to content

How does a Kubernetes PersistentVolumeClaim end up bound to a PersistentVolume, and what do the PersistentVolume phases Available, Bound, Released and Failed mean?

level: middleimportance: must knowfreq 58%

answer

  1. Match on class, access mode, volumeMode, size, selector
  2. Smallest sufficient PV wins
  3. Binding = claimRef + volumeName, one-to-one
  4. Released ≠ Available (stale claimRef)
  5. pvc-protection finalizer → Terminating while in use

basics

~20 s

A controller matches an unbound claim to a volume with a compatible class, access mode, volume mode and at least the requested capacity, then binds them one-to-one. PV phases: Available (free), Bound (claimed), Released (its claim was deleted but the volume is not reclaimed yet), Failed (automatic reclamation errored).

solid answer

~50 s

Binding is done by the PersistentVolume controller. For an unbound PVC it looks for a PV where `storageClassName` matches exactly, the access modes and `volumeMode` are compatible, the capacity is at least the request, and any `selector` on the claim matches the PV's labels. It picks the smallest sufficient match, writes `claimRef` on the PV and `volumeName` on the PVC, and both become `Bound`. Binding is **exclusive and one-to-one**: a PV serves at most one PVC, forever. If no PV matches and the claim names a class with a provisioner, dynamic provisioning creates one — that is the static/dynamic split. With no provisioner and no match, the claim stays `Pending`. PV phases: **Available** (no claim yet), **Bound**, **Released** (claim deleted, data may still be there, PV not yet reusable), **Failed** (reclamation failed). A `Released` PV is never automatically re-offered to another claim, even with `Retain` — an admin must clear the stale `claimRef` or recreate the PV.

code

bash · 10 lines
bash
kubectl get pvc,pv
kubectl describe pvc data | tail -20

# why is that free-looking PV not binding?
kubectl get pv pv-nfs-01 -o jsonpath='{.status.phase} {.spec.claimRef.name}{"\n"}'
# Released app-data

# make it Available again (data is untouched)
kubectl patch pv pv-nfs-01 --type=json \
  -p='[{"op":"remove","path":"/spec/claimRef"}]'

go deeper

for a junior

State that a claim is a request, a volume is the resource, they bind one-to-one, and name the four PV phases.

for a middle

List the matching attributes, explain smallest-sufficient selection, and explain why Released is not reusable.

for a senior

Run the Pending checklist from events, recover a Released PV by clearing claimRef, and explain protection finalizers and the static/dynamic fallthrough.

for a principal

Discuss whether the platform offers a static pool at all, who owns recovery of Released and Failed volumes, and how orphaned backend volumes turn into cost and compliance debt.

## Two objects, one match A **PersistentVolume (PV)** is a cluster-scoped object describing a piece of real storage: its capacity, access modes, volume mode, storage class name, reclaim policy, node affinity and the driver-specific handle. A **PersistentVolumeClaim (PVC)** is a namespaced request: how much, which access mode, which class, optionally a label selector. The PersistentVolume controller in kube-controller-manager continuously reconciles the two. For each unbound PVC it evaluates candidate PVs on: - **storageClassName** — must be equal, including the "both empty" case. - **accessModes** — the PV must support every mode the claim asks for. - **volumeMode** — `Filesystem` versus `Block` must match. - **capacity** — the PV must be at least the requested size (it may be larger; the claim gets the whole volume, not a slice). - **selector** — if the claim carries `spec.selector`, the PV's labels must satisfy it. - **node affinity / topology** — evaluated by the scheduler when binding is delayed. Among matches it prefers the smallest sufficient volume, to avoid wasting a 1Ti PV on a 5Gi request. Binding is recorded on both sides: `pv.spec.claimRef` names the claim (namespace, name, UID) and `pvc.spec.volumeName` names the PV. It is **exclusive** — one PV, one PVC — and it is **permanent**: a PV is never re-matched to a different claim on its own. ## Static versus dynamic, in one paragraph *Static provisioning* means an administrator has created PV objects in advance, and binding is pure matchmaking against that pool. *Dynamic provisioning* means the claim names a StorageClass with a real provisioner, and when nothing in the pool matches, a volume and its PV are manufactured on demand. The two coexist: the controller always tries to match an existing PV first, and only falls through to provisioning. This is why a claim with `storageClassName: ""` can consume a hand-made PV — no provisioner is ever consulted. ## PV phases - **Available** — the PV exists, has no `claimRef`, and can be matched. Typically only seen with static provisioning; dynamically provisioned PVs are born pre-bound. - **Bound** — matched to a claim. The steady state. - **Released** — the PVC was deleted, so the PV no longer serves anyone, but reclamation has not finished (or, under `Retain`, will never happen automatically). The data is generally still on the volume. Crucially, a Released PV is **not** returned to the Available pool: it still carries the stale `claimRef`, so it will not bind to a new claim even one with the same name. Recovering it means editing out `claimRef` (making it Available) or deleting the PV object and re-creating it pointing at the same backend volume. - **Failed** — automatic reclamation failed, for example the driver could not delete the backing volume. Needs manual investigation; the underlying resource may still exist and still be billed. PVC phases are simpler: **Pending** (unmatched/unprovisioned, or waiting for a first consumer under delayed binding), **Bound**, and **Lost** (its PV disappeared from under it). ## Why a PVC sits Pending The honest checklist, in the order it pays to check: 1. `kubectl describe pvc` — events name the cause more often than not. 2. Class mismatch: the claim's class does not exist, or is `""` while every PV has a class (or vice versa). 3. No PV large enough, or with the required access mode / volume mode. 4. A `selector` on the claim that no PV's labels satisfy. 5. The candidate PV is `Released`, carrying a stale `claimRef` from a deleted claim — the single most common "but there is clearly a free volume right there" case. 6. Delayed binding: the class waits for a consuming Pod, which is normal and resolves when a Pod is scheduled. 7. Provisioning failure from the driver: quota exhausted, bad parameters, missing credentials. ## Protection finalizers Two finalizers guard the objects. `kubernetes.io/pvc-protection` keeps a PVC that is in use by a Pod from actually disappearing — it shows `Terminating` until the last consuming Pod is gone. `kubernetes.io/pv-protection` does the same for a PV bound to an existing claim. This is deliberate: it prevents yanking storage out from under a running workload, and it is the reason a `kubectl delete pvc` sometimes appears to hang. ## Interview-ready summary Binding is exclusive, attribute-based matchmaking performed by a controller; the PV's phase tells you where it is in that lifecycle; and `Released` is the phase people misread, because it looks free and behaves as if it were not.

  • A PersistentVolume is Released and clearly unused, yet a new PVC with identical requirements stays Pending. Why?
    The Released PV still holds a claimRef pointing at the deleted claim, including its UID, and the controller refuses to rebind a volume that references another claim. Removing spec.claimRef returns the PV to Available so it can be matched again, with the data intact. Deleting and re-creating the PV object against the same backend volume is the alternative.
  • A PVC requests 5Gi and the only matching PersistentVolume is 100Gi. What happens?
    They bind, and the claim gets the entire 100Gi volume — PVs are not subdivided. The claim's status capacity reflects the real 100Gi. This is why the controller prefers the smallest sufficient PV, and why static pools are usually created in a few size buckets rather than one giant volume.
  • Why does deleting a PVC sometimes leave it in Terminating?
    The kubernetes.io/pvc-protection finalizer blocks removal while any Pod still references the claim, so storage is never pulled out from under a running workload. It clears once the last consuming Pod is gone and the object is then removed. If it persists, look for a leftover Pod — often a completed Job's Pod — still holding the reference.

saying these in an interview costs you the question

  • Thinking a Released PV automatically becomes Available for the next claim
  • Believing multiple PVCs can share one PV by splitting its capacity
  • Assuming a larger PV is trimmed to the requested size
  • Ignoring exact storageClassName equality, including the empty-string case
  • Treating a Terminating PVC as a stuck API server rather than a protection finalizer

context