How to Set Up Storage Classes in Kubernetes

How to Set Up Storage Classes in Kubernetes

Storage is where a lot of Kubernetes beginners get tripped up, mostly because the abstraction layers — StorageClass, PersistentVolume, PersistentVolumeClaim — don’t map cleanly onto anything from traditional server administration. Once the model clicks, though, it’s genuinely elegant: developers request storage by characteristics, not by specific disks, and the cluster figures out the rest. This guide covers that model end to end.

The Storage Abstraction Chain

  • StorageClass: describes a class of storage — which provisioner to use, what parameters (disk type, IOPS, filesystem), and reclaim behavior. Cluster-scoped, defined once by an admin.
  • PersistentVolume (PV): an actual piece of provisioned storage, either created dynamically from a StorageClass or manually by an admin.
  • PersistentVolumeClaim (PVC): a request for storage made by a Pod’s owner — “I need 10Gi, ReadWriteOnce, from the fast-ssd class.” Kubernetes binds it to a matching PV.

Dynamic provisioning means developers never touch PVs directly — they just write a PVC, and the StorageClass’s provisioner creates the backing PV automatically.

Checking Available Storage Classes

kubectl get storageclass

Output on a typical cloud cluster:

NAME                PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE
gp3 (default)       ebs.csi.aws.com         Delete          WaitForFirstConsumer
gp3-immediate       ebs.csi.aws.com         Delete          Immediate

Defining a Custom StorageClass

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  iops: "6000"
  throughput: "250"
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

Key fields worth understanding individually:

  • reclaimPolicy: Retain vs Delete — whether the underlying disk survives after the PVC is deleted. Retain is safer for anything holding data you can’t easily regenerate.
  • volumeBindingMode: WaitForFirstConsumer — delays provisioning until a Pod actually needs the volume, so the disk gets created in the same zone as the Pod that will use it (critical for zone-locked block storage like EBS).
  • allowVolumeExpansion: true — lets PVCs be resized later without recreating them.

Apply it:

kubectl apply -f fast-ssd-storageclass.yaml

Requesting Storage with a PVC

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-pvc
  namespace: production
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: fast-ssd
  resources:
    requests:
      storage: 20Gi
kubectl apply -f data-pvc.yaml
kubectl get pvc data-pvc -n production

Output:

NAME       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS
data-pvc   Bound    pvc-8f3a1e2b-...                            20Gi       RWO            fast-ssd

If volumeBindingMode: WaitForFirstConsumer is set, the PVC will show Pending until a Pod referencing it is scheduled — that’s expected, not an error.

Mounting the PVC in a Pod

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: production
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16
          ports:
            - containerPort: 5432
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
          env:
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 50Gi

Using volumeClaimTemplates in a StatefulSet is the standard pattern for databases — each replica gets its own dedicated PVC, created and bound automatically as the StatefulSet scales.

Access Modes Explained

  • ReadWriteOnce (RWO): one node can mount read-write. Most block storage (EBS, Azure Disk, GCE PD).
  • ReadOnlyMany (ROX): many nodes, read-only.
  • ReadWriteMany (RWX): many nodes, read-write — requires a filesystem-based backend (NFS, EFS, Azure Files, CephFS), not raw block storage.
  • ReadWriteOncePod: newer mode restricting mount to a single Pod rather than a single node — useful for guaranteeing exclusivity in clusters using shared-node storage.

Resizing a PVC

With allowVolumeExpansion: true set on the StorageClass:

kubectl patch pvc data-pvc -n production \
  -p '{"spec":{"resources":{"requests":{"storage":"50Gi"}}}}'
kubectl get pvc data-pvc -n production -w

The underlying filesystem typically needs the Pod to restart (or, for some CSI drivers, an online resize happens automatically) before the OS sees the new size — check the specific CSI driver’s docs for whether online expansion is supported.

Multiple StorageClasses for Different Workloads

A production cluster commonly runs several classes side by side:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard-hdd
provisioner: ebs.csi.aws.com
parameters:
  type: st1
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: shared-nfs
provisioner: efs.csi.aws.com
parameters:
  provisioningMode: efs-ap
  fileSystemId: fs-0123456789abcdef0
reclaimPolicy: Retain

Use fast-ssd for databases, standard-hdd for logs/backups, and shared-nfs wherever multiple Pods genuinely need concurrent read-write access to the same files.

Monitoring Storage Usage

kubectl get pv
kubectl describe pvc data-pvc -n production
kubectl top pod -n production --containers

For deeper visibility, kube-state-metrics (commonly paired with Grafana, as in the monitoring setup) exposes kube_persistentvolumeclaim_resource_requests_storage_bytes and related metrics for dashboarding actual vs. requested capacity.

What’s Actually Happening Under the Hood: CSI

Every modern StorageClass ultimately delegates to a CSI (Container Storage Interface) driver — a standardized plugin interface that lets Kubernetes talk to any storage backend without the backend’s code living inside Kubernetes core itself. When a PVC is created, three things happen in sequence: the external-provisioner sidecar watches for the PVC and calls the CSI driver’s CreateVolume method; the driver talks to the actual backend (an AWS API call for EBS, for instance) to provision a real disk; and a PV object is created in Kubernetes representing that disk, which then gets bound to the PVC. When a Pod using that PVC is scheduled, the kubelet calls the CSI driver’s NodePublishVolume to actually attach and mount it on that specific node.

kubectl get csidrivers
NAME              ATTACHREQUIRED   PODINFOONMOUNT
ebs.csi.aws.com   true             true
efs.csi.aws.com   false            true

ATTACHREQUIRED is a meaningful distinction — block storage like EBS needs an explicit attach step tying the volume to a specific node before it can be mounted, while filesystem storage like EFS doesn’t, since many nodes can mount it concurrently without any per-node attachment step.

Volume Snapshots

Beyond simple provisioning, CSI drivers that support the snapshot extension enable point-in-time backups managed entirely through the Kubernetes API:

apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
  name: csi-snapclass
driver: ebs.csi.aws.com
deletionPolicy: Retain
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: data-pvc-snapshot
  namespace: production
spec:
  volumeSnapshotClassName: csi-snapclass
  source:
    persistentVolumeClaimName: data-pvc
kubectl apply -f data-pvc-snapshot.yaml
kubectl get volumesnapshot -n production
NAME                READYTOUSE   SOURCEPVC
data-pvc-snapshot   true         data-pvc

Restoring from a snapshot into a brand-new PVC:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: restored-pvc
  namespace: production
spec:
  storageClassName: fast-ssd
  dataSource:
    name: data-pvc-snapshot
    kind: VolumeSnapshot
    apiGroup: snapshot.storage.k8s.io
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi

This is the foundation most Kubernetes-native backup tools (like Velero) build on for application-consistent disaster recovery, rather than relying purely on cloud-provider snapshot tooling outside Kubernetes’ own API.

Storage Class Defaults and Immutability

Exactly one StorageClass in a cluster can be marked default, via an annotation:

metadata:
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"

Worth noting: most fields on a StorageClass are immutable after creation — changing provisioner or parameters on an existing StorageClass has no effect on already-provisioned PVs and often isn’t even accepted by the API server. To change these meaningfully, create a new StorageClass under a new name and migrate PVCs to it deliberately, rather than expecting an in-place edit to propagate.

Local Persistent Volumes for Performance-Sensitive Workloads

Network-attached block storage (EBS, Azure Disk) adds latency that some workloads — databases with strict I/O requirements, certain caching layers — can’t tolerate. Local PersistentVolumes expose a node’s actual local disk through the same PVC/PV abstraction, trading portability for raw performance:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-fast
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv-node1
spec:
  capacity:
    storage: 100Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-fast
  local:
    path: /mnt/disks/ssd1
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - node1

The critical trade-off: a Pod using this PVC can only ever be scheduled onto node1, since the data physically lives there. If that node fails, the data isn’t automatically available elsewhere — this pattern is only appropriate for workloads that handle their own replication at the application layer (many distributed databases do exactly this), not as a general-purpose default.

Generic Ephemeral Volumes

For storage that should be sized and scaled like a PVC but doesn’t need to outlive the Pod — scratch space for a batch job, temporary large-file processing — generic ephemeral volumes provide PVC semantics (including StorageClass-driven provisioning) without any persistence guarantee:

apiVersion: v1
kind: Pod
metadata:
  name: batch-processor
spec:
  containers:
    - name: processor
      image: registry.example.com/processor:1.0.0
      volumeMounts:
        - mountPath: /scratch
          name: scratch-data
  volumes:
    - name: scratch-data
      ephemeral:
        volumeClaimTemplate:
          spec:
            accessModes: ["ReadWriteOnce"]
            storageClassName: fast-ssd
            resources:
              requests:
                storage: 100Gi

This volume is created alongside the Pod and deleted automatically when the Pod is deleted — genuinely useful for workloads needing more scratch space than emptyDir‘s node-local disk can comfortably offer, without the operational overhead of manually cleaning up leftover PVCs after every batch run.

Common Mistakes

  • Setting reclaimPolicy: Delete on StorageClasses backing databases, then losing data permanently after an accidental PVC deletion.
  • Requesting ReadWriteMany on a StorageClass backed by block storage that fundamentally can’t support it — the PVC will sit Pending forever with a scheduling error.
  • Ignoring volumeBindingMode and hitting cross-zone scheduling failures where the Pod and its EBS volume end up in different availability zones.
  • Not setting allowVolumeExpansion, requiring a full data migration later just to grow a volume by a few gigabytes.

Summary

StorageClasses turn “give me a disk” into a declarative, provider-agnostic request: developers specify access mode and size in a PVC, the StorageClass’s provisioner and parameters determine what actually gets created underneath. Getting the details right — reclaim policy, binding mode, access mode — up front avoids the two most painful storage incidents in Kubernetes: silent data loss and unschedulable Pods.

References

  • Kubernetes Storage Classes: https://kubernetes.io/docs/concepts/storage/storage-classes/
  • Persistent Volumes: https://kubernetes.io/docs/concepts/storage/persistent-volumes/
  • Container Storage Interface (CSI): https://kubernetes-csi.github.io/docs/
  • CNCF storage landscape: https://landscape.cncf.io/category=cloud-native-storage
Total
1
Shares

Leave a Reply

Previous Post
How to Implement PodAntiAffinity in Kubernetes

How to Implement PodAntiAffinity in Kubernetes

Next Post
How to Use External DNS in Kubernetes

How to Use External DNS in Kubernetes

Related Posts