1. Introduction

Containers are, by their essence, ephemeral. As a container orchestration framework, Kubernetes works within these bounds via isolated namespaces and temporary volumes. However, the system also builds on top of these concepts by providing persistent storage, available within a pod, a node, or even between nodes. Regardless of their application, persistent volumes have limited sizes. So, we might want to change their size. Although resizing a volume might not be an issue, applying the new size to pods that use it can be challenging.

In this tutorial, we explore persistent volumes, persistent volume claims, and how to resize a persistent volume (PV) and persistent volume claim (PVC) in Kubernetes. First, we go through the defining characteristics of a PV. Next, we create PV and PVC using definitions. After that, we confirm the status of both. Finally, we demonstrate ways to resize both PV and PVC.

We tested the code in this tutorial on Debian 12 (Bookworm) with GNU Bash 5.2.15. Unless otherwise specified, it should work in most POSIX-compliant environments.

2. Persistent Volume (PV)

Volumes are a way to attach storage to a Kubernetes object.

There are two main storage types within Kubernetes:

  • ephemeral
  • persistent

Naturally, persistent volumes preserve their contents between pod, service, and container restarts.

2.1. Types

There are currently only six non-deprecated types of persistent volumes:

  • csi: Container Storage Interface (CSI)
  • fc: Fibre Channel (FC) storage
  • hostPath: maps a given host node path
  • iscsi: iSCSI (SCSI over IP) storage
  • local: local node storage devices
  • nfs: Network FileSystem (NFS) storage

Each comes with specifics, but the idea behind Kubernetes is to partially hide these. One way to do so comes down to the concept of a Storage Class, i.e., a type definition that serves as a higher-level storage definition.

Yet, we can use a simple PV as well.

2.2. Provisioning and Usage

When it comes to persistent volumes, we provision them in two ways:

  • static: PV is an object, PVC finds and uses a specific PV
  • dynamic: PVC attempts to dynamically create a volume during runtime

Either way, a PVC is Bound to a PV. In the case of dynamic provisioning, the relationship is always between the same PV-PVC pair.

Once we have a resolved PVC, pods can get it assigned and use it.

2.3. Reclaim Policy

After a volume has served its purpose via an associated claim, Kubernetes can perform one of three actions:

  • Retain: consider PV Released, but prevent further claims, enabling manual intervention to inspect, free data, or make available
  • Delete: delete and wipe the PV
  • Recycle: wipe the PV and enable new claims

Effectively, Retain blocks further claims on the Released volume, instead forcing a new PV to be created for a particular storage medium. In fact, we can later change the status to Available.

2.4. Status

In general, a PV can be in one of several phases:

  • Available: free, not bound to PVC
  • Bound: bound to PVC
  • Released: PVC deleted, PV expects manual intervention
  • Failed: failed automatic reclamation

Usually, it’s easy to check the current state via the get subcommand of kubectl.

2.5. Access Modes

Volumes provide different access modes according to the way they can be mounted:

  • ReadWriteOnce (RWO): read-write by a single node
  • ReadOnlyMany (ROX): read-write by a single pod
  • ReadWriteMany (RWX): read-only by many nodes
  • ReadWriteOncePod (RWOP): read-write by many nodes

Still, we can only use one mode per mount and PVC.

3. Create Persistent Volume (PV)

To begin with, let’s create a persistent volume (PV):

$ kubectl apply --filename=<(echo '
apiVersion: v1
kind: PersistentVolume
metadata:
  name: hostpath-vol0
  namespace: default
spec:
  storageClassName: xclass
  capacity:
    storage: 6Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/mnt/hostpath-vol0-source-path"
    type: DirectoryOrCreate
')
persistentvolume/hostpath-vol0 created

In this case, we create a hostPath volume from the /mnt/hostpath-vol0-source-path/ path, which gets created if it doesn’t exist. Its storage class is xclass.

Importantly, the storage capacity is 6Gi and the access mode is ReadWriteOnce.

4. Create Persistent Volume Claim (PVC)

Although it can initially be a bit confusing, storage claims in Kubernetes are just storage requests. Such claims can be part of the dynamic pod definition or separate objects.

Let’s create a separate persistent volume claim (PVC) object:

$ kubectl apply --filename=<(echo '
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: hostpath-vol0-claim
  namespace: default
spec:
  storageClassName: xclass
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
')
persistentvolumeclaim/hostpath-vol0-claim created

By specifying a storageClassName of xclass, we link this PVC to the PV of the same class. If all other specifics match and there is enough space, they should connect.

Notably, claims don’t directly specify a PV. Instead, a PVC is like a filter with criteria for any existing PV that suits the needs. There are different algorithms for resolving multiple matches into one.

In addition, let’s create a PVC without a storage class name:

$ kubectl apply --filename=<(echo '
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pod0-claim
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi
')
persistentvolumeclaim/pod0-claim created

Although we didn’t create a volume to satisfy this claim beforehand, it should get Bound.

5. Check PV and PVC

At this point, we can check how the default namespace claims look via the get subcommand and the pvc type:

$ kubectl get pvc
NAME                  STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
hostpath-vol0-claim   Bound    hostpath-vol0                              6Gi        RWO            xclass         27m
pod0-claim            Bound    pvc-04ae85d9-6c3b-4f56-8ed0-7e666f965991   2Gi        RWO            standard       2m10s

The STATUS of both claims is Bound, meaning a satisfactory volume was found for each. Further, this VOLUME is hostpath-vol0 for hostpath-vol0-claim. On the other hand, pvc-04ae85d9-6c3b-4f56-8ed0-7e666f965991 is the assignment of pod0-claim.

Next, let’s check each PV:

$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                         STORAGECLASS   REASON   AGE
hostpath-vol0                              6Gi        RWO            Retain           Bound    default/hostpath-vol0-claim   xclass                  29m
pvc-04ae85d9-6c3b-4f56-8ed0-7e666f965991   2Gi        RWO            Delete           Bound    default/pod0-claim            standard                4m15s

As we can see, a new volume was created dynamically based on the second claim.

Should we no longer need either volume, hostpath-vol0 would be [Retain]ed, while pvc-04ae85d9-6c3b-4f56-8ed0-7e666f965991 should be [Delete]d.

6. Resize Existing Persistent Volume

Indeed, it’s fairly simple to resize a PV:

$ kubectl edit pv/hostpath-vol0
[...]
capacity:
  storage: 7Gi
[...]
persistentvolume/hostpath-vol0 edited

Thus, by changing the storage under capacity, we resize the persistent volume.

The same works for the generated PV:

$ kubectl edit pv/pvc-04ae85d9-6c3b-4f56-8ed0-7e666f965991
[...]
capacity:
  storage: 3Gi
[...]
persistentvolume/pvc-04ae85d9-6c3b-4f56-8ed0-7e666f965991 edited

This time, we successfully changed 2Gi to 3Gi.

However, even if we modify the PV capacity, each PVC remains the same.

7. Resize Existing Persistent Volume Claim

Since a PVC effectively functions as the storage for a given pod, we may sometimes want to expand it.

7.1. Direct Claim Resizing

Of course, we rarely want to create another claim or PV and move data around.

So, we might be tempted to directly modify the PVC dynamically:

$ kubectl edit pvc/hostpath-vol0-claim
[...]
resources:
  requests:
    storage: 2Gi
storageClassName: xclass
[...]
error: persistentvolumeclaims "hostpath-vol0-claim" could not be patched: persistentvolumeclaims "hostpath-vol0-claim" is forbidden: only dynamically provisioned pvc can be resized and the storageclass that provisions the pvc must support resize
You can run `kubectl replace -f /tmp/kubectl-edit-3476441128.yaml` to try this update again.

In this case, we attempt to change the storage of hostpath-vol0-claim from 1Gi to 2Gi. However, the edit fails with a hint.

Yet, we might be able to get around this limitation.

7.2. Change Volume Reclaim Policy

To work around the PVC resizing restraints, we can recreate the PVC with a new size.

However, to protect the underlying PV and its data from deletion, we first ensure the PV reclaim policy is Retain:

$ kubectl edit pv/hostpath-vol0
[...]
   persistentVolumeReclaimPolicy: Retain
[...]
persistentvolume/hostpath-vol0 edited

In particular, we can check and change persistentVolumeReclaimPolicy. Importantly, this can happen at the StorageClass level as well.

7.3. Delete Current Claim

At this point, because of the reclaim policy, we should be able to remove the PVC without losing information on the PV:

$ kubectl delete pvc/hostpath-vol0-claim
persistentvolumeclaim "hostpath-vol0-claim" deleted

Let’s verify the PV status:

$ kubectl get pv/hostpath-vol0
NAME            CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS     CLAIM                         STORAGECLASS   REASON   AGE
hostpath-vol0   7Gi        RWO            Retain           Released   default/hostpath-vol0-claim   xclass                  107m

As expected, hostpath-vol0 is now Released.

7.4. Recreate Claim With Changed Capacity

Now, we create a new resized claim:

$ kubectl apply --filename=<(echo '
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: hostpath-vol0-claim-resized
  namespace: default
spec:
  storageClassName: xclass
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 3Gi
')
persistentvolumeclaim/hostpath-vol0-claim-resized created

Let’s also verify the STATUS:

$ kubectl get pvc/hostpath-vol0-claim-resized
NAME                          STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
hostpath-vol0-claim-resized   Pending                                      xclass         43s

Notably, the new PVC is now live, but Pending. To get it Bound, we should make the original PV available.

7.5. Make Volume Available

To change the status of a PV, we can edit it:

$ kubectl edit pv/hostpath-vol0
[...]
claimRef: null
capacity: [...]
persistentvolume/hostpath-vol0 edited

The only change we make is to set claimRef to null and delete all lines that define the claimRef. Another way to do this change is a patch:

$ kubectl patch pv/hostpath-vol0 --patch='{"spec":{"claimRef": null}}'
persistentvolume/hostpath-vol0 patched

Either way, we should see the PVC is now Bound:

$ kubectl get pvc/hostpath-vol0-claim-resized
NAME                          STATUS   VOLUME          CAPACITY   ACCESS MODES   STORAGECLASS   AGE
hostpath-vol0-claim-resized   Bound    hostpath-vol0   7Gi        RWO            xclass         6m25s

As expected, hostpath-vol0 is the PV of the new PVC.

8. Summary

In this article, we explored persistent volumes, persistent volume claims, and ways to resize both.

In conclusion, although there isn’t a direct mechanism to resize a PVC, we can use the underlying PV with a new PVC and preserve the data in the process.