Skip to main content
mastering ckad certified kubernetes application developer

PVCs, Access Modes, and Storage Classes

11 min read Chapter 42 of 87
Summary

Covers PersistentVolumeClaims as storage requests, access modes (RWO,...

Covers PersistentVolumeClaims as storage requests, access modes (RWO, ROX, RWX, RWOP), PVC YAML structure, StorageClass as a dynamic provisioning template, the Kind default StorageClass, mounting PVCs in Pods with complete YAML, and three hands-on exercises covering PVC creation, emptyDir sharing, and reclaim policy observation.

PVCs, Access Modes, and Storage Classes

PersistentVolumeClaims: Requesting Storage

A PersistentVolumeClaim (PVC) is a namespaced resource that represents a Pod’s request for storage. Think of the PV as a disk sitting in a storage pool and the PVC as a requisition form: “I need 2Gi of ReadWriteOnce storage from the standard class.”

When you create a PVC, Kubernetes searches for a PV that satisfies all of the claim’s requirements — capacity, access mode, and StorageClass. If a matching PV exists (static provisioning), Kubernetes binds them. If no PV matches but the requested StorageClass has a provisioner, Kubernetes creates a new PV dynamically.

The binding is one-to-one. A PV can be bound to exactly one PVC, and a PVC binds to exactly one PV. Once bound, the PVC stays bound until explicitly deleted.

PVC YAML Structure

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app-data
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi
  storageClassName: standard

Field breakdown:

  • accessModes: A list of access modes the PVC requires. The PV must support at least the requested modes. Most PVCs request a single mode.
  • resources.requests.storage: The minimum capacity. Kubernetes may bind the PVC to a larger PV if no PV with the exact capacity exists — a 2Gi request can bind to a 5Gi PV, but a 10Gi request cannot bind to a 5Gi PV.
  • storageClassName: Which StorageClass to use. If omitted, the cluster’s default StorageClass is used. If set to "" (empty string), dynamic provisioning is disabled, and only pre-existing PVs with no StorageClass will match.

Create the PVC imperatively (limited — you still need YAML for full control):

# No direct imperative command for PVCs, but you can generate a template:
kubectl apply -f - <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app-data
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi
  storageClassName: standard
EOF

Verify the PVC status:

kubectl get pvc app-data

Expected output (with dynamic provisioning):

NAME       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
app-data   Bound    pvc-3f4a9b7c-8d12-4e56-a789-1234567890ab   2Gi        RWO            standard       5s

The STATUS column shows Bound, and the VOLUME column shows the auto-generated PV name. If the PVC stays Pending, either no matching PV exists or the StorageClass provisioner is not functioning.

Access Modes in Detail

Access modes define how a volume can be mounted by nodes and Pods. They are specified both on PVs (what the storage supports) and PVCs (what the workload requires). A PVC can only bind to a PV whose access modes are a superset of the requested modes.

ReadWriteOnce (RWO)

The volume can be mounted as read-write by a single node. Multiple Pods on the same node can all mount the volume, but Pods on different nodes cannot. This is the most common mode for block storage devices (AWS EBS, GCP Persistent Disk, Azure Disk), which can physically attach to only one machine at a time.

When to use: Single-replica databases, application servers that write to local storage, any workload that runs on one node.

ReadOnlyMany (ROX)

The volume can be mounted as read-only by many nodes simultaneously. Any number of Pods on any number of nodes can read the data, but none can write. Useful for distributing static assets, trained ML models, or configuration bundles that many Pods consume.

When to use: Shared reference data, static content distribution, model serving across replicas.

ReadWriteMany (RWX)

The volume can be mounted as read-write by many nodes simultaneously. This requires a storage backend that supports concurrent writes — NFS servers, CephFS, Azure Files, or AWS EFS. Block storage (EBS, GCE PD) does not support RWX.

When to use: Shared file uploads, distributed logging, applications that need concurrent file access from multiple replicas.

ReadWriteOncePod (RWOP)

Introduced as stable in Kubernetes 1.29, this mode restricts the volume to be mounted as read-write by a single Pod across the entire cluster. Unlike RWO (which allows multiple Pods on the same node), RWOP guarantees exclusive access at the Pod level.

When to use: Write-ahead logs, databases that require exclusive file locks, any workload where concurrent writes from even co-located Pods could cause corruption.

CKAD relevance: Know all four modes and their abbreviations (RWO, ROX, RWX, RWOP). The exam may ask you to create a PVC with a specific access mode or identify which mode is appropriate for a given scenario.

Access Modes Comparison

ModeAbbreviationNodesPodsReadWrite
ReadWriteOnceRWO1Many (same node)YesYes
ReadOnlyManyROXManyManyYesNo
ReadWriteManyRWXManyManyYesYes
ReadWriteOncePodRWOP11YesYes

StorageClass: The Dynamic Provisioning Template

A StorageClass is a cluster-level resource that defines a “class” of storage. It tells Kubernetes: “When someone requests storage of this class, use this provisioner with these parameters to create a PV.”

A StorageClass has three essential fields:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-ssd
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
  • provisioner: The storage plugin responsible for creating volumes. Each storage backend has its own provisioner (e.g., kubernetes.io/gce-pd for Google Cloud, ebs.csi.aws.com for AWS, rancher.io/local-path for local Kind clusters).
  • parameters: Provisioner-specific settings. For GCE, type: pd-ssd requests SSD-backed disks. For AWS EBS, type: gp3 requests general-purpose SSDs.
  • reclaimPolicy: The default reclaim policy for PVs created by this class. Either Delete (default) or Retain.
  • volumeBindingMode: Controls when PV provisioning and binding happens. Immediate creates the PV as soon as the PVC is created. WaitForFirstConsumer delays provisioning until a Pod actually mounts the PVC — this ensures the PV is created in the same availability zone as the Pod.

The Default StorageClass

One StorageClass in a cluster can be marked as the default. Any PVC that omits storageClassName will use the default class. You can identify it with:

kubectl get sc

Output:

NAME                 PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      AGE
standard (default)   rancher.io/local-path   Delete          WaitForFirstConsumer   30d

The (default) annotation appears next to the class name. The default is controlled by the annotation storageclass.kubernetes.io/is-default-class: "true" on the StorageClass object:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: rancher.io/local-path
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer

Kind’s Default StorageClass

Kind ships with the standard StorageClass backed by rancher.io/local-path-provisioner. This provisioner creates PVs as directories on the host filesystem — the same machine running the Kind container. It is a single-node solution with no replication or redundancy, suitable for development and CKAD practice.

Key characteristics of Kind’s standard class:

  • Provisioner: rancher.io/local-path
  • Reclaim policy: Delete — PV and data are cleaned up when the PVC is deleted
  • Volume binding: WaitForFirstConsumer — PV creation is deferred until a Pod mounts the PVC
  • Access modes: Supports ReadWriteOnce only
  • Storage location: Directories under /opt/local-path-provisioner/ inside the Kind node container

Mounting a PVC in a Pod

Once a PVC is bound, Pods reference it through the volumes and volumeMounts fields. This is the same pattern used for ConfigMaps and Secrets — define the volume at the Pod level, then mount it in one or more containers.

Complete Pod + PVC Example

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: web-storage
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: standard
---
apiVersion: v1
kind: Pod
metadata:
  name: web-app
spec:
  containers:
    - name: nginx
      image: nginx:1.25
      volumeMounts:
        - name: html-volume
          mountPath: /usr/share/nginx/html
  volumes:
    - name: html-volume
      persistentVolumeClaim:
        claimName: web-storage

Save this as web-with-storage.yaml and apply it:

kubectl apply -f web-with-storage.yaml

Verify the PVC is bound and the Pod is running:

kubectl get pvc web-storage
kubectl get pod web-app

Write a file to the persistent volume:

kubectl exec web-app -- sh -c "echo '<h1>Persistent Storage Works</h1>' > /usr/share/nginx/html/index.html"

Test the web server:

kubectl exec web-app -- curl -s localhost

Output:

<h1>Persistent Storage Works</h1>

Delete and recreate the Pod (without deleting the PVC):

kubectl delete pod web-app
kubectl apply -f web-with-storage.yaml

Wait for the Pod to start, then verify the data survived:

kubectl exec web-app -- curl -s localhost

Output:

<h1>Persistent Storage Works</h1>

The data persists because the PVC (and the underlying PV) was not deleted. The new Pod mounts the same PVC, which is still bound to the same PV containing the written file.

Volume Mount Details

The connection between a Pod and a PVC happens in two places:

Pod spec → volumes: Declares that a volume named html-volume should come from the PVC web-storage:

volumes:
  - name: html-volume
    persistentVolumeClaim:
      claimName: web-storage

Container spec → volumeMounts: Declares that the volume named html-volume should appear at /usr/share/nginx/html inside the container:

volumeMounts:
  - name: html-volume
    mountPath: /usr/share/nginx/html

The name field is the glue. It must match between volumes[].name and volumeMounts[].name. A mismatch — even a single character — means the mount fails silently (the volume exists but the container does not see it at the expected path).

Optional fields for volumeMounts:

  • subPath: Mount a specific subdirectory or file within the volume instead of the root. Useful when multiple containers share one PVC but need different subdirectories.
  • readOnly: Set to true to mount the volume as read-only. Defaults to false.
volumeMounts:
  - name: html-volume
    mountPath: /usr/share/nginx/html
    subPath: web-content
    readOnly: false

PVC Lifecycle and Deletion

Understanding what happens when you delete a PVC is critical for the CKAD and for production operations:

  1. Kubernetes checks if any Pod is using the PVC. If a Pod has the PVC mounted, the PVC enters a Terminating state but is not actually deleted until the Pod releases it. This is called storage object in use protection.

  2. Once no Pod is using the PVC, it is deleted.

  3. The bound PV transitions to Released status.

  4. The reclaim policy determines the next step:

    • Delete: The PV and its backing storage are removed automatically.
    • Retain: The PV persists in Released status. The data remains, but no new PVC can bind to it until an administrator manually removes the claimRef or deletes and recreates the PV.

You can check the PV status after deleting a PVC:

kubectl get pv

A Released PV with Retain policy:

NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS     CLAIM              STORAGECLASS   AGE
pv-demo   5Gi        RWO            Retain           Released   default/app-data   manual         10m

Clean up:

kubectl delete pvc web-storage
kubectl delete pod web-app --ignore-not-found

Exercises

Work through these exercises on your Kind cluster. Complete YAML manifests and step-by-step solutions are provided in Chapter 15.

Exercise 1: Create a PVC and Mount It in a Pod

Create a PersistentVolumeClaim named exercise-pvc requesting 1Gi of storage with ReadWriteOnce access mode using the standard StorageClass. Create a Pod named storage-pod running busybox:1.36 with command sleep 3600. Mount the PVC at /data inside the container. Verify the PVC is bound, then write a file to /data/hello.txt containing “persistent storage works”. Delete the Pod, recreate it with the same PVC mount, and confirm the file still exists.

Exercise 2: Two Containers Sharing an emptyDir Volume

Create a Pod named sidecar-pod with two containers. The first container (writer) runs busybox:1.36 and executes sh -c "while true; do date >> /shared/timestamps.txt; sleep 5; done". The second container (reader) runs busybox:1.36 and executes tail -f /shared/timestamps.txt. Both containers must share an emptyDir volume. The writer container mounts it at /shared and the reader container mounts it at /shared. Verify that the reader container’s logs show the timestamps written by the writer.

Exercise 3: Reclaim Policy Observation — Retain vs Delete

Part A (Delete policy): Create a PVC named delete-test (1Gi, RWO, standard StorageClass). Record the dynamically created PV name with kubectl get pvc delete-test -o jsonpath='{.spec.volumeName}'. Delete the PVC. Check whether the PV still exists with kubectl get pv. Explain what happened.

Part B (Retain policy): Create a PV named retain-pv (1Gi, RWO, hostPath at /mnt/retain-data, StorageClass manual, reclaim policy Retain). Create a PVC named retain-test (1Gi, RWO, StorageClass manual). Verify binding. Delete the PVC. Check the PV status — it should show Released. Explain why the PV is not Available and what an administrator must do to reuse it.