Kubernetes Storage Classes Guide — Dynamic Provisioning
In this tutorial, you'll learn about Kubernetes Storage Classes Guide. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Kubernetes Storage Classes define the type of storage you want for PersistentVolumeClaims, enabling dynamic provisioning of volumes with specific performance, Replication, and Accessibility characteristics.
What You'll Learn
You'll master Storage Classes — provisioners for different cloud providers, reclaim policies (Retain, Delete, Recycle), volume binding modes (Immediate, WaitForFirstConsumer), allowed topologies, and custom StorageClass configuration for production.
Why This Problem Matters
Without Storage Classes, administrators must manually provision PersistentVolumes. Storage Classes automate volume creation, ensuring each workload gets the right storage type — fast SSDs for databases, replicated network storage for shared files, and cold storage for backups.
Real-World Use
DodaZIP's file storage pipeline uses multiple Storage Classes: fast-ssd for metadata databases (io2 EBS), standard for file blobs (gp3 EBS), and cold-archive for backups (S3 via CSI driver).
Storage Class Architecture
flowchart TB User[User] -->|Create PVC| PVC[PersistentVolumeClaim] PVC --> SC[StorageClass] SC --> Provisioner[Provisioner
ebs.csi.aws.com] Provisioner --> CreateVol[Create Volume in Cloud] CreateVol --> PV[PersistentVolume] PV --> PVC PVC --> Pod[Pod uses PVC] subgraph SCConfig Params[Parameters:
type, iops, size] Policy[Reclaim Policy:
Delete/Retain] Binding[Binding Mode:
Immediate/WaitForFirstConsumer] end SC --> SCConfig
Basic Storage Class
# storage-class-fast.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
type: io2
iopsPerGB: "50"
encrypted: "true"
csi.storage.k8s.io/fstype: ext4
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
kubectl apply -f storage-class-fast.yaml
kubectl get storageclass
Expected output:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
fast-ssd (default) ebs.csi.aws.com Delete WaitForFirstConsumer true 10s
gp2 kubernetes.io/aws-ebs Delete Immediate false 30d
PVC Using the Storage Class
# pvc-fast.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd
resources:
requests:
storage: 100Gi
kubectl apply -f pvc-fast.yaml
kubectl get pvc
Expected output:
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
postgres-data Bound pvc-a1b2c3d4-e5f6-7890-abcd-ef1234567890 100Gi RWO fast-ssd 5s
Cloud Provider Storage Classes
# AWS EBS
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: aws-gp3
provisioner: ebs.csi.aws.com
parameters:
type: gp3
iops: "3000"
throughput: "125"
---
# GCE Persistent Disk
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gce-ssd
provisioner: pd.csi.storage.gke.io
parameters:
type: pd-ssd
replication-type: none
---
# Azure Disk
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: azure-premium
provisioner: disk.csi.azure.com
parameters:
skuname: Premium_LRS
---
# NFS via CSI
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
server: nfs-server.internal
share: /exports/data
mountPermissions: "0777"
Volume Binding Modes
# Immediate binding (default)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard
volumeBindingMode: Immediate
---
# Wait for first consumer (pod scheduling)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: topology-aware
volumeBindingMode: WaitForFirstConsumer
allowedTopologies:
- matchLabelExpressions:
- key: topology.ebs.csi.aws.com/zone
values:
- us-east-1a
- us-east-1b
Allowed Topologies
Restrict provisioning to specific zones:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: zone-restricted
provisioner: ebs.csi.aws.com
parameters:
type: gp3
volumeBindingMode: WaitForFirstConsumer
allowedTopologies:
- matchLabelExpressions:
- key: topology.kubernetes.io/zone
values:
- us-east-1a
- us-east-1c
Reclaim Policies
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: retain-data
provisioner: ebs.csi.aws.com
reclaimPolicy: Retain # PV persists after PVC deletion
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: auto-cleanup
provisioner: ebs.csi.aws.com
reclaimPolicy: Delete # PV and disk deleted with PVC (default)
Storage Class Selector in Applications
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 3
template:
spec:
containers:
- name: postgres
image: postgres:16
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 100Gi
Dynamic Provisioning Simulator
import time
import uuid
class PV:
def __init__(self, name: str, size: str, sc: str, zone: str):
self.name = name
self.size = size
self.storage_class = sc
self.zone = zone
self.status = "Available"
self.bound_to = None
class StorageClass:
def __init__(self, name: str, provisioner: str,
reclaim: str = "Delete",
binding: str = "Immediate"):
self.name = name
self.provisioner = provisioner
self.reclaim_policy = reclaim
self.binding_mode = binding
self.allow_topology = []
class DynamicProvisioner:
def __init__(self):
self.pvs = {}
self.scs = {}
def add_storage_class(self, sc: StorageClass):
self.scs[sc.name] = sc
def provision(self, pvc: dict) -> PV:
sc_name = pvc.get("storageClassName", "standard")
sc = self.scs.get(sc_name)
if not sc:
return None
size = pvc.get("resources", {}).get("requests", {}).get("storage", "10Gi")
zone = pvc.get("zone", "us-east-1a")
pv_name = f"pvc-{uuid.uuid4().hex[:12]}"
pv = PV(pv_name, size, sc_name, zone)
pv.status = "Bound"
pv.bound_to = pvc.get("name")
self.pvs[pv_name] = pv
return pv
def delete_pvc(self, pvc_name: str):
to_delete = [
name for name, pv in self.pvs.items()
if pv.bound_to == pvc_name
]
for name in to_delete:
pv = self.pvs[name]
sc = self.scs.get(pv.storage_class)
if sc and sc.reclaim_policy == "Delete":
del self.pvs[name]
print(f"Deleted PV {name} (reclaim policy: Delete)")
elif sc and sc.reclaim_policy == "Retain":
pv.status = "Released"
print(f"Released PV {name} (reclaim policy: Retain)")
else:
pv.status = "Released"
provisioner = DynamicProvisioner()
provisioner.add_storage_class(StorageClass("fast-ssd", "ebs.csi.aws.com", "Delete"))
provisioner.add_storage_class(StorageClass("retain-sc", "ebs.csi.aws.com", "Retain"))
pvc1 = {"name": "db-data", "storageClassName": "fast-ssd", "resources": {"requests": {"storage": "50Gi"}}
pvc2 = {"name": "archive-data", "storageClassName": "retain-sc", "resources": {"requests": {"storage": "200Gi"}}
pv1 = provisioner.provision(pvc1)
pv2 = provisioner.provision(pvc2)
print(f"Provisioned {pv1.name} ({pv1.size}) via {pv1.storage_class}")
print(f"Provisioned {pv2.name} ({pv2.size}) via {pv2.storage_class}")
print("\nDeleting PVCs...")
provisioner.delete_pvc("db-data")
provisioner.delete_pvc("archive-data")
print(f"\nRemaining PVs: {list(provisioner.pvs.keys())}")
Expected output:
Provisioned pvc-a1b2c3d4e5f6 (50Gi) via fast-ssd
Provisioned pvc-b2c3d4e5f6a7 (200Gi) via retain-sc
Deleting PVCs...
Deleted PV pvc-a1b2c3d4e5f6 (reclaim policy: Delete)
Released PV pvc-b2c3d4e5f6a7 (reclaim policy: Retain)
Remaining PVs: ['pvc-b2c3d4e5f6a7']
Volume Expansion
# Edit PVC to request more storage
kubectl edit pvc postgres-data
# Change: resources.requests.storage: 100Gi -> 200Gi
# Verify expansion
kubectl get pvc postgres-data -w
Expected output:
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
postgres-data Bound pvc-... 200Gi RWO fast-ssd 2m
Common Mistakes
1. Using Immediate Binding for Topology-Aware Workloads
Immediate binding provisions a volume before the pod is scheduled. If the volume is in us-east-1a but the pod schedules to us-east-1b, it can't attach. Use WaitForFirstConsumer so the volume is provisioned in the same zone as the pod.
2. Not Setting allowVolumeExpansion
Without allowVolumeExpansion: true, you can't resize PVCs. To expand an existing volume, both the StorageClass and the CSI driver must support it.
3. Wrong Reclaim Policy for Important Data
Delete reclaim policy removes the cloud disk when the PVC is deleted. For production databases, use Retain so the volume survives accidental PVC deletion.
4. Forgetting CSI Driver Installation
StorageClasses depend on CSI drivers. If ebs.csi.aws.com is not installed, PVCs stay Pending. Verify driver installation: kubectl get pods -n kube-system | grep csi.
5. Mixing Access Modes
ReadWriteOnce can only be mounted by one node. ReadOnlyMany can be mounted by many nodes but is read-only. ReadWriteMany is the only mode for shared concurrent access but requires network file systems (NFS, EFS).
6. Ignoring IOPS Limits
Setting iopsPerGB: 100 on a 10Gi volume requests 1000 IOPS. Setting it on a 16Ti volume requests 1.6M IOPS, which exceeds cloud provider limits. Always check maximums.
7. No Default StorageClass
Without a default, PVCs without storageClassName stay Pending. Set a default: kubectl patch storageclass gp2 -p '{"metadata": {"annotations":{"storageclass.Kubernetes.io/is-default-class":"true"}}'.
Practice Questions
1. What is the difference between Immediate and WaitForFirstConsumer volume binding?
Immediate provisions the volume when the PVC is created. WaitForFirstConsumer provisions only after a pod using the PVC is scheduled to a node, ensuring the volume is in the same availability zone as the pod.
2. What happens when a PVC is deleted with reclaimPolicy: Retain?
The PV enters the "Released" state. The underlying storage volume (EBS disk, GCE PD) is NOT deleted. An administrator must manually reclaim the volume by removing the claimRef from the PV, or delete it through the cloud console.
3. How do you set a default StorageClass?
Annotate a StorageClass with storageclass.Kubernetes.io/is-default-class: "true". Only one StorageClass should be default. PVCs without storageClassName use the default.
4. Can you change the StorageClass of an existing PVC?
No. StorageClass is immutable after creation. You must create a new PVC with the desired StorageClass and migrate data (e.g., using a tool like rsync or storage-level Replication).
5. Challenge: Design a tiered storage strategy for a log aggregation system.
Logs are written at high throughput, stored for 7 days in fast storage (SSD), 30 days in standard (HDD), and 1 year in cold (S3/Glacier). Design StorageClasses, PVCs, and a data lifecycle controller that moves data between tiers based on age.
Mini Project: Storage Provisioner Simulator
class StorageTopology:
def __init__(self):
self.volumes = {}
self.pod_zones = {}
def schedule_pod(self, pod_name: str, zone: str):
self.pod_zones[pod_name] = zone
def create_pvc(self, name: str, sc_name: str,
binding: str, size: str, pod: str = None):
if binding == "WaitForFirstConsumer":
if pod and pod in self.pod_zones:
zone = self.pod_zones[pod]
self.volumes[name] = {
"zone": zone,
"size": size,
"sc": sc_name,
"status": "Bound"
}
print(f"Created {size} volume in {zone} for PVC/{name}")
else:
print(f"PVC/{name}: Waiting for pod to schedule...")
self.volumes[name] = {
"zone": None, "size": size,
"sc": sc_name, "status": "Pending"
}
else:
zone = "us-east-1a" # Default zone
self.volumes[name] = {
"zone": zone, "size": size,
"sc": sc_name, "status": "Bound"
}
print(f"Immediately created {size} volume in {zone} for PVC/{name}")
topology = StorageTopology()
topology.create_pvc("db-data", "fast-ssd", "WaitForFirstConsumer",
"100Gi", "postgres-pod")
topology.schedule_pod("postgres-pod", "us-east-1b")
print(f"Pod scheduled in us-east-1b, re-provisioning PVC...")
topology.create_pvc("db-data", "fast-ssd", "WaitForFirstConsumer",
"100Gi", "postgres-pod")
Expected output:
PVC/db-data: Waiting for pod to schedule...
Created 100Gi volume in us-east-1b for PVC/db-data
FAQ
What's Next
Congratulations on completing this Storage Classes guide! Here's where to go from here:
- Practice daily — Create custom StorageClasses for different workloads
- Build a project — Set up tiered storage with multiple StorageClasses
- Explore related topics — CSI driver development, backup/restore with Velero, storage quotas
- Join the community — Share your storage configurations and get feedback
Remember: every expert was once a beginner. Keep storing!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro