Skip to main content

8. Storage

Mục lục


1. Giới thiệu phần Storage

Xoá nhầm một Pod đang chạy MySQL, và toàn bộ dữ liệu khách hàng vừa nhập vào biến mất — không dấu vết, không thùng rác nào để khôi phục. Đây không phải tình huống hiếm gặp: theo thiết kế mặc định, Pod trong Kubernetes hoàn toàn transient — dữ liệu bên trong nó sống và chết cùng Pod. Storage là mảng kiến thức trả lời đúng câu hỏi đó: làm sao để dữ liệu tồn tại lâu hơn vòng đời của Pod đã tạo ra nó?

Phần này đi qua ba câu hỏi cốt lõi. Thứ nhất, storage hoạt động ra sao bên dưới một container runtime như Docker, vì mọi khái niệm storage của Kubernetes đều xây trên nền đó. Thứ hai, Kubernetes trừu tượng hoá storage bên ngoài (cloud disk, NFS, SAN,...) bằng cơ chế nào để không phải sửa core code mỗi khi có thêm một vendor mới. Thứ ba, làm sao tách vai trò "ai tạo storage" (administrator) khỏi vai trò "ai dùng storage" (application) thông qua Persistent Volume và Persistent Volume Claim — đúng mô hình sẽ gặp lại trong hầu hết cluster production thực tế.

Có rất nhiều giải pháp storage khác nhau ngoài kia, và tuỳ môi trường, các lựa chọn có thể khác nhau — xem xét toàn bộ các lựa chọn đó nằm ngoài phạm vi tài liệu này. Trọng tâm ở đây là phía Kubernetes của storage: một khi nắm được cơ chế đó, hoàn toàn có thể áp dụng cho bất kỳ giải pháp storage bên thứ ba nào khác.


2. Storage trong Kubernetes: Tổng quan qua Storage Docker

Vì sao một tài liệu về Kubernetes storage lại mở đầu bằng Docker? Vì gần như toàn bộ thuật ngữ storage trong Kubernetes — volume, mount, driver — đều được định nghĩa lại từ đúng những khái niệm Docker đã có trước. Nắm vững cách storage hoạt động ở tầng Docker trước, mọi thứ ở tầng Kubernetes phía sau sẽ chỉ là "thêm một lớp quản lý tập trung", không phải kiến thức hoàn toàn mới.

Có hai khái niệm cần phân biệt ngay từ đầu: storage driver — quản lý cách Docker lưu image và layer của container, và volume driver plugin — quản lý nơi dữ liệu được persist. Hai khái niệm này độc lập nhau, dễ nhầm lẫn nếu không tách rõ ngay từ lúc học. Phần 3 bàn về storage driver, phần 4 bàn về volume.


3. Docker Storage Drivers và File Systems

Khi cài Docker trên một hệ thống, nó tạo cấu trúc thư mục tại /var/lib/docker, với nhiều thư mục con bên dưới:

/var/lib/docker/
containers/
image/
overlay2/
volumes/

Đây là nơi Docker lưu toàn bộ dữ liệu của nó theo mặc định. Mọi file liên quan tới container nằm dưới containers, dữ liệu image nằm dưới image, mọi volume do container tạo ra nằm dưới volumes (bàn kỹ ở phần 4). Thư mục thứ tư — tên thay đổi tuỳ storage driver Docker đang dùng (overlay2, btrfs, vfs,...) — là nơi diễn ra toàn bộ cơ chế của phần này: kiến trúc layer của image.

Kiến trúc layered (nhiều lớp) của Docker image. Khi Docker build image, nó build theo kiến trúc nhiều lớp. Mỗi dòng lệnh trong Dockerfile tạo ra một layer mới, chỉ chứa phần thay đổi so với layer trước đó — không copy lại toàn bộ. Ví dụ: layer đầu tiên là base Ubuntu distribution, layer thứ hai cài các gói apt, layer thứ ba cài các gói Python, layer thứ tư copy source code, layer cuối cùng set ENTRYPOINT.

Vì mỗi layer chỉ lưu phần thay đổi, dung lượng cũng phản ánh đúng điều đó: base Ubuntu image khoảng 120 MB, layer cài gói apt cộng thêm khoảng 300 MB, các layer còn lại nhỏ hơn nhiều.

💡 Hình dung: một cuốn giáo trình photocopy dùng chung cho cả lớp chính là image layer — không ai được viết trực tiếp lên đó. Ai muốn ghi chú riêng thì dán một tờ giấy nhớ đè lên đúng trang cần sửa, chứ không đục thủng trang gốc. Tờ giấy nhớ đó là container layer: viết gì lên cũng được, nhưng xé tờ giấy đó đi (xoá container) thì mọi ghi chú trên đó mất theo, còn giáo trình gốc vẫn nguyên vẹn cho người mượn tiếp theo.

Lợi ích thực tế của cách chia layer này lộ rõ khi build image thứ hai có Dockerfile tương tự — cùng base Ubuntu, cùng dependency Python và Flask, nhưng khác source code và ENTRYPOINT. Khi chạy docker build, vì ba layer đầu giống hệt, Docker tái sử dụng chúng từ cache và chỉ build hai layer cuối. Điều này cũng áp dụng khi cập nhật app.py: Docker tái sử dụng mọi layer trước đó, chỉ rebuild nhanh layer chứa source code mới — tiết kiệm đáng kể thời gian mỗi lần deploy.

Có thể nhìn thấy các layer này bằng lệnh docker history:

docker history myapp:latest

# Output minh họa
IMAGE CREATED CREATED BY SIZE
a1b2c3d4e5f6 3 minutes ago ENTRYPOINT ["python3" "app.py"] 0B
f6e5d4c3b2a1 3 minutes ago COPY app.py /app/app.py 4.2kB
9d8c7b6a5e4f 5 minutes ago RUN pip install flask 12.4MB
3c2b1a0f9e8d 2 weeks ago RUN apt-get install -y python3 287MB
0f1e2d3c4b5a 3 weeks ago /bin/sh -c #(nop) ADD ubuntu.tar 120MB

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Mỗi dòng là một layer, từ mới nhất (trên cùng) xuống base image (dưới cùng). Cột SIZE cho thấy đúng điều vừa giải thích: layer copy app.py chỉ nặng vài KB dù nằm trên một image hàng trăm MB, vì nó không copy lại toàn bộ image — chỉ lưu phần khác biệt.

Xếp chồng layer. Toàn bộ layer này — từ Ubuntu base tới ENTRYPOINT — do docker build tạo ra và là read-only. Một khi build xong, không có cách nào sửa nội dung bên trong; muốn thay đổi phải build lại từ đầu.

Chạy container từ image bằng docker run sẽ tạo thêm một writable layer mới trên cùng — gọi là container layer. Layer này lưu mọi dữ liệu do container sinh ra: log file application ghi, file tạm, hay file bị sửa khi ai đó login vào container. Layer chỉ tồn tại chừng nào container còn sống; xoá container là xoá luôn layer này và mọi thứ trong đó. Điều quan trọng: cùng một image layer được chia sẻ giữa mọi container tạo ra từ image đó — mười container cùng chạy từ một image không có nghĩa là dung lượng image bị nhân mười lần.

Docker image: layer read-only + container layer ghi đượcContainer layer (read-write)temp.txt, log, app.py (copy)ENTRYPOINTapp.py (source code)Python dependenciesapt packages (~300MB)Ubuntu base image (~120MB)Image layers(read-only)Container layer(read-write)copy-on-writedocker build tạo các layer read-only bên dưới.docker run thêm container layer (writable) — nơi copy-on-write xảy ra.

Copy-on-write. File trong image layer là read-only — nhưng nếu login vào container và sửa app.py để test một thay đổi, lệnh sửa vẫn chạy được bình thường. Bí mật nằm ở chỗ: trước khi ghi thay đổi, Docker tự động copy file đó từ image layer lên container layer, rồi mọi thay đổi tiếp theo diễn ra trên bản copy đó. Đây là cơ chế copy-on-write — image layer "read-only" không có nghĩa file trong đó bất biến tuyệt đối với mọi container, chỉ có nghĩa chính bản gốc trong image sẽ không bao giờ bị sửa, dù có sửa bao nhiêu bản copy ở container layer.

Hệ quả trực tiếp: xoá container là mất luôn app.py đã sửa và mọi file tạo mới như temp.txt, vì tất cả nằm trên container layer. Nếu cần giữ lại dữ liệu này — ví dụ dữ liệu của một database — phải gắn thêm một volume vào container. Đó là chủ đề của phần 4.

Storage driver. Ai chịu trách nhiệm cho toàn bộ cơ chế này — duy trì kiến trúc layer, tạo container layer, thực hiện copy-on-write? Đó là storage driver. Một số storage driver phổ biến: overlay2, btrfs, vfs, device mapper, và các driver cũ hơn không còn dùng nữa là aufs và bản overlay gốc (tiền thân của overlay2).

Docker tự động chọn storage driver phù hợp dựa trên OS bên dưới. Trên hầu hết bản phân phối Linux hiện đại — bao gồm Ubuntu — mặc định hiện nay là overlay2: ổn định, không cần cấu hình thêm, và là driver được khuyến nghị cho mọi distro còn được hỗ trợ chính thức. aufs — driver từng phổ biến trên Ubuntu ở các phiên bản Docker cũ hơn nhiều — đã bị deprecate và gỡ bỏ hoàn toàn khỏi Docker Engine (chi tiết phiên bản ở ghi chú nguồn cuối bài). Với các bản Docker Engine dùng containerd image store làm mặc định, tầng lưu trữ thực tế còn chuyển hẳn sang snapshotter overlayfs của containerd — về bản chất vẫn cùng công nghệ overlay filesystem, chỉ khác tầng quản lý.


4. Volumes trong Docker

Vừa thấy: xoá container là mất mọi dữ liệu trên container layer. Để dữ liệu sống lâu hơn container, cần tách nó ra khỏi container layer và đặt vào một volume — một vùng lưu trữ nằm ngoài vòng đời của bất kỳ container cụ thể nào.

Tạo và mount volume. Cách đơn giản nhất: tạo volume trước bằng docker volume create.

docker volume create data_volume

Lệnh này tạo một thư mục tên data_volume dưới /var/lib/docker/volumes. Khi chạy container, mount volume này vào bằng option -v.

docker run -v data_volume:/var/lib/mysql mysql

Cú pháp là <tên volume>:<đường dẫn trong container> — ở đây MySQL ghi dữ liệu vào /var/lib/mysql. Mọi dữ liệu database từ giờ nằm trên volume ở Docker host; xoá container, dữ liệu vẫn còn.

Nếu bỏ qua bước docker volume create và chạy thẳng với một tên volume chưa từng tồn tại, Docker tự tạo volume đó giúp.

docker run -v data_volume2:/var/lib/mysql mysql

Cách này — mount một volume nằm dưới /var/lib/docker/volumes — gọi là volume mounting.

Nhưng nếu dữ liệu cần lưu đã có sẵn ở một vị trí khác trên host, ví dụ /data, thay vì để Docker tự quản lý thư mục, có thể trỏ thẳng tới đường dẫn đó.

docker run -v /data:/var/lib/mysql mysql

Đây gọi là bind mounting — khác volume mounting ở chỗ thư mục nguồn có thể nằm ở bất kỳ đâu trên host, không bắt buộc dưới /var/lib/docker/volumes.

docker run -v: Volume Mount vs Bind MountVolume mountdocker volume createdata_volumeContainer: /var/lib/mysqlDocker tự tạo, tự quản lý thư mục nguồndưới /var/lib/docker/volumes(không cần biết đường dẫn thật trên host)VSBind mount/data(đường dẫn admin tự chọn)Container: /var/lib/mysqlNgười dùng tự chỉ định full pathcó thể nằm ở bất kỳ đâu trên host(hợp khi dữ liệu đã có sẵn ở vị trí cụ thể)

💡 Hình dung: volume mount giống gửi đồ ở tủ khóa của trung tâm thương mại — không cần biết tủ nằm ở dãy nào, cứ đưa mã vé (tên volume) là lấy đúng đồ, mọi việc quản lý vị trí do trung tâm lo. Bind mount giống mang ổ khóa riêng ra khoá thẳng vào một ngăn tủ cụ thể tự chỉ định trong kho — tiện khi đã biết chính xác vị trí cần dùng, nhưng phải tự nhớ và tự quản lý địa chỉ đó.

Cú pháp mới: --mount. Dùng -v là cách viết cũ, ngắn gọn nhưng gộp nhiều tham số vào một chuỗi khó đọc. Cách được ưu tiên hiện nay là --mount, tường minh hơn vì tách rõ từng tham số dạng key=value.

docker run --mount type=bind,source=/data,target=/var/lib/mysql mysql

typebind hoặc volume, source là vị trí trên host, target là vị trí trong container.

Ai quản lý volume? Volume không do storage driver quản lý (storage driver chỉ lo image layer và container layer đã bàn ở phần 3) — volume do volume driver plugin quản lý, một cơ chế pluggable hoàn toàn tách biệt.

Volume driver plugin mặc định là local — tạo volume ngay trên Docker host, lưu dưới /var/lib/docker/volumes. Có nhiều volume driver khác cho phép provision volume trên các giải pháp bên thứ ba: Azure File Storage, Convoy, DigitalOcean Block Storage, Flocker, Google Compute Persistent Disks, GlusterFS, NetApp, RexRay, Portworx, VMware vSphere Storage — chỉ là một vài ví dụ trong số rất nhiều lựa chọn.

Một số volume driver hỗ trợ nhiều storage provider cùng lúc. Ví dụ, RexRay có thể provision storage trên AWS EBS, S3, các mảng storage EMC (Isilon, ScaleIO), Google Persistent Disks, hoặc OpenStack Cinder. Chọn dùng RexRay EBS driver khi chạy container nghĩa là tạo container và attach thẳng một volume từ AWS — khi container thoát, dữ liệu vẫn an toàn trên cloud.

Đây chính là ý tưởng nền tảng mà Kubernetes sẽ mở rộng ở quy mô cluster: tách vai trò "nơi lưu dữ liệu thật sự" ra khỏi vòng đời container, và cho phép nhiều backend storage khác nhau cùng cắm vào qua một cơ chế pluggable. Phần tiếp theo xem cách Kubernetes chuẩn hoá chính ý tưởng pluggable này thành một interface chung — không chỉ cho storage, mà cho cả runtime và networking.


5. Container Storage Interface (CSI)

Docker cho phép cắm nhiều volume driver khác nhau (local, RexRay, Portworx,...) — nhưng đó là cơ chế riêng của Docker. Kubernetes cần một cơ chế tương tự ở quy mô lớn hơn, và không chỉ cho storage.

Trước đây, Kubernetes chỉ hỗ trợ containerd làm container runtime, với toàn bộ code tích hợp nhúng thẳng trong Kubernetes source code. Khi các runtime khác xuất hiện — rkt, CRI-O — việc mở rộng hỗ trợ mà không phải sửa Kubernetes core mỗi lần trở nên cấp thiết. Đó là lý do ra đời Container Runtime Interface (CRI): một chuẩn định nghĩa cách Kubernetes giao tiếp với bất kỳ container runtime nào. Runtime mới chỉ cần tuân theo chuẩn CRI, không cần đụng vào Kubernetes source code hay làm việc trực tiếp với đội ngũ phát triển Kubernetes.

Tương tự với networking: Container Network Interface (CNI) ra đời để bất kỳ networking vendor nào cũng tự viết plugin theo chuẩn, mà Kubernetes core không cần biết chi tiết bên trong từng giải pháp.

Và với storage: Container Storage Interface (CSI) ra đời theo đúng khuôn mẫu đó. Portworx, Amazon EBS, Azure Disk, Dell EMC (Isilon, PowerMax, Unity, XtremIO), NetApp, Nutanix, HPE, Hitachi, Pure Storage — mỗi bên tự viết CSI driver riêng, tuân theo cùng một chuẩn.

CRI, CNI, CSI: cùng một khuôn mẫu chuẩn hoá interfaceKubernetes(container orchestrator)CRI(container runtime)CNI(networking)CSI(storage)containerd, CRI-O,Docker (cri-dockerd)Calico, Flannel,Cilium,...EBS CSI, Portworx,NetApp,...Kubernetes chỉ gọi đúng chuẩn interface — vendor tự hiện thực phía sau, không đụng core code.

💡 Hình dung: CRI, CNI, CSI giống ổ cắm điện theo chuẩn quốc gia. Nhà sản xuất thiết bị điện không cần biết nhà máy điện tạo ra dòng điện thế nào — chỉ cần làm đúng phích cắm theo chuẩn ổ cắm là thiết bị nào cũng dùng chung được hạ tầng điện đó. Kubernetes core đóng vai trò ổ cắm: định nghĩa chuẩn giao tiếp, còn runtime/network/storage vendor tự lo phần "nhà máy điện" của riêng họ.

CSI không phải chuẩn riêng của Kubernetes — nó được thiết kế như một chuẩn phổ quát. Nếu một storage vendor triển khai CSI driver, driver đó dùng được với bất kỳ container orchestrator nào hỗ trợ CSI, không chỉ Kubernetes. Hiện tại Kubernetes, Cloud Foundry, và Mesos đều đã tích hợp CSI.

Có thể tự kiểm tra cluster đang chạy CSI driver nào.

kubectl get csidrivers

# Output minh họa
NAME ATTACHREQUIRED PODINFOONMOUNT STORAGECAPACITY TOKENREQUESTS REQUIRESREPUBLISH MODES AGE
ebs.csi.aws.com true false true false false Persistent 45d
efs.csi.aws.com false false false false false Persistent 45d

(Output minh họa — cấu trúc và tên field đúng theo lệnh thật, số liệu cụ thể chỉ mang tính ví dụ.)

Mỗi dòng là một CSI driver đã đăng ký với cluster. ATTACHREQUIRED: true nghĩa là driver cần một bước attach riêng (như attach EBS volume vào EC2 instance) trước khi mount được vào Pod.

Cấu trúc CSI. CSI định nghĩa một tập RPC (remote procedure call) mà container orchestrator gọi, và storage driver phải hiện thực. Khi một Pod cần volume, Kubernetes gọi RPC CreateVolume kèm tên volume và các tham số cần thiết; storage driver xử lý request, provision volume mới trên storage array, và trả kết quả. Tương tự, Kubernetes gọi DeleteVolume khi cần xoá volume, driver phải decommission volume khỏi array. Đặc tả CSI còn quy định chi tiết tham số nào bên gọi phải gửi, cái gì driver phải trả về, và error code nào cần trao đổi khi có lỗi — toàn bộ nằm trong CSI specification công khai trên GitHub (xem ghi chú nguồn cuối bài).


6. Volumes trong Kubernetes

Docker container mang tính transient — tồn tại trong thời gian ngắn, xử lý dữ liệu rồi bị huỷ, và dữ liệu bên trong bị xoá theo. Pod trong Kubernetes có đặc tính y hệt: khi Pod bị xoá, dữ liệu nó xử lý cũng mất theo, trừ khi có một volume được gắn vào.

Ví dụ đơn giản. Trên một single-node cluster, tạo một Pod sinh số ngẫu nhiên từ 1 đến 100 rồi ghi vào file /opt/number.out. Không có volume, file này biến mất cùng Pod khi Pod bị xoá. Để giữ lại, tạo một volume dùng directory /data trên host, rồi mount volume đó vào /opt bên trong container qua field volumeMounts.

spec:
containers:
- name: myapp
volumeMounts:
- mountPath: /opt
name: data-volume
volumes:
- name: data-volume
hostPath:
path: /data

Số ngẫu nhiên giờ được ghi vào /opt trong container, thực chất là /data trên host — Pod bị xoá, file vẫn còn.

Vì sao hostPath không dùng được cho multi-node cluster? Vì Pod có thể được lập lịch lên bất kỳ node nào, và /data trên node này hoàn toàn khác /data trên node kia — không có gì đảm bảo chúng chứa cùng dữ liệu, trừ khi tự cấu hình thêm một giải pháp replicated storage bên ngoài. hostPath chỉ phù hợp cho single-node cluster hoặc lab thử nghiệm, không nên dùng trong production.

Với multi-node cluster, Kubernetes hỗ trợ nhiều loại storage backend: NFS, GlusterFS, Flocker, Fibre Channel, CephFS, ScaleIO, hoặc các giải pháp cloud như AWS EBS, Azure Disk/File, Google Persistent Disk. Ví dụ, để dùng AWS EBS làm storage cho volume, thay hostPath bằng awsElasticBlockStore.

volumes:
- name: data-volume
awsElasticBlockStore:
volumeID: <volume-id>
fsType: <file-system-type>

(Lưu ý cho Kubernetes hiện tại: awsElasticBlockStore là volume type in-tree — tức code tích hợp thẳng vào Kubernetes core thay vì qua CSI driver riêng — và đã bị gỡ bỏ hoàn toàn khỏi Kubernetes. Azure Disk/File và vSphere Volume cũng là in-tree type đã đi theo đúng lộ trình đó. Ví dụ trên vẫn đúng về mặt khái niệm "gắn một cloud disk cụ thể làm volume", nhưng trên cluster thật hiện nay phải làm qua CSI driver tương ứng của từng cloud — xem ghi chú nguồn cuối bài để biết mốc phiên bản cụ thể.)

Phần tiếp theo bàn về Persistent Volumes — cách quản lý storage tập trung thay vì khai báo lặp lại trong từng Pod definition file.


7. Persistent Volumes

Cách cấu hình volume ở phần trước có một vấn đề về vận hành: cấu hình storage nằm ngay trong pod definition file. Trong một môi trường có nhiều user deploy nhiều Pod, mỗi người phải tự khai báo storage cho từng Pod — và mỗi khi cần đổi gì đó (loại disk, kích thước,...), phải sửa lại trên tất cả các Pod liên quan.

Cách tốt hơn: quản lý storage tập trung — administrator tạo sẵn một pool storage lớn, user chỉ việc "cắt" ra phần mình cần. Đó là vai trò của Persistent Volume (PV): một pool storage ở phạm vi cluster (cluster-wide), do administrator cấu hình sẵn, để bất kỳ ứng dụng nào trên cluster cũng có thể lấy storage từ đó thông qua Persistent Volume Claim.

💡 Hình dung: PV giống một dãy kho chứa hàng mà bộ phận vận hành xây sẵn — mỗi kho có dung lượng và quy cách ra vào (access mode) riêng, xây trước, chưa gán cho bộ phận nào cụ thể. Ứng dụng nào cần chỗ chứa thì viết một phiếu yêu cầu (Persistent Volume Claim) nêu rõ cần bao nhiêu chỗ và quy cách gì — hệ thống tự khớp phiếu đó với đúng một kho phù hợp, rồi khoá kho đó lại cho riêng phiếu này dùng.

Tạo Persistent Volume. Từ template PersistentVolume, đặt tên pv-vol1. Dưới spec:

  • accessModes — định nghĩa volume được mount theo chế độ nào: ReadOnlyMany (nhiều node cùng mount read-only), ReadWriteOnce (mount read-write trên một node duy nhất — nhiều Pod trên cùng node đó vẫn mount chung được), ReadWriteMany (nhiều node cùng mount read-write), hoặc ReadWriteOncePod — chế độ chặt hơn ReadWriteOnce: giới hạn mount read-write vào đúng một Pod duy nhất trên toàn cluster, kể cả khi có Pod khác cùng nằm trên cùng node.
  • capacity — dung lượng dự trữ cho volume, ví dụ 1Gi.
  • Volume type — ví dụ hostPath, dùng storage từ local directory của node (không nên dùng trong production, chỉ phù hợp để học hoặc test).
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-vol1
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 1Gi
hostPath:
path: /tmp/data

Tạo bằng kubectl create -f pv-vol1.yaml, xem lại bằng kubectl get persistentvolume (hoặc kubectl get pv). Có thể thay hostPath bằng bất kỳ storage type nào đã hỗ trợ ở phần 6, ví dụ AWS EBS.

Phần tiếp theo dùng Persistent Volume Claim để "claim" storage từ các PV vừa cấu hình.


8. Persistent Volume Claims

Persistent Volume (PV) và Persistent Volume Claim (PVC) là hai object riêng biệt trong cluster: administrator tạo ra một tập PV, user tạo PVC để "xin" dùng storage đó. Một khi PVC được tạo, Kubernetes tự động bind một PV vào claim dựa trên các request và property đã khai báo trên cả hai phía — không cần thao tác thủ công nào thêm.

Trong quá trình binding, Kubernetes tìm một PV có đủ capacity theo yêu cầu của claim, cùng các property khác như access mode, volume mode, storage class. Nếu có nhiều PV cùng khớp và muốn chỉ định đúng một PV cụ thể, có thể dùng label và selector. Một claim nhỏ hơn vẫn có thể bind vào một PV lớn hơn, nếu mọi tiêu chí khác đều khớp và không có lựa chọn nào tốt hơn — nhưng phần dung lượng dư ra của PV đó bị "khoá" theo luôn, không claim nào khác dùng được.

Persistent Volume / Persistent Volume Claim: binding 1-1PV pool (Admin tạo trước)PV: pv-vol1 — 1Gi, RWOstatus: AvailablePV: pv-log — 100Mi, RWXstatus: Bound (claim-log-1)PV: pv-vol2 — 5Gi, RWOstatus: AvailableAdmin tạo PV trước, độc lập với ứng dụng nào sẽ dùng.User tạo PVCPVC: claim-log-1request: 50Mi, RWXUser khai báo nhu cầu, không chọn PV cụ thể.khớp RWX + đủ capacityQuan hệ luôn là 1-1: pv-log đã bind cho claim-log-1 thì không claim nào khác dùng được, kể cả phần dư.PVC nhỏ hơn PV vẫn có thể bind nếu accessMode khớp và không còn lựa chọn nào tốt hơn.

Quan hệ giữa claim và volume luôn là một-một. Nếu không có PV nào khả dụng, PVC sẽ ở trạng thái Pending cho tới khi có PV mới xuất hiện trên cluster, lúc đó claim sẽ tự động được bind.

Tạo Persistent Volume Claim. Từ template PersistentVolumeClaim, đặt tên myclaim.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: myclaim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Mi

Tạo bằng kubectl create -f myclaim.yaml, xem bằng kubectl get persistentvolumeclaim (hoặc kubectl get pvc) — claim ở trạng thái Pending ngay khi vừa tạo.

Kubernetes sau đó nhìn vào PV pv-vol1 đã tạo ở phần trước: access mode khớp, và dù claim chỉ yêu cầu 500Mi trong khi PV có 1Gi, vì không có PV nào khác phù hợp hơn, claim vẫn được bind vào pv-vol1. Chạy lại kubectl get pv sẽ thấy PV đã chuyển sang trạng thái Bound.

Điều gì xảy ra khi xoá PVC? Chạy kubectl delete persistentvolumeclaim. Hành vi của PV bên dưới phụ thuộc vào persistentVolumeReclaimPolicy.

Reclaim policyHành vi
Retain (mặc định)PV vẫn còn, chuyển sang trạng thái Released, chờ administrator xoá thủ công — không tự động khả dụng lại cho claim khác.
DeletePV bị xoá ngay khi claim bị xoá.
Recycle (đã deprecated)Dữ liệu trong volume được scrub trước khi PV khả dụng trở lại cho claim khác.

Recycle là option cũ và đã bị deprecated — recycle controller nguyên bản chỉ làm một kiểu "best-effort wipe": khởi chạy một recycler pod nhỏ, mount volume, và chạy rm -rf /scrub/* để xoá file, về cơ bản cố thay administrator làm việc dọn dẹp bằng một thao tác xoá file đơn giản ở mức file-level. Cách này không đủ trong thực tế: không đảm bảo xoá dữ liệu an toàn (secure erasure), không xử lý snapshot hay metadata riêng của từng provider, và chỉ hoạt động với một số in-tree volume plugin nhất định. Trên backend cloud/block như EBS, Google Cloud PD, Azure Disk, hay trên network storage như NFS/CSI driver, cleanup đúng cách có thể cần unmount, detach, format lại file system, xử lý snapshot, retain policy, encryption, key rotation, hoặc lệnh delete ở cấp provider — một rm -rf đơn thuần để lại inode metadata và có thể fail vì permission.

Vì những khoảng trống về tính portable và bảo mật này, Kubernetes đã chuyển hẳn sang một model hiện đại hơn: dynamic provisioning với StorageClassCSI driver (bàn ở phần 11). Tính tới thời điểm hiện tại, Recycle vẫn chỉ ở trạng thái deprecated — chưa bị gỡ bỏ hoàn toàn khỏi Kubernetes, nhưng không nên dùng cho bất kỳ workload mới nào (xem ghi chú nguồn cuối bài).


9. Sử dụng PVC trong Pod

Sau khi tạo PVC, dùng nó trong một pod definition file bằng cách chỉ định tên claim (PVC) dưới mục persistentVolumeClaim trong phần volumes, như sau.

apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: myfrontend
image: nginx
volumeMounts:
- mountPath: "/var/www/html"
name: mypd
volumes:
- name: mypd
persistentVolumeClaim:
claimName: myclaim

Điều tương tự cũng áp dụng cho ReplicaSet hoặc Deployment — thêm đoạn cấu hình này vào phần pod template của một Deployment hoặc ReplicaSet.

Tham khảo: kubernetes.io — Claims as Volumes.


10. Lab: Persistent Volumes và Persistent Volume Claims

Bài lab này đi qua đúng vòng đời PV/PVC vừa học: tạo, bind, đổi access mode, chuyển Pod sang dùng PVC, và quan sát hành vi khi xoá claim.

Xem log của pod webapp đang chạy. Application lưu log tại /log/app.log bên trong pod. Dùng kubectl exec kèm tên pod và command để xem file.

kubectl exec webapp -- cat /log/app.log

Thấy log có một số event do application ghi lại.

Nếu pod bị xoá, có xem được log nữa không? Kiểm tra volume hiện có bằng:

kubectl describe pod webapp

Không có volume nào khác ngoài volume mặc định để truy cập Kube API. Vì vậy, mọi thứ ghi vào /log/app.log chỉ nằm bên trong container của pod — nếu pod bị xoá, log cũng bị xoá theo, và sẽ không xem được log nữa.

Cấu hình một volume để lưu log này tại một path trên host. Hiện log được lưu tại /log/app.log bên trong pod/container. Cần dùng một volume để lưu log tại đường dẫn /pv/data/log-webapp trên host (hiện thư mục này chưa có gì).

Edit pod bằng kubectl edit trên webapp. Trong file có volume mount mặc định để truy cập Kube API server, và volume mặc định tương ứng bên dưới. Thêm volume riêng: dưới volumes, đặt tên volume là log-volume (dùng để lưu log), kiểu là hostPath, với path/pv/data/log-webapp. Sau đó thêm một volumeMount với mountPath/var/log/webapp, và tên volume tương ứng là log-volume.

volumes:
- name: log-volume
hostPath:
path: /pv/data/log-webapp
volumeMounts:
- name: log-volume
mountPath: /var/log/webapp

Vì đây là Pod, không thể save trực tiếp thay đổi — phải thực hiện replace cưỡng bức (kubectl replace --force). Sau khi pod được recreate, kiểm tra path /var/log/webapp — thấy file log app.log tại /var/log/webapp/app.log, chứa log của pod — xác nhận log của pod giờ đã có sẵn trên host tại path này.

Tạo Persistent Volume theo spec cho trước. Dùng template PersistentVolume từ trang tài liệu Kubernetes, tạo file pv.yaml.

apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-log
spec:
accessModes:
- ReadWriteMany
capacity:
storage: 100Mi
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /pv/log

(Template gốc còn có field volumeMode, nhưng không cần dùng cho lab này.) Tạo PV bằng kubectl create -f pv.yaml. Kiểm tra bằng kubectl get pv — thấy PV pv-log, capacity như đã cấu hình, accessModes ReadWriteMany, reclaim policy Retain.

Tạo Persistent Volume Claim theo spec cho trước. Dùng template PersistentVolumeClaim (bỏ phần selector nâng cao vì không cần cho use case này), tạo file pvc.yaml.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: claim-log-1
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Mi

Tạo bằng kubectl create -f pvc.yaml.

Trạng thái của PVC là gì? Kiểm tra kubectl get pvc — trạng thái Pending.

Trạng thái của PV là gì? Kiểm tra kubectl get pv — trạng thái Available.

Vì sao claim không được bind vào persistent volume đang Available? PV có capacity 100Mi, PVC yêu cầu 50Mi — không phải vấn đề capacity mismatch, vì một volume có capacity cao hơn vẫn có thể bind vào claim yêu cầu ít hơn. Tên của PV và PVC không quan trọng khi xét binding — thứ được xem xét là access mode. PV được tạo với access mode ReadWriteMany, còn PVC là ReadWriteOnce — đây là lý do access mode mismatch khiến claim không được bind.

Cập nhật access mode trên claim để nó bind vào PV. Sửa PVC, đổi accessModes thành ReadWriteMany (khớp với PV), rồi áp dụng lại.

kubectl replace -f pvc.yaml

Sau khi replace, capacity nào khả dụng cho PVC? Dù PVC chỉ request 50Mi, kiểm tra kubectl get pv / kubectl get pvc cho thấy capacity của PVC là 100Mi — bằng đúng capacity của PV mà nó được bind vào.

Cập nhật pod webapp để dùng PVC làm storage. Trước đó PV dùng hostPath tại /pv/log để lưu log, hiện thư mục này còn trống. Edit pod webapp, giữ nguyên mountPath, nhưng đổi phần volumes từ hostPath sang persistentVolumeClaim.

volumes:
- name: log-volume
persistentVolumeClaim:
claimName: claim-log-1

Lưu file, replace pod cưỡng bức (kubectl replace --force -f ...). Kiểm tra /pv/log trên host — thấy log tại /pv/log/app.log. Vậy giờ PV dùng hostPath làm nơi lưu dữ liệu, PV được claim bởi PVC, và PVC được cấu hình làm volume cho Pod, qua đó được mount vào pod.

Reclaim policy trên persistent volume pv-log là gì? Kiểm tra kubectl get pv pv-log — reclaim policy là Retain.

Điều gì xảy ra với PV nếu PVC bị xoá? Vì reclaim policy là Retain, PV sẽ được giữ lại (retain) — không bị xoá cùng PVC.

Thử xoá PVC và quan sát điều gì xảy ra.

kubectl delete pvc claim-log-1

Việc xoá bị kẹt — PVC vào trạng thái Terminating. Kiểm tra bằng kubectl get pvckubectl describe pvc — thấy nó kẹt ở Terminating vì đang được một Pod sử dụng làm volume (pod webapp).

Xoá pod webapp.

kubectl delete pod webapp

Sau khi pod bị xoá, PVC hết kẹt và hoàn tất xoá.

Trạng thái cuối cùng của PVC và PV? PVC đã bị xoá hoàn toàn. Kiểm tra PV — nó ở trạng thái Released (được giữ lại theo reclaim policy Retain, không bị xoá, nhưng cũng không còn ở trạng thái Available để tái sử dụng).


11. StorageClasses

Ở các phần trước, tạo PV rồi tạo PVC để claim storage đó gọi là static provisioning: trước khi PV được tạo, phải tự tay provision disk trên hạ tầng thật (ví dụ tạo Persistent Disk trên Google Cloud), rồi tạo definition file dùng đúng ID của disk đó. Mỗi lần một ứng dụng cần thêm storage, lại lặp lại đúng quy trình thủ công này.

StorageClass giải quyết đúng điểm nghẽn đó bằng dynamic provisioning: định nghĩa một provisioner có khả năng tự động tạo storage trên hạ tầng thật và gắn vào Pod ngay khi có claim mới — không ai phải tự tay tạo disk hay viết PV definition file nữa.

💡 Hình dung: static provisioning giống việc phải tự chuẩn bị trước từng phần ăn mỗi khi có khách, còn StorageClass giống một hệ thống gọi món tự động — khách chỉ cần chọn "gói tiêu chuẩn" hay "gói cao cấp" (StorageClass), hệ thống tự lo phần chuẩn bị (provisioner) phía sau, phần ăn (PV) tự xuất hiện đúng lúc cần.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: google-storage
provisioner: kubernetes.io/gce-pd

(Lưu ý cho Kubernetes hiện tại: kubernetes.io/gce-pd là in-tree provisioner — vẫn còn tồn tại, nhưng mọi thao tác của nó hiện được tự động chuyển hướng ("CSI migration") sang chạy qua CSI driver pd.csi.storage.gke.io phía dưới, và cơ chế chuyển hướng này không thể tắt được nữa. Ví dụ trên vẫn đúng cú pháp và vẫn chạy được, nhưng StorageClass viết mới trên cluster thật nên khai provisioner: pd.csi.storage.gke.io trực tiếp — xem ghi chú nguồn cuối bài.)

Dynamic provisioning: từ PVC tới PV tự độngPVCclass: fastStorageClassname: fastCSI DriverCreateVolumeCloud backenddisk mới tạoPV tự tạobind → PVCPVC: Pending → BoundKhông ai tự tay tạo disk hay PV — StorageClass tự làm hộ ngay khi PVC xuất hiện.

Không cần định nghĩa PV theo cách thủ công nữa — PV cùng storage liên quan sẽ được tạo tự động khi StorageClass hoạt động. Để một PVC dùng StorageClass, chỉ định storageClassName trong PVC definition.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: myclaim
spec:
accessModes:
- ReadWriteOnce
storageClassName: google-storage
resources:
requests:
storage: 500Mi

Từ đó, mỗi khi PVC này được tạo, StorageClass gắn với nó dùng provisioner đã định nghĩa để tạo một disk mới đúng kích thước yêu cầu, tạo PersistentVolume tương ứng, rồi bind PVC vào PV đó — vẫn tạo ra PV như cũ, chỉ khác là không ai phải tạo tay.

Có nhiều provisioner khác ngoài GCE: AWS EBS, Azure File, Azure Disk, CephFS, Portworx, ScaleIO,... Mỗi provisioner nhận thêm các tham số riêng biệt — ví dụ với Google Persistent Disk, có thể chỉ định type (standard hoặc SSD) và chế độ replication (none hoặc Regional Persistent Disk). Nhờ vậy có thể tạo nhiều "class" dịch vụ khác nhau — ví dụ silver dùng disk standard, gold dùng SSD, platinum dùng SSD kèm replication — đúng như cái tên StorageClass gợi ý. Từ đó, tạo PVC mới chỉ cần chỉ định đúng class mong muốn.


12. Lab: StorageClasses

Bài lab này thực hành StorageClass: kiểm tra provisioner và volumeBindingMode có sẵn, rồi tạo Pod để kích hoạt binding vốn đang bị trì hoãn bởi WaitForFirstConsumer.

Có bao nhiêu StorageClass tồn tại trên cluster?

kubectl get storageclass

(có thể dùng dạng ngắn kubectl get sc.) Thấy có một StorageClass.

Sau khi tạo thêm vài StorageClass, hiện có bao nhiêu? Kiểm tra lại — có ba StorageClass.

StorageClass nào không hỗ trợ dynamic volume provisioning? provisioner là thứ định nghĩa cách volume của StorageClass được provision — StorageClass có provisioner là kubernetes.io/no-provisioner là StorageClass không hỗ trợ dynamic volume provisioning. Đó là StorageClass tên local-storage.

Volume binding mode của StorageClass đó là gì? volumeBindingModeWaitForFirstConsumer. (Nếu StorageClass không khai báo field này, giá trị mặc định là Immediate — bind và provision ngay khi PVC được tạo, không chờ Pod nào tiêu thụ nó.)

Provisioner của StorageClass tên "Portworx I/O Priority High" là gì? Provisioner là portworx-volume. (Lưu ý cho Kubernetes hiện tại: đây là in-tree provisioner — đã ở trạng thái deprecated từ v1.25, nhưng tính tới thời điểm hiện tại chưa bị gỡ bỏ hoàn toàn khỏi core; CSI driver ngoài của Portworx là lựa chọn khuyến nghị cho StorageClass viết mới — xem ghi chú nguồn cuối bài.)

Có PersistentVolumeClaim nào đang dùng PV tên local-pv không? Kiểm tra PV và PVC hiện có — không có PVC nào cả. Đáp án: Không.

Tạo một PersistentVolumeClaim tên local-pvc để bind vào volume local-pv. Để bind được, PVC phải có cùng capacity (500Mi), accessModes phải khớp (ReadWriteOnce), và storage class phải khớp (local-storage). Dùng template PersistentVolumeClaim (bỏ phần selector nâng cao), tạo file PVC.yaml — trong lab, tên PVC thực tế được đặt trong YAML là local-pvc-volume.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: local-pvc-volume
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-storage
resources:
requests:
storage: 500Mi

Trạng thái của PVC vừa tạo là gì? Trạng thái Pending.

Vì sao PVC vẫn Pending dù request hợp lệ (500Mi, ReadWriteOnce, storage class local-storage đều khớp)? Xem event bằng:

kubectl describe pvc local-pvc

Lý do là WaitForFirstConsumer — đang chờ pod tiêu thụ (consumer) đầu tiên được tạo trước khi bind. Vì volumeBindingMode của StorageClass local-storage được set là WaitForFirstConsumer, việc binding và provisioning PersistentVolume sẽ bị trì hoãn cho tới khi có một Pod dùng PersistentVolumeClaim này được tạo ra.

Tạo một Pod tên nginx dùng image nginx:alpine, dùng PVC local-pvc và mount volume tại /var/www/html; PV local-pv phải chuyển sang trạng thái Bound. Tạo Pod với image nginx:alpine (không phải nginx-alpine). Cấu hình volume lấy từ PVC theo mẫu "claims as volumes" trong tài liệu Kubernetes.

apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:alpine
volumeMounts:
- mountPath: /var/www/html
name: local-pvc-volume
volumes:
- name: local-pvc-volume
persistentVolumeClaim:
claimName: local-pvc

Sau khi pod chạy (trạng thái Running), kiểm tra lại trạng thái PVC — chuyển sang Bound. Vậy ngay khi có một pod dùng PVC này, nó chuyển sang trạng thái bound.

Tạo một StorageClass mới tên delayed-volume-sc với provisioner và volumeBindingMode cho trước. Dùng template StorageClass, tạo file delayed-volume-sc.yaml.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: delayed-volume-sc
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

Tạo StorageClass và kiểm tra lại — StorageClass được tạo với provisioner kubernetes.io/no-provisioner và volume binding mode WaitForFirstConsumer.

Qua toàn bộ phần Storage, bức tranh tổng thể đã rõ: Docker quản lý storage bằng cặp storage driver (layer + copy-on-write) và volume driver plugin; Kubernetes nâng cấp ý tưởng đó lên quy mô cluster bằng CSI, rồi tách vai trò cấp phát (Persistent Volume, StorageClass) khỏi vai trò sử dụng (Persistent Volume Claim) — đúng mô hình sẽ gặp lại trong hầu hết cluster production thực tế. Đây cũng là mảng kiến thức cốt lõi cuối cùng của khóa CKA được ghi chú trong bộ tài liệu này tính đến thời điểm hiện tại.


Nguồn tham khảo

Nguồn gốc: Khóa "Certified Kubernetes Administrator (CKA)" — phần "Storage" (Storage trong Docker: storage driver, layered image, copy-on-write, volume và bind mount; Container Storage Interface; Volumes, Persistent Volumes và Persistent Volume Claims trong Kubernetes; StorageClasses và dynamic provisioning), nền tảng KodeKloud. Giảng viên: Mumshad Mannambeth.

Using PVC in Pods: kubernetes.io — Claims as Volumes.

Repo ghi chú, link tài liệu, và đáp án các practice question của toàn bộ khóa học: kodekloudhub/certified-kubernetes-administrator-course.

Fact-check:

  • Storage driver mặc định trên các bản Linux hiện được hỗ trợ (gồm Ubuntu) là overlay2, không phải AUFS — aufs đã deprecated từ Docker Engine v19.03 và bị gỡ bỏ hoàn toàn từ v24.0 — 2026-09-16, Docker Docs — Select a storage driver, Docker Docs — Deprecated Engine Features
  • In-tree volume plugin awsElasticBlockStore (AWS EBS) và azureDisk đã bị gỡ bỏ hoàn toàn khỏi Kubernetes kể từ v1.27 (deprecated từ v1.19); azureFile gỡ bỏ từ v1.30 (deprecated từ v1.21); vsphereVolume gỡ bỏ từ v1.30 (deprecated từ v1.19) — thay thế bằng CSI driver ngoài (out-of-tree) tương ứng — 2026-09-16, Kubernetes Docs — Volumes
  • In-tree volume plugin gcePersistentDisk (Google Persistent Disk) chưa bị gỡ khỏi Kubernetes, nhưng mọi thao tác của nó hiện được chuyển hướng bắt buộc (CSI migration, không thể tắt) sang chạy qua CSI driver pd.csi.storage.gke.io — 2026-09-16, Kubernetes Docs — Volumes
  • Reclaim policy Recycle vẫn ở trạng thái deprecated, chưa bị gỡ bỏ hoàn toàn khỏi Kubernetes — khuyến nghị hiện tại là dùng dynamic provisioning thay thế — 2026-09-16, Kubernetes Docs — Persistent Volumes
  • volumeBindingMode: WaitForFirstConsumer và pattern provisioner: kubernetes.io/no-provisioner cho local storage vẫn là cơ chế hiện hành, không đổi — 2026-09-16, Kubernetes Docs — Storage Classes
  • CSI spec: name do container orchestrator sinh ra là field bắt buộc cho RPC CreateVolume, còn volume_id do storage driver sinh ra là field bắt buộc cho RPC DeleteVolume (hai giá trị này có thể khác nhau) — mô tả RPC trong bài khớp đặc tả hiện hành — 2026-09-16, container-storage-interface/spec
  • Access mode ReadWriteOncePod — giới hạn mount read-write vào đúng một Pod duy nhất trên toàn cluster (khác ReadWriteOnce chỉ giới hạn theo node, vẫn cho nhiều Pod cùng node mount chung) — alpha từ v1.22, beta từ v1.27, graduate lên stable/GA từ v1.29, chỉ hỗ trợ cho volume dùng CSI driver — 2026-09-23, Kubernetes Blog — Single Pod Access Mode for PersistentVolumes Graduates to Stable
  • volumeBindingMode của StorageClass khi không khai báo mặc định là Immediate (bind và provision ngay khi PVC được tạo, không chờ Pod tiêu thụ) — 2026-09-23, Kubernetes Docs — Storage Classes
  • In-tree volume plugin portworxVolume đã deprecated từ v1.25, tính tới thời điểm hiện tại vẫn còn trong core (chưa bị gỡ bỏ hoàn toàn như EBS/Azure Disk/Azure File/vSphere) — 2026-09-23, Kubernetes Docs — Volumes