Skip to content
Menu

Virtualization6 min read

PV, PVC & StorageClass

Kubernetes persistent storage — abstracting infrastructure from applications

The PV/PVC Abstraction

PersistentVolume (PV)

A piece of storage provisioned by an admin or dynamically. Represents the actual storage — an NFS share, an EBS volume, a local disk, a Ceph RBD image. It is a cluster-level resource (not namespaced).

PersistentVolumeClaim (PVC)

A request for storage by a user or pod. "I need 10Gi of RWO storage." PVCs are namespaced — they live alongside the pods that use them.

Separation of Concerns

Infra Team
  • Provisions PVs (static) or configures StorageClasses (dynamic)
  • Manages underlying storage backends
  • Sets reclaim policies, access modes, capacity
App Team
  • Creates PVCs — "give me 10Gi of fast storage"
  • Doesn't care whether it's NFS, EBS, or Ceph
  • Mounts PVC into pod spec as a volume

A PVC binds to a matching PV based on: capacity, access mode, storage class, and optional label selectors.

The Storage Chain

  1. Pod

    Mounts a volume via volumes + volumeMounts

  2. PVC

    App team's request: "10Gi, RWO, fast-ssd"

  3. PV

    Infra team's provisioned storage resource

  4. Actual Storage

    Disk / NFS / EBS / Ceph RBD / cloud volume

Access Modes

Mode Abbreviation Description Typical Backend
ReadWriteOnce RWO Single node read-write Block storage (EBS, local disk)
ReadOnlyMany ROX Multiple nodes read-only NFS, CephFS
ReadWriteMany RWX Multiple nodes read-write NFS, CephFS, GlusterFS
ReadWriteOncePod RWOP Single pod read-write (K8s 1.27+) Block storage

Note

Note: Access modes are capabilities, not enforcement. RWO means the volume can be mounted read-write on one node — but if two pods land on the same node, both can mount it. RWOP closes that gap by restricting to a single pod.

Reclaim Policies

Policy Behavior Use Case
Retain PV kept after PVC deletion, data preserved, requires manual cleanup Production data — you don't want accidental deletion
Delete PV and underlying storage deleted when PVC is deleted Dynamic provisioning, dev/staging environments
Recycle (Deprecated) Runs rm -rf /thevolume/* on the volume Don't use — deprecated in favor of dynamic provisioning

Warning

Warning: With Delete reclaim policy, deleting a PVC also deletes the underlying storage (e.g., the EBS volume). This is permanent data loss. Use Retain for anything you care about.

StorageClass: Dynamic Provisioning

Instead of pre-creating PVs manually, define a StorageClass and let Kubernetes create PVs automatically when PVCs request storage from that class.

Static Provisioning
  • Admin creates PVs ahead of time
  • PVCs bind to existing PVs
  • Manual capacity planning
  • Admin must know what teams need in advance
Dynamic Provisioning
  • Admin creates StorageClass once
  • PVC triggers automatic PV creation
  • No pre-provisioning needed
  • Self-service for app teams

StorageClass YAML

yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: ebs.csi.aws.com        # CSI driver for AWS EBS
parameters:
  type: gp3                         # EBS volume type
  iops: "5000"                       # provisioned IOPS
reclaimPolicy: Delete                 # what happens when PVC is deleted
volumeBindingMode: WaitForFirstConsumer # delay provisioning until pod is scheduled

PVC Referencing a StorageClass

yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-data
spec:
  storageClassName: fast-ssd    # references the StorageClass above
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi

volumeBindingMode

Immediate

PV is provisioned as soon as PVC is created.

  • May provision in the wrong availability zone
  • Pod can't schedule if volume is in a different zone
  • Acceptable for zone-agnostic storage (NFS, CephFS)
WaitForFirstConsumer

PV provisioning is delayed until a pod using the PVC is scheduled.

  • Zone-aware: volume is created in the same zone as the node
  • Recommended for cloud block storage
  • Avoids zone mismatch entirely

Tip

Best practice: Always use WaitForFirstConsumer for cloud block storage (EBS, GCE PD, Azure Disk). Use Immediate only for storage that is accessible from any zone (NFS, CephFS).

Volume Types (Inline and PV-backed)

emptyDir

Temporary per-pod storage

  • Created when the pod starts, deleted when the pod is removed
  • All containers in a pod share it — useful for sidecar communication
  • medium: Memory backed by tmpfs (RAM) — fast but uses the pod's memory limit
  • Use cases: scratch space, cache, sharing files between init container and app container
yaml
volumes:
  - name: scratch
    emptyDir: {}              # disk-backed
  - name: cache
    emptyDir:
      medium: Memory         # tmpfs (RAM-backed)
      sizeLimit: 256Mi

hostPath

Mount a path from the node's filesystem

  • Mounts a file or directory from the host node into the pod
  • Types: Directory, File, Socket, CharDevice, BlockDevice
  • Data is node-specific — no portability, no replication
  • If pod moves to another node, the data is gone
yaml
volumes:
  - name: host-logs
    hostPath:
      path: /var/log
      type: Directory

Warning

Security risk: hostPath lets pods access any host file. A pod with hostPath: / has root-level access to the entire node filesystem. Avoid in production. Use PodSecurityStandards / OPA / Kyverno to restrict hostPath usage.

configMap / secret

Mount configuration and secrets as files

  • Mount ConfigMap or Secret data as files inside the container
  • Updates propagate eventually (kubelet sync period, ~1 minute by default)
  • subPath: mount a single key as a file at a specific path (but disables auto-update)
console
volumes:
  - name: config
    configMap:
      name: app-config
  - name: certs
    secret:
      secretName: tls-certs

# Using subPath to mount a single key as a file
volumeMounts:
  - name: config
    mountPath: /etc/app/config.yaml
    subPath: config.yaml          # mount only this key

Note

Note: When using subPath, config/secret updates will not be reflected in the mounted file. The pod must be restarted to pick up changes.

NFS

Network File System

  • Classic shared storage — the original "distributed filesystem"
  • Supports RWX — multiple pods on multiple nodes can read-write simultaneously
  • Good for legacy workloads, shared file access patterns
  • Performance can be a bottleneck for high-IOPS workloads
yaml
volumes:
  - name: shared-data
    nfs:
      server: nfs.example.com
      path: /exports/data

local

Real local disk (not hostPath)

  • Uses a real local disk or partition — not just a directory like hostPath
  • Requires node affinity — pod must schedule on the node with the disk
  • K8s handles the scheduling constraint automatically
  • Good for distributed databases that manage their own replication (Cassandra, CockroachDB, Elasticsearch)
yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv
spec:
  capacity:
    storage: 100Gi
  accessModes:
    - ReadWriteOnce
  local:
    path: /mnt/disks/ssd0
  nodeAffinity:                     # required for local volumes
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - worker-node-01

PV/PVC Lifecycle

  1. Provisioning
    Static: Admin manually creates a PV with a specific capacity, access mode, and backend.
    Dynamic: A StorageClass automatically creates a PV when a PVC is submitted.
  2. Binding
    PVC binds to a matching PV. The binding is one-to-one — a PV can only be bound to a single PVC. The control plane matches on capacity (must be >= requested), access modes, storage class, and selectors.
  3. Using
    Pod mounts the PVC as a volume. The kubelet calls the storage backend (or CSI driver) to mount the volume into the container's filesystem.
  4. Releasing
    When the PVC is deleted, the PV enters the Released state. The data still exists on the volume, but it cannot be bound to a new PVC until reclaimed.
  5. Reclaiming
    Based on the reclaim policy: Retain (manual cleanup), Delete (PV + underlying storage removed), or Recycle (deprecated).

Lifecycle State Diagram

Available
Bound
Released
Deleted / Retained

Note

Note: A Released PV with Retain policy cannot be automatically rebound. To reuse it, an admin must: (1) delete the PV object (keeping the data), (2) optionally clean up data on the storage backend, (3) create a new PV pointing to the same storage.

Quick Reference: Pod Volume Mount

yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
    - name: app
      image: my-app:latest
      volumeMounts:                # where to mount inside the container
        - name: data
          mountPath: /app/data
        - name: cache
          mountPath: /tmp/cache
  volumes:                         # define volumes at the pod level
    - name: data
      persistentVolumeClaim:
        claimName: my-data         # references a PVC
    - name: cache
      emptyDir:
        medium: Memory

Tip

Pattern: volumes[] defines what storage to use. volumeMounts[] defines where to mount it in the container. They are linked by name.

Solidnines — solidnines.com