# 💾 Volume, PersistentVolume, PersistentVolumeClaim và StorageClass

## Mục tiêu bài học

Sau bài này, bạn sẽ:

- Phân biệt được volume gắn vòng đời với Pod (`emptyDir`, `hostPath`) và storage bền vững (PV, PVC).
- Đọc được vòng đời binding của PVC: `Pending`, `Bound`, `Released`, và biết vì sao PVC bị kẹt.
- Viết được cặp PVC + Pod/Deployment để gắn storage bền vững cho ứng dụng có state.
- Hiểu StorageClass làm gì, nhất là `volumeBindingMode` và `reclaimPolicy`.

## Ôn nhanh

Bài trước bạn đã mount ConfigMap và Secret vào Pod qua `volumes`. Cơ chế mount y hệt bài này, chỉ khác một điểm cốt lõi: ConfigMap/Secret là dữ liệu cấu hình, xoá Pod là xong; còn dữ liệu của database thì phải sống lâu hơn Pod. Đó chính là lý do tồn tại của PV và PVC.

## Lý thuyết

### 1. Volume trong Pod: vòng đời ngắn

Mọi container trong Kubernetes đều có filesystem tạm, mất sạch khi container restart. `volume` ở cấp Pod giải quyết việc chia sẻ và lưu tạm:

| Loại | Dùng khi nào | Cạm bẫy |
| --- | --- | --- |
| `emptyDir` | Cache, file tạm, chia sẻ file giữa các container trong cùng Pod | Mất trắng khi Pod bị xoá hoặc bị reschedule sang node khác |
| `hostPath` | Debug, agent cần đọc thư mục của node | Gắn chặt vào một node, không portable, rủi ro bảo mật nếu mount `/` |

Quy tắc nhớ nhanh: ==`emptyDir` có vòng đời bằng vòng đời của Pod, không phải của container==. Container restart thì dữ liệu còn, Pod bị xoá thì mất.

### 2. PV, PVC và StorageClass: tách người dùng khỏi hạ tầng

- **PersistentVolume (PV)**: một mẩu storage thật (EBS, disk, NFS, local SSD). Đây là tài nguyên cấp cluster, không thuộc namespace nào.
- **PersistentVolumeClaim (PVC)**: yêu cầu storage của dev, nằm trong namespace. Dev xin "10Gi, read-write một node", không cần biết bên dưới là ổ gì.
- **StorageClass (SC)**: "menu" các loại storage. Có SC thì cluster tự tạo PV khi bạn tạo PVC, gọi là dynamic provisioning.

PVC và PV bind với nhau theo quan hệ 1-1. Một PVC đã `Bound` thì không PVC nào khác dùng được PV đó.

### 3. Vòng đời binding

```mermaid
flowchart TD
    Dev["👩‍💻<br/>Dev tạo PVC"] --> SC["📦<br/>StorageClass"]
    SC --> Prov["⚙️<br/>Provisioner"]
    Prov --> PV["💾<br/>PersistentVolume"]
    PV --> Bound["🔗<br/>PVC Bound"]
    Bound --> Pod["🚀<br/>Pod mount"]
```

Bốn trạng thái bạn sẽ gặp khi `kubectl get pvc`:

- `Pending`: chưa tìm được PV. Thường do sai `storageClassName`, xin quá nhiều dung lượng, hoặc access mode không PV nào đáp ứng.
- `Bound`: đã ghép với PV, dùng được.
- `Released`: PVC bị xoá, PV còn dữ liệu nhưng chưa được giải phóng (khi `reclaimPolicy: Retain`).
- `Failed`: provisioner hoặc việc xoá volume gặp lỗi.

Hai thông số của StorageClass hay gây bất ngờ:

- `volumeBindingMode: Immediate` tạo PV ngay khi có PVC. `WaitForFirstConsumer` trì hoãn tạo PV cho tới khi có Pod được schedule, nhờ đó PV nằm đúng zone/AZ với node.
- `reclaimPolicy: Delete` xoá volume thật khi PVC bị xoá. `Retain` giữ lại, và bạn phải tự dọn.

### 4. Access mode

| Mode | Viết tắt | Ý nghĩa thật |
| --- | --- | --- |
| ReadWriteOnce | RWO | Read-write bởi **một node**, nhiều Pod trên node đó vẫn đọc ghi được |
| ReadOnlyMany | ROX | Nhiều node chỉ đọc |
| ReadWriteMany | RWX | Nhiều node đọc ghi (cần NFS, CephFS...) |
| ReadWriteOncePod | RWOP | Chỉ đúng một Pod được ghi |

Đây là chỗ hiểu sai nhiều nhất: {{c1::ReadWriteOnce}} không có nghĩa là một Pod, mà là một node. Nếu 3 replica cùng mount một PVC RWO và bị schedule lên 3 node khác nhau, bạn sẽ thấy lỗi mount.

Vài con số cần nhớ: PVC binding với PV theo tỉ lệ {{c2::1-1}}, và `reclaimPolicy: {{c3::Retain}}` giữ dữ liệu lại sau khi PVC bị xoá.

## Ví dụ từng bước

**Bước 1: Tạo PVC xin 1Gi, chế độ RWO.**

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-pvc
spec:
  accessModes: ["ReadWriteOnce"]
  storageClassName: standard
  resources:
    requests:
      storage: 1Gi
```

**Bước 2: Kiểm tra binding.**

```bash
kubectl apply -f pvc.yaml
kubectl get pvc data-pvc
# NAME       STATUS   VOLUME    CAPACITY   ACCESS MODES   STORAGECLASS
# data-pvc   Bound    pvc-a1b2  1Gi        RWO            standard
```

Nếu `STATUS` là `Pending`, chạy `kubectl describe pvc data-pvc` và đọc phần `Events`. Câu trả lời gần như luôn nằm ở đó.

**Bước 3: Pod dùng PVC.**

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  containers:
    - name: app
      image: nginx:alpine
      volumeMounts:
        - name: data
          mountPath: /usr/share/nginx/html
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: data-pvc
```

**Bước 4: Kiểm chứng dữ liệu bền vững.**

```bash
kubectl exec web -- sh -c 'echo "hello k8s" > /usr/share/nginx/html/index.html'
kubectl delete pod web
kubectl apply -f pod.yaml
kubectl exec web -- cat /usr/share/nginx/html/index.html
# hello k8s
```

Pod mới, IP mới, node có thể mới, nhưng file vẫn còn. Đó là toàn bộ giá trị của PVC.

## Thực hành có hướng dẫn

**Bài 1.** Tạo PVC tên `logs-pvc` xin 500Mi, `storageClassName: standard`, RWO. Kiểm tra trạng thái.

<details><summary>Gợi ý và đáp án</summary>

Sửa `storage: 500Mi` và `metadata.name: logs-pvc` từ ví dụ trên, apply, rồi `kubectl get pvc logs-pvc`. Nếu `Bound`, chuyển bài 2. Nếu `Pending`, đọc `kubectl describe pvc` và tự trả lời: dung lượng có vượt quota không, `storageClassName` có tồn tại (`kubectl get sc`) không.
</details>

**Bài 2.** Mount `logs-pvc` vào một Pod tại `/var/log/app`, ghi một file, xoá Pod, tạo lại và đọc file đó.

<details><summary>Gợi ý và đáp án</summary>

Thêm block `volumes` với `persistentVolumeClaim.claimName: logs-pvc` và `volumeMounts` với `mountPath: /var/log/app`. Ghi file bằng `kubectl exec`. Sau khi xoá và apply lại, `kubectl exec ... cat /var/log/app/test.txt` phải trả về nội dung cũ. Nếu file mất, bạn rất có thể đang dùng `emptyDir`.
</details>

**Bài 3.** Sửa PVC `logs-pvc` lên 1Gi. Chuyện gì xảy ra?

<details><summary>Gợi ý và đáp án</summary>

Chỉ tăng được dung lượng, không giảm được. Việc tăng chỉ thành công nếu StorageClass có `allowVolumeExpansion: true`. Kiểm tra bằng `kubectl get sc standard -o yaml`. Lệnh `kubectl edit pvc` khi PVC đã `Bound` sẽ bị từ chối nếu bạn cố đổi `storageClassName` hoặc `accessModes`.
</details>

**Bài 4.** Giải thích vì sao PVC sau đây bị `Pending`:

```yaml
spec:
  accessModes: ["ReadWriteMany"]
  storageClassName: standard
  resources:
    requests:
      storage: 20Gi
```

<details><summary>Gợi ý và đáp án</summary>

Hai nghi phạm: SC `standard` thường chỉ hỗ trợ RWO (block storage không hỗ trợ RWX), và 20Gi có thể vượt dung lượng volume tối đa. Cách kiểm chứng: `kubectl get sc standard -o yaml` xem có `ReadWriteMany` trong danh sách hỗ trợ không, và đọc `Events` của PVC xem provisioner báo gì.
</details>

## Góc chuyên gia

- **Đừng đoán, hãy đọc Events.** 90% lỗi storage nằm trong `kubectl describe pvc`, không phải trong YAML của bạn.
- **Đừng dùng `hostPath` cho workload thật.** Nó biến node thành điểm chết duy nhất và là lỗ hổng bảo mật khi Pod bị chiếm.
- **Chọn `WaitForFirstConsumer` cho cloud đa zone.** `Immediate` có thể tạo disk ở zone A trong khi Pod bị schedule sang zone B, và Pod sẽ kẹt ở `Pending` mãi.
- **Nhớ xoá PVC khi dọn dẹp.** Xoá Deployment không xoá PVC. Với `Retain`, bill cloud vẫn chạy dù chẳng ai dùng.
- **Rule of thumb:** dữ liệu phải sống sót qua việc xoá Pod thì dùng PVC. Chỉ cần chia sẻ tạm giữa các container thì `emptyDir` là đủ.

> [!WARNING]
> Không bao giờ mount chung một PVC RWO cho nhiều replica chạy trên nhiều node. Hãy dùng RWX nếu storage hỗ trợ, hoặc để StatefulSet cấp PVC riêng cho từng replica.

## Tóm tắt và bước tiếp theo

Bạn vừa đi qua bốn tầng: volume gắn với Pod (`emptyDir`, `hostPath`), PV là storage thật ở cấp cluster, PVC là yêu cầu trong namespace, và StorageClass là thứ tự động hoá việc tạo PV. Nhớ ba điểm: PVC bind 1-1 với PV, `ReadWriteOnce` là một node chứ không phải một Pod, và `reclaimPolicy: Retain` là lưới an toàn cho dữ liệu quan trọng.

Bài tiếp theo là checkpoint của module. Bạn sẽ ghép ConfigMap, Secret và PVC vào một ứng dụng hoàn chỉnh, đồng thời gỡ các tình huống lỗi mà ta vừa điểm qua.

```flashcard
[
  { "front": "Volume emptyDir mất dữ liệu khi nào?", "back": "Khi Pod bị xoá hoặc reschedule; container restart thì dữ liệu vẫn còn." },
  { "front": "hostPath có nhược điểm gì?", "back": "Gắn chặt vào một node cụ thể, không portable và rủi ro bảo mật." },
  { "front": "PersistentVolume thuộc phạm vi nào?", "back": "Cấp cluster (cluster-scoped), không nằm trong namespace nào." },
  { "front": "PVC thuộc phạm vi nào?", "back": "Thuộc một namespace, do dev tạo để xin storage." },
  { "front": "Quan hệ giữa PVC và PV là gì?", "back": "1-1: một PVC đã Bound thì PV đó không chia sẻ cho PVC khác." },
  { "front": "StorageClass dùng để làm gì?", "back": "Khai báo loại storage và cho phép dynamic provisioning, tự tạo PV khi có PVC." },
  { "front": "volumeBindingMode: WaitForFirstConsumer có tác dụng gì?", "back": "Trì hoãn tạo PV cho tới khi Pod được schedule, giúp PV nằm đúng zone với node." },
  { "front": "ReadWriteOnce (RWO) thực chất nghĩa là gì?", "back": "Read-write bởi một node, không phải một Pod." },
  { "front": "reclaimPolicy: Retain làm gì khi PVC bị xoá?", "back": "Giữ lại PV và dữ liệu, PV chuyển sang trạng thái Released." },
  { "front": "Trạng thái Pending của PVC thường do đâu?", "back": "Không có PV khớp dung lượng, access mode, hoặc sai storageClassName." },
  { "front": "Lệnh nào giúp chẩn đoán PVC bị kẹt?", "back": "kubectl describe pvc, đọc phần Events." },
  { "front": "Có thể giảm dung lượng PVC không?", "back": "Không. Chỉ tăng được, và chỉ khi StorageClass bật allowVolumeExpansion." },
  { "front": "Xoá Deployment có xoá PVC không?", "back": "Không. PVC tồn tại độc lập, phải xoá riêng." }
]
```

Ôn nhanh bằng cloze:

- Volume `emptyDir` có vòng đời bằng vòng đời của {{c4::Pod}}.
- PVC và PV bind với nhau theo quan hệ {{c5::1-1}}.
- Mode {{c6::ReadWriteOncePod}} giới hạn chỉ đúng một Pod được ghi.
