Skip to content

Kubernetes Storage Classes: Dynamic Provisioning Architecture

How does dynamic volume provisioning actually work?

A StorageClass provides a way for cluster administrators to describe the "classes" of storage they offer within a Kubernetes cluster. Kubernetes itself is fundamentally unopinionated about what these classes represent; they might map to different quality-of-service (QoS) levels, specific backup policies, or arbitrary custom policies determined by the infrastructure team. The concept of a StorageClass is highly analogous to "profiles" in traditional storage system designs.

To understand the architecture of a StorageClass, one must view it as the engine of dynamic volume provisioning. It completely abstracts the underlying storage implementation from the developers consuming it, ensuring that users do not need to understand the complex nuances of how storage is provisioned on various cloud providers or on-premises SANs.

How Dynamic Provisioning Works End-to-End

Historically, storage in Kubernetes required manual intervention. A cluster administrator had to manually make API calls to a cloud provider (like AWS or Google Cloud) to create a physical storage volume, and then manually create a PersistentVolume (PV) object in Kubernetes to represent that physical disk.

Dynamic volume provisioning eliminates this operational bottleneck. It allows storage volumes to be created on-demand the moment a user requests them. The implementation of this workflow relies on the StorageClass API object.

Here is the end-to-end architectural workflow:

  1. Administrator Configuration: The cluster administrator creates one or more StorageClass objects. Each class specifies a volume plugin (known as a "provisioner," typically a Container Storage Interface or CSI driver) and a set of opaque parameters to pass to that provisioner.
  2. User Request (The PVC): A developer creates a PersistentVolumeClaim (PVC), which acts as a request for storage. In this claim, the user specifies the desired capacity, access modes, and requests a specific storage class via the storageClassName field.
  3. Admission and Provisioning: When the Kubernetes API server receives the PVC, a control loop watches for the new claim. Because the PVC requests a specific StorageClass and no existing static PV matches the claim, the cluster triggers dynamic provisioning.
  4. Driver Execution: The designated provisioner (e.g., the AWS EBS CSI driver) reads the custom parameters defined in the StorageClass and communicates with the external storage provider's API to physically create the storage asset.
  5. Binding: Once the physical volume is created, the provisioner automatically generates a PersistentVolume object in Kubernetes that represents the new asset. The control loop then binds the newly created PV to the user's PVC. The volume is now ready to be mounted by a Pod.

Core StorageClass Configurations

When an administrator defines a StorageClass, they configure several critical fields that govern the lifecycle, expandability, and scheduling behavior of dynamically provisioned volumes.

1. Reclaim Policy (reclaimPolicy)

The reclaimPolicy dictates what happens to the underlying persistent volume when the user deletes the associated PersistentVolumeClaim. If no policy is explicitly specified in the StorageClass, it defaults to Delete.

  • Delete: This is the default for dynamically provisioned volumes. When the PVC is deleted, Kubernetes removes both the PersistentVolume object from the cluster and the associated physical storage asset from the external infrastructure (e.g., deleting the AWS EBS volume). This ensures no orphaned resources incur cloud costs, but it guarantees data destruction.
  • Retain: This policy allows for the manual reclamation of the resource. When the PVC is deleted, the PersistentVolume object continues to exist, and its status transitions to Released. The volume is not immediately available for another claim because the previous claimant's data remains on the disk. An administrator must manually extract the data, delete the PV, and clean up the underlying storage asset. This policy is highly recommended for critical databases to prevent accidental data loss.
  • Recycle (Deprecated): This legacy policy performs a basic scrub (running rm -rf /thevolume/* inside a temporary container) and makes the volume available for a new claim. The Kubernetes project strongly recommends using dynamic provisioning instead of this deprecated feature.

2. Volume Binding Mode (volumeBindingMode)

The volumeBindingMode field controls exactly when volume binding and dynamic provisioning should occur.

  • Immediate (Default): This mode dictates that volume binding and dynamic provisioning occur the instant the PersistentVolumeClaim is created. While fast, this creates a severe architectural flaw in multi-zone clusters. If a storage backend is topology-constrained (e.g., an AWS EBS volume is locked to us-east-1a), the volume might be provisioned in a zone where the Pod cannot be scheduled due to CPU constraints, node selectors, or pod anti-affinity rules. This results in an unschedulable Pod.
  • WaitForFirstConsumer: This is the recommended mode for modern clusters. It delays the binding and provisioning of the PersistentVolume until a Pod that actually uses the PVC is created and successfully scheduled. By delaying provisioning, the Kubernetes scheduler can evaluate all of the Pod's constraints (node resources, taints, tolerations, and affinity) and select the optimal Node. The storage driver is then instructed to provision the volume specifically in the topology (zone/region) where the Pod's designated Node resides.

3. Volume Expansion (allowVolumeExpansion)

Data requirements grow over time. Kubernetes supports expanding existing volumes without data loss, provided the StorageClass explicitly enables it by setting the allowVolumeExpansion field to true.

When enabled, a user can simply edit their existing PVC object to request a larger size. Kubernetes handles the complex orchestration of calling the CSI driver to expand the physical block device on the cloud provider, and the kubelet handles the online expansion of the filesystem (such as XFS or ext4) while the Pod is running. It is critical to note that Kubernetes only supports growing volumes; you cannot shrink a volume.

When to Use Multiple Storage Classes

While a cluster can function with a single default StorageClass, mature production architectures heavily rely on multiple classes to address diverse operational requirements.

  • Quality of Service (QoS) Tiering: A common use case is providing different performance tiers to developers. An administrator might create a fast StorageClass backed by SSDs (e.g., AWS io1 with guaranteed IOPS) for latency-critical databases, and a slow or standard StorageClass backed by cheaper HDDs for bulk file storage or log aggregation.
  • Multi-Tenant Isolation: In shared clusters, different tenants might have varying data compliance requirements. Administrators can configure separate StorageClasses per tenant. This ensures that one tenant's storage utilizes a specific backup policy or is physically segregated onto a dedicated storage array, ensuring strict data-plane isolation.
  • Protocol Variations: Applications have different fundamental storage needs. A cluster will typically need one StorageClass that provides block storage with ReadWriteOnce access (like Azure Disk or AWS EBS) for databases, and another StorageClass that provides distributed Network File System (NFS) capabilities with ReadWriteMany access for applications that require concurrent file sharing across multiple nodes.

The Default StorageClass and Missing Defaults

Administrators can mark a specific StorageClass as the default by applying the annotation storageclass.kubernetes.io/is-default-class: "true" to the object.

When the DefaultStorageClass admission controller is enabled, any PVC submitted without a specified storageClassName is intercepted, and the admission controller automatically injects the name of the default class into the request. This simplifies the developer experience, as they do not need to memorize the names of the cluster's storage profiles.

What happens if there is no default StorageClass? If the admission controller is turned off or no class is marked as default, a PVC created without a storageClassName will remain indefinitely unbound. It will strictly look for a statically provisioned PV that also has no class specified.

To prevent PVCs from being permanently orphaned when an administrator is transitioning between default classes, Kubernetes 1.28 introduced retroactive default StorageClass assignment. If a PVC is pending without a class, and an administrator later creates a new default StorageClass, the control plane will proactively find the pending PVC and update its storageClassName to match the newly established default, allowing provisioning to proceed.

Bypassing the Default: If a user explicitly wants to consume a statically pre-provisioned volume and wants to ensure the dynamic provisioner ignores their request, they must explicitly set the storageClassName to "" (an empty string). The empty string serves as a strict opt-out instruction, telling the admission controller not to inject the default class, and ensuring the PVC only binds to a static PV that also has an empty storage class.

Real-World YAML Examples

Here are practical examples of how to configure StorageClass objects for common providers.

Example 1: AWS Elastic Block Store (EBS)

This configuration provisions high-performance SSDs (io1) on AWS using the CSI driver. It requests a specific IOPS rate and enforces encryption at the block storage layer. It uses WaitForFirstConsumer to ensure the disk is created in the same Availability Zone as the Pod.

yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ebs-sc
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
  csi.storage.k8s.io/fstype: xfs
  type: io1
  iopsPerGB: "50"
  encrypted: "true"
  tagSpecification_1: "key1=value1"

Example 2: Network File System (NFS)

NFS allows for ReadWriteMany access. Because Kubernetes does not include a built-in internal NFS dynamic provisioner, you must rely on an external provisioner deployment (like NFS subdir external provisioner). The parameters define the target server and export path.

yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: example-nfs
provisioner: example.com/external-nfs
parameters:
  server: nfs-server.example.com
  path: /share
  readOnly: "false"

Example 3: Local Ephemeral/Static Storage

For latency-critical applications like bare-metal databases, you may want to use the physical disks attached directly to the Kubernetes worker nodes (local storage). Because local volumes do not support automated dynamic provisioning, the provisioner is explicitly set to kubernetes.io/no-provisioner. The class exists purely to enforce the WaitForFirstConsumer binding mode, delaying PVC binding until the scheduler selects a specific node, thereby guaranteeing the Pod lands on the exact machine where the local disk physically resides.

yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

Based on Kubernetes v1.35 (Timbernetes). Changelog.