Kubernetes storage · Azure · interactive
A claim is a request.
A volume is the thing.
PersistentVolumeClaim — "I need 30 GiB, writable, by one pod."
StorageClass — the factory that knows how to make that.
PersistentVolume — the object standing in for a real disk.
Managed disk — the real disk, sitting in one Azure zone.
Three of those four live in the cluster and are cheap to recreate. The fourth is where your data physically is — and it is the only one with a blast radius. This page walks the binding lifecycle, then breaks things on purpose to show exactly what Azure's default storage classes do and don't survive.
01The lifecycle
How a claim becomes a disk
There are two ways a claim gets satisfied, and they run in opposite directions. In dynamic provisioning a StorageClass manufactures the volume on demand — nothing on the right-hand side exists until the claim asks for it. In static provisioning the storage was there first, and a human wrote the PV by hand to describe it. Switch modes and watch the order of events reverse.
Pending
A PVC stuck in Pending almost never means "Kubernetes is broken",
and the mode tells you where to look. Dynamic: the provisioner is still
waiting on the cloud, the StorageClass name is wrong, or
WaitForFirstConsumer is holding out for a pod to schedule first so
it knows which zone to build in. Static: no existing PV satisfies the
claim — usually capacity, an access mode the PV doesn't offer, or a PV still
Released from a previous owner.
02Access modes
Why one pod gets the disk and three pods get the file share
An access mode isn't a policy setting — it's a physical property of the storage.
A managed disk attaches to exactly one VM at a time, so no amount of YAML makes
it ReadWriteMany. Try to schedule pods across nodes below and watch
which volumes refuse.
This is the single constraint that decides your topology, and it is why
charts that mount an RWO volume usually carry a hard guard against
replicaCount > 1. The classic casualty is an embedded
single-writer database — SQLite on an RWO disk cannot be shared by two
replicas, and would corrupt if it could.
03Blast radius
Break it on purpose
Now the question you actually asked. Three volumes, three different redundancy
SKUs, one region. Inject failures and watch what survives. LRS is the default
everywhere — managed-csi, default,
managed-csi-premium all give you LRS unless you write your own
StorageClass.
Azure managed disks have no built-in disaster recovery. LRS keeps three synchronous copies inside one datacenter — that covers a failed drive or a failed rack, and nothing larger. Zone loss needs a ZRS SKU you have to ask for by name. Region loss needs a backup product; there is no replication flag that gets you there, because a managed disk is a regional resource and cannot be geo-replicated.
Reference
What Azure actually hands you
Every one of these is a CSI driver — provisioning is pluggable, and a single
cluster can run many side by side. A local dev cluster (k3d, minikube, kind)
usually ships exactly one, rancher.io/local-path: a node-local
directory with zero replication, which is why nothing you learn about
durability there transfers. A managed AKS cluster typically has these:
| StorageClass | Driver | Access | Default SKU | Survives |
|---|---|---|---|---|
| default managed-csi |
disk.csi.azure.com | RWO | StandardSSD_LRS | drive / rack only |
| managed-csi-premium | disk.csi.azure.com | RWO | Premium_LRS | drive / rack only |
| custom — you write it | disk.csi.azure.com | RWO | Premium_ZRS StandardSSD_ZRS |
+ zone loss |
| azurefile azurefile-csi |
file.csi.azure.com | RWX · ROX · RWO | Standard_LRS | configurable to GRS |
| azurefile-csi-premium | file.csi.azure.com | RWX · ROX · RWO | Premium_LRS | zone at best — no GRS on premium |
| add-on | blob.csi.azure.com | RWX | account-level | up to GZRS |
| add-on | netapp.csi.azure.com | RWX · RWO | ANF pool | cross-region replication |
Zone pinning. An LRS disk lives in one availability zone, and the PV
carries a nodeAffinity to match. Its pod can only ever schedule
in that zone — so a single RWO PVC quietly makes a workload zonal, even in a
three-zone cluster.
Reclaim policy. default and managed-csi are
reclaimPolicy: Delete. Delete the PVC and the Azure disk is
destroyed with it — no soft-delete, no recycle bin. Anything you would miss
needs a copy of the class with Retain.
The Retain variant, in full
# A zone-redundant, expandable, non-destructive disk class. # None of these four properties are on by default. apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: managed-zrs-retain provisioner: disk.csi.azure.com parameters: skuName: Premium_ZRS # survives one zone dying reclaimPolicy: Retain # PVC delete leaves the disk alone allowVolumeExpansion: true # grow later without a migration volumeBindingMode: WaitForFirstConsumer # build it where the pod lands
WaitForFirstConsumer is the one to internalise. With the default
Immediate binding, Azure picks the zone before the scheduler has
picked a node — and if it guesses a zone with no capacity for your pod, the pod
is unschedulable forever against a perfectly healthy disk.
Ownership
Who actually asks for the volume
It is tempting to read the lifecycle as "the pod asks and Kubernetes allows."
Neither half is quite right, and the difference is what makes storage outlive
workloads. The claim is its own object. It is not a field inside the pod,
and nothing grants it permission — it is a standing request that a provisioner
either fulfils or leaves Pending.
Three separate roles get conflated:
-
Helm / Kustomize / plain YAML
Asks for nothing. A templating engine renders manifests.
It has no opinion about storage and no runtime presence — swap Helm for
kubectl apply -fand every object below is identical. - kind: PersistentVolumeClaim This is the ask. A namespaced object with its own lifecycle. It exists whether or not a pod references it, and it survives the workload being deleted. Nothing else in your manifests requests storage.
-
kind: Deployment
Only consumes. It borrows an existing claim by name
through
claimName. Delete the Deployment and the PVC — and the disk behind it — are untouched.
# The ask. Its own object, its own lifecycle. apiVersion: v1 kind: PersistentVolumeClaim metadata: name: app-state spec: accessModes: [ReadWriteOnce] storageClassName: managed-csi resources: requests: storage: 30Gi
# The consumer. Borrows it by name. kind: Deployment spec: template: spec: containers: - name: app volumeMounts: - name: state # ② pod-local alias mountPath: /state volumes: - name: state # ② same alias persistentVolumeClaim: claimName: app-state # ① the only real link
Note the two levels of indirection, which trip people up constantly. The
volumeMounts name and the volumes name are a
pod-local alias that means nothing outside this spec. Only
claimName points at the actual PVC object. The alias and the
claim name are frequently different, and neither has to match the mount path.
No — it is a built-in core resource, compiled into the API server and
stable since Kubernetes 1.0. The apiVersion: v1 gives it away:
that is the core API group, the one with no group name. Every custom
resource carries a group instead — cert-manager.io/v1,
argoproj.io/v1alpha1. Confirm it on any cluster:
kubectl get crd will not list PVCs, while
kubectl api-resources --api-group='' will. Same for
PersistentVolume and StorageClass, though StorageClass lives in the
storage.k8s.io/v1 group rather than core.
The one case where a workload does mint claims
A StatefulSet is the exception. Its volumeClaimTemplates
generate a PVC per replica — data-web-0, data-web-1 —
each following its pod across reschedules. That is how you give every replica
its own disk, and it is the standard answer when RWO blocks you from scaling a
Deployment. Note the tradeoff: those PVCs are not garbage-collected
when the StatefulSet is deleted. That is deliberate, and it means scaling down
leaves disks — and their bills — behind.
Because the claim and the consumer are separate objects, a conditional like
{{ if .Values.storage.enabled }} has to be repeated in
both files. Let them drift and you get one of two failures: an
orphaned PVC quietly holding a disk nobody mounts, or a pod stuck in
Pending forever with persistentvolumeclaim not found.
The second is loud; the first just shows up on the invoice.
Compare an emptyDir, which is declared inline in the pod
spec with no separate object and no claim. That is the whole distinction:
ephemeral storage lives inside the workload and dies with it, while persistent
storage is a separate object referenced by name precisely because it has to
outlive every pod that mounts it. If a claim were a field on a pod, it
could not survive the pod — and then none of it would be persistent.