Skip to content

Kubernetes Storage Guide – SC, PV, PVC

Complete reference for CKA. Covers binding rules, selectors, node affinity, and mounting.


1. The Three Objects

StorageClass  →  defines HOW storage is provisioned
PersistentVolume (PV)  →  actual storage resource
PersistentVolumeClaim (PVC)  →  request for storage (used by Pods)

Flow:

Pod → PVC → PV → Actual Storage (disk/NFS/cloud)

    StorageClass (optional, defines provisioning)

2. How PVC Binds to PV (Decision Tree)

PVC Created


┌─────────────────────────────┐
│ Does PVC have `volumeName`? │
└─────────────────────────────┘
    │YES                  │NO
    ▼                     ▼
Bind to that       ┌─────────────────────────────┐
specific PV        │ Does PVC have `selector`?   │
                   └─────────────────────────────┘
                       │YES              │NO
                       ▼                 ▼
              Match PV by label    ┌─────────────────────────────┐
              + storageClass       │ Match by storageClassName   │
              + capacity           │ + capacity + accessModes    │
              + accessModes        └─────────────────────────────┘

3. Binding Rules – All Must Match

RulePVC FieldPV FieldNotes
StorageClassspec.storageClassNamespec.storageClassNameMust be identical (or both empty "")
Capacityspec.resources.requests.storagespec.capacity.storagePV capacity ≥ PVC request
Access Modesspec.accessModesspec.accessModesPV must include PVC's mode
Selectorspec.selector.matchLabelsmetadata.labelsPV labels must match (if selector used)
Volume Namespec.volumeNamemetadata.nameDirect binding (overrides all)

4. storageClassName Matching

PVC storageClassNamePV storageClassNameResult
manualmanual✅ Can bind
manualother❌ No bind
"" (empty string)"" (empty string)✅ Can bind
(not specified)anythingUses default SC
manual""❌ No bind

Important: Empty string "" is NOT the same as omitting the field!


5. Selector-Based Binding

When PVC has a selector, it only binds to PVs with matching labels:

PV:

yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-fast
  labels:
    type: ssd
    tier: premium
spec:
  capacity:
    storage: 100Mi
  accessModes: [ReadWriteOnce]
  storageClassName: ""
  hostPath:
    path: /mnt/fast

PVC:

yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-ssd
spec:
  accessModes: [ReadWriteOnce]
  storageClassName: ""
  resources:
    requests:
      storage: 50Mi
  selector:
    matchLabels:
      type: ssd

Result: PVC binds to pv-fast because labels match.


6. volumeName – Direct Binding

Force PVC to bind to a specific PV (ignores capacity/selector):

yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-direct
spec:
  accessModes: [ReadWriteOnce]
  storageClassName: ""
  volumeName: pv-specific  # Must match PV name exactly
  resources:
    requests:
      storage: 50Mi

Note: If PV doesn't exist or is already bound, PVC stays Pending.


7. volumeBindingMode

In StorageClass:

ModeWhen PVC BindsUse Case
ImmediateAs soon as PVC is createdCloud volumes, NFS
WaitForFirstConsumerWhen Pod using PVC is scheduledLocal volumes, node-specific storage

Why WaitForFirstConsumer matters:

  • For local PVs, scheduler needs to know which node has the storage
  • Binding happens AFTER Pod scheduling decision
  • Required for local volume type
yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

8. Node Affinity for Local Volumes

Local PVs must specify which node they're on:

yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-local
spec:
  capacity:
    storage: 100Mi
  accessModes: [ReadWriteOnce]
  storageClassName: local-storage
  local:
    path: /mnt/data
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - worker-node-1

Pod will only run on worker-node-1 because that's where the PV is.


9. Access Modes

ModeAbbreviationMeaning
ReadWriteOnceRWOSingle node, read-write
ReadOnlyManyROXMultiple nodes, read-only
ReadWriteManyRWXMultiple nodes, read-write

hostPath and local only support RWO!


10. Reclaim Policy

What happens to PV when PVC is deleted:

PolicyPV Status AfterDataUse Case
RetainReleasedPreservedProduction data
DeletePV deletedDeletedTemporary storage
RecycleAvailableWipedDeprecated

11. Mounting PVC in Pod

Basic mount:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  containers:
  - name: app
    image: nginx
    volumeMounts:
    - name: data
      mountPath: /var/data
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: my-pvc

Multiple mounts:

yaml
spec:
  containers:
  - name: app
    image: nginx
    volumeMounts:
    - name: config
      mountPath: /etc/app/config
      readOnly: true
    - name: data
      mountPath: /var/data
  volumes:
  - name: config
    persistentVolumeClaim:
      claimName: pvc-config
  - name: data
    persistentVolumeClaim:
      claimName: pvc-data

SubPath (mount specific directory):

yaml
volumeMounts:
- name: data
  mountPath: /app/logs
  subPath: logs

12. Complete Examples

Example 1: Static Provisioning (Manual PV)

yaml
# StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: manual
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: Immediate
---
# PV
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-manual
spec:
  capacity:
    storage: 1Gi
  accessModes: [ReadWriteOnce]
  storageClassName: manual
  hostPath:
    path: /mnt/data
---
# PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-manual
spec:
  accessModes: [ReadWriteOnce]
  storageClassName: manual
  resources:
    requests:
      storage: 500Mi
---
# Pod
apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  containers:
  - name: app
    image: busybox
    command: ['sleep', '3600']
    volumeMounts:
    - name: storage
      mountPath: /data
  volumes:
  - name: storage
    persistentVolumeClaim:
      claimName: pvc-manual

Example 2: Local Volume with Node Affinity

yaml
# StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
---
# PV
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-local
spec:
  capacity:
    storage: 500Mi
  accessModes: [ReadWriteOnce]
  storageClassName: local-storage
  local:
    path: /mnt/local
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - node01
---
# PVC (stays Pending until Pod is created)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-local
spec:
  accessModes: [ReadWriteOnce]
  storageClassName: local-storage
  resources:
    requests:
      storage: 200Mi

Example 3: Selector-Based Binding

yaml
# PV with labels
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-premium
  labels:
    tier: premium
    type: ssd
spec:
  capacity:
    storage: 100Mi
  accessModes: [ReadWriteOnce]
  storageClassName: ""
  hostPath:
    path: /mnt/premium
---
# PVC with selector
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-premium
spec:
  accessModes: [ReadWriteOnce]
  storageClassName: ""
  resources:
    requests:
      storage: 50Mi
  selector:
    matchLabels:
      tier: premium

13. Troubleshooting

PVC Stuck in Pending

bash
kubectl describe pvc <name>  # Check events
kubectl get pv               # Check available PVs
kubectl get sc               # Check StorageClasses

Common causes:

SymptomCauseFix
"no persistent volumes available"No matching PVCreate PV or check storageClassName
"storageclass not found"SC doesn't existCreate SC or fix PVC
"does not match"Capacity/accessMode mismatchCreate PV with correct specs

Pod FailedMount

bash
kubectl describe pod <name>  # Check events

Common causes:

  • PVC not bound yet
  • hostPath directory doesn't exist on node
  • Permission issues on host directory
  • Node affinity mismatch (local volumes)

14. Quick Commands

bash
# List all storage objects
kubectl get sc,pv,pvc

# Check PVC binding
kubectl get pvc -o wide

# Check PV with node info
kubectl get pv -o wide

# See which PVC a PV is bound to
kubectl describe pv <name> | grep -A2 "Claim"

# Check events
kubectl describe pvc <name>

# Create directory on node for hostPath
kubectl debug node/<node> -it --image=busybox -- mkdir -p /mnt/data

15. CKA Exam Tips

  1. Always check storageClassName matches between PVC and PV
  2. Use kubectl get pvc -o wide to see bound PV quickly
  3. For local volumes, always add volumeBindingMode: WaitForFirstConsumer
  4. Empty string "" is different from not specifying storageClassName
  5. hostPath requires directory to exist on the node
  6. Check events with kubectl describe when things don't bind

Released under the MIT License.