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.

Provisioning mode
Reading a stuck 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 everywheremanaged-csi, default, managed-csi-premium all give you LRS unless you write your own StorageClass.

The answer, stated plainly

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
Two traps in that table

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:

# 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.

Is PersistentVolumeClaim a CRD?

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.

The templating trap

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.

Why this separation exists at all

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.