StorageStorage
Thuộc bộ tài liệu kiến thức theo Kubernetes Roadmap.
Tổng quan
Container vốn ephemeral theo thiết kế: khi một Pod chết, lớp container ghi được của nó chết theo. Điều đó ổn với service stateless, nhưng hệ thống thực cần giữ dữ liệu — database, message queue, file upload, cache, artifact build. Storage của Kubernetes là tập các lớp trừu tượng cho phép Pod dùng storage sống lâu hơn chính nó, có thể gắn vào bất kỳ node nào Pod đáp xuống, và được cấp phát theo yêu cầu từ cloud disk, network filesystem, hay media cục bộ. Hệ thống được thiết kế để người viết ứng dụng yêu cầu storage theo nhu cầu (“tôi cần 20Gi storage nhanh, read-write-once”) mà không cần biết backend nào đáp ứng.
Trọng tâm của thiết kế là tách bạch giữa đòi (claim) và cấp phát (provision). Một workload khai báo cái gì nó cần bằng một PersistentVolumeClaim (PVC); một StorageClass mô tả làm thế nào để tạo ra nó, và một CSI driver thực sự cấp phát một PersistentVolume (PV) — một cloud disk, một NFS export, bất cứ thứ gì. Sự tách rời này cho phép cùng một manifest chạy y nguyên trên AWS, GKE, on-prem, hay laptop: workload yêu cầu một lớp storage, và các driver được cấu hình của cluster đáp ứng nó. Hiểu chuỗi PVC → StorageClass → PV → CSI, cùng với access mode và reclaim policy, là thứ phân biệt “dữ liệu của tôi sống sót qua sự cố node” với “database của tôi mất rồi.”
Một mô hình tư duy
Storage trong Kubernetes là một chợ yêu cầu-đáp ứng. PVC là đơn đặt hàng (“20Gi, ReadWriteOnce, class=fast”) do team ứng dụng viết. StorageClass là mục trong catalog (“fast” = SSD do AWS EBS CSI driver cấp phát, với các tham số này). PV là hàng đã giao — một volume thật bound tới đúng một PVC. CSI driver là nhà cung cấp sản xuất và gắn nó. App không bao giờ gọi tên một disk cụ thể; nó gọi tên một yêu cầu, và cluster đáp ứng. Sự gián tiếp này chính là điểm cốt lõi: storage portable, self-service, cấp phát động. Cấp phát tĩnh (admin tạo sẵn PV bằng tay) vẫn tồn tại, nhưng cấp phát động qua StorageClass mới là chuẩn mực.
Kiến thức nền tảng
Ephemeral vs persistent storage
Quyết định đầu tiên là tuổi thọ. Kubernetes cung cấp nhiều tầng, từ ephemeral nhất tới ít nhất:
| Storage | Tuổi thọ | Sống sót qua | Dùng điển hình |
|---|---|---|---|
| Filesystem của container | Restart container hủy nó | Không gì | Scratch trong một container |
Volume emptyDir | Gắn với Pod | Restart container (không phải xóa Pod) | Scratch dùng chung giữa các container của Pod, cache |
Volume configMap / secret | Gắn với Pod | File config/secret được tiêm | Config và credential chỉ đọc |
| PersistentVolume (qua PVC) | Độc lập với mọi Pod | Xóa Pod, sự cố node, reschedule | Database, dữ liệu người dùng — bất cứ gì phải tồn tại lâu |
Quy tắc kinh nghiệm: nếu mất dữ liệu khi Pod bị xóa là chấp nhận được, dùng ephemeral volume; nếu không, bạn cần PersistentVolume. Đừng cố lưu dữ liệu thật bằng hostPath (nó buộc dữ liệu vào một node và là rủi ro bảo mật) — hãy dùng PVC.
Volume: lớp trừu tượng trong Pod
Một Volume là một thư mục, được backing bởi một media nào đó, mount vào một hay nhiều container của Pod tại một mountPath được chọn. Volume được khai báo trong spec.volumes của Pod và mount qua containers[].volumeMounts. Các loại volume phổ biến:
emptyDir— một thư mục rỗng tạo khi Pod được gán vào node và xóa khi Pod bị gỡ. Dùng chung bởi mọi container trong Pod; lý tưởng cho scratch space hoặc truyền dữ liệu giữa init container và container chính. Có thể backing bằng disk hoặc RAM (medium: Memory).configMap/secret— mount cấu hình hoặc dữ liệu nhạy cảm dưới dạng file. Cập nhật object nguồn lan tới file đã mount (cuối cùng), khác với env var vốn đóng băng lúc khởi động.hostPath— mount một file hoặc thư mục từ filesystem của node vào Pod. Nguy hiểm (buộc Pod vào một node, có thể thoát isolation); chỉ hợp lệ cho các agent hệ thống mức node (log collector, plugin CSI/CNI) trong DaemonSet — không bao giờ cho dữ liệu app.persistentVolumeClaim— mount một PV đã được một PVC đòi. Đây là cách gắn storage thật bền và portable, sẽ nói ở dưới.- projected — gộp nhiều nguồn (Secret, ConfigMap, token ServiceAccount, downward API) vào một thư mục.
apiVersion: v1
kind: Pod
metadata: { name: demo }
spec:
containers:
- name: app
image: nginx
volumeMounts:
- { name: cache, mountPath: /var/cache } # scratch ephemeral
- { name: config, mountPath: /etc/app, readOnly: true }
- { name: data, mountPath: /var/lib/app } # bền
volumes:
- name: cache
emptyDir: {}
- name: config
configMap: { name: app-config }
- name: data
persistentVolumeClaim: { claimName: app-data } # → PV → disk thật
Tam giác PV / PVC / StorageClass
Ba object phối hợp để cung cấp storage bền:
- PersistentVolume (PV) — một mảnh storage phạm vi cluster (một cloud disk, NFS share, v.v.). Nó có capacity, access mode, reclaim policy, và tham chiếu tới driver backing. PV là một resource, giống như Node.
- PersistentVolumeClaim (PVC) — một yêu cầu storage theo namespace: “tôi muốn XGi với các access mode này và StorageClass này.” Pod tham chiếu PVC, không phải PV.
- StorageClass — một template mô tả một loại storage: provisioner nào (CSI driver), tham số gì (loại disk, IOPS, filesystem, replication), reclaim policy, và binding mode. Đây là thứ khiến cấp phát động khả thi.
Vòng đời binding: một PVC được tạo → control plane tìm hoặc (động) cấp phát một PV khớp → chúng bind một-một → một Pod mount PVC được gắn PV vào node của nó. Cặp PVC/PV đã bound là độc quyền; PV không thể bound tới một claim khác.
- 01PVC "app-data"namespace: prod, yêu cầu: 20Gi, RWO, class: fast
- khớp02StorageClass "fast"provisioner: ebs.csi.aws.com, type: gp3
- cấp phát động03PV "pvc-8f3c…"20Gi, RWO, bound tới app-data
- backing bởi04AWS EBS volume vol-0abc…disk thật, gắn vào node của Pod
Cấp phát tĩnh vs động
- Tĩnh: một administrator tạo sẵn PV bằng tay (ví dụ trỏ tới các NFS export sẵn có). Một PVC rồi bind tới một PV có sẵn mà size/access mode/class thỏa mãn nó. Cồng kềnh và hiếm dùng cho cloud disk.
- Động (chuẩn mực): PVC gọi tên một StorageClass; khi không có PV khớp nào tồn tại, provisioner của class tạo một cái theo yêu cầu. Không cần hành động của admin, không cấp phát trước. Một cluster thường có một StorageClass mặc định (đánh dấu bằng annotation
storageclass.kubernetes.io/is-default-class: "true") dùng khi PVC bỏ trốngstorageClassName.
Khái niệm chính
Access mode
Access mode mô tả bao nhiêu node có thể mount một volume và ở chế độ nào. Chúng là thuộc tính của cả PV lẫn yêu cầu PVC, và được thực thi lúc attach:
| Mode | Viết tắt | Ý nghĩa |
|---|---|---|
| ReadWriteOnce | RWO | Mount read-write bởi một node duy nhất (nhiều Pod trên node đó dùng chung được) |
| ReadOnlyMany | ROX | Mount read-only bởi nhiều node |
| ReadWriteMany | RWX | Mount read-write bởi nhiều node đồng thời |
| ReadWriteOncePool | RWOP | Read-write bởi một Pod duy nhất (chặt hơn RWO; cần CSI hỗ trợ) |
Sắc thái then chốt: RWO là theo node, không phải theo Pod. Đa số block storage (AWS EBS, GCE PD, Azure Disk) là RWO — nó chỉ attach được vào một node tại một thời điểm, nên một Deployment 2 replica đáp xuống các node khác nhau sẽ để một Pod kẹt ContainerCreating chờ một volume không attach được. Để read-write đa node thực sự bạn cần RWX, thứ đòi một backend file/mạng (NFS, CephFS, AWS EFS, Azure Files, GlusterFS) — block volume không làm RWX được. Chọn topology của workload mà không khớp access mode với backend là lỗi storage phổ biến nhất.
Volume mode: Filesystem vs Block
volumeMode của một PV là Filesystem (mặc định) — volume được format với một filesystem và mount như một thư mục — hoặc Block — thiết bị block thô được phơi cho container tại một device path (/dev/xvda) không có filesystem. Block thô dành cho database và phần mềm storage tự quản định dạng trên đĩa và muốn bỏ qua tầng filesystem để tăng hiệu năng.
Reclaim policy
Khi một PVC bị xóa, persistentVolumeReclaimPolicy của PV quyết định số phận dữ liệu bên dưới:
Delete(mặc định cho cấp phát động) — PV và cloud volume backing bị xóa. Tiện, nhưng xóa một PVC hủy dữ liệu không cách nào khôi phục. Nguy hiểm cho bất cứ thứ gì có giá trị.Retain— PV và dữ liệu của nó được giữ lại sau khi PVC bị xóa; PV chuyển sangReleasedvà admin phải reclaim hoặc dọn thủ công. Đây là lựa chọn an toàn cho dữ liệu quan trọng.Recycle— đã deprecated; bỏ qua.
Hướng dẫn thực dụng: StorageClass production cho dữ liệu stateful nên dùng reclaimPolicy: Retain (hoặc chụp snapshot), để một kubectl delete pvc vô ý không xóa sạch database. Mặc định Delete là một cái bẫy cho dữ liệu thật.
Ví dụ thực tế: StorageClass + PVC
# StorageClass: SSD trên AWS, reclaim an toàn, mở rộng được, bind muộn
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: { name: fast }
provisioner: ebs.csi.aws.com
parameters:
type: gp3
iops: "5000"
fsType: ext4
reclaimPolicy: Retain # giữ dữ liệu nếu PVC bị xóa
allowVolumeExpansion: true # PVC của class này grow được
volumeBindingMode: WaitForFirstConsumer # cấp phát trong zone/node của Pod
---
# PVC: yêu cầu 20Gi, RWO, từ class "fast"
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: app-data, namespace: prod }
spec:
accessModes: [ReadWriteOnce]
storageClassName: fast
resources:
requests: { storage: 20Gi }
volumeBindingMode: WaitForFirstConsumer quan trọng và ít được dùng: với mặc định Immediate, một PV được cấp phát ngay khi PVC được tạo — có thể trong một zone mà Pod không bao giờ được schedule tới (một lỗi cross-AZ kinh điển trên cloud block storage). WaitForFirstConsumer hoãn cấp phát tới khi một Pod thực sự dùng PVC, nên volume được tạo trong đúng zone/node. Dùng nó cho block storage theo zone.
Container Storage Interface (CSI)
CSI là chuẩn interface plugin giữa Kubernetes và các hệ storage. Trước đây, storage driver (“in-tree volume plugin”) được biên dịch thẳng vào Kubernetes; mỗi backend mới đồng nghĩa với đổi code lõi. CSI tách toàn bộ đó thành các out-of-tree driver — thành phần độc lập (thường là một controller Deployment + một node DaemonSet) cài đặt provisioning, attach/detach, mount/unmount, snapshot, và expansion. Kubernetes đã gỡ các plugin in-tree cũ để ưu tiên CSI.
Thực tế: để dùng AWS EBS bạn cài aws-ebs-csi-driver; cho GCE PD là driver pd.csi.storage.gke.io; cho on-prem là ceph-csi, nfs-csi, democratic-csi, v.v. CSI driver tự đăng ký, provisioner của StorageClass gọi tên nó, và nó cũng mở khóa các tính năng nâng cao — volume snapshot, online volume expansion, topology awareness, và cloning — do driver cài đặt. Khi “PVC kẹt Pending,” một CSI driver thiếu hoặc không khỏe là nghi phạm hàng đầu.
StatefulSet và volumeClaimTemplates
Một Deployment chia sẻ một PVC cho mọi replica (hoặc, thường hơn, không thể — vì RWO block storage không attach vào nhiều node được). Workload stateful cần một volume riêng, ổn định cho mỗi replica. Đó chính xác là điều volumeClaimTemplates của StatefulSet cung cấp: cho mỗi Pod, controller StatefulSet tạo một PVC riêng (tên <template>-<statefulset>-<ordinal>, ví dụ data-postgres-0), và PVC đó đi theo Pod qua các lần reschedule.
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: postgres }
spec:
serviceName: postgres # headless Service cho DNS Pod ổn định
replicas: 3
selector: { matchLabels: { app: postgres } }
template:
metadata: { labels: { app: postgres } }
spec:
containers:
- name: postgres
image: postgres:16
ports: [{ containerPort: 5432 }]
volumeMounts:
- { name: data, mountPath: /var/lib/postgresql/data }
volumeClaimTemplates: # một PVC MỖI replica, tạo tự động
- metadata: { name: data }
spec:
accessModes: [ReadWriteOnce]
storageClassName: fast
resources: { requests: { storage: 50Gi } }
Điều này cho mỗi replica (postgres-0, postgres-1, postgres-2) một PVC 50Gi riêng tồn tại qua restart và reschedule, và — ghép với một headless Service — một tên DNS ổn định. Lưu ý một tính năng an toàn có chủ đích: xóa một StatefulSet không xóa các PVC của nó, nên scale down hay gỡ set sẽ không hủy dữ liệu trừ khi bạn xóa PVC một cách rõ ràng.
Volume snapshot
Các CSI driver hỗ trợ cho phép bạn chụp snapshot một PVC tại một thời điểm và khôi phục PVC mới từ chúng — nền tảng cho backup và workflow clone-để-test. Nó dùng ba object: một VolumeSnapshotClass (giống StorageClass, cho snapshot), một VolumeSnapshot (yêu cầu snapshot một PVC), và một VolumeSnapshotContent (resource snapshot thật).
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata: { name: pg-snap-2026-07-19, namespace: prod }
spec:
volumeSnapshotClassName: csi-snapclass
source:
persistentVolumeClaimName: data-postgres-0
---
# Khôi phục: một PVC mới có dataSource là snapshot
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: data-postgres-restore, namespace: prod }
spec:
accessModes: [ReadWriteOnce]
storageClassName: fast
resources: { requests: { storage: 50Gi } }
dataSource:
name: pg-snap-2026-07-19
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
Snapshot là crash-consistent, không phải application-consistent — với database, hãy quiesce/flush trước khi snapshot (hoặc dùng công cụ backup của chính DB). Chúng là một khối xây dựng, không phải giải pháp backup hoàn chỉnh; các công cụ như Velero dàn dựng snapshot cộng với storage ngoài cluster.
Mở rộng volume
Hết dung lượng không còn nghĩa là phải tạo lại volume. Nếu StorageClass đặt allowVolumeExpansion: true và CSI driver hỗ trợ, bạn grow một PVC bằng cách sửa spec.resources.requests.storage lên giá trị lớn hơn. CSI driver mở rộng volume backing và (với filesystem mode) mở rộng filesystem, thường online không downtime. Bạn chỉ grow được, không bao giờ shrink. Nếu expansion cần restart Pod, PVC hiện điều kiện FileSystemResizePending.
kubectl patch pvc app-data -n prod -p '{"spec":{"resources":{"requests":{"storage":"40Gi"}}}}'
kubectl get pvc app-data -n prod -w # theo dõi capacity cập nhật
Best Practices
- Dùng PVC cho bất cứ gì phải sống sót qua một Pod — không bao giờ
hostPathhay filesystem container.hostPathghim dữ liệu vào một node và là lỗ hổng bảo mật; lớp container bị hủy lúc restart. Dữ liệu bền thuộc về PersistentVolume. - Đặt
reclaimPolicy: Retain(hoặc snapshot đều đặn) cho dữ liệu có giá trị. Mặc định cấp-phát-độngDeletehủy volume backing khi PVC bị xóa; mộtkubectl delete pvcvô ý có thể xóa sạch database. Retain giữ dữ liệu cho khôi phục thủ công. - Khớp access mode với backend và topology. RWO block storage chỉ attach vào một node — read-write đa node đòi RWX trên backend file (EFS/NFS/CephFS). Yêu cầu RWX từ một class chỉ-block sẽ âm thầm mắc kẹt Pod.
- Ưu tiên binding
WaitForFirstConsumercho block storage theo zone. BindingImmediatecó thể cấp phát volume trong một zone Pod không tới được; hoãn tới khi scheduling đảm bảo volume nằm nơi Pod nằm. - Dùng StatefulSet với
volumeClaimTemplatescho app stateful. Mỗi replica có PVC ổn định riêng đi theo nó qua các lần reschedule — primitive đúng cho database và kho cluster, khác với việc chia sẻ một PVC cho cả Deployment. - Đặt
allowVolumeExpansion: truetrên StorageClass production. Nó cho phép grow volume online không phải tạo lại; retrofit khả năng mở rộng vào một class đã tồn tại sau khi hết chỗ đau đớn hơn nhiều. - Giữ secret khỏi volume thường và bật encryption at rest. Mount credential qua Secret volume (hoặc một external secrets operator), và bật encryption cho cả Secret trong etcd lẫn backend storage (CSI encryption / cloud KMS).
- Snapshot trước thao tác rủi ro và test khôi phục. Chụp VolumeSnapshot trước upgrade/migration; một backup bạn chưa từng khôi phục không phải là backup. Tự động hóa bằng Velero và diễn tập recovery.
- Right-size request và đặt ResourceQuota cho storage. Over-request lãng phí tiền vào cloud disk; dùng quota
requests.storagemỗi namespace để giới hạn tiêu thụ và ngăn cấp phát mất kiểm soát. - Chọn StorageClass có ý thức; định nghĩa một mặc định rõ ràng. Các class khác nhau (SSD vs HDD, replicated vs single, tầng IOPS) có chi phí/hiệu năng rất khác. Đừng dựa vào bất kỳ mặc định nào tình cờ tồn tại — đặt một cái có chủ đích và chọn theo từng workload.
- Hiểu vòng đời volume vs dữ liệu của StatefulSet. Xóa một StatefulSet để lại PVC theo thiết kế; PVC khi scale-down cũng được giữ. Dọn chúng có chủ đích, và đừng bao giờ giả định
kubectl delete statefulsetgiải phóng storage. - Đảm bảo CSI driver được cài, khỏe, và khớp version. PVC Pending và attach kẹt thường truy về một CSI controller hoặc node driver thiếu/hỏng; giám sát nó như bất kỳ thành phần hệ thống tới hạn nào và giữ tương thích với version Kubernetes.
- Dùng
emptyDir(với giới hạn kích thước) cho scratch, không cho nhu cầu bền. Nó nhanh và đơn giản cho cache và chia sẻ liên-container, nhưng chết theo Pod; đặtsizeLimitđể một tiến trình mất kiểm soát không lấp đầy disk của node. - Giám sát capacity volume và đặt alert. Một PVC đầy có thể crash hoặc corrupt database mà không tự khắc phục. Theo dõi mức dùng PV (qua metric
kubelet_volume_stats_*) và cảnh báo trước khi volume đầy. - Với database, ưu tiên local NVMe hoặc class IOPS cao và backup application-consistent. Network block storage thêm độ trễ; chọn tầng IOPS/throughput phù hợp, và dựa vào backup nhất quán của chính database thay vì chỉ snapshot crash-consistent thô.
Tài liệu tham khảo
- Kubernetes — Volumes
- Kubernetes — Persistent Volumes (PV, PVC, reclaim, mode)
- Kubernetes — Storage Classes
- Kubernetes — Dynamic Volume Provisioning
- Kubernetes — Volume Snapshots
- Kubernetes — Expanding Persistent Volumes
- Kubernetes — Ephemeral Volumes
- Kubernetes — StatefulSets
- Kubernetes CSI documentation
- AWS EBS CSI driver · GCP PD CSI · Ceph CSI
- Velero — backup và restore
- roadmap.sh — Kubernetes
Part of the Kubernetes Roadmap knowledge base.
Overview
Containers are ephemeral by design: when a Pod dies, its writable container layer dies with it. That is fine for stateless services, but real systems need to keep data — databases, message queues, uploaded files, caches, build artifacts. Kubernetes storage is the set of abstractions that let Pods consume storage that outlives them, that can be attached to whichever node a Pod lands on, and that is provisioned on demand from cloud disks, network filesystems, or local media. The system is designed so application authors ask for storage by requirement (“I need 20Gi of fast, read-write-once storage”) without knowing which backend fulfills it.
The heart of the design is a claim-and-provision separation of concerns. A workload declares what it needs with a PersistentVolumeClaim (PVC); a StorageClass describes how to make it, and a CSI driver does the actual provisioning of a PersistentVolume (PV) — a cloud disk, an NFS export, whatever. This decoupling is what lets the same manifest run unchanged on AWS, GKE, on-prem, or a laptop: the workload asks for a class of storage, and the cluster’s configured drivers satisfy it. Understanding the PVC → StorageClass → PV → CSI chain, plus access modes and reclaim policies, is what separates “my data survived a node failure” from “my database is gone.”
A mental model
Storage in Kubernetes is a request-fulfillment marketplace. The PVC is a purchase order (“20Gi, ReadWriteOnce, class=fast”) written by the app team. The StorageClass is a catalog entry (“fast” = SSD provisioned by the AWS EBS CSI driver, with these parameters). The PV is the delivered goods — an actual volume bound to exactly one PVC. The CSI driver is the supplier that manufactures and attaches it. The app never names a specific disk; it names a requirement, and the cluster fulfills it. This indirection is the whole point: portable, self-service, dynamically provisioned storage. Static provisioning (an admin pre-creating PVs by hand) still exists, but dynamic provisioning through StorageClasses is the norm.
Fundamentals
Ephemeral vs persistent storage
The first decision is lifetime. Kubernetes offers several tiers, from most to least ephemeral:
| Storage | Lifetime | Survives | Typical use |
|---|---|---|---|
| Container filesystem | Container restart destroys it | Nothing | Scratch inside one container |
emptyDir volume | Tied to the Pod | Container restart (not Pod deletion) | Shared scratch between a Pod’s containers, cache |
configMap / secret volume | Tied to the Pod | Injected config/secret files | Read-only config and credentials |
| PersistentVolume (via PVC) | Independent of any Pod | Pod deletion, node failure, rescheduling | Databases, user data — anything that must persist |
The rule of thumb: if losing the data when the Pod is deleted is acceptable, use an ephemeral volume; if not, you need a PersistentVolume. Do not attempt to persist real data with hostPath (it ties data to one node and is a security risk) — use a PVC.
Volumes: the in-Pod abstraction
A Volume is a directory, backed by some medium, mounted into one or more of a Pod’s containers at a chosen mountPath. Volumes are declared in the Pod spec.volumes and mounted via containers[].volumeMounts. Common volume types:
emptyDir— an empty directory created when the Pod is assigned to a node and deleted when the Pod is removed. Shared by all containers in the Pod; ideal for scratch space or passing data between an init container and the main container. Can be backed by disk or RAM (medium: Memory).configMap/secret— mount configuration or sensitive data as files. Updates to the source object propagate to the mounted files (eventually), unlike env vars which are frozen at start.hostPath— mounts a file or directory from the node’s filesystem into the Pod. Dangerous (ties the Pod to one node, can escape isolation); legitimate only for node-level system agents (log collectors, CSI/CNI plugins) in DaemonSets — never for app data.persistentVolumeClaim— mounts a PV that was claimed by a PVC. This is the durable, portable way to attach real storage, covered below.- projected — combines several sources (Secrets, ConfigMaps, the ServiceAccount token, downward API) into one directory.
apiVersion: v1
kind: Pod
metadata: { name: demo }
spec:
containers:
- name: app
image: nginx
volumeMounts:
- { name: cache, mountPath: /var/cache } # ephemeral scratch
- { name: config, mountPath: /etc/app, readOnly: true }
- { name: data, mountPath: /var/lib/app } # durable
volumes:
- name: cache
emptyDir: {}
- name: config
configMap: { name: app-config }
- name: data
persistentVolumeClaim: { claimName: app-data } # → PV → real disk
The PV / PVC / StorageClass triangle
Three objects cooperate to deliver durable storage:
- PersistentVolume (PV) — a cluster-scoped piece of storage (a cloud disk, NFS share, etc.). It has a capacity, access modes, a reclaim policy, and a reference to the backing driver. A PV is a resource, like a Node.
- PersistentVolumeClaim (PVC) — a namespaced request for storage: “I want XGi with these access modes and this StorageClass.” Pods reference PVCs, not PVs.
- StorageClass — a template describing a kind of storage: which provisioner (CSI driver), what parameters (disk type, IOPS, filesystem, replication), reclaim policy, and binding mode. It’s what makes dynamic provisioning possible.
The binding lifecycle: a PVC is created → the control plane finds or (dynamically) provisions a matching PV → they bind one-to-one → a Pod mounting the PVC gets the PV attached to its node. A bound PVC/PV pair is exclusive; the PV cannot be bound to another claim.
- 01PVC "app-data"namespace: prod, request: 20Gi, RWO, class: fast
- matches02StorageClass "fast"provisioner: ebs.csi.aws.com, type: gp3
- dynamically provisions03PV "pvc-8f3c…"20Gi, RWO, bound to app-data
- backed by04AWS EBS volume vol-0abc…real disk, attached to the Pod's node
Static vs dynamic provisioning
- Static: an administrator pre-creates PVs by hand (e.g., pointing at existing NFS exports). A PVC then binds to a pre-existing PV whose size/access modes/class satisfy it. Tedious and rarely used for cloud disks.
- Dynamic (the norm): the PVC names a StorageClass; when no matching PV exists, the class’s provisioner creates one on demand. No admin action, no pre-provisioning. A cluster typically has a default StorageClass (marked with the
storageclass.kubernetes.io/is-default-class: "true"annotation) used when a PVC omitsstorageClassName.
Key Concepts
Access modes
Access modes describe how many nodes can mount a volume and in what mode. They are a property of both the PV and the PVC request, and are enforced at attach time:
| Mode | Abbrev | Meaning |
|---|---|---|
| ReadWriteOnce | RWO | Mounted read-write by a single node (multiple Pods on that node can share it) |
| ReadOnlyMany | ROX | Mounted read-only by many nodes |
| ReadWriteMany | RWX | Mounted read-write by many nodes simultaneously |
| ReadWriteOncePool | RWOP | Read-write by a single Pod (stricter than RWO; requires CSI support) |
The crucial nuance: RWO is per-node, not per-Pod. Most block storage (AWS EBS, GCE PD, Azure Disk) is RWO — it can attach to only one node at a time, so a Deployment with 2 replicas landing on different nodes will leave one Pod stuck ContainerCreating waiting for a volume it can’t attach. For genuine multi-node shared read-write you need RWX, which requires a file/network backend (NFS, CephFS, AWS EFS, Azure Files, GlusterFS) — block volumes cannot do RWX. Choosing a workload topology without matching the access mode to the backend is the single most common storage mistake.
Volume modes: Filesystem vs Block
A PV’s volumeMode is either Filesystem (default) — the volume is formatted with a filesystem and mounted as a directory — or Block — the raw block device is exposed to the container at a device path (/dev/xvda) with no filesystem. Raw block is for databases and storage software that manage their own on-disk format and want to skip the filesystem layer for performance.
Reclaim policies
When a PVC is deleted, the PV’s persistentVolumeReclaimPolicy decides the fate of the underlying data:
Delete(default for dynamic provisioning) — the PV and the backing cloud volume are deleted. Convenient, but a deleted PVC destroys data irrecoverably. Dangerous for anything valuable.Retain— the PV and its data are kept after the PVC is deleted; the PV moves toReleasedand an admin must manually reclaim or clean it. This is the safe choice for important data.Recycle— deprecated; ignore it.
The practical guidance: production StorageClasses for stateful data should use reclaimPolicy: Retain (or take snapshots), so an accidental kubectl delete pvc doesn’t wipe your database. The default Delete is a footgun for real data.
Worked example: StorageClass + PVC
# StorageClass: SSD on AWS, safe reclaim, expandable, bind late
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: { name: fast }
provisioner: ebs.csi.aws.com
parameters:
type: gp3
iops: "5000"
fsType: ext4
reclaimPolicy: Retain # keep data if the PVC is deleted
allowVolumeExpansion: true # PVCs of this class can be grown
volumeBindingMode: WaitForFirstConsumer # provision in the Pod's zone/node
---
# PVC: request 20Gi, RWO, from the "fast" class
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: app-data, namespace: prod }
spec:
accessModes: [ReadWriteOnce]
storageClassName: fast
resources:
requests: { storage: 20Gi }
volumeBindingMode: WaitForFirstConsumer is important and underused: with the default Immediate, a PV is provisioned as soon as the PVC is created — possibly in a zone where the Pod can never be scheduled (a classic cross-AZ failure on cloud block storage). WaitForFirstConsumer delays provisioning until a Pod actually consumes the PVC, so the volume is created in the right zone/node. Use it for zonal block storage.
The Container Storage Interface (CSI)
CSI is the standard plugin interface between Kubernetes and storage systems. Historically, storage drivers (“in-tree volume plugins”) were compiled into Kubernetes itself; every new backend meant changing core code. CSI extracted all of that into out-of-tree drivers — independent components (typically a controller Deployment + a node DaemonSet) that implement provisioning, attach/detach, mount/unmount, snapshots, and expansion. Kubernetes has removed the old in-tree plugins in favor of CSI.
Practically: to use AWS EBS you install the aws-ebs-csi-driver; for GCE PD the pd.csi.storage.gke.io driver; for on-prem, ceph-csi, nfs-csi, democratic-csi, etc. The CSI driver registers itself, the StorageClass’s provisioner names it, and it also unlocks advanced features — volume snapshots, online volume expansion, topology awareness, and cloning — that the driver implements. When “PVCs are stuck Pending,” a missing or unhealthy CSI driver is a prime suspect.
StatefulSets and volumeClaimTemplates
A Deployment shares one PVC across all replicas (or, more often, can’t — because RWO block storage won’t attach to multiple nodes). Stateful workloads need one dedicated, stable volume per replica. That is exactly what a StatefulSet’s volumeClaimTemplates provides: for each Pod, the StatefulSet controller creates a separate PVC (named <template>-<statefulset>-<ordinal>, e.g., data-postgres-0), and that PVC follows the Pod across rescheduling.
apiVersion: apps/v1
kind: StatefulSet
metadata: { name: postgres }
spec:
serviceName: postgres # headless Service for stable Pod DNS
replicas: 3
selector: { matchLabels: { app: postgres } }
template:
metadata: { labels: { app: postgres } }
spec:
containers:
- name: postgres
image: postgres:16
ports: [{ containerPort: 5432 }]
volumeMounts:
- { name: data, mountPath: /var/lib/postgresql/data }
volumeClaimTemplates: # one PVC PER replica, created automatically
- metadata: { name: data }
spec:
accessModes: [ReadWriteOnce]
storageClassName: fast
resources: { requests: { storage: 50Gi } }
This gives each replica (postgres-0, postgres-1, postgres-2) its own 50Gi PVC that persists across restarts and reschedules, and — paired with a headless Service — a stable DNS name. Note a deliberate safety feature: deleting a StatefulSet does not delete its PVCs, so scaling down or removing the set won’t destroy data unless you delete the PVCs explicitly.
Volume snapshots
CSI drivers that support it let you take point-in-time snapshots of a PVC and restore new PVCs from them — the basis of backups and clone-for-test workflows. It uses three objects: a VolumeSnapshotClass (like a StorageClass, for snapshots), a VolumeSnapshot (the request to snapshot a PVC), and a VolumeSnapshotContent (the actual snapshot resource).
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata: { name: pg-snap-2026-07-19, namespace: prod }
spec:
volumeSnapshotClassName: csi-snapclass
source:
persistentVolumeClaimName: data-postgres-0
---
# Restore: a new PVC whose dataSource is the snapshot
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: data-postgres-restore, namespace: prod }
spec:
accessModes: [ReadWriteOnce]
storageClassName: fast
resources: { requests: { storage: 50Gi } }
dataSource:
name: pg-snap-2026-07-19
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
Snapshots are crash-consistent, not application-consistent — for databases, quiesce/flush before snapshotting (or use the DB’s own backup tooling). They are a building block, not a complete backup solution; tools like Velero orchestrate snapshotting plus off-cluster storage.
Expanding volumes
Running out of space no longer means recreating the volume. If the StorageClass sets allowVolumeExpansion: true and the CSI driver supports it, you grow a PVC by editing its spec.resources.requests.storage to a larger value. The CSI driver expands the backing volume and (for filesystem mode) the filesystem, often online with no downtime. You can only grow, never shrink. If expansion needs a Pod restart the PVC shows a FileSystemResizePending condition.
kubectl patch pvc app-data -n prod -p '{"spec":{"resources":{"requests":{"storage":"40Gi"}}}}'
kubectl get pvc app-data -n prod -w # watch capacity update
Best Practices
- Use PVCs for anything that must survive a Pod — never
hostPathor the container filesystem.hostPathpins data to one node and is a security hole; the container layer is destroyed on restart. Durable data belongs on a PersistentVolume. - Set
reclaimPolicy: Retain(or snapshot regularly) for valuable data. The dynamic-provisioning defaultDeletedestroys the backing volume when the PVC is deleted; one accidentalkubectl delete pvccan wipe a database. Retain keeps the data for manual recovery. - Match access mode to backend and topology. RWO block storage attaches to one node only — multi-node read-write requires RWX on a file backend (EFS/NFS/CephFS). Requesting RWX from a block-only class silently strands Pods.
- Prefer
WaitForFirstConsumerbinding for zonal block storage.Immediatebinding can provision a volume in a zone the Pod can’t reach; delaying until scheduling ensures the volume lands where the Pod does. - Use StatefulSets with
volumeClaimTemplatesfor stateful apps. Each replica gets its own stable PVC that follows it across reschedules — the correct primitive for databases and clustered stores, unlike sharing one PVC across a Deployment. - Set
allowVolumeExpansion: trueon production StorageClasses. It lets you grow volumes online without recreation; retrofitting expandability onto an existing class after you’re out of space is far more painful. - Keep secrets out of regular volumes and enable encryption at rest. Mount credentials via Secret volumes (or an external secrets operator), and turn on encryption for both etcd Secrets and the storage backend (CSI encryption / cloud KMS).
- Snapshot before risky operations and test restores. Take VolumeSnapshots before upgrades/migrations; a backup you have never restored is not a backup. Automate with Velero and rehearse recovery.
- Right-size requests and set ResourceQuotas on storage. Over-requesting wastes money on cloud disks; use
requests.storagequotas per namespace to cap consumption and prevent runaway provisioning. - Choose the StorageClass consciously; define an explicit default. Different classes (SSD vs HDD, replicated vs single, IOPS tiers) have very different cost/performance. Don’t rely on whichever default happens to exist — set one intentionally and pick per workload.
- Understand volume vs data lifecycle for StatefulSets. Deleting a StatefulSet leaves its PVCs behind by design; scale-down PVCs are also retained. Clean them up deliberately, and never assume
kubectl delete statefulsetfrees storage. - Ensure the CSI driver is installed, healthy, and version-matched. Pending PVCs and stuck attaches usually trace to a missing/broken CSI controller or node driver; monitor it like any critical system component and keep it compatible with your Kubernetes version.
- Use
emptyDir(with size limits) for scratch, not persistent needs. It’s fast and simple for caches and inter-container sharing, but it dies with the Pod; setsizeLimitso a runaway process can’t fill the node’s disk. - Monitor volume capacity and set alerts. A full PVC can crash or corrupt a database with no automatic remediation. Track PV usage (via
kubelet_volume_stats_*metrics) and alert before volumes fill. - For databases, prefer local NVMe or high-IOPS classes and app-consistent backups. Network block storage adds latency; pick an IOPS/throughput tier that fits, and rely on the database’s own consistent backup rather than raw crash-consistent snapshots alone.
References
- Kubernetes — Volumes
- Kubernetes — Persistent Volumes (PV, PVC, reclaim, modes)
- Kubernetes — Storage Classes
- Kubernetes — Dynamic Volume Provisioning
- Kubernetes — Volume Snapshots
- Kubernetes — Expanding Persistent Volumes
- Kubernetes — Ephemeral Volumes
- Kubernetes — StatefulSets
- Kubernetes CSI documentation
- AWS EBS CSI driver · GCP PD CSI · Ceph CSI
- Velero — backup and restore
- roadmap.sh — Kubernetes