Loading learning content...
In the previous page, we explored Persistent Volumes and their manual provisioning model. While static provisioning provides control, it creates a fundamental bottleneck: every storage request requires administrator intervention. In a cluster with hundreds of applications deploying daily, this model doesn't scale.
Storage Classes transform Kubernetes storage from a manually provisioned resource into a self-service, on-demand capability. They define storage profiles that describe the type of storage available (fast SSD, cheap archival, replicated, etc.) and the provisioner that can create volumes matching those characteristics automatically.
With Storage Classes, developers request storage by specifying a class name, and Kubernetes handles the rest—provisioning volumes from the appropriate backend, applying specified parameters, and binding claims without administrator involvement.
By the end of this page, you will understand Storage Class architecture, provisioner configuration, parameter customization, binding modes for topology-aware scheduling, default storage classes, and enterprise patterns for multi-tier storage strategies in production Kubernetes clusters.
A Storage Class is a cluster-scoped Kubernetes resource that describes a class of storage available for provisioning. It serves three primary purposes:
Core Storage Class fields:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
# StorageClass defines a class of storage and its provisionerapiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: fast-ssd labels: tier: premium environment: production annotations: # Documentation for users description: "High-performance SSD storage for databases" storageclass.kubernetes.io/is-default-class: "false" # Provisioner: what creates volumes for this class# Can be in-tree (kubernetes.io/*) or CSI (*.csi.*)provisioner: ebs.csi.aws.com # Parameters passed to the provisioner# These are provisioner-specificparameters: type: gp3 # AWS EBS volume type iops: "16000" # Provisioned IOPS throughput: "1000" # MB/s throughput encrypted: "true" # Encryption at rest fsType: ext4 # Filesystem type # Reclaim policy for dynamically provisioned PVs# Default: Delete (for dynamic), can be: Retain, DeletereclaimPolicy: Retain # Allow volume expansion after creationallowVolumeExpansion: true # Mount options applied to all volumes of this classmountOptions: - discard - noatime # Volume binding mode: when to provision and bind# Immediate: provision on PVC creation# WaitForFirstConsumer: wait for pod schedulingvolumeBindingMode: WaitForFirstConsumer # Topology restrictions for provisioned volumesallowedTopologies: - matchLabelExpressions: - key: topology.kubernetes.io/zone values: - us-east-1a - us-east-1bKey Storage Class components explained:
| Field | Purpose | Impact |
|---|---|---|
| provisioner | Specifies which provisioner creates volumes | Determines what storage backend is used |
| parameters | Provisioner-specific configuration | Controls volume characteristics (IOPS, encryption, etc.) |
| reclaimPolicy | What happens when PVC is deleted | Retain preserves data, Delete destroys it |
| allowVolumeExpansion | Whether volumes can be resized | Enables PVC resource request increases |
| mountOptions | Mount flags for volumes | Applied when volumes are mounted to pods |
| volumeBindingMode | When PV is provisioned | Immediate or wait for pod scheduling |
| allowedTopologies | Where volumes can be created | Restricts provisioning to specific zones/nodes |
The provisioner is the engine that creates Persistent Volumes in response to PVCs. Understanding provisioner types and their capabilities is essential for properly configuring Storage Classes.
Provisioner categories:
kubernetes.io/<type>. Being migrated to CSI. Examples: kubernetes.io/aws-ebs, kubernetes.io/gce-pd, kubernetes.io/azure-disk.ebs.csi.aws.com, pd.csi.storage.gke.io, disk.csi.azure.com.ceph.rook.io/block, nfs.csi.k8s.io, longhorn-driver.CSI driver architecture:
CSI drivers consist of multiple components working together:
Controller component: Runs as a Deployment, handles volume provisioning, deletion, attach/detach, and snapshot operations. Communicates with the storage backend API.
Node component: Runs as a DaemonSet on each node, handles mounting/unmounting volumes on the node, filesystem formatting, and volume statistics.
CSI sidecars: Container images from the Kubernetes CSI community that handle communication between Kubernetes and the CSI driver:
csi-provisioner: Watches PVCs and triggers CreateVolumecsi-attacher: Watches VolumeAttachments and triggers ControllerPublishcsi-resizer: Handles volume expansioncsi-snapshotter: Manages volume snapshotscsi-node-driver-registrar: Registers the CSI driver with kubelet12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
# Example: AWS EBS CSI Driver Controller Deployment (simplified)apiVersion: apps/v1kind: Deploymentmetadata: name: ebs-csi-controller namespace: kube-systemspec: replicas: 2 selector: matchLabels: app: ebs-csi-controller template: spec: serviceAccountName: ebs-csi-controller-sa containers: # Main CSI driver - communicates with AWS EBS API - name: ebs-plugin image: public.ecr.aws/ebs-csi-driver/aws-ebs-csi-driver:v1.25.0 args: - controller - --endpoint=$(CSI_ENDPOINT) - --logging-format=json # AWS credentials via IAM Roles for Service Accounts # CSI Provisioner sidecar - watches PVCs - name: csi-provisioner image: registry.k8s.io/sig-storage/csi-provisioner:v3.6.0 args: - --csi-address=$(ADDRESS) - --feature-gates=Topology=true - --leader-election=true # CSI Attacher sidecar - handles volume attachments - name: csi-attacher image: registry.k8s.io/sig-storage/csi-attacher:v4.4.0 args: - --csi-address=$(ADDRESS) - --leader-election=true # CSI Resizer sidecar - handles volume expansion - name: csi-resizer image: registry.k8s.io/sig-storage/csi-resizer:v1.9.0 args: - --csi-address=$(ADDRESS) - --leader-election=true # CSI Snapshotter sidecar - handles snapshots - name: csi-snapshotter image: registry.k8s.io/sig-storage/csi-snapshotter:v6.3.0 args: - --csi-address=$(ADDRESS) - --leader-election=trueUse kubectl get csidrivers to list installed CSI drivers and their capabilities. The CSIDriver object declares whether the driver supports volume expansion, snapshots, topology, etc. Always verify driver capabilities match Storage Class requirements.
Storage Class parameters are passed directly to the provisioner and control the characteristics of provisioned volumes. These parameters are highly provisioner-specific—understanding them requires knowledge of the storage backend.
Common parameter patterns across cloud providers:
12345678910111213141516171819202122232425262728293031323334353637383940414243
# High-performance SSD (gp3)apiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: aws-fastprovisioner: ebs.csi.aws.comparameters: type: gp3 iops: "16000" # Up to 16,000 IOPS throughput: "1000" # Up to 1,000 MB/s encrypted: "true" kmsKeyId: alias/ebs-key # Customer-managed encryption key fsType: ext4reclaimPolicy: RetainvolumeBindingMode: WaitForFirstConsumer ---# Provisioned IOPS SSD (io2) for critical databasesapiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: aws-ultraprovisioner: ebs.csi.aws.comparameters: type: io2 iops: "64000" # Up to 64,000 IOPS encrypted: "true" blockExpress: "true" # io2 Block Express for > 64,000 IOPSreclaimPolicy: RetainvolumeBindingMode: WaitForFirstConsumer ---# Cold storage (sc1) for archivesapiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: aws-archiveprovisioner: ebs.csi.aws.comparameters: type: sc1 encrypted: "true"reclaimPolicy: DeletevolumeBindingMode: ImmediateKubernetes does not validate Storage Class parameters at creation time. Invalid parameters only cause errors when a PVC triggers dynamic provisioning. Always test new Storage Classes with a sample PVC before production use.
Volume binding mode controls when a PersistentVolume is provisioned and bound to a PVC. This seemingly simple configuration has profound implications for pod scheduling and storage topology.
The two binding modes:
Why WaitForFirstConsumer matters:
Consider a cluster spanning three availability zones. With Immediate binding:
With WaitForFirstConsumer:
12345678910111213141516171819202122232425262728293031323334
# WaitForFirstConsumer is essential for zone-local storageapiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: ebs-topology-awareprovisioner: ebs.csi.aws.comparameters: type: gp3volumeBindingMode: WaitForFirstConsumer # Critical for EBS ---# Immediate is acceptable for network storage (available in all zones)apiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: efs-sharedprovisioner: efs.csi.aws.comparameters: provisioningMode: efs-apvolumeBindingMode: Immediate # EFS is available cluster-wide ---# PVC using topology-aware classapiVersion: v1kind: PersistentVolumeClaimmetadata: name: database-dataspec: accessModes: [ReadWriteOnce] storageClassName: ebs-topology-aware resources: requests: storage: 100Gi# Note: PV won't be created until a pod references this PVCUse WaitForFirstConsumer for all zone-local storage (EBS, GCE PD, Azure Disk, Local PVs). Use Immediate only for storage accessible from any zone (NFS, CephFS, cloud file services). When in doubt, WaitForFirstConsumer is safer.
Kubernetes supports designating one Storage Class as the cluster default. When a PVC doesn't specify a storageClassName, it uses the default class for dynamic provisioning.
Setting the default:
The default class is indicated by an annotation:
1234567891011121314151617181920212223242526272829303132333435363738
apiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: standard annotations: # This annotation marks the class as default storageclass.kubernetes.io/is-default-class: "true"provisioner: ebs.csi.aws.comparameters: type: gp3volumeBindingMode: WaitForFirstConsumerreclaimPolicy: Delete # Common for default to auto-cleanup ---# PVC without storageClassName uses the defaultapiVersion: v1kind: PersistentVolumeClaimmetadata: name: my-dataspec: accessModes: [ReadWriteOnce] # storageClassName: omitted - will use default class resources: requests: storage: 10Gi ---# Explicitly requesting no class (static binding only)apiVersion: v1kind: PersistentVolumeClaimmetadata: name: static-onlyspec: accessModes: [ReadWriteOnce] storageClassName: "" # Empty string = no dynamic provisioning resources: requests: storage: 10GiImportant default class behaviors:
Multiple defaults: If multiple classes have the default annotation, PVCs without a class will fail with an error. Ensure exactly one default.
No default: If no class is marked default and a PVC omits storageClassName, behavior depends on Kubernetes version and DefaultStorageClass admission controller configuration.
Explicit empty string: Setting storageClassName: "" explicitly opts out of dynamic provisioning. The PVC will only bind to pre-existing PVs without a class.
Changing the default: To change the default class, remove the annotation from the old default before adding it to the new one:
# Remove default from old class
kubectl patch storageclass old-default -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
# Set new default
kubectl patch storageclass new-default -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
Existing PVCs are not affected by changing the default class. They remain bound to their original PVs. The default class only affects new PVCs that don't specify a class.
Storage needs grow over time. Kubernetes supports online volume expansion—increasing the size of a Persistent Volume while workloads continue running. This capability must be explicitly enabled in the Storage Class.
Enabling volume expansion:
12345678910111213141516171819202122232425262728293031323334353637
# Storage Class with expansion enabledapiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: expandable-ssdprovisioner: ebs.csi.aws.comparameters: type: gp3allowVolumeExpansion: true # Enables PVC size increasesvolumeBindingMode: WaitForFirstConsumer ---# Original PVCapiVersion: v1kind: PersistentVolumeClaimmetadata: name: database-storespec: accessModes: [ReadWriteOnce] storageClassName: expandable-ssd resources: requests: storage: 100Gi ---# To expand, increase the storage request# Use kubectl patch or edit the PVCapiVersion: v1kind: PersistentVolumeClaimmetadata: name: database-storespec: accessModes: [ReadWriteOnce] storageClassName: expandable-ssd resources: requests: storage: 200Gi # Increased from 100GiThe expansion process:
Volume expansion happens in two phases:
Controller expansion: The storage backend extends the volume. For cloud disks, this involves an API call to resize the underlying disk. This happens automatically when you increase the PVC request.
Filesystem expansion: The filesystem on the volume must be grown to use the additional space. This requires:
Monitoring expansion status:
Check PVC conditions to monitor expansion progress:
123456789101112131415161718192021
# Check expansion statuskubectl get pvc database-store -o yaml # Look for conditions:# status:# conditions:# - type: Resizing# status: "True"# - type: FileSystemResizePending# status: "True" # Once complete:# status:# capacity:# storage: 200Gi # Matches requested # Wait for filesystem expansionkubectl describe pvc database-store# Events:# Normal Resizing 2m external-resizer External resizer is resizing volume# Normal FileSystemResizeSuccessful 1m kubelet MountVolume.NodeExpandVolume succeededVolume expansion is one-directional—you cannot shrink a PVC. Always consider future growth; shrinking requires backing up data, creating a new smaller volume, and restoring. Some CSI drivers have minimum resize increments or cooldowns between operations.
Storage Classes can restrict where volumes are provisioned using allowedTopologies. This is essential for multi-zone clusters where you need to control storage placement for performance, compliance, or cost reasons.
Topology constraint use cases:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
# Restrict provisioning to specific zonesapiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: zone-restricted-ssdprovisioner: ebs.csi.aws.comparameters: type: gp3volumeBindingMode: WaitForFirstConsumerallowedTopologies: - matchLabelExpressions: # Only provision in us-east-1a and us-east-1b # Excludes us-east-1c (perhaps for cost or capacity reasons) - key: topology.kubernetes.io/zone values: - us-east-1a - us-east-1b ---# Data residency: EU zones onlyapiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: eu-compliantprovisioner: pd.csi.storage.gke.ioparameters: type: pd-ssdvolumeBindingMode: WaitForFirstConsumerallowedTopologies: - matchLabelExpressions: - key: topology.kubernetes.io/zone values: - europe-west1-b - europe-west1-c - europe-west1-d ---# Multiple topology keys (zone and node type)apiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: nvme-zonesprovisioner: local.csi.kubernetes.ioparameters: nodetype: nvmevolumeBindingMode: WaitForFirstConsumerallowedTopologies: - matchLabelExpressions: - key: topology.kubernetes.io/zone values: [us-west-2a, us-west-2b] - key: node.kubernetes.io/instance-type values: [i3.large, i3.xlarge, i3.2xlarge]When using allowedTopologies with WaitForFirstConsumer, the scheduler restricts pod placement to nodes in allowed zones before triggering provisioning. This prevents scheduling pods to zones where storage cannot be created.
Enterprise Kubernetes environments typically require multiple Storage Classes to address varying workload needs. A well-designed storage tier strategy balances performance, cost, and durability across different use cases.
Common storage tier model:
| Tier | Class Name | Use Case | Performance | Cost | Durability |
|---|---|---|---|---|---|
| Premium | fast-ssd | Production databases, critical workloads | High IOPS, low latency | $$$ | High (replicated) |
| Standard | standard | General-purpose workloads | Balanced | $$ | High (replicated) |
| Economy | cheap-hdd | Dev/test, non-critical | Lower performance | $ | Standard |
| Archive | cold-storage | Backups, logs, rarely-accessed | High latency | $ | High |
| Ephemeral | ephemeral-local | Temp data, caches | Very high (local) | $ | None |
| Shared | nfs-shared | Shared across pods | Moderate | $$ | High |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
# Premium tier: Production databasesapiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: fast-ssd labels: tier: premium cost: highprovisioner: ebs.csi.aws.comparameters: type: io2 iops: "64000" encrypted: "true"reclaimPolicy: RetainallowVolumeExpansion: truevolumeBindingMode: WaitForFirstConsumer ---# Standard tier: Default for most workloadsapiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: standard annotations: storageclass.kubernetes.io/is-default-class: "true" labels: tier: standard cost: mediumprovisioner: ebs.csi.aws.comparameters: type: gp3 iops: "3000" throughput: "125" encrypted: "true"reclaimPolicy: DeleteallowVolumeExpansion: truevolumeBindingMode: WaitForFirstConsumer ---# Economy tier: Development environmentsapiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: cheap-hdd labels: tier: economy cost: lowprovisioner: ebs.csi.aws.comparameters: type: st1 # Throughput-optimized HDD encrypted: "true"reclaimPolicy: DeleteallowVolumeExpansion: truevolumeBindingMode: WaitForFirstConsumer ---# Shared tier: RWX volumes via EFSapiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: nfs-shared labels: tier: shared access: rwxprovisioner: efs.csi.aws.comparameters: provisioningMode: efs-ap fileSystemId: fs-abc123 directoryPerms: "700"reclaimPolicy: DeletevolumeBindingMode: Immediate # EFS is zone-agnosticUse ResourceQuotas to limit storage consumption per tier: kubectl create quota premium-limit --hard=fast-ssd.storageclass.storage.k8s.io/requests.storage=500Gi. This prevents runaway costs from premium storage overuse.
Storage Classes transform Kubernetes storage from a manually provisioned resource into a self-service capability that scales with your organization. They are the control plane for storage policy, enabling automated provisioning, tiering, and lifecycle management.
What's next:
With Persistent Volumes and Storage Classes covered, we'll explore StatefulSet storage—how Kubernetes manages volumes for stateful applications that need stable identities and ordered operations. StatefulSets combine PVCs with stable pod identities to enable reliable database and stateful service deployments.
You now understand Storage Classes comprehensively—from provisioners and parameters through binding modes, expansion, topology constraints, and multi-tier strategies. This knowledge enables you to design and operate storage infrastructure that serves diverse workload needs at scale.