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
| Rule | PVC Field | PV Field | Notes |
|---|---|---|---|
| StorageClass | spec.storageClassName | spec.storageClassName | Must be identical (or both empty "") |
| Capacity | spec.resources.requests.storage | spec.capacity.storage | PV capacity ≥ PVC request |
| Access Modes | spec.accessModes | spec.accessModes | PV must include PVC's mode |
| Selector | spec.selector.matchLabels | metadata.labels | PV labels must match (if selector used) |
| Volume Name | spec.volumeName | metadata.name | Direct binding (overrides all) |
4. storageClassName Matching
| PVC storageClassName | PV storageClassName | Result |
|---|---|---|
manual | manual | ✅ Can bind |
manual | other | ❌ No bind |
"" (empty string) | "" (empty string) | ✅ Can bind |
| (not specified) | anything | Uses 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/fastPVC:
yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-ssd
spec:
accessModes: [ReadWriteOnce]
storageClassName: ""
resources:
requests:
storage: 50Mi
selector:
matchLabels:
type: ssdResult: 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: 50MiNote: If PV doesn't exist or is already bound, PVC stays Pending.
7. volumeBindingMode
In StorageClass:
| Mode | When PVC Binds | Use Case |
|---|---|---|
Immediate | As soon as PVC is created | Cloud volumes, NFS |
WaitForFirstConsumer | When Pod using PVC is scheduled | Local 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
localvolume type
yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer8. 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-1Pod will only run on worker-node-1 because that's where the PV is.
9. Access Modes
| Mode | Abbreviation | Meaning |
|---|---|---|
| ReadWriteOnce | RWO | Single node, read-write |
| ReadOnlyMany | ROX | Multiple nodes, read-only |
| ReadWriteMany | RWX | Multiple nodes, read-write |
hostPath and local only support RWO!
10. Reclaim Policy
What happens to PV when PVC is deleted:
| Policy | PV Status After | Data | Use Case |
|---|---|---|---|
Retain | Released | Preserved | Production data |
Delete | PV deleted | Deleted | Temporary storage |
Recycle | Available | Wiped | Deprecated |
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-pvcMultiple 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-dataSubPath (mount specific directory):
yaml
volumeMounts:
- name: data
mountPath: /app/logs
subPath: logs12. 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-manualExample 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: 200MiExample 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: premium13. Troubleshooting
PVC Stuck in Pending
bash
kubectl describe pvc <name> # Check events
kubectl get pv # Check available PVs
kubectl get sc # Check StorageClassesCommon causes:
| Symptom | Cause | Fix |
|---|---|---|
| "no persistent volumes available" | No matching PV | Create PV or check storageClassName |
| "storageclass not found" | SC doesn't exist | Create SC or fix PVC |
| "does not match" | Capacity/accessMode mismatch | Create PV with correct specs |
Pod FailedMount
bash
kubectl describe pod <name> # Check eventsCommon 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/data15. CKA Exam Tips
- Always check storageClassName matches between PVC and PV
- Use
kubectl get pvc -o wideto see bound PV quickly - For local volumes, always add
volumeBindingMode: WaitForFirstConsumer - Empty string
""is different from not specifying storageClassName - hostPath requires directory to exist on the node
- Check events with
kubectl describewhen things don't bind