# 🔗 PV, PVC: binding, access modes và reclaim policy

> Bài này là phần thực hành. Mình sẽ không giảng lý thuyết dài, mà đi thẳng vào 5 bài tập nhỏ. Làm xong 5 bài này là bạn xử lý được gần như mọi câu PV/PVC trong đề CKA.

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

Sau khoảng 30 phút, bạn làm được:

- Phân biệt PV (cluster-scoped) và PVC (namespaced), hiểu ai "xin" và ai "cấp".
- Tạo một cặp PV + PVC khớp nhau và đưa PVC sang trạng thái `Bound`.
- Đọc đúng 4 access modes, và nhớ rằng `ReadWriteOnce` không phải "một pod".
- Chọn `persistentVolumeReclaimPolicy` phù hợp và biết cách đưa PV từ `Released` về `Available`.

## Ôn nhanh từ bài trước

Bài trước bạn đã dùng `emptyDir`, `hostPath` và volume từ ConfigMap/Secret. Cả ba đều buộc vào vòng đời pod: pod bị xóa, volume bị xóa hoặc mồ côi.

PV/PVC ra đời để **tách vòng đời dữ liệu khỏi vòng đời pod**. Pod chết, PVC còn, dữ liệu còn. Đó là toàn bộ lý do cặp đôi này tồn tại.

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

Cách mình hay hình dung: **PV là ổ đĩa có sẵn, PVC là đơn xin cấp ổ đĩa, control plane (PersistentVolume controller) là người se duyên.** Bạn viết đơn, nó đi tìm ổ đĩa khớp, khớp thì gắn hai bên lại.

```mermaid
flowchart TD
    A["📝 Tạo PVC"] --> B{"🔍 Có PV khớp?"}
    B -->|"storageClassName + accessModes + capacity"| C["🔗 PVC Bound"]
    B -->|"lệch một điều kiện"| D["⏳ PVC Pending"]
    D --> E["🛠️ Sửa PVC hoặc tạo PV to hơn"]
    E --> B
```

### Bước 1: tạo PV

Không có lệnh imperative kiểu `kubectl create pv`, nên bạn phải viết YAML. Tạo file `pv-demo.yaml`:

```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-demo
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: ""
  hostPath:
    path: /mnt/data/pv-demo
    type: DirectoryOrCreate
```

```bash
kubectl apply -f pv-demo.yaml
kubectl get pv
```

```
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   AGE
pv-demo   1Gi        RWO            Retain           Available                        10s
```

### Bước 2: tạo PVC

Tạo file `pvc-demo.yaml`. Xin ít hơn PV đang có, đây là chuyện bình thường:

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-demo
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 500Mi
  storageClassName: ""
```

```bash
kubectl apply -f pvc-demo.yaml
kubectl get pvc
```

```
NAME       STATUS   VOLUME    CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pvc-demo   Bound    pv-demo   1Gi        RWO                           5s
```

Để ý: PVC xin 500Mi nhưng nhận cả 1Gi. ==Capacity của PV phải lớn hơn hoặc bằng `requests.storage` của PVC==, không cần bằng nhau.

### Bước 3: dùng PVC trong pod

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-demo
spec:
  containers:
    - name: app
      image: busybox
      command: ["sh", "-c", "sleep 3600"]
      volumeMounts:
        - name: data
          mountPath: /data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: pvc-demo
```

```bash
kubectl apply -f pod-demo.yaml
kubectl exec pod-demo -- sh -c "echo hello-cka > /data/note.txt && cat /data/note.txt"
```

Nếu in ra `hello-cka`, PVC đã được mount đúng.

### Bước 4: xem reclaim policy hành động

```bash
kubectl delete pod pod-demo
kubectl delete pvc pvc-demo
kubectl get pv
```

```
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS     CLAIM             STORAGECLASS   AGE
pv-demo   1Gi        RWO            Retain           Released   default/pvc-demo                 3m
```

PV không bị xóa, nó chuyển sang `Released`. Dữ liệu vẫn nằm trên đĩa nhưng PV chưa bind lại được, vì `spec.claimRef` vẫn trỏ tới PVC đã chết. Bước 5 là cách cứu:

```bash
kubectl patch pv pv-demo -p '{"spec":{"claimRef":null}}'
kubectl get pv pv-demo
```

```
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   AGE
pv-demo   1Gi        RWO            Retain           Available                         4m
```

## Luyện tập có hướng dẫn

Làm đúng thứ tự, mỗi bài tự bấm giờ. Bài 1 và 2 là ví dụ trên, bài 3 đến 5 là phần ăn điểm trong đề thi.

### Bài 1 (5 phút): tạo cặp PV/PVC Bound

Tạo `pv-web` 2Gi, `ReadWriteOnce`, reclaim policy `Retain`, `storageClassName: ""`, hostPath `/mnt/data/pv-web`, và `pvc-web` xin 1Gi cùng access mode, cùng storageClassName.

**Kiểm tra:**

```bash
kubectl get pvc pvc-web -o wide
kubectl get pvc pvc-web -o jsonpath='{.spec.volumeName}{"\n"}'
```

Kỳ vọng: `STATUS = Bound`, và lệnh jsonpath in ra `pv-web`. Đây là cách nhanh nhất để biết PVC đang cắm vào PV nào.

### Bài 2 (5 phút): ghi dữ liệu qua PVC

Tạo pod `pod-web` dùng image `nginx`, mount `pvc-web` vào `/usr/share/nginx/html`, sau đó ghi một file:

```bash
kubectl exec pod-web -- sh -c "echo 'CKA storage ok' > /usr/share/nginx/html/index.html"
kubectl exec pod-web -- cat /usr/share/nginx/html/index.html
```

Xóa pod, tạo lại pod mới cùng PVC, đọc lại file. Dữ liệu vẫn còn, đó chính là điểm khác biệt so với `emptyDir` ở bài trước.

### Bài 3 (7 phút): cố tình lệch accessModes

Tạo `pvc-rwx.yaml` xin `ReadWriteMany`, 1Gi, `storageClassName: ""`:

```bash
kubectl apply -f pvc-rwx.yaml
kubectl get pvc pvc-rwx
kubectl describe pvc pvc-rwx | tail -n 8
```

Kỳ vọng: `STATUS = Pending`, event kiểu:

```
Warning  ProvisioningFailed  persistentvolume-controller  no persistent volumes available for this claim and no storage class is set
```

Giờ sửa. **Bẫy ở đây:** bạn không thể `kubectl edit` để đổi accessModes, API sẽ trả về `Forbidden: updates to persistentvolumeclaims spec are forbidden`. Cách đúng là xóa và tạo lại:

```bash
kubectl delete pvc pvc-rwx
# sửa accessModes thành ReadWriteOnce rồi apply lại
```

Tự kiểm tra: nếu xóa PVC rồi apply lại với `ReadWriteOnce` và 1Gi, PVC có tự bind vào `pv-web` (2Gi, RWO) không? Đáp án là có.

### Bài 4 (6 phút): lệch capacity

Tạo PVC `pvc-big` xin **3Gi**, `ReadWriteOnce`, `storageClassName: ""`. Không có PV nào đủ lớn, nên PVC `Pending`.

Đừng sửa PVC. Hãy tạo thêm một PV đủ lớn:

```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-big
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: ""
  hostPath:
    path: /mnt/data/pv-big
    type: DirectoryOrCreate
```

```bash
kubectl apply -f pv-big.yaml
kubectl get pvc pvc-big
```

PV mới xuất hiện và binder tự gắn ngay, không cần can thiệp. Bài học: **bạn chỉ cần làm cho phía PV khớp, không nhất thiết phải sửa đơn xin.** Và nếu có nhiều PV cùng khớp, Kubernetes chọn PV **nhỏ nhất vẫn đủ lớn**.

### Bài 5 (7 phút): Retain, Released và claimRef

```bash
kubectl delete pod pod-web --ignore-not-found
kubectl delete pvc pvc-web
kubectl get pv
```

Kỳ vọng: `pv-web` ở trạng thái `Released`, cột `CLAIM` vẫn ghi `default/pvc-web`.

Giờ thử bind lại mà chưa xử lý gì:

```bash
kubectl apply -f pvc-web.yaml
kubectl get pvc pvc-web
```

PVC mới sẽ `Pending`, vì PV đang `Released` chứ không phải `Available`. Sửa bằng cách gỡ `claimRef`:

```bash
kubectl patch pv pv-web -p '{"spec":{"claimRef":null}}'
kubectl get pv pv-web
```

PV về `Available`, và PVC `Pending` ở trên sẽ tự bind trong vài giây. Đây là mẹo rất hay bị hỏi trong đề: **`Retain` + `Released` + xóa `claimRef` là bộ ba đi liền nhau.**

## Lỗi thường gặp và cách xử lý

| Triệu chứng | Nguyên nhân | Cách xử lý |
| --- | --- | --- |
| PVC `Pending`, event `no persistent volumes available for this claim and no storage class is set` | Không có PV nào khớp `accessModes`, capacity hoặc `storageClassName` | So `kubectl describe pv` với `kubectl describe pvc`, sửa bên lệch |
| PVC `Pending` dù đã tạo PV đúng | PV có `storageClassName: manual` nhưng PVC để trống, hoặc ngược lại | Hai bên phải ghi **giống hệt nhau**, kể cả chuỗi rỗng `""` |
| `kubectl edit pvc` báo `Forbidden: updates to persistentvolumeclaims spec are forbidden` | `spec.accessModes`, `storageClassName` gần như immutable | Xóa PVC và tạo lại, hoặc tạo PV đúng ý |
| PV `Released` mãi, tạo PVC mới vẫn `Pending` | `spec.claimRef` còn trỏ tới PVC cũ | `kubectl patch pv <ten> -p '{"spec":{"claimRef":null}}'` |
| Xóa PVC báo lỗi hoặc treo ở `Terminating` | Finalizer `kubernetes.io/pvc-protection` vì còn pod đang dùng | Xóa pod trước, rồi xóa PVC |
| Pod `Pending` dù PVC đã `Bound` | Pod có thể đang thiếu node hoặc sai `claimName` | `kubectl describe pod` xem event, đối chiếu tên PVC |

> [!WARNING]
> Có một trường hợp rất dễ sửa sai: PVC `Pending` không phải lúc nào cũng do PV. Nếu cluster của bạn có StorageClass mặc định, PVC không ghi `storageClassName` sẽ bị dynamic provisioning "cướp" và bind vào một PV khác hoàn toàn. Trong bài này mình luôn ghi `storageClassName: ""` để tránh chuyện đó.

> [!NOTE]
> `ReadWriteOnce` nghĩa là **một node** mount read-write, không phải một pod. Hai pod trên cùng một node vẫn dùng chung một PVC RWO bình thường. Chỉ `ReadWriteOncePod` mới giới hạn đúng một pod trong toàn cluster.

## Góc chuyên gia

**Bảng access modes, học đúng 4 dòng này là đủ:**

| Access mode | Viết tắt | Ý nghĩa thực tế |
| --- | --- | --- |
| `ReadWriteOnce` | RWO | Một node mount read-write |
| `ReadOnlyMany` | ROX | Nhiều node mount read-only |
| `ReadWriteMany` | RWX | Nhiều node mount read-write, cần backend hỗ trợ (NFS, CephFS) |
| `ReadWriteOncePod` | RWOP | Đúng một pod trong cluster mount read-write |

**Bảng reclaim policy:**

| Policy | Sau khi PVC bị xóa | Dùng khi |
| --- | --- | --- |
| `Retain` | PV sang `Released`, dữ liệu còn, phải xử lý tay | Dữ liệu quan trọng, an toàn khi thi |
| `Delete` | Xóa PV object và backing storage | Lab, dữ liệu tạm |
| `Recycle` | Xóa nội dung rồi về `Available` | Đã deprecated, đừng dùng |

Mặc định: PV tạo thủ công là `Retain`, PV do dynamic provisioning tạo là `Delete`. Cứ ghi rõ ra trong YAML cho chắc.

**Mẹo thi:**

- `kubectl explain pv.spec --recursive` là phao cứu sinh khi quên field. Không cần nhớ hết, chỉ cần nhớ lệnh này.
- Muốn chắc chắn PVC bind đúng PV, thêm `volumeName: pv-demo` vào `spec` của PVC. Nó ép binder chỉ xét đúng PV đó, tránh bị PV khác "giành".
- Thứ tự khớp khi bind: `storageClassName` → `accessModes` → capacity → `selector`. Lệch ở bước nào, `describe pvc` sẽ dừng ở bước đó.
- Trước khi nộp, luôn chạy `kubectl get pvc` xem `STATUS`. `Bound` mới là xong, `Pending` là mất điểm.
- Đừng dùng `hostPath` cho câu hỏi mô tả production. NHƯNG trong lab CKA thì `hostPath` là cách nhanh nhất để có một PV hợp lệ, vì bạn không có NFS server.

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

Năm dòng để bạn ghi ra giấy nháp:

1. PV là cluster-scoped, PVC là namespaced.
2. Bind thành công khi `storageClassName`, `accessModes` khớp và capacity PV lớn hơn hoặc bằng request.
3. RWO là per-node, không phải per-pod.
4. `Retain` để lại PV ở `Released`, muốn dùng lại thì xóa `claimRef`.
5. PVC gần như immutable: sai access mode thì xóa tạo lại.

**Kiểm tra miệng 60 giây:**

```cloze
PV là resource cấp {{c1::cluster}}, còn PVC là resource cấp {{c2::namespace}}.
Access mode cho phép nhiều node mount read-write là {{c3::ReadWriteMany}}.
Reclaim policy mặc định của PV tạo thủ công là {{c4::Retain}}, còn PV do StorageClass tạo động mặc định là {{c5::Delete}}.
PVC ở trạng thái {{c6::Pending}} nghĩa là chưa tìm được PV phù hợp.
Muốn PV đang ở trạng thái Released bind lại được, ta xóa {{c7::claimRef}} trong spec của PV.
```

**Bộ thẻ ôn tập:**

```flashcard
[
  { "front": "PV là resource cấp cluster hay namespace?", "back": "Cluster-scoped. PVC mới là namespaced." },
  { "front": "Kể tên 4 access modes của PV", "back": "ReadWriteOnce, ReadOnlyMany, ReadWriteMany, ReadWriteOncePod." },
  { "front": "ReadWriteOnce giới hạn theo node hay theo pod?", "back": "Theo node. Nhiều pod cùng node vẫn dùng chung được." },
  { "front": "PVC chuyển sang Bound khi nào?", "back": "Khi controller tìm được PV khớp storageClassName, accessModes và capacity." },
  { "front": "PV 1Gi có bind được với PVC xin 2Gi không?", "back": "Không. Capacity PV phải lớn hơn hoặc bằng requests.storage." },
  { "front": "Có nhiều PV cùng khớp thì chọn cái nào?", "back": "PV nhỏ nhất vẫn đủ lớn." },
  { "front": "Ba reclaim policy của PV?", "back": "Retain, Delete và Recycle (Recycle đã deprecated)." },
  { "front": "Reclaim policy mặc định của PV tạo thủ công?", "back": "Retain." },
  { "front": "Reclaim policy mặc định của PV do dynamic provisioning tạo?", "back": "Delete." },
  { "front": "PV ở trạng thái Released nghĩa là gì?", "back": "PVC đã bị xóa, dữ liệu còn nhưng PV chưa bind lại được." },
  { "front": "Làm sao đưa PV từ Released về Available?", "back": "Gỡ spec.claimRef bằng kubectl patch pv <ten> -p '{\"spec\":{\"claimRef\":null}}'." },
  { "front": "Sửa accessModes của PVC đã tạo được không?", "back": "Không. Spec gần như immutable, phải xóa và tạo lại." },
  { "front": "storageClassName rỗng (\"\") trên PVC có ý nghĩa gì?", "back": "Chỉ bind với PV không có StorageClass, bỏ qua StorageClass mặc định." },
  { "front": "Lệnh xem PVC đang bind vào PV nào?", "back": "kubectl get pvc <ten> -o jsonpath='{.spec.volumeName}'." }
]
```

**Bài tiếp theo:** bạn đã tự tay tạo PV, nghĩa là đang làm **static provisioning**. Bài 5.3 sẽ bỏ hẳn bước đó: chỉ cần một StorageClass, PVC tự sinh PV. Bạn sẽ thấy vì sao hầu hết cluster thật không ai viết PV bằng tay, và vì sao `WaitForFirstConsumer` là field khiến nhiều người mất điểm oan.
