⚙️ 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:

  1. Tạo một StorageClass bằng manifest và giải thích được 4 trường quan trọng: provisioner, reclaimPolicy, volumeBindingMode, allowVolumeExpansion.
  2. Tạo PVC cấp phát động (dynamic provisioning) và biết chắc khi nào PVC Pending là bình thường, khi nào là lỗi.
  3. Đặ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 định reclaimPolicy: 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 Pending với WaitForFirstConsumer là 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ý do kubectl describe pvc sẽ ghi event kiểu waiting 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ạy kubectl get pvc để xác nhận Bound.
  • Nhớ 2 giá trị mặc định dễ bị hỏi: volumeBindingMode mặc định là Immediate; reclaimPolicy của PV tạo động mặc định là Delete.
  • kubernetes.io/no-provisioner không phải để tạo động. Nó dành cho bài "tạo PV tay + StorageClass tĩnh", thường đi kèm WaitForFirstConsumer. Thấy provisioner này thì hiểu ngay là phải tự viết PV.
  • Luôn dùng kubectl delete pvc trước kubectl delete sc, và nhớ rằng xóa StorageClass không xóa PV nào cả. PV chỉ chết theo PVC khi reclaimPolicy: 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:

  1. StorageClass là cluster-scoped, khai báo provisioner + reclaimPolicy + volumeBindingMode; PVC chỉ cần storageClassName là có volume.
  2. WaitForFirstConsumer khiến PVC Pending cho tới khi có Pod dùng nó. Đó là thiết kế, không phải lỗi.
  3. Default class được đánh dấu bằng annotation storageclass.kubernetes.io/is-default-class: "true"; bỏ trống storageClassName thì dùng default, đặt "" thì tắt dynamic provisioning.
  4. reclaimPolicy: Delete là 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".
  • volumeBindingMode mặc định là {{c3::Immediate}}, còn {{c4::WaitForFirstConsumer}} sẽ trì hoãn provisioning đến khi có Pod dùng PVC.
  • reclaimPolicy mặ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 đó." }
]