PV, PVC & StorageClass
Kubernetes persistent storage — abstracting infrastructure from applications
The PV/PVC Abstraction
PersistentVolume (PV)
A piece of storage provisioned by an admin or dynamically. Represents the actual storage — an NFS share, an EBS volume, a local disk, a Ceph RBD image. It is a cluster-level resource (not namespaced).
PersistentVolumeClaim (PVC)
A request for storage by a user or pod. "I need 10Gi of RWO storage." PVCs are namespaced — they live alongside the pods that use them.
Separation of Concerns
- Provisions PVs (static) or configures StorageClasses (dynamic)
- Manages underlying storage backends
- Sets reclaim policies, access modes, capacity
- Creates PVCs — "give me 10Gi of fast storage"
- Doesn't care whether it's NFS, EBS, or Ceph
- Mounts PVC into pod spec as a volume
A PVC binds to a matching PV based on: capacity, access mode, storage class, and optional label selectors.
The Storage Chain
Pod
Mounts a volume via
volumes+volumeMountsPVC
App team's request: "10Gi, RWO, fast-ssd"
PV
Infra team's provisioned storage resource
Actual Storage
Disk / NFS / EBS / Ceph RBD / cloud volume
Access Modes
| Mode | Abbreviation | Description | Typical Backend |
|---|---|---|---|
ReadWriteOnce |
RWO | Single node read-write | Block storage (EBS, local disk) |
ReadOnlyMany |
ROX | Multiple nodes read-only | NFS, CephFS |
ReadWriteMany |
RWX | Multiple nodes read-write | NFS, CephFS, GlusterFS |
ReadWriteOncePod |
RWOP | Single pod read-write (K8s 1.27+) | Block storage |
Note
Note: Access modes are capabilities, not enforcement. RWO means the volume can be mounted read-write on one node — but if two pods land on the same node, both can mount it. RWOP closes that gap by restricting to a single pod.
Reclaim Policies
| Policy | Behavior | Use Case |
|---|---|---|
Retain |
PV kept after PVC deletion, data preserved, requires manual cleanup | Production data — you don't want accidental deletion |
Delete |
PV and underlying storage deleted when PVC is deleted | Dynamic provisioning, dev/staging environments |
Recycle |
(Deprecated) Runs rm -rf /thevolume/* on the volume |
Don't use — deprecated in favor of dynamic provisioning |
Warning
Warning: With Delete reclaim policy, deleting a PVC also deletes the underlying storage (e.g., the EBS volume). This is permanent data loss. Use Retain for anything you care about.
StorageClass: Dynamic Provisioning
Instead of pre-creating PVs manually, define a StorageClass and let Kubernetes create PVs automatically when PVCs request storage from that class.
- Admin creates PVs ahead of time
- PVCs bind to existing PVs
- Manual capacity planning
- Admin must know what teams need in advance
- Admin creates StorageClass once
- PVC triggers automatic PV creation
- No pre-provisioning needed
- Self-service for app teams
StorageClass YAML
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com # CSI driver for AWS EBS
parameters:
type: gp3 # EBS volume type
iops: "5000" # provisioned IOPS
reclaimPolicy: Delete # what happens when PVC is deleted
volumeBindingMode: WaitForFirstConsumer # delay provisioning until pod is scheduledPVC Referencing a StorageClass
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-data
spec:
storageClassName: fast-ssd # references the StorageClass above
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10GivolumeBindingMode
PV is provisioned as soon as PVC is created.
- May provision in the wrong availability zone
- Pod can't schedule if volume is in a different zone
- Acceptable for zone-agnostic storage (NFS, CephFS)
PV provisioning is delayed until a pod using the PVC is scheduled.
- Zone-aware: volume is created in the same zone as the node
- Recommended for cloud block storage
- Avoids zone mismatch entirely
Tip
Best practice: Always use WaitForFirstConsumer for cloud block storage (EBS, GCE PD, Azure Disk). Use Immediate only for storage that is accessible from any zone (NFS, CephFS).
Volume Types (Inline and PV-backed)
emptyDir
Temporary per-pod storage
- Created when the pod starts, deleted when the pod is removed
- All containers in a pod share it — useful for sidecar communication
medium: Memorybacked by tmpfs (RAM) — fast but uses the pod's memory limit- Use cases: scratch space, cache, sharing files between init container and app container
volumes:
- name: scratch
emptyDir: {} # disk-backed
- name: cache
emptyDir:
medium: Memory # tmpfs (RAM-backed)
sizeLimit: 256MihostPath
Mount a path from the node's filesystem
- Mounts a file or directory from the host node into the pod
- Types:
Directory,File,Socket,CharDevice,BlockDevice - Data is node-specific — no portability, no replication
- If pod moves to another node, the data is gone
volumes:
- name: host-logs
hostPath:
path: /var/log
type: DirectoryWarning
Security risk: hostPath lets pods access any host file. A pod with hostPath: / has root-level access to the entire node filesystem. Avoid in production. Use PodSecurityStandards / OPA / Kyverno to restrict hostPath usage.
configMap / secret
Mount configuration and secrets as files
- Mount ConfigMap or Secret data as files inside the container
- Updates propagate eventually (kubelet sync period, ~1 minute by default)
subPath: mount a single key as a file at a specific path (but disables auto-update)
volumes:
- name: config
configMap:
name: app-config
- name: certs
secret:
secretName: tls-certs
# Using subPath to mount a single key as a file
volumeMounts:
- name: config
mountPath: /etc/app/config.yaml
subPath: config.yaml # mount only this keyNote
Note: When using subPath, config/secret updates will not be reflected in the mounted file. The pod must be restarted to pick up changes.
NFS
Network File System
- Classic shared storage — the original "distributed filesystem"
- Supports RWX — multiple pods on multiple nodes can read-write simultaneously
- Good for legacy workloads, shared file access patterns
- Performance can be a bottleneck for high-IOPS workloads
volumes:
- name: shared-data
nfs:
server: nfs.example.com
path: /exports/datalocal
Real local disk (not hostPath)
- Uses a real local disk or partition — not just a directory like
hostPath - Requires node affinity — pod must schedule on the node with the disk
- K8s handles the scheduling constraint automatically
- Good for distributed databases that manage their own replication (Cassandra, CockroachDB, Elasticsearch)
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteOnce
local:
path: /mnt/disks/ssd0
nodeAffinity: # required for local volumes
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worker-node-01PV/PVC Lifecycle
- Provisioning
Static: Admin manually creates a PV with a specific capacity, access mode, and backend.
Dynamic: A StorageClass automatically creates a PV when a PVC is submitted. - Binding
PVC binds to a matching PV. The binding is one-to-one — a PV can only be bound to a single PVC. The control plane matches on capacity (must be >= requested), access modes, storage class, and selectors. - Using
Pod mounts the PVC as a volume. The kubelet calls the storage backend (or CSI driver) to mount the volume into the container's filesystem. - Releasing
When the PVC is deleted, the PV enters theReleasedstate. The data still exists on the volume, but it cannot be bound to a new PVC until reclaimed. - Reclaiming
Based on the reclaim policy:Retain(manual cleanup),Delete(PV + underlying storage removed), orRecycle(deprecated).
Lifecycle State Diagram
Note
Note: A Released PV with Retain policy cannot be automatically rebound. To reuse it, an admin must: (1) delete the PV object (keeping the data), (2) optionally clean up data on the storage backend, (3) create a new PV pointing to the same storage.
Quick Reference: Pod Volume Mount
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
containers:
- name: app
image: my-app:latest
volumeMounts: # where to mount inside the container
- name: data
mountPath: /app/data
- name: cache
mountPath: /tmp/cache
volumes: # define volumes at the pod level
- name: data
persistentVolumeClaim:
claimName: my-data # references a PVC
- name: cache
emptyDir:
medium: MemoryTip
Pattern: volumes[] defines what storage to use. volumeMounts[] defines where to mount it in the container. They are linked by name.