Skip to content

Kubernetes Storage Classes Guide — Dynamic Provisioning

DodaTech Updated 2026-06-24 8 min read

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 CSI drivers should I install for my cloud provider?

AWS: ebs.csi.aws.com (block) and efs.csi.aws.com (file). GCP: pd.csi.storage.gke.io (block) and filestore.csi.storage.gke.io (file). Azure: disk.csi.azure.com (block) and file.csi.azure.com (file). Check the CSI driver docs for your specific provider.

How is StorageClass different from PersistentVolume?

StorageClass is a template for dynamic provisioning. PersistentVolume is a pre-existing or dynamically provisioned storage resource. Think of StorageClass as a "printer" and PVs as the printed pages — the StorageClass produces PVs on demand.

What storage provisioner is used on-premises?

For on-premises clusters, use Kubernetes.io/no-provisioner (static provisioning) or deploy a CSI driver for your storage array (e.g., Dell EMC, NetApp, Pure Storage). OpenEBS, Longhorn, and Rook/Ceph provide software-defined storage with their own CSI drivers.

What's Next

Kubernetes Network Policies Guide
Kubernetes Persistent Volumes
Kubernetes StatefulSets Guide

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