Volume: ephemeral, hostPath và projected
Part IX là về lưu trữ, và viên gạch nền là volume. Tài liệu nêu hai vấn đề volume sinh ra để giải. Một: "On-disk files in a container are ephemeral ... when a container crashes or is stopped ... all of the files that were created or modified during the lifetime of the container are lost." Hai: "when multiple containers are running in a Pod and need to share files ... challenging to set up a shared filesystem." Volume vá cả hai: nó sống lâu hơn container và chia sẻ được giữa các container trong pod.
Điểm phải nắm trước khi vào chi tiết là vòng đời: tài liệu phân biệt "Ephemeral volume types have a lifetime linked to a specific Pod, but persistent volumes exist beyond the lifetime of any individual Pod." Bài này lo các volume ephemeral (sống-chết theo pod) và một loại đặc biệt mượn host; volume bền (sống lâu hơn pod) là PV/PVC ở các bài sau. Cách dùng luôn hai phần: khai nguồn ở spec.volumes, gắn vào container ở spec.containers[*].volumeMounts.
emptyDir: vùng nhớ tạm, chia sẻ trong pod
emptyDir là volume đơn giản nhất. Tài liệu: tạo ra "when the Pod is assigned to a node", ban đầu rỗng, và — đây là điểm vòng đời quan trọng — "When a Pod is removed from a node for any reason, the data in the emptyDir is deleted permanently", nhưng "A container crashing does not remove a Pod from a node. The data in an emptyDir volume is safe across container crashes." Tức nó sống theo pod, không theo container: container chết-sống lại vẫn thấy data, nhưng pod biến mất là sạch.
Công dụng chính là chia sẻ giữa các container trong pod. Một pod hai container dùng chung một emptyDir — writer ghi, reader đọc:
apiVersion: v1
kind: Pod
metadata: {name: shared-vol}
spec:
containers:
- name: writer
image: busybox:1.36
command: ["sh","-c","while true; do date +%T > /data/now.txt; sleep 2; done"]
volumeMounts: [{name: d, mountPath: /data}] # writer thấy ở /data
- name: reader
image: busybox:1.36
command: ["sh","-c","sleep 3600"]
volumeMounts: [{name: d, mountPath: /shared}] # reader thấy ở /shared
volumes:
- {name: d, emptyDir: {}} # cùng một volume
Hai container cùng gắn volume d (mount ở đường dẫn khác nhau). Đọc từ reader:
kubectl exec shared-vol -c reader -- cat /shared/now.txt
17:35:40
reader đọc đúng cái timestamp writer vừa ghi — chúng nhìn vào cùng một vùng lưu trữ dù mount path khác. Đây là cơ chế nền của pattern sidecar (Bài 19): init/sidecar chuẩn bị dữ liệu vào emptyDir, app đọc ra. (Thêm medium: Memory thì emptyDir thành tmpfs trong RAM — chính cái ta dùng để gây OOM/eviction ở Bài 22 và 38; sizeLimit giới hạn dung lượng.)
hostPath: mượn thư mục của node
hostPath mount một file/thư mục từ filesystem của node vào pod. Tạo một marker trên host worker-0 rồi mount nó:
ssh worker-0 'echo "toi-la-file-tren-host-worker-0" | sudo tee /var/k8s-demo/marker.txt'
apiVersion: v1
kind: Pod
metadata: {name: host-vol}
spec:
nodeName: worker-0 # ghim đúng node có file
containers:
- name: c
image: busybox:1.36
command: ["sh","-c","sleep 3600"]
volumeMounts: [{name: h, mountPath: /host-data, readOnly: true}]
volumes:
- name: h
hostPath: {path: /var/k8s-demo, type: Directory}
kubectl exec host-vol -- cat /host-data/marker.txt
toi-la-file-tren-host-worker-0
Pod đọc thẳng file trên host worker-0. Lưu ý hai điều: phải ghim node (nodeName: worker-0) vì file chỉ có trên node đó — đặt pod lên worker-1 sẽ không thấy (hoặc thấy thư mục khác); và hostPath nguy hiểm về bảo mật — pod chạm được filesystem node, mount / hay /var/run/docker.sock là cửa hậu chiếm node, nên Pod Security Standards (Part XI) chặn nó ở mức baseline/restricted. hostPath chỉ nên dùng cho agent hệ thống (DaemonSet đọc /var/log, như Bài 26), không cho app thường.
projected: gộp nhiều nguồn vào một chỗ
projected không phải lưu trữ mới — nó gộp nhiều nguồn có sẵn vào một thư mục. Tài liệu: "A projected volume maps several existing volume sources into the same directory." Các nguồn gộp được: secret, configMap, downwardAPI (Bài 22, 31), và serviceAccountToken. Một pod gộp cả bốn:
volumes:
- name: all
projected:
sources:
- configMap: {name: proj-cm, items: [{key: app.conf, path: cm/app.conf}]}
- secret: {name: proj-sec, items: [{key: db.pass, path: sec/db.pass}]}
- downwardAPI: {items: [{path: meta/labels, fieldRef: {fieldPath: metadata.labels}}]}
- serviceAccountToken: {audience: api, expirationSeconds: 3600, path: token/sa.jwt}
kubectl exec projected-vol -- sh -c '
echo "configMap: $(cat /projected/cm/app.conf)"
echo "secret: $(cat /projected/sec/db.pass)"
echo "downwardAPI: $(cat /projected/meta/labels)"
echo "saToken: $(cut -c1-25 /projected/token/sa.jwt)..."'
configMap: mode=prod
secret: hunter2
downwardAPI: team="storage"
saToken: eyJhbGciOiJSUzI1NiIsImtpZ...
Bốn nguồn khác loại — cấu hình, bí mật, metadata pod, và token — nằm gọn trong một cây thư mục /projected, mỗi nguồn một đường dẫn con. Tiện cho app chỉ cần trỏ vào một mount point. Đáng chú ý nhất là serviceAccountToken: nó tiêm một JWT ngắn hạn, có ràng buộc (eyJhbGci... là header JWT). Khác token kiểu cũ (Secret vĩnh viễn), token này có audience (chỉ dùng được với đúng người nhận), expirationSeconds (hết hạn rồi kubelet tự làm mới file) — an toàn hơn nhiều. Đó là cách pod hiện đại tự xác thực với API server, và ta sẽ gặp lại ở Bài 53 (ServiceAccount & token).
🧹 Dọn dẹp
kubectl delete pod shared-vol host-vol projected-vol --now
kubectl delete configmap proj-cm ; kubectl delete secret proj-sec
ssh worker-0 'sudo rm -rf /var/k8s-demo' # xóa marker trên host
Pod và object trong cluster xóa là sạch; riêng marker hostPath nằm trên host nên phải xóa tay trên worker-0 (một hệ quả nữa của việc hostPath sống ngoài vòng đời pod). Cụm về CoreDNS + metrics-server. Manifest ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 41-volumes.
Tổng kết
Volume vá hai vấn đề của file-trong-container: mất khi container restart, và không chia sẻ được giữa các container. Vòng đời là tiêu chí phân loại: emptyDir sống theo pod (an toàn qua container crash, mất khi pod biến mất) và chia sẻ được trong pod — ta thấy reader đọc đúng timestamp writer ghi vào cùng volume. hostPath mượn thư mục của node (sống ngoài pod, phải ghim node, nguy hiểm bảo mật — chỉ cho agent hệ thống). projected gộp configMap/secret/downwardAPI/serviceAccountToken vào một thư mục — đặc biệt serviceAccountToken tiêm JWT ngắn hạn có audience/expiry để pod xác thực với API server. Tất cả đều ephemeral (sống-chết theo pod). Bài 42 bước sang lưu trữ bền — PersistentVolume / PersistentVolumeClaim: tách "yêu cầu lưu trữ" (PVC, do app khai) khỏi "lưu trữ thật" (PV, do admin/CSI tạo), và làm rõ cái gì bind cái gì, cái gì tạo ra cái gì.