K8s by Example: Persistent Volumes

PersistentVolumes (PVs) are cluster-wide storage resources provisioned by admins or dynamically. They abstract underlying storage (cloud disks, NFS, local). Pods claim storage via PersistentVolumeClaims without knowing details.

pv.yaml

PV defines a piece of storage in the cluster. capacity specifies size. accessModes controls how it can be mounted. persistentVolumeReclaimPolicy controls cleanup.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-data
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: standard
  hostPath:              # For testing only
    path: /data/pv-data
pv-access-modes.yaml

Access modes define how volumes can be mounted. RWO (ReadWriteOnce): single node read-write. ROX (ReadOnlyMany): multiple nodes read-only. RWX (ReadWriteMany): multiple nodes read-write. RWOP (ReadWriteOncePod, 1.22+): single Pod only. AWS EBS/Azure Disk: RWO only. NFS: RWO, ROX, RWX.

spec:
  accessModes:
    - ReadWriteOnce
pv-reclaim.yaml

Reclaim policies control what happens when a PVC is deleted. Retain: PV remains, data preserved, manual cleanup needed. Delete: PV and underlying storage deleted automatically. After PVC deletion with Retain, PV status changes to “Released” and an admin must delete the PV or remove claimRef to reuse it.

spec:
  persistentVolumeReclaimPolicy: Retain
pv-cloud.yaml

Cloud provider volumes use specific volume sources. First example: AWS EBS. Second example: GCE Persistent Disk. Third example: NFS. For production, use dynamic provisioning via StorageClass instead of manually creating PVs.

spec:
  capacity:
    storage: 100Gi
  accessModes: [ReadWriteOnce]
  awsElasticBlockStore:
    volumeID: vol-0123456789abcdef0
    fsType: ext4
---
spec:
  gcePersistentDisk:
    pdName: my-disk
    fsType: ext4
---
spec:
  nfs:
    server: nfs-server.example.com
    path: /exports/data
pv-local.yaml

Node affinity constrains which nodes can access the PV. Required for local volumes and some storage types. The PV can only be used by Pods scheduled to matching nodes.

spec:
  capacity:
    storage: 100Gi
  accessModes: [ReadWriteOnce]
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:
    path: /mnt/disks/ssd1
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - node-1
pv-block.yaml

Volume modes: Filesystem (default) mounts as a directory. Block exposes raw block device without filesystem. Second example shows Block volume in Pod using volumeDevices (not volumeMounts). Block mode is for databases that manage their own storage format.

spec:
  volumeMode: Filesystem
  capacity:
    storage: 10Gi
---
spec:
  containers:
    - name: app
      volumeDevices:
        - name: data
          devicePath: /dev/xvda
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: block-pvc
terminal

Debug PV issues by checking status and events. Common problems: wrong access mode, capacity mismatch, storage class mismatch, node affinity preventing binding.

$ kubectl get pv
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS
pv-data   10Gi       RWO            Retain           Available
pv-db     50Gi       RWO            Delete           Bound

$ kubectl describe pv pv-data

$ kubectl patch pv pv-data -p '{"spec":{"claimRef":null}}'
persistentvolume/pv-data patched

$ kubectl get pv pv-data -o yaml | grep -A10 status
status:
  phase: Available

$ kubectl get pvc --all-namespaces | grep Pending
default   db-data   Pending   <none>

$ kubectl get storageclass
NAME                 PROVISIONER
standard (default)   ebs.csi.aws.com

Index | GitHub | Use arrow keys to navigate |