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

```mermaid
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)

```bash
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

```yaml
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

```bash
kubectl apply -f fast-sc.yaml
kubectl get sc fast
```

### Bước 4: Tạo PVC dùng class đó

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-fast
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: fast
  resources:
    requests:
      storage: 1Gi
```

```bash
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

```yaml
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
```

```bash
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

```bash
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

```bash
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.

```bash
kubectl get sc
kubectl get sc -o yaml | grep -E "name:|provisioner:|is-default-class"
```

<details><summary>Đáp án và mẹo</summary>

`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.
</details>

### 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.

<details><summary>Đáp án</summary>

```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: sc-immediate
provisioner: rancher.io/local-path
volumeBindingMode: Immediate
```

```bash
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.
</details>

### 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`.

<details><summary>Đáp án</summary>

```bash
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
```
</details>

### 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.

```bash
kubectl get pvc <tên-pvc-mới> -o jsonpath='{.spec.storageClassName}'
```

<details><summary>Đáp án</summary>

```bash
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.
</details>

### 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.

<details><summary>Đáp án</summary>

```yaml
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.
</details>

### 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.

```bash
kubectl patch pvc pvc-imm -p '{"spec":{"resources":{"requests":{"storage":"1Gi"}}}}'
kubectl get pvc pvc-imm
```

<details><summary>Đáp án</summary>

```bash
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 **đủ**.
</details>

### 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ả.

<details><summary>Đáp án</summary>

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`.
</details>

## 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ì:

```bash
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)`:

```bash
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.

```flashcard
[
  { "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 đó." }
]
```
