📁 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?

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

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.

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:

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

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:

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:

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.

kubectl create secret generic db-secret --from-literal=password=S3cr3tPass
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:

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.
Xem gợi ý và đáp án
echo "hello CKA" > index.html
kubectl create configmap web-html --from-file=index.html
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
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.

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.
Xem gợi ý và đáp án

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

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.

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.

Xem gợi ý và đáp án
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
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.

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.

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
Xem gợi ý và đáp án
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.

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

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.

Xem gợi ý và đáp án
  volumes:
  - name: data
    hostPath:
      path: /tmp/cka-data
      type: DirectoryOrCreate
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.


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

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

[
  { "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 đó.