StorageClass và dynamic provisioning
Mục tiêu bài học
Sau bài này, bạn phải làm được 3 việc sau mà không cần mở tài liệu:
- Tạo một StorageClass bằng manifest và giải thích được 4 trường quan trọng:
provisioner,reclaimPolicy,volumeBindingMode,allowVolumeExpansion. - Tạo PVC cấp phát động (dynamic provisioning) và biết chắc khi nào PVC
Pendinglà bình thường, khi nào là lỗi. - Đặt default StorageClass cho cluster bằng một lệnh
kubectl patch, và biết cách gỡ default khỏi class cũ.
[!NOTE] Ôn lại nhanh 2 câu từ bài trước: PV là tài nguyên cluster-scoped do admin tạo tay (static), PVC là "đơn xin" trong namespace và bind vào PV theo
storageClassName+accessModes+ dung lượng. PV tạo tay mặc địnhreclaimPolicy: Retain.Bài này bỏ bước "admin tạo PV tay": ta để provisioner tự tạo PV khi có PVC. Đó chính là dynamic provisioning.
Sơ đồ luồng bạn cần nhớ:
flowchart LR
POD["🚀<br/>Pod"] --> PVC["📦<br/>PVC"]
PVC --> SC["⚙️<br/>StorageClass"]
SC --> PROV["🔧<br/>Provisioner"]
PROV --> PV["💾<br/>PV pvc-xxxx"]
PV -.-> PVC
Ví dụ từng bước: từ StorageClass đến PVC Bound
Lab dưới đây chạy trên cluster local (kind hoặc minikube). Điểm mấu chốt: provisioner phải là provisioner có thật trong cluster của bạn, không được bịa tên.
Bước 1: Xem cluster đang có gì (30 giây)
kubectl get sc
Kết quả dạng:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
standard (default) rancher.io/local-path Delete WaitForFirstConsumer false 12d
Ghi lại 2 thứ: tên provisioner và class nào đang là (default). Trong toàn bộ bài này, chỗ nào thấy rancher.io/local-path thì thay bằng provisioner lab của bạn (ví dụ k8s.io/minikube-hostpath, kubernetes.io/aws-ebs, pd.csi.storage.gke.io).
Bước 2: Viết manifest StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast
provisioner: rancher.io/local-path
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
Giải thích nhanh 4 trường (đây là 4 câu hỏi thi hay gặp):
| Trường | Ý nghĩa | Giá trị hay dùng |
|---|---|---|
provisioner |
Driver tạo volume thật | lấy từ kubectl get sc |
reclaimPolicy |
Xóa PVC thì PV bị xử lý sao | Delete (mặc định khi tạo động) hoặc Retain |
volumeBindingMode |
Khi nào provision | Immediate (mặc định) hoặc WaitForFirstConsumer |
allowVolumeExpansion |
Cho phép tăng dung lượng PVC | true nếu provisioner hỗ trợ |
Bước 3: Apply và kiểm tra
kubectl apply -f fast-sc.yaml
kubectl get sc fast
Bước 4: Tạo PVC dùng class đó
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-fast
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast
resources:
requests:
storage: 1Gi
kubectl apply -f pvc.yaml
kubectl get pvc data-fast
Vì fast dùng WaitForFirstConsumer, PVC sẽ Pending, và đó là hành vi đúng. Đừng vội sửa gì cả.
[!WARNING] ==PVC
PendingvớiWaitForFirstConsumerlà bình thường, không phải lỗi.== Provisioner cố tình chờ để biết Pod sẽ chạy ở node nào rồi mới tạo volume ở đúng node đó. Đây là lý dokubectl describe pvcsẽ ghi event kiểuwaiting for first consumer to be created before binding.
Bước 5: Tạo Pod để "kích hoạt" provisioning
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: nginx
image: nginx:1.25
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html
volumes:
- name: data
persistentVolumeClaim:
claimName: data-fast
kubectl apply -f pod.yaml
kubectl get pvc data-fast
kubectl get pv
Bây giờ PVC chuyển Bound, và một PV tên dạng pvc-1f3a... xuất hiện. Bạn không hề viết manifest cho PV đó: provisioner đã tạo hộ.
Bước 6: Đặt fast làm default class
kubectl patch storageclass fast \
-p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
kubectl patch storageclass standard \
-p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
kubectl get sc
Giờ fast có chữ (default). Từ đây, PVC nào không ghi storageClassName sẽ tự dùng fast.
Bước 7: Dọn dẹp
kubectl delete pod web
kubectl delete pvc data-fast
kubectl get pv # PV pvc-xxxx biến mất vì reclaimPolicy: Delete
Luyện tập có hướng dẫn
Làm lần lượt, mỗi bài tự bấm lệnh verify trước khi mở đáp án. Tổng thời gian khoảng 15 phút.
Bài 1 (2 phút): Đọc cluster như người thi
Yêu cầu: Cho biết tên các StorageClass, provisioner của từng class, và class nào là default.
kubectl get sc
kubectl get sc -o yaml | grep -E "name:|provisioner:|is-default-class"
Đáp án và mẹo
kubectl get sc đã in sẵn chữ (default) cạnh tên class mặc định, không cần jsonpath. Trong phòng thi, đây là bước bạn nên làm trước khi viết bất kỳ manifest storage nào, vì đề thường cho sẵn class và chỉ yêu cầu PVC + Pod.
Bài 2 (3 phút): StorageClass với Immediate
Yêu cầu: Tạo StorageClass tên sc-immediate dùng đúng provisioner của lab, volumeBindingMode: Immediate. Tạo PVC pvc-imm 500Mi dùng class này. Quan sát trạng thái PVC khi chưa có Pod.
Đáp án
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: sc-immediate
provisioner: rancher.io/local-path
volumeBindingMode: Immediate
kubectl apply -f sc-immediate.yaml
kubectl create -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-imm
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: sc-immediate
resources:
requests:
storage: 500Mi
EOF
kubectl get pvc pvc-imm
PVC lên Bound ngay, PV pvc-... xuất hiện ngay, dù chưa có Pod nào. Đây chính là khác biệt cốt lõi so với bài 3.
Bài 3 (3 phút): StorageClass với WaitForFirstConsumer
Yêu cầu: Tạo sc-wffc với volumeBindingMode: WaitForFirstConsumer, PVC pvc-wffc 500Mi. Xác nhận PVC Pending, đọc event, rồi tạo Pod để thấy PVC chuyển Bound.
Đáp án
kubectl get pvc pvc-wffc # Pending
kubectl describe pvc pvc-wffc | tail -5 # waiting for first consumer
kubectl run tmp --image=nginx:1.25 --restart=Never --overrides='
{"spec":{"containers":[{"name":"tmp","image":"nginx:1.25",
"volumeMounts":[{"name":"d","mountPath":"/data"}]}],
"volumes":[{"name":"d","persistentVolumeClaim":{"claimName":"pvc-wffc"}}]}}'
kubectl get pvc pvc-wffc # Bound sau vài giây
Bài 4 (3 phút): Đổi default class an toàn
Yêu cầu: Đặt sc-immediate làm default, đồng thời gỡ default khỏi class đang là default. Sau đó tạo PVC không có storageClassName và kiểm tra nó nhận class nào.
kubectl get pvc <tên-pvc-mới> -o jsonpath='{.spec.storageClassName}'
Đáp án
kubectl patch storageclass sc-immediate \
-p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
kubectl patch storageclass standard \
-p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
PVC không ghi storageClassName sẽ được admission controller gán sc-immediate vào spec.storageClassName. Lưu ý: cluster có thể có nhiều class cùng đánh dấu default, Kubernetes không chặn, nhưng class default được tạo sau cùng sẽ thắng, và đây là nguồn lỗi rất khó chịu trong đề thi.
Bài 5 (3 phút): Tắt dynamic provisioning
Yêu cầu: Tạo PVC pvc-noclass 100Mi với storageClassName: "" và giải thích tại sao nó Pending dù cluster có default class.
Đáp án
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-noclass
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ""
resources:
requests:
storage: 100Mi
Chuỗi rỗng là tín hiệu rõ ràng "tôi không muốn dynamic provisioning". PVC này chỉ bind được với một PV không có storageClassName. Cluster của bạn gần như chắc chắn không có PV như vậy, nên PVC Pending vĩnh viễn. Đây là bẫy kinh điển: bỏ trống storageClassName và đặt storageClassName: "" là hai chuyện hoàn toàn khác nhau.
Bài 6 (3 phút): Tăng dung lượng PVC
Yêu cầu: Bật allowVolumeExpansion: true trên sc-immediate (hoặc class của bạn), rồi patch PVC từ 500Mi lên 1Gi. Ghi lại điều gì xảy ra.
kubectl patch pvc pvc-imm -p '{"spec":{"resources":{"requests":{"storage":"1Gi"}}}}'
kubectl get pvc pvc-imm
Đáp án
kubectl patch storageclass sc-immediate \
-p '{"allowVolumeExpansion":true}'
Nếu PVC vẫn 500Mi hoặc event báo does not support expansion, nguyên nhân là provisioner không hỗ trợ resize (các provisioner dựa trên hostPath/local-path thường vậy), chứ không phải bạn gõ sai. Bài học: allowVolumeExpansion là điều kiện cần, không phải điều kiện đủ.
Bài 7 (2 phút): Xóa PVC, xem PV ra sao
Yêu cầu: Xóa pvc-imm và cho biết PV của nó còn hay mất. Sau đó sửa reclaimPolicy của StorageClass thành Retain, lặp lại thí nghiệm với một PVC mới, và giải thích kết quả.
Đáp án
Với reclaimPolicy: Delete (mặc định của PV tạo động), PV bị xóa theo, dữ liệu mất sạch. Với Retain, PV còn lại và chuyển trạng thái Released, nhưng PVC mới không bind lại được vào PV đó cho tới khi bạn tự tay xóa PV và tạo lại (hoặc gỡ claimRef).
Quan trọng: đổi reclaimPolicy trên StorageClass không hồi tố. PV đã tạo vẫn giữ giá trị cũ trong spec.persistentVolumeReclaimPolicy.
Lỗi thường gặp và cách xử lý
[!WARNING] Ba lỗi dưới đây chiếm phần lớn số điểm bị mất ở dạng bài storage.
1. Tưởng Pending là lỗi khi dùng WaitForFirstConsumer. Đọc event trước khi sửa bất cứ thứ gì:
kubectl describe pvc <tên> | sed -n '/Events/,$p'
Nếu event là waiting for first consumer, việc cần làm là tạo Pod, không phải sửa PVC.
2. Gõ sai tên provisioner. PVC sẽ Pending kèm event provisioning failed hoặc storageclass not found. Luôn copy provisioner từ kubectl get sc -o yaml thay vì gõ lại từ trí nhớ.
3. Quên mất cluster có nhiều default class. kubectl get sc chỉ hiện (default) cạnh mọi class được đánh dấu, nên hãy nhìn kỹ có mấy chữ (default):
kubectl get sc -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.annotations.storageclass\.kubernetes\.io/is-default-class}{"\n"}{end}'
Bảng tra nhanh theo triệu chứng:
| Triệu chứng | Nguyên nhân hay gặp | Cách xử lý |
|---|---|---|
PVC Pending, event waiting for first consumer |
WaitForFirstConsumer |
Tạo Pod dùng PVC |
PVC Pending, event provisioning failed |
Sai provisioner, thiếu quyền, hết dung lượng | Sửa provisioner hoặc kiểm tra log provisioner |
PVC Pending, không có event nào |
storageClassName: "" |
Bỏ trường này để dùng default class |
PVC Pending, event storageclass not found |
Sai tên class | kubectl get sc rồi sửa lại |
Patch PVC báo does not support expansion |
Chưa bật allowVolumeExpansion hoặc provisioner không hỗ trợ |
Bật trên StorageClass; nếu vẫn lỗi thì tạo PVC mới |
PV Released không dùng lại được |
reclaimPolicy: Retain |
Xóa PV rồi tạo lại, hoặc gỡ claimRef |
Một lưu ý về annotation: dùng đúng storageclass.kubernetes.io/is-default-class. Bản beta cũ storageclass.beta.kubernetes.io/is-default-class đã deprecated, dán vào có thể không có tác dụng.
Góc chuyên gia: mẹo thi và quy tắc ngón tay cái
- Đọc đề trước, tạo SC sau. Nếu đề cho sẵn StorageClass, bạn chỉ cần PVC + Pod. Tạo thêm PV tay trong trường hợp đó là tự làm khó mình và dễ sai ràng buộc.
- Không có
kubectl create storageclass. Không tồn tại generator này, đừng mất 30 giây gõ thử. Cách nhanh nhất trong phòng thi làkubectl apply -f -với heredoc, hoặc copy mẫu từ kubernetes.io/docs (được phép mở trong CKA). - Quy tắc 30 giây: trước mọi bài storage, chạy
kubectl get scđể biết provisioner và default class. Sau mọi bài storage, chạykubectl get pvcđể xác nhậnBound. - Nhớ 2 giá trị mặc định dễ bị hỏi:
volumeBindingModemặc định làImmediate;reclaimPolicycủa PV tạo động mặc định làDelete. kubernetes.io/no-provisionerkhông phải để tạo động. Nó dành cho bài "tạo PV tay + StorageClass tĩnh", thường đi kèmWaitForFirstConsumer. Thấy provisioner này thì hiểu ngay là phải tự viết PV.- Luôn dùng
kubectl delete pvctrướckubectl delete sc, và nhớ rằng xóa StorageClass không xóa PV nào cả. PV chỉ chết theo PVC khireclaimPolicy: Delete. - Đặt tên class theo mục đích (
fast,slow,retain) thay vì tên ngẫu nhiên; đề thi đôi khi bắt bạn tham chiếu lại đúng tên đó ở task khác.
Tóm tắt và bước tiếp theo
Bốn dòng để bạn ghi ra giấy nháp:
StorageClasslà cluster-scoped, khai báoprovisioner+reclaimPolicy+volumeBindingMode; PVC chỉ cầnstorageClassNamelà có volume.WaitForFirstConsumerkhiến PVCPendingcho tới khi có Pod dùng nó. Đó là thiết kế, không phải lỗi.- Default class được đánh dấu bằng annotation
storageclass.kubernetes.io/is-default-class: "true"; bỏ trốngstorageClassNamethì dùng default, đặt""thì tắt dynamic provisioning. reclaimPolicy: Deletelà mặc định của PV tạo động, nên xóa PVC là mất dữ liệu.
Kiểm tra nhanh trước khi sang bài sau, điền vào chỗ trống:
- StorageClass là tài nguyên {{c1::cluster-scoped}}, không thuộc namespace nào.
- Annotation để đánh dấu class mặc định là {{c2::storageclass.kubernetes.io/is-default-class}} với giá trị
"true". volumeBindingModemặc định là {{c3::Immediate}}, còn {{c4::WaitForFirstConsumer}} sẽ trì hoãn provisioning đến khi có Pod dùng PVC.reclaimPolicymặc định của PV được tạo động là {{c5::Delete}}.- Muốn PVC tăng được dung lượng, StorageClass phải bật {{c6::allowVolumeExpansion}}.
Bài tiếp theo: Troubleshoot PVC Pending và lỗi mount volume. Chúng ta sẽ lấy đúng những triệu chứng vừa gặp ở trên (Pending, provisioning failed, mount fail khi pod chạy) và biến chúng thành quy trình chẩn đoán 3 bước: xem event, xem describe, xem log. Bạn sẽ thấy vì sao 90% lỗi storage trong đề thi chỉ cần đọc đúng dòng event.
[
{ "front": "StorageClass thuộc phạm vi nào?", "back": "Cluster-scoped, không nằm trong namespace nào." },
{ "front": "Annotation nào đánh dấu một StorageClass là mặc định?", "back": "storageclass.kubernetes.io/is-default-class, đặt bằng chuỗi true." },
{ "front": "PVC không khai báo storageClassName thì điều gì xảy ra?", "back": "PVC nhận StorageClass mặc định của cluster nếu cluster có default class." },
{ "front": "storageClassName với chuỗi rỗng có tác dụng gì?", "back": "Tắt dynamic provisioning, PVC chỉ bind được với PV không có class." },
{ "front": "volumeBindingMode mặc định của StorageClass là gì?", "back": "Immediate." },
{ "front": "WaitForFirstConsumer gây hiện tượng gì với PVC?", "back": "PVC ở trạng thái Pending cho đến khi có Pod sử dụng nó." },
{ "front": "reclaimPolicy mặc định của PV do dynamic provisioning tạo ra?", "back": "Delete, nên xóa PVC là xóa luôn PV và dữ liệu." },
{ "front": "Trường nào trên StorageClass phải bật để PVC được tăng dung lượng?", "back": "allowVolumeExpansion: true, và provisioner cũng phải hỗ trợ." },
{ "front": "Provisioner nào dùng khi bạn muốn tự tạo PV thủ công?", "back": "kubernetes.io/no-provisioner." },
{ "front": "PV do dynamic provisioning tạo ra có tên dạng gì?", "back": "pvc-<uid của PVC>." },
{ "front": "Cách nhanh để đặt một StorageClass làm mặc định?", "back": "kubectl patch storageclass <tên> -p với annotation is-default-class đặt thành true." },
{ "front": "apiVersion và kind của StorageClass?", "back": "storage.k8s.io/v1 và kind: StorageClass." },
{ "front": "kubectl create có generator cho StorageClass không?", "back": "Không có, phải viết manifest rồi kubectl apply -f." },
{ "front": "Đổi parameters hay reclaimPolicy trên StorageClass có ảnh hưởng PV đã tạo không?", "back": "Không, PV đã tạo giữ nguyên giá trị cũ." },
{ "front": "PVC Pending khi dùng WaitForFirstConsumer thì nên làm gì?", "back": "Tạo Pod mount PVC đó, provisioning sẽ chạy ngay sau đó." }
]