Kubernetes itself doesn’t magically make stateful applications easier to manage; it just provides a more robust, opinionated framework for doing so.

Let’s see what this actually looks like. Imagine a simple database pod running directly on a Kubernetes node.

apiVersion: v1
kind: Pod
metadata:
  name: my-db-pod
spec:
  containers:
  - name: database
    image: postgres:13
    ports:
    - containerPort: 5432
    volumeMounts:
    - name: db-data
      mountPath: /var/lib/postgresql/data
  volumes:
  - name: db-data
    persistentVolumeClaim:
      claimName: my-db-pvc

This Pod definition is pretty standard, but the magic here is the PersistentVolumeClaim (PVC). The my-db-pvc isn’t just a directory on the node. It’s a request for storage that Kubernetes will fulfill by binding to a PersistentVolume (PV). This PV is the actual piece of storage – it could be an AWS EBS volume, an NFS share, or even a local disk on the node (though that’s less common for production).

When the pod starts, Kubernetes ensures that the PersistentVolume bound to the my-db-pvc is mounted at /var/lib/postgresql/data inside the container. If the pod is rescheduled to a different node, Kubernetes doesn’t just start a new pod. It ensures that the same PersistentVolume is attached to the new node and then mounts it into the new pod. The database data, therefore, persists across pod restarts and rescheduling.

The problem this solves is the ephemeral nature of containers. By default, any data written to a container’s filesystem is lost when the container dies or is deleted. Stateful applications, like databases, message queues, or key-value stores, absolutely require their data to survive these lifecycle events. Without persistent storage, these applications would be useless for anything beyond temporary caching.

Here’s how you’d set up the PVC and PV for that pod:

First, the PersistentVolumeClaim:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-db-pvc
spec:
  accessModes:
    - ReadWriteOnce # Can be mounted as read-write by a single node
  resources:
    requests:
      storage: 10Gi # Request 10 Gigabytes of storage

This PVC is a request. It says, "I need storage that can be mounted read-write by a single node, and I need 10 GiB." Kubernetes then looks for an available PersistentVolume that meets these criteria.

If you’re using dynamic provisioning (which is the most common and recommended approach), you’d have a StorageClass defined. A StorageClass tells Kubernetes how to provision storage on demand. For example, an AWS EBS StorageClass might look like this:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: aws-ebs-gp2
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp2 # General Purpose SSD
  fsType: ext4 # Filesystem type

When the my-db-pvc is created, Kubernetes sees it needs 10GiB with ReadWriteOnce access. If no existing PV matches, and if a StorageClass is specified (either explicitly in the PVC or as a default for the cluster), Kubernetes will instruct the provisioner (in this case, kubernetes.io/aws-ebs) to create a new EBS volume of 10GiB and automatically bind it to the PVC as a PersistentVolume.

The accessModes are crucial. ReadWriteOnce (RWO) is the most common for single-instance databases. ReadOnlyMany (ROX) is for when multiple pods need to read from the same volume (e.g., shared configuration or read-only datasets). ReadWriteMany (RWX) is for when multiple pods need to write to the same volume simultaneously, which usually requires network-attached storage like NFS or CephFS.

The real power isn’t just data persistence, but the abstraction it provides. You, as the application operator, don’t need to know where the EBS volume is physically located or how to attach it to a specific EC2 instance. Kubernetes handles the orchestration of attaching the underlying storage to the node where your pod is scheduled. If your pod is evicted or fails, Kubernetes will reschedule it to another node, and the storage will be re-attached to that new node. This makes your stateful applications highly available and resilient to node failures.

The one thing most people don’t realize is that PersistentVolumeClaims are namespace-scoped, but PersistentVolumes are cluster-scoped. This means a PVC in namespace-a can only bind to a PV that is available cluster-wide. If you have a PV that’s already bound to a PVC in namespace-b, the PVC in namespace-a cannot use it, even if it meets all the other criteria. This isolation is a key security and management feature, preventing accidental data sharing or contention between different applications or teams.

The next complexity you’ll run into is managing multiple replicas of a stateful application, which leads to the StatefulSets controller.

Want structured learning?

Take the full Storage course →