# 📁 Volume emptyDir, hostPath và volume từ ConfigMap, Secret

Chào bạn, mình đã đỗ CKA sau một lần thi và bài này là bài mình thấy "ăn điểm" nhất trong domain Storage. Lý do rất đơn giản: các câu hỏi về volume trong đề CKA thường ngắn, rõ ràng, và bạn có thể làm xong trong 3 đến 5 phút nếu nhớ đúng 4 loại volume cơ bản. Ngược lại, mình đã thấy rất nhiều bạn mất 15 phút cho một câu chỉ vì quên rằng ConfigMap mount dạng volume thì **không** cập nhật khi dùng `subPath`. Bài này sẽ giúp bạn tránh đúng cái bẫy đó.

---

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

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

- Phân biệt được vòng đời và mục đích dùng của `emptyDir`, `hostPath`, volume `configMap` và volume `secret`.
- Viết được YAML mount cả 4 loại volume trên vào pod, kể cả `items`, `defaultMode` và `subPath`.
- Biết chính xác khi nào dữ liệu trong volume tự cập nhật và khi nào không.
- Tạo nhanh ConfigMap và Secret bằng imperative command để tiết kiệm thời gian thi.
- Nhận ra các lỗi mount phổ biến (che mất file gốc, `subPath` không refresh, thiếu key) trước khi chúng làm hỏng bài thi.

---

## Ôn lại nhanh (5 phút)

Trước bài này bạn đã làm việc nhiều với pod spec, resource requests/limits và scheduling. Ba thứ cần nhớ lại ngay:

1. Pod spec có hai phần liên quan đến volume: `spec.volumes` (khai báo volume ở cấp pod) và `spec.containers[].volumeMounts` (gắn volume vào đường dẫn trong container). Tên volume ở hai chỗ phải trùng nhau.
2. Mọi container trong cùng một pod chia sẻ network namespace, và nếu cùng mount một volume thì chia sẻ luôn dữ liệu. Đây là nền tảng của pattern sidecar.
3. `kubectl explain` là người bạn tốt nhất khi quên field: `kubectl explain pod.spec.volumes.emptyDir`.

> [!NOTE]
> Volume không phải PVC. Volume là thứ gắn vào pod, PVC là thứ yêu cầu storage. Bài này chỉ nói về volume "nhẹ", bài sau mới sang PV/PVC.

---

## Lý thuyết

### 1. Bức tranh tổng thể: chọn loại volume nào?

```mermaid
flowchart TD
    Start["Cần đưa dữ liệu vào Pod"] --> Q1{"Dữ liệu chỉ cần sống cùng Pod?"}
    Q1 -->|"Có"| EmptyDir["📦<br/>emptyDir"]
    Q1 -->|"Không"| Q2{"Là config hoặc thông tin nhạy cảm?"}
    Q2 -->|"Có"| CM["🔐<br/>ConfigMap / Secret"]
    Q2 -->|"Không"| Q3{"Phải chạm filesystem của node?"}
    Q3 -->|"Có"| HostPath["🖥️<br/>hostPath"]
    Q3 -->|"Không"| PVC["💾<br/>PV / PVC (bài sau)"]
```

### 2. emptyDir

`emptyDir` là volume được tạo ra khi pod được gán vào node, và **bị xóa vĩnh viễn khi pod bị xóa** (hoặc khi pod bị chuyển sang node khác, ví dụ node bị drain).

Đặc điểm cần nhớ:

- Khởi tạo rỗng. Nếu container restart trong cùng pod, dữ liệu vẫn còn (vì pod chưa bị xóa).
- Dùng tốt nhất cho: scratch space, cache tạm, và trao đổi file giữa các container trong cùng pod (pattern sidecar).
- `medium: Memory` biến nó thành tmpfs nằm trong RAM, và **dung lượng này tính vào memory limit của container**. Đây là chi tiết nhiều bạn quên khi debug OOMKilled.

```yaml
volumes:
- name: scratch
  emptyDir:
    medium: Memory
    sizeLimit: 64Mi
```

### 3. hostPath

`hostPath` mount một đường dẫn từ filesystem của node vào container.

| `type` | Ý nghĩa |
| --- | --- |
| `DirectoryOrCreate` | Tạo thư mục trên node nếu chưa có, quyền 0755 |
| `Directory` | Thư mục phải tồn tại sẵn |
| `FileOrCreate` | Tạo file nếu chưa có, quyền 0644 |
| `File` | File phải tồn tại sẵn |
| `Socket`, `CharDevice`, `BlockDevice` | Dùng cho các thiết bị đặc biệt |

Rủi ro: pod gắn chặt với một node cụ thể. Pod bị schedule sang node khác là mất dữ liệu, và nội dung trên mỗi node lại khác nhau. Trong đề thi, `hostPath` hay xuất hiện ở các câu về DaemonSet hoặc node-level agent (thu thập log từ `/var/log`). Nếu đề cho phép, hãy dùng `DirectoryOrCreate` để tránh lỗi "path not found" khi node chưa có thư mục đó.

> [!WARNING]
> `hostPath` mount `/` hoặc `/var/lib/kubelet` là lỗ hổng bảo mật nghiêm trọng và có thể bị Pod Security Admission chặn ở namespace có label `restricted` hoặc `baseline`. Đừng tự thêm vào nếu đề không yêu cầu.

### 4. Volume từ ConfigMap

Khi mount ConfigMap dưới dạng volume, **mỗi key trở thành một file**: tên file là key, nội dung file là value.

```yaml
volumes:
- name: cfg
  configMap:
    name: app-config
    defaultMode: 0640
    items:
    - key: app.properties
      path: app.properties
    - key: feature.flag
      path: flags/feature.flag
```

- `items` cho phép chọn tập con các key và đổi đường dẫn tương đối (`path`) bên trong `mountPath`.
- `defaultMode` mặc định là `0644`. Secret volume cũng mặc định `0644`.
- Volume ConfigMap và Secret luôn là **read-only**. Container không ghi được vào đó.

**Cơ chế cập nhật:** kubelet đồng bộ định kỳ, nên khi bạn sửa ConfigMap, file trong pod sẽ được cập nhật sau khoảng 1 đến 2 phút. Kubelet làm việc này bằng cách tạo thư mục `..data` mới và đổi symlink, nên bạn sẽ thấy các file là symlink trỏ tới `..data/`. Đây là lý do việc cập nhật mang tính atomic.

**Ngoại lệ quan trọng nhất:** nếu bạn mount bằng `subPath`, file **không bao giờ** tự cập nhật. Phải xóa và tạo lại pod.

> [!IMPORTANT]
> Env var lấy từ ConfigMap (`valueFrom.configMapKeyRef`) cũng không cập nhật khi ConfigMap đổi. Muốn thấy giá trị mới thì phải restart pod. Đây là câu hỏi lý thuyết rất hay gặp.

### 5. Volume từ Secret

Cách hoạt động giống hệt ConfigMap, chỉ khác nguồn dữ liệu. Điểm cần nhớ:

- Giá trị Secret lưu trong `data` ở dạng base64. Khi mount thành file, nội dung đã được **giải mã**, tức là plaintext bên trong container.
- Secret có thể khai báo bằng `stringData` để viết thẳng plaintext trong YAML, Kubernetes sẽ tự encode.
- Nên đặt `defaultMode: 0400` cho file chứa mật khẩu.
- Secret dạng volume cũng không nên dùng làm nơi lưu credential cho ứng dụng đọc một lần lúc khởi động, vì lý do cập nhật ở trên.

### 6. Bảng so sánh nhanh

| Tiêu chí | emptyDir | hostPath | configMap | secret |
| --- | --- | --- | --- | --- |
| Sống theo pod | Có | Không (theo node) | Không (theo object) | Không (theo object) |
| Ghi được | Có | Có | Không | Không |
| Chia sẻ giữa các pod | Không | Có, nếu cùng node | Có | Có |
| Tự cập nhật khi nguồn đổi | Không áp dụng | Không áp dụng | Có, trừ `subPath` | Có, trừ `subPath` |
| Dùng trong đề thi cho | sidecar, cache | log agent, DaemonSet | config file | credential, TLS cert |

---

## Ví dụ có hướng dẫn

### Ví dụ 1: emptyDir chia sẻ dữ liệu giữa hai container

Mục tiêu: container `writer` ghi log, container `reader` đọc cùng file.

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: shared-scratch
spec:
  containers:
  - name: writer
    image: busybox
    command: ["sh", "-c", "while true; do date >> /scratch/log.txt; sleep 5; done"]
    volumeMounts:
    - name: scratch
      mountPath: /scratch
  - name: reader
    image: busybox
    command: ["sh", "-c", "tail -f /scratch/log.txt"]
    volumeMounts:
    - name: scratch
      mountPath: /scratch
  volumes:
  - name: scratch
    emptyDir: {}
```

Cách kiểm tra:

```bash
kubectl apply -f shared-scratch.yaml
kubectl logs shared-scratch -c reader
kubectl exec shared-scratch -c reader -- df -h /scratch
```

Đọc kết quả: hai container thấy cùng một file vì cùng mount `scratch`. Xóa pod là dữ liệu mất, đúng như bản chất `emptyDir`.

### Ví dụ 2: mount ConfigMap thành nhiều file cấu hình

Bước 1, tạo ConfigMap bằng imperative command (nhanh hơn viết YAML):

```bash
kubectl create configmap app-config \
  --from-literal=app.properties=$'mode=production\ntimeout=30' \
  --from-literal=feature.flag=on
```

Bước 2, tạo pod mount toàn bộ ConfigMap:

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: cfg-demo
spec:
  containers:
  - name: app
    image: busybox
    command: ["sh", "-c", "sleep 3600"]
    volumeMounts:
    - name: cfg
      mountPath: /etc/app
  volumes:
  - name: cfg
    configMap:
      name: app-config
      defaultMode: 0640
```

Bước 3, kiểm tra:

```bash
kubectl exec cfg-demo -- ls -l /etc/app
kubectl exec cfg-demo -- cat /etc/app/app.properties
kubectl exec cfg-demo -- cat /etc/app/feature.flag
```

Bạn sẽ thấy `app.properties` và `feature.flag` đều là symlink trỏ vào `..data/`. Đó là dấu hiệu volume đang hoạt động đúng cơ chế cập nhật atomic.

### Ví dụ 3: Secret với subPath để ghi đè đúng một file

Tình huống thực tế: bạn muốn mount mỗi file password vào một đường dẫn cụ thể, không muốn mount cả thư mục.

```bash
kubectl create secret generic db-secret --from-literal=password=S3cr3tPass
```

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: secret-demo
spec:
  containers:
  - name: app
    image: busybox
    command: ["sh", "-c", "sleep 3600"]
    volumeMounts:
    - name: db-secret
      mountPath: /etc/app/db-password
      subPath: db-password
      readOnly: true
  volumes:
  - name: db-secret
    secret:
      secretName: db-secret
      defaultMode: 0400
      items:
      - key: password
        path: db-password
```

Kiểm tra:

```bash
kubectl exec secret-demo -- cat /etc/app/db-password
kubectl exec secret-demo -- ls -l /etc/app/db-password
```

Lưu ý: với `subPath`, `mountPath` phải là đường dẫn file, không phải thư mục. Và nhớ rằng file này sẽ không tự cập nhật.

---

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

Phần này là trọng tâm. Hãy làm từng bài, gõ lệnh bằng tay, đừng copy. Mục tiêu là phản xạ với cú pháp `volumes` và `volumeMounts` trong khoảng 2 phút mỗi bài.

### Bài 1: ConfigMap từ file, mount vào thư mục

Yêu cầu:

1. Tạo file `index.html` với nội dung `hello CKA`.
2. Tạo ConfigMap tên `web-html` từ file đó (`--from-file`).
3. Tạo pod `web` dùng image `nginx`, mount ConfigMap `web-html` vào `/usr/share/nginx/html`, nhưng chỉ lấy key `index.html`.
4. Kiểm tra bằng cách exec vào pod và đọc file.

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

```bash
echo "hello CKA" > index.html
kubectl create configmap web-html --from-file=index.html
```

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  containers:
  - name: nginx
    image: nginx
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
  volumes:
  - name: html
    configMap:
      name: web-html
      items:
      - key: index.html
        path: index.html
```

```bash
kubectl apply -f web.yaml
kubectl exec web -- cat /usr/share/nginx/html/index.html
```

Bẫy ở đây: nếu bạn mount vào `/usr/share/nginx/html` mà ConfigMap có nhiều key rác, thư mục sẽ chứa lẫn các file đó. Dùng `items` để kiểm soát.

</details>

### Bài 2: Sửa ConfigMap và quan sát cơ chế cập nhật

Yêu cầu:

1. Tạo pod `watcher` (image `busybox`, `sleep 3600`) mount ConfigMap `app-config` từ Ví dụ 2 vào `/etc/app`.
2. Sửa ConfigMap bằng `kubectl edit configmap app-config`, đổi `feature.flag` thành `off`.
3. Theo dõi file trong pod, ghi lại thời gian tới khi giá trị thay đổi.
4. Bây giờ tạo thêm pod `watcher-sub` cũng mount key `feature.flag` nhưng bằng `subPath`, và lặp lại bước 2 và 3.

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

Theo dõi bằng vòng lặp:

```bash
while true; do
  echo "$(date +%H:%M:%S) $(kubectl exec watcher -- cat /etc/app/feature.flag 2>/dev/null)"
  sleep 10
done
```

Kết quả mong đợi: `watcher` thấy giá trị mới sau khoảng 30 giây đến 2 phút. `watcher-sub` **không bao giờ** thấy giá trị mới, dù chờ 10 phút. Đó chính là lý do phải xóa và tạo lại pod.

</details>

### Bài 3: emptyDir với medium Memory

Yêu cầu: tạo pod `memcache`, mount một `emptyDir` có `medium: Memory` và `sizeLimit: 32Mi` vào `/cache`, sau đó kiểm tra filesystem type và dung lượng.

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

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: memcache
spec:
  containers:
  - name: app
    image: busybox
    command: ["sh", "-c", "sleep 3600"]
    volumeMounts:
    - name: cache
      mountPath: /cache
  volumes:
  - name: cache
    emptyDir:
      medium: Memory
      sizeLimit: 32Mi
```

```bash
kubectl apply -f memcache.yaml
kubectl exec memcache -- df -h /cache
kubectl exec memcache -- mount | grep /cache
```

`df -h` sẽ hiển thị khoảng 32M và `mount` cho thấy `tmpfs`. Nhớ rằng dung lượng này ăn vào memory limit của container, nên nếu ứng dụng cần nhiều RAM, đừng đặt `sizeLimit` quá lớn.

</details>

### Bài 4: Tìm và sửa lỗi mount che mất file gốc

Yêu cầu: áp dụng pod dưới đây, quan sát nginx không khởi động được và giải thích nguyên nhân, sau đó sửa.

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: broken-nginx
spec:
  containers:
  - name: nginx
    image: nginx
    volumeMounts:
    - name: cfg
      mountPath: /etc/nginx
  volumes:
  - name: cfg
    configMap:
      name: app-config
```

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

```bash
kubectl apply -f broken-nginx.yaml
kubectl get pod broken-nginx
kubectl describe pod broken-nginx | tail -20
kubectl logs broken-nginx
```

Nguyên nhân: mount vào `/etc/nginx` đã che toàn bộ nội dung gốc của thư mục đó, bao gồm `nginx.conf`. Container không tìm thấy config và thoát.

Cách sửa đúng: mount vào thư mục con mà nginx tự đọc, ví dụ `/etc/nginx/conf.d`, hoặc dùng `subPath` để ghi đè đúng một file cụ thể chứ không che cả thư mục.

```yaml
    volumeMounts:
    - name: cfg
      mountPath: /etc/nginx/conf.d/default.conf
      subPath: default.conf
```

</details>

### Bài 5 (nâng cao, làm nếu còn thời gian): hostPath với DirectoryOrCreate

Yêu cầu: tạo pod `hostwriter` dùng `hostPath` type `DirectoryOrCreate` trỏ tới `/tmp/cka-data`, sau đó xóa pod và kiểm tra dữ liệu còn hay mất.

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

```yaml
  volumes:
  - name: data
    hostPath:
      path: /tmp/cka-data
      type: DirectoryOrCreate
```

```bash
kubectl exec hostwriter -- sh -c "echo persist > /data/proof.txt"
kubectl delete pod hostwriter
```

Sau khi xóa pod, nếu tạo lại một pod khác cùng mount `/tmp/cka-data`, file `proof.txt` vẫn còn, vì dữ liệu nằm trên node chứ không nằm trong pod. Đây chính là điểm khác biệt cốt lõi so với `emptyDir`.

Trên cluster nhiều node, muốn kiểm chứng bạn cần biết pod chạy ở node nào (`kubectl get pod -o wide`) và sau đó dùng `kubectl debug node/<node>` để xem file.

</details>

---

## Góc chuyên gia

**Những lỗi mình từng mắc, hoặc thấy người khác mắc trong phòng thi:**

- Mount ConfigMap vào một thư mục đã có nội dung quan trọng. Kubernetes sẽ che toàn bộ thư mục đó. Nguyên tắc: mount vào thư mục chuyên dụng, hoặc dùng `subPath` cho đúng một file.
- Quên rằng `volumeMounts.name` phải khớp chính xác với `volumes.name`. Đây là lỗi gõ tay phổ biến nhất khi làm bài nhanh. Lệnh `kubectl apply --dry-run=server` sẽ không bắt được lỗi này, nhưng `kubectl describe pod` thì có.
- Tưởng rằng sửa ConfigMap là pod tự nhận config mới ngay lập tức. Không có chuyện đó với env var và với `subPath`.
- Dùng `hostPath` cho dữ liệu cần bền vững rồi ngạc nhiên khi Deployment scale hoặc reschedule làm mất dữ liệu.
- Quên `defaultMode: 0400` cho Secret chứa mật khẩu. Không sai về chức năng nhưng mất điểm "chi tiết" nếu đề yêu cầu rõ.

**Mẹo tốc độ cho CKA:**

- Tạo ConfigMap/Secret luôn bằng imperative, không viết YAML tay:

```bash
kubectl create configmap app-config --from-literal=key1=v1 --from-literal=key2=v2
kubectl create secret generic db-secret --from-literal=password=S3cr3t
kubectl create configmap from-file-cm --from-file=./config.yaml
```

- Dùng `kubectl explain` khi quên field, và `--dry-run=client -o yaml` để lấy khung YAML:

```bash
kubectl run tmp --image=busybox --dry-run=client -o yaml -- sleep 3600 > pod.yaml
```

- Khi cần mount vào một Deployment đang chạy, dùng `kubectl edit deploy` thay vì apply lại toàn bộ file. Nhanh hơn và ít rủi ro sai sót.
- Luôn kiểm tra bằng `kubectl exec`. Đừng nộp bài chỉ vì `kubectl get pod` báo Running.

**Quy tắc ngón tay:**

- Dữ liệu tạm, chia sẻ trong pod: `emptyDir`.
- Config hoặc credential: `configMap` hoặc `secret`, ưu tiên volume nếu cần cập nhật động, ưu tiên env nếu chỉ đọc một lần.
- Cần chạm node: `hostPath`, và chỉ khi đề nói rõ.
- Còn lại: chờ bài sau về PV/PVC.

> [!TIP]
> Trong đề CKA, nếu câu hỏi nói "update the pod so it reads the new ConfigMap" thì 90% đáp án là xóa và tạo lại pod, hoặc kiểm tra xem có `subPath` không. Đọc kỹ chữ "automatically" trước khi trả lời.

---

## Kiểm tra nhanh bằng cloze

```cloze
emptyDir chỉ sống theo {{c1::Pod}}: pod bị xóa thì dữ liệu cũng mất.
Đặt {{c2::medium: Memory}} cho emptyDir để dữ liệu nằm trong RAM dưới dạng tmpfs.
Khi mount ConfigMap dạng volume, mỗi {{c3::key}} trở thành một file riêng.
Mount bằng {{c4::subPath}} thì file sẽ KHÔNG tự cập nhật khi ConfigMap thay đổi.
Quyền file mặc định khi mount ConfigMap hoặc Secret là {{c5::0644}}.
hostPath type {{c6::DirectoryOrCreate}} sẽ tạo thư mục trên node nếu chưa tồn tại.
```

---

## Thẻ ôn tập

```flashcard
[
  { "front": "emptyDir tồn tại trong bao lâu?", "back": "Trọn vòng đời của Pod; bị xóa khi Pod bị xóa hoặc bị chuyển sang node khác." },
  { "front": "emptyDir với medium: Memory dùng gì và tính vào đâu?", "back": "tmpfs trong RAM, dung lượng tính vào memory limit của container." },
  { "front": "hostPath type nào tạo thư mục trên node nếu chưa có?", "back": "DirectoryOrCreate" },
  { "front": "Rủi ro chính của hostPath là gì?", "back": "Dữ liệu gắn với một node cụ thể; pod reschedule sang node khác là mất dữ liệu, và đây là lỗ hổng bảo mật." },
  { "front": "Mỗi key trong ConfigMap khi mount dạng volume trở thành gì?", "back": "Một file riêng, tên file là key, nội dung là value." },
  { "front": "Field nào chọn key và đổi đường dẫn file khi mount ConfigMap?", "back": "items với cặp key và path, kết hợp defaultMode để đặt quyền." },
  { "front": "ConfigMap thay đổi thì file trong volume có tự cập nhật không?", "back": "Có, sau khoảng 1 đến 2 phút, do kubelet đồng bộ định kỳ." },
  { "front": "Điều kiện nào khiến volume ConfigMap KHÔNG tự cập nhật?", "back": "Khi mount bằng subPath; phải xóa và tạo lại Pod." },
  { "front": "Env var lấy từ ConfigMap có tự cập nhật khi ConfigMap đổi không?", "back": "Không, phải restart Pod để nhận giá trị mới." },
  { "front": "defaultMode mặc định của volume ConfigMap và Secret là bao nhiêu?", "back": "0644" },
  { "front": "Nội dung Secret khi mount thành file ở dạng nào?", "back": "Đã giải mã base64, tức là plaintext bên trong container." },
  { "front": "Volume ConfigMap và Secret cho phép ghi hay chỉ đọc?", "back": "Chỉ đọc (read-only)." },
  { "front": "Lệnh tạo nhanh ConfigMap từ hai literal?", "back": "kubectl create configmap app-config --from-literal=k1=v1 --from-literal=k2=v2" },
  { "front": "Lỗi gì xảy ra khi mount ConfigMap vào /etc/nginx?", "back": "Toàn bộ nội dung gốc của thư mục bị che, gồm nginx.conf, nên nginx không khởi động được." }
]
```

---

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

Chốt lại 5 dòng để bạn ghi ra giấy nháp:

1. `emptyDir` sống cùng pod, xóa pod là mất, dùng cho cache và chia sẻ giữa các container.
2. `hostPath` bám vào filesystem của node, tiện cho agent nhưng nguy hiểm và không bền vững.
3. ConfigMap và Secret mount dạng volume thì mỗi key thành một file, mặc định `0644`, read-only.
4. `items`, `defaultMode`, `subPath` là ba field quyết định hình dạng cuối cùng của file trong container.
5. `subPath` và env var thì không tự cập nhật. Đây là bẫy thi phổ biến nhất của domain Storage.

Bài sau chúng ta bước sang phần nặng ký hơn: **PV, PVC với binding, access modes và reclaim policy**. Những gì bạn vừa học về `volumes` và `volumeMounts` vẫn dùng nguyên, chỉ khác là nguồn dữ liệu lúc đó sẽ là một PVC thay vì ConfigMap hay `emptyDir`. Bạn sẽ thấy `kubectl get pvc` báo `Pending` rất nhiều lần, và bài 5.4 sẽ dạy cách gỡ đúng những lỗi đó.
