Storage in Kubernetes: Persistent Volumes (PV), Persistent Volume Claims (PVC), and Storage Classes
📅 Published: May 2026
⏱️ Estimated Reading Time: 18 minutes
🏷️ Tags: Kubernetes, Persistent Volumes, PVC, Storage Classes, Stateful Applications
Introduction: The Storage Problem in Kubernetes
Containers are ephemeral. When a container restarts, anything written to its filesystem disappears. For stateless applications, this is fine. But databases, message queues, and file storage applications need their data to survive beyond the life of a single container.
Kubernetes solves this problem with Persistent Volumes (PV) and Persistent Volume Claims (PVC). These abstractions separate the provision of storage from its consumption, making it easier for application developers to request storage without understanding the underlying infrastructure.
Think of it like a library. The library (Storage Class) owns many books (Persistent Volumes). You fill out a request form (Persistent Volume Claim) asking for a specific type of book. The librarian finds a matching book and gives it to you. You don't need to know where the book came from or how the library manages its inventory.
This guide explains how storage works in Kubernetes, covering Persistent Volumes, Persistent Volume Claims, and Storage Classes.
Part 1: The Storage Abstractions
The Three Key Concepts
| Concept | What It Is | Analogy |
|---|---|---|
| Persistent Volume (PV) | Storage resource provisioned by an administrator | A physical hard drive |
| Persistent Volume Claim (PVC) | Request for storage by a user | A requisition form for a hard drive |
| Storage Class | Definition of different storage tiers | A catalog of drive types (SSD, HDD) |
┌─────────────────────────────────────────────────────────────────┐ │ Cluster Administrator │ │ │ │ │ ▼ │ │ ┌─────────────────────┐ │ │ │ Storage Class │ │ │ │ (ssd, standard) │ │ │ └──────────┬──────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ PV │ │ PV │ │ PV │ │ │ │ (100Gi SSD) │ │ (50Gi HDD) │ │ (200Gi SSD) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ │ │ (bound) │ │ ▼ │ │ ┌─────────────────────┐ │ │ │ PVC │ │ │ │ Request: 50Gi SSD │ │ │ └──────────┬──────────┘ │ │ │ │ │ ▼ │ │ ┌─────────┐ │ │ │ Pod │ │ │ └─────────┘ │ │ │ │ │ Application Developer │ └─────────────────────────────────────────────────────────────────┘
Part 2: Persistent Volumes (PV)
What is a Persistent Volume?
A Persistent Volume is a piece of storage in the cluster provisioned by an administrator. It is a cluster resource, just like CPU and memory. PVs are independent of any individual Pod and exist at the cluster level.
PV YAML Example
apiVersion: v1 kind: PersistentVolume metadata: name: manual-pv labels: type: local spec: capacity: storage: 10Gi volumeMode: Filesystem accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: standard local: path: /mnt/data nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - worker-node-1
PV Spec Fields Explained
| Field | Purpose | Common Values |
|---|---|---|
| capacity | How much storage | storage: 100Gi |
| volumeMode | Raw block or filesystem | Filesystem, Block |
| accessModes | How the volume can be mounted | See table below |
| persistentVolumeReclaimPolicy | What happens after PVC is deleted | Retain, Delete, Recycle |
| storageClassName | Which Storage Class this PV belongs to | standard, ssd |
| mountOptions | Filesystem mount options | hard, nfsvers=4.1 |
Access Modes
| Mode | Description | Use Case |
|---|---|---|
| ReadWriteOnce (RWO) | Read and write by a single node | Database, single Pod |
| ReadOnlyMany (ROX) | Read-only by many nodes | Config files, shared data |
| ReadWriteMany (RWX) | Read and write by many nodes | Shared filesystems, web uploads |
| ReadWriteOncePod (RWOP) | Read and write by a single Pod (K8s 1.27+) | Exclusive access |
PV Reclaim Policies
| Policy | What Happens | When to Use |
|---|---|---|
| Retain | PV remains with its data | Production data, needs manual cleanup |
| Delete | PV and underlying storage are deleted | Ephemeral storage, testing |
| Recycle | rm -rf /volume/* (deprecated) | Legacy workloads |
Static Provisioning
In static provisioning, the administrator creates PVs manually:
# Create a PV backed by local storage kubectl apply -f local-pv.yaml # List PVs kubectl get pv # Delete a PV kubectl delete pv manual-pv
Part 3: Persistent Volume Claims (PVC)
What is a Persistent Volume Claim?
A PVC is a request for storage by a user or application. It specifies the amount of storage needed, access mode, and optionally the Storage Class. When a PVC is created, Kubernetes binds it to a matching PV.
PVC YAML Example
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: my-pvc namespace: default spec: accessModes: - ReadWriteOnce volumeMode: Filesystem resources: requests: storage: 5Gi storageClassName: standard
PVC Spec Fields
| Field | Purpose |
|---|---|
| accessModes | Must match PV's access modes |
| resources.requests.storage | How much storage you need |
| storageClassName | Which Storage Class to use |
| selector | Match specific PV labels |
| volumeName | Bind to a specific PV |
Using PVC in a Pod
apiVersion: v1 kind: Pod metadata: name: my-pod spec: containers: - name: app image: nginx volumeMounts: - name: storage mountPath: /data volumes: - name: storage persistentVolumeClaim: claimName: my-pvc
PVC Lifecycle
1. PVC Created → Pending (waiting for PV) 2. PV Found → Bound (PVC bound to PV) 3. Pod Uses PVC → Mounted (Pod can read/write) 4. PVC Deleted → Released (PV becomes Released) 5. PV Reclaim Policy → Available, Retained, or Deleted
PVC Operations
# Create PVC kubectl apply -f pvc.yaml # List PVCs kubectl get pvc kubectl get pvc -n my-namespace # Describe PVC kubectl describe pvc my-pvc # Delete PVC kubectl delete pvc my-pvc # Check binding status kubectl get pvc,pv
Part 4: Storage Classes
What is a Storage Class?
A Storage Class defines different tiers of storage. It allows administrators to offer different quality-of-service levels: faster SSD storage for databases, slower HDD storage for backups, or region-specific storage for compliance.
Storage Classes enable dynamic provisioning: PVCs can request storage without pre-provisioned PVs.
Storage Class YAML Example
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: fast-ssd annotations: storageclass.kubernetes.io/is-default-class: "true" provisioner: kubernetes.io/aws-ebs parameters: type: gp3 fsType: ext4 iops: "3000" throughput: "125" reclaimPolicy: Delete allowVolumeExpansion: true volumeBindingMode: WaitForFirstConsumer
Storage Class Fields
| Field | Purpose |
|---|---|
| provisioner | Which storage plugin to use |
| parameters | Provisioner-specific settings |
| reclaimPolicy | What happens when PVC is deleted |
| allowVolumeExpansion | Whether PVC size can be increased |
| volumeBindingMode | When to bind PV to PVC |
Common Provisioners
| Cloud | Provisioner | Example |
|---|---|---|
| AWS | ebs.csi.aws.com | gp2, gp3 volumes |
| GCP | pd.csi.storage.gke.io | pd-ssd, pd-standard |
| Azure | disk.csi.azure.com | managed-premium, managed-standard |
| Local | kubernetes.io/no-provisioner | Static provisioning only |
| NFS | nfs.csi.k8s.io | Shared filesystem |
AWS EBS Storage Class Example
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: aws-gp3 provisioner: ebs.csi.aws.com parameters: type: gp3 fsType: ext4 iops: "3000" throughput: "125" encrypted: "true" reclaimPolicy: Delete allowVolumeExpansion: true volumeBindingMode: WaitForFirstConsumer
GCP Persistent Disk Example
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: gcp-ssd provisioner: pd.csi.storage.gke.io parameters: type: pd-ssd replication-type: none fsType: ext4 allowVolumeExpansion: true volumeBindingMode: WaitForFirstConsumer
Azure Disk Example
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: azure-premium provisioner: disk.csi.azure.com parameters: skuname: Premium_LRS cachingmode: ReadOnly kind: managed allowVolumeExpansion: true volumeBindingMode: WaitForFirstConsumer
Part 5: Dynamic Provisioning
What is Dynamic Provisioning?
Dynamic provisioning automatically creates PVs when PVCs are created. Instead of administrators pre-creating PVs, the system creates them on demand using a Storage Class.
How Dynamic Provisioning Works
1. User creates PVC with Storage Class 2. Provisioner detects the PVC 3. Provisioner creates storage in cloud (EBS, GCE PD, Azure Disk) 4. Kubernetes creates PV representing that storage 5. PVC binds to the new PV 6. Pod uses the PVC 7. When PVC is deleted, PV may be deleted (depends on reclaimPolicy)
Default Storage Class
Set a default Storage Class so PVCs without a class use it:
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: standard annotations: storageclass.kubernetes.io/is-default-class: "true" provisioner: kubernetes.io/aws-ebs parameters: type: gp2
Now PVCs without storageClassName automatically use the standard class.
Part 6: Volume Binding Modes
| Mode | Description | Use Case |
|---|---|---|
| Immediate | PV is created and bound immediately | Standard workloads |
| WaitForFirstConsumer | PV created when Pod is scheduled | Local storage, topology-aware |
WaitForFirstConsumer Example
WaitForFirstConsumer delays PV creation until a Pod uses the PVC. This ensures the PV is created in the same availability zone as the Pod.
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: fast-ssd provisioner: kubernetes.io/aws-ebs volumeBindingMode: WaitForFirstConsumer parameters: type: gp3
Part 7: StatefulSets and Storage
StatefulSets for Stateful Applications
StatefulSets are designed for stateful applications where each Pod needs its own persistent storage. They provide stable network identities and ordered deployment.
apiVersion: apps/v1 kind: StatefulSet metadata: name: mysql spec: serviceName: mysql replicas: 3 selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: containers: - name: mysql image: mysql:8.0 volumeMounts: - name: data mountPath: /var/lib/mysql volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] storageClassName: fast-ssd resources: requests: storage: 100Gi
How VolumeClaimTemplates Work
Each replica gets its own PVC
PVCs are named
data-mysql-0,data-mysql-1,data-mysql-2Pods retain their PVC even if rescheduled
StatefulSet: mysql │ ├── Pod: mysql-0 → PVC: data-mysql-0 → PV: pv-001 ├── Pod: mysql-1 → PVC: data-mysql-1 → PV: pv-002 └── Pod: mysql-2 → PVC: data-mysql-2 → PV: pv-003
Real-World Scenarios
Scenario 1: Database with Persistent Storage
A PostgreSQL database needs 100GB of fast, reliable storage that survives Pod restarts.
# Storage Class for fast storage apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: fast-ssd provisioner: ebs.csi.aws.com parameters: type: gp3 iops: "3000" --- # PVC requesting 100GB from fast-ssd apiVersion: v1 kind: PersistentVolumeClaim metadata: name: postgres-pvc spec: accessModes: - ReadWriteOnce storageClassName: fast-ssd resources: requests: storage: 100Gi --- # Deployment using the PVC apiVersion: apps/v1 kind: Deployment metadata: name: postgres spec: replicas: 1 selector: matchLabels: app: postgres template: metadata: labels: app: postgres spec: containers: - name: postgres image: postgres:15 volumeMounts: - name: data mountPath: /var/lib/postgresql/data volumes: - name: data persistentVolumeClaim: claimName: postgres-pvc
Scenario 2: Shared Filesystem for Web Uploads
A web application allows users to upload files. Multiple Pods need read-write access to the same directory.
# Using NFS or ReadWriteMany storage apiVersion: v1 kind: PersistentVolumeClaim metadata: name: uploads-pvc spec: accessModes: - ReadWriteMany # Multiple Pods can write storageClassName: nfs-csi resources: requests: storage: 500Gi --- # Deployment with multiple replicas sharing the PVC apiVersion: apps/v1 kind: Deployment metadata: name: webapp spec: replicas: 3 selector: matchLabels: app: webapp template: metadata: labels: app: webapp spec: containers: - name: app image: mywebapp:latest volumeMounts: - name: uploads mountPath: /app/uploads volumes: - name: uploads persistentVolumeClaim: claimName: uploads-pvc
Scenario 3: Resizing a PVC
A database is running out of space. You need to increase its storage.
# First, expand the PVC # Edit the PVC and change storage request kubectl edit pvc postgres-pvc # Change: resources.requests.storage: 200Gi # Check that expansion is allowed kubectl get storageclass fast-ssd -o yaml | grep allowVolumeExpansion # should show: allowVolumeExpansion: true # Check expansion progress kubectl get pvc postgres-pvc # Status: FilesystemResizePending → Resizing
Scenario 4: Clone a PVC (K8s 1.16+)
Create a new PVC from an existing one (for backups or testing).
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: cloned-pvc spec: accessModes: - ReadWriteOnce storageClassName: fast-ssd dataSource: name: postgres-pvc kind: PersistentVolumeClaim resources: requests: storage: 100Gi
Storage Operations Commands
# List Storage Classes kubectl get storageclass kubectl get sc # Describe a Storage Class kubectl describe sc fast-ssd # Set default Storage Class kubectl patch storageclass standard -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}' # List Persistent Volumes kubectl get pv kubectl get pv -o wide # List Persistent Volume Claims kubectl get pvc kubectl get pvc -A # Check binding status kubectl get pvc,pv # Describe PVC kubectl describe pvc my-pvc # Delete PVC (also deletes PV if reclaimPolicy is Delete) kubectl delete pvc my-pvc
Summary
| Concept | Purpose | Who Creates |
|---|---|---|
| Storage Class | Defines storage tiers | Administrator |
| Persistent Volume (PV) | Actual storage resource | Administrator or provisioner |
| Persistent Volume Claim (PVC) | Request for storage | Application developer |
| Static Provisioning | Administrator creates PV | Administrator |
| Dynamic Provisioning | PV created automatically | Storage Class provisioner |
Best Practices
Use Storage Classes: Always define Storage Classes for different performance tiers
Set reclaimPolicy to Retain for production: Prevent accidental data loss
Use WaitForFirstConsumer: For topology-aware storage (EBS, GCE PD)
Set resource requests: Always specify storage size
Use StatefulSets for databases: They handle PVCs properly
Monitor storage usage: Set up alerts for low disk space
Regular backups: PVs are not backed up by Kubernetes
Practice Questions
What is the difference between a Persistent Volume and a Persistent Volume Claim?
When would you use ReadWriteMany access mode?
What is dynamic provisioning and why is it useful?
How does WaitForFirstConsumer volume binding mode work?
What is the difference between StatefulSet and Deployment for storage?
Learn More
Practice Kubernetes storage with hands-on exercises in our interactive labs:
https://devops.trainwithsky.com/
Comments
Post a Comment