One PersistentVolumeClaim omits the storageClassName field entirely; another sets storageClassName to the empty string "". Explain what Kubernetes does in each case, and how a cluster declares which storage tier is the default.
answer
- nil vs "" vs name = three states
- Annotation is-default-class: true
- "" = opt out, static PV only
- Two defaults → newest wins silently
- Field immutable after creation
basics
~20 sOmitting storageClassName lets the DefaultStorageClass admission controller fill in the cluster's default class, marked by the annotation storageclass.kubernetes.io/is-default-class: "true". Setting it to "" opts out of dynamic provisioning entirely, so the claim can only bind to a pre-created PV that also has no class.
solid answer
~50 sThey are three different states, not two. `storageClassName` **unset (nil)** means "I'll take whatever the cluster considers default": the `DefaultStorageClass` admission plugin rewrites it to the class annotated `storageclass.kubernetes.io/is-default-class: "true"`. If no default exists, the field stays nil and the claim waits, and in current Kubernetes it will be filled in retroactively if a default is later created. `storageClassName: ""` means "explicitly no class": admission leaves it alone, dynamic provisioning is disabled, and the claim only binds to a manually created PV that also carries no class. This is how you pin a workload to a static PV. `storageClassName: some-name` binds to that class specifically. Operational notes: exactly one class should carry the default annotation — if several do, Kubernetes picks the most recently created one, which is a silent trap. Charts that omit the field are portable across clusters; charts that hardcode a class name are not.
code
yaml · 10 linesapiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: legacy-nfs
spec:
storageClassName: "" # not omitted: explicitly no class
accessModes: ["ReadWriteMany"]
resources:
requests:
storage: 100Gigo deeper
Know that a cluster usually has a default class, that omitting the field uses it, and that kubectl get sc shows which one is default.
Distinguish all three states cleanly, name the is-default-class annotation, and explain why "" is required to consume a static PV.
Add operational consequences: two-default resolution by newest, immutability after creation, and portability rules for charts across clusters.
Discuss the default class as a platform-wide blast-radius decision — a Delete-policy default means namespace deletion destroys data for every team that never typed a class name.
## Three states, not two The `spec.storageClassName` field of a PersistentVolumeClaim is a pointer that can be *absent*, *empty*, or *set*, and all three mean different things. This is one of the most common sources of "it works on my cluster" storage bugs. **Absent (nil).** The field is not present in the manifest at all. When the PVC hits the API server, the `DefaultStorageClass` admission plugin (enabled on essentially every managed and kubeadm cluster) looks for a StorageClass carrying the annotation `storageclass.kubernetes.io/is-default-class: "true"` and writes that name into the claim. From that moment the PVC behaves exactly as if the author had typed the class name. If no default class exists at admission time, the field stays nil and the claim sits Pending; in current Kubernetes versions the assignment is *retroactive*, so creating a default class later causes those still-nil, still-unbound claims to pick it up. **Empty string ("").** The author is saying "no class, deliberately". Admission does not touch it. No provisioner will act on the claim, because provisioners key off a class name. The claim can only bind to a PersistentVolume that itself has no `storageClassName`. This is the correct way to consume a statically created PV — an NFS export or a `local` volume an administrator wrote by hand — in a cluster that also has a default class. Leave the field out instead, and the default class hijacks the claim and provisions a cloud disk you never wanted. **Set to a name.** Ordinary case: dynamic provisioning by that class, or binding to a pre-created PV whose `storageClassName` matches that exact string. ## Declaring the default The default is not a field; it is an annotation on the StorageClass: `storageclass.kubernetes.io/is-default-class: "true"` Managed distributions ship one pre-annotated (`gp2`/`gp3` on EKS, `standard-rwo` on GKE, `default` on AKS). Changing it is a two-step edit: remove the annotation from the old class, add it to the new one. Doing only the second step leaves the cluster with *two* defaults. Kubernetes does not reject that state; it resolves it by choosing the **most recently created** default class, which means the winner can change if someone recreates a class, and no error is ever logged. `kubectl get sc` prints `(default)` next to each annotated class — seeing two is the tell. There is also a legacy beta annotation, `storageclass.beta.kubernetes.io/is-default-class`, still honored by some tooling; prefer the GA one. ## Consequences for chart and manifest authors Because the default class differs per cluster, the portability rule is: - **Library/app charts:** leave `storageClassName` out entirely (or expose it as a value that defaults to unset). The claim then adapts to whatever the target cluster considers normal. - **Something that must not be dynamically provisioned:** set `""` and supply the PV. - **Something that genuinely needs a specific tier** (a database that must be on SSD with `Retain`): set the name explicitly, and document that the cluster must offer that name. A subtle detail: the field is immutable once the PVC is created, and admission fills it in at creation time. So the default is captured at creation, not evaluated continuously — changing the cluster default later does not migrate existing claims. Their PVs keep the class they were born with. Also remember what the default does *not* control. It does not make binding mode, expansion or reclaim policy uniform; those come from whichever class ends up selected. A cluster whose default class has `reclaimPolicy: Delete` (the norm) will destroy the backing disk when a namespace is deleted, and teams that never typed a class name are the ones most surprised by that. ## Diagnosing it Three commands answer nearly every incident: - `kubectl get sc` — which classes exist, which say `(default)`. - `kubectl get pvc <name> -o yaml` — what `storageClassName` actually ended up as after admission (compare with what was in git). - `kubectl describe pvc <name>` — events explaining why it is Pending: no default class, no matching PV, or a provisioning error. The classic failure story: a chart that worked on a cloud cluster is deployed to a bare-metal cluster with no default class. Every PVC stays Pending with no events at all, because there is literally nothing to act on the claim — not an error, just silence.
- Your cluster ends up with two StorageClasses both annotated as default. What does Kubernetes do?It does not fail the PVC. The DefaultStorageClass admission plugin picks the most recently created class among the defaults and assigns that one. Nothing is logged as an error, so identical manifests can land on different tiers depending on class creation order. The fix is to keep exactly one annotated class and check `kubectl get sc` after any storage change.
- You change the cluster's default StorageClass. What happens to PVCs that were created earlier without a class?Nothing. The class name was written into each PVC at admission time and the field is immutable, so bound claims keep their original class and their PVs keep the original reclaim policy and parameters. Only new claims, and still-unbound nil-class claims eligible for retroactive assignment, see the new default. Migrating existing data requires creating a new PVC on the new class and copying.
saying these in an interview costs you the question
- Treating omitted and "" as the same thing
- Thinking the default is a field on the StorageClass spec rather than an annotation
- Believing changing the cluster default retroactively moves existing bound PVCs
- Assuming Kubernetes errors out when two classes are marked default
- Hardcoding a cloud-specific class name in a chart meant to be portable