Ephemeral container và kubectl debug
Mấy bài trước ta hay kubectl exec ... -- sh để chui vào pod xem xét. Nhưng cách đó dựa trên một giả định: container có shell. Trong production thì giả định này thường sai, và sai một cách cố ý. Image distroless (chỉ chứa app + thư viện, không shell, không ps, không cat) là khuyến nghị bảo mật phổ biến vì nó cắt gần hết bề mặt tấn công. Cái giá phải trả: khi pod có vấn đề, kubectl exec không còn gì để chạy. Tài liệu nói thẳng vấn đề này:
"Since distroless images do not include a shell or any debugging utilities, it's difficult to troubleshoot distroless images using
kubectl execalone."
Lời giải là ephemeral container: nhét tạm thời một container công cụ (busybox, hay image debug chuyên dụng) vào pod đang chạy, không phải restart pod, không phải sửa image. Bài này đào ngữ nghĩa ephemeral container rồi kiểm chứng cả ba chế độ của lệnh gói nó lại: kubectl debug.
Vì sao không thể chỉ "thêm một container nữa"
Câu hỏi đầu tiên: sao không khai thêm container debug vào spec.containers cho xong? Vì spec của pod đang chạy gần như bất biến: sửa spec.containers đòi tạo lại pod, mà tạo lại pod nghĩa là làm mất hiện trường ta đang cần xem. Ephemeral container sinh ra để lách đúng chỗ này. Tài liệu định nghĩa:
"Ephemeral containers differ from other containers in that they lack guarantees for resources or execution, and they will never be automatically restarted, so they are not appropriate for building applications."
Và mục đích:
"a special type of container that runs temporarily in an existing Pod to accomplish user-initiated actions such as troubleshooting. You use ephemeral containers to inspect services rather than to build applications."
Vì là công cụ gỡ rối chứ không phải để chạy ứng dụng, nó bị cắt nhiều thứ:
"Ephemeral containers may not have ports, so fields such as
ports,livenessProbe,readinessProbeare disallowed." và "Pod resource allocations are immutable, so settingresourcesis disallowed."
Quan trọng nhất là cách thêm nó, không qua spec thông thường:
"Ephemeral containers are created using a special
ephemeralcontainershandler in the API rather than by adding them directly topod.spec, so it's not possible to add an ephemeral container usingkubectl edit."
Tức có một subresource API riêng tên ephemeralcontainers để gắn nó vào pod đang sống. Ta sẽ không gọi API thô vì kubectl debug lo việc đó. Và một khi đã gắn, không gỡ ra hay sửa được nữa ("you may not change or remove an ephemeral container after you have added it"); nó sống tới khi pod chết.
Chế độ 1: gắn ephemeral container vào pod đang chạy
Dựng một pod cố tình không có shell để tái hiện ca distroless. Image pause của Kubernetes là ví dụ hoàn hảo: nó chỉ chạy một binary /pause ngủ mãi, không kèm shell hay tiện ích nào:
apiVersion: v1
kind: Pod
metadata:
name: noshell
spec:
containers:
- name: app
image: registry.k8s.io/pause:3.10.2
Thử exec vào nó như thói quen:
kubectl exec -it noshell -- /bin/sh -c 'echo hi'
error: Internal error occurred: ... OCI runtime exec failed: exec failed:
unable to start container process: exec: "/bin/sh": stat /bin/sh: no such file or directory
Không có /bin/sh để chạy, đúng cảnh distroless. kubectl exec bó tay. Giờ dùng kubectl debug gắn một ephemeral container busybox vào, kèm cờ --target:
kubectl debug noshell -it --image=busybox:1.36 --target=app --container=debugger
# (ở đây chạy non-interactive để chụp output:)
kubectl debug noshell -it=false --image=busybox:1.36 --target=app --container=debugger -- ps -ef
Targeting container "app". If you don't see processes from this container it may be
because the container runtime doesn't support this feature.
PID USER TIME COMMAND
1 65535 0:00 /pause
14 root 0:00 ps -ef
Container app không hề có shell, nhưng ta vẫn ps -ef được, vì lệnh chạy trong container debugger (busybox, có đủ công cụ), không phải trong app. Và để ý dòng PID 1 /pause: ta nhìn thấy tiến trình của container app từ trong debugger. Đó là nhờ cờ --target=app.
--target bật process namespace sharing giữa debugger và container đích: debugger được nhập vào namespace tiến trình của app, nên ps trong debugger liệt kê cả /pause của app. Không có --target thì debugger chỉ thấy tiến trình của chính nó. Đây là cách gỡ rối distroless: image đích không có ps/ls/cat, nhưng từ debugger ta soi được tiến trình, và (nếu chia sẻ thêm) cả filesystem của nó qua /proc/<pid>/root. (Cảnh báo "container runtime doesn't support this feature" chỉ là dự phòng; containerd của ta hỗ trợ, nên ta thấy được /pause.)
Ephemeral container hiện ra ở đâu, và nó không đụng tới app
Sau khi gắn, pod ghi nhận ephemeral container ở hai chỗ riêng, không lẫn vào containers:
kubectl get pod noshell -o jsonpath='{range .spec.ephemeralContainers[*]}name={.name} image={.image} target={.targetContainerName}{"\n"}{end}'
kubectl get pod noshell -o jsonpath='{range .status.ephemeralContainerStatuses[*]}name={.name} state={.state}{"\n"}{end}'
name=debugger image=busybox:1.36 target=app
name=debugger state={"terminated":{"exitCode":0,"reason":"Completed",...}}
spec.ephemeralContainers giữ khai báo (kèm targetContainerName: app), status.ephemeralContainerStatuses giữ trạng thái. Lệnh ps chạy xong nên debugger terminated/Completed, và đây là điểm cần khắc: ephemeral container không bao giờ tự restart. Khác hẳn container thường (Bài 18) hay sidecar (Bài 19), nó chạy một lần rồi thôi. Nếu muốn một phiên debug lâu, cho nó chạy sleep hoặc shell tương tác (-it).
Còn container app thì sao? Hoàn toàn không bị động:
kubectl get pod noshell
kubectl get pod noshell -o jsonpath='app.restartCount={.status.containerStatuses[0].restartCount}{"\n"}'
NAME READY STATUS RESTARTS AGE
noshell 1/1 Running 0 28s
app.restartCount=0
1/1 Running, restartCount=0: gắn debugger không restart pod, không sửa image, không làm gián đoạn app. Đúng thứ ta cần khi đang điều tra một sự cố sống: giữ nguyên hiện trường.
Chế độ 2: nhân bản pod với --copy-to
Đôi khi không muốn (hoặc không được phép) đụng vào pod gốc, hoặc cần đổi command/image để xem app hành xử khác đi, mà ephemeral container thì không cho sửa command của container có sẵn. Khi đó dùng --copy-to: kubectl debug tạo một bản sao của pod, ta tùy biến trên bản sao, pod gốc giữ nguyên.
kubectl debug noshell -it=false --copy-to=noshell-dbg \
--image=busybox:1.36 --container=debugger --share-processes -- sleep 3600
NAME READY STATUS RESTARTS AGE
noshell-dbg 2/2 Running 0 8s
Bản sao noshell-dbg có hai container: app gốc giữ nguyên image, cộng thêm debugger:
kubectl get pod noshell-dbg -o jsonpath='{range .spec.containers[*]}container={.name} image={.image}{"\n"}{end}'
container=app image=registry.k8s.io/pause:3.10.2
container=debugger image=busybox:1.36
Ở chế độ copy không có --target (debug container ở đây là container thường, không phải ephemeral), nên để thấy tiến trình của app ta dùng cờ --share-processes, nó bật shareProcessNamespace cho cả pod sao. Vào debugger kiểm chứng:
kubectl exec noshell-dbg -c debugger -- ps -ef
PID USER TIME COMMAND
1 65535 0:00 /pause
7 65535 0:00 /pause
13 root 0:00 sleep 3600
19 root 0:00 ps -ef
Debugger thấy cả /pause của container app (PID 7) lẫn lệnh sleep 3600 của chính nó (PID 13), đúng nghĩa chia sẻ process namespace toàn pod. Vì là bản sao, nó nằm trên IP/pod riêng và không ảnh hưởng pod gốc; gỡ rối xong xóa bản sao là sạch. --copy-to hợp khi pod gốc đang CrashLoopBackOff (Bài 18): copy ra và đổi command thành sleep để pod đứng yên cho ta mổ xẻ thay vì cứ chết-đẻ liên tục.
Chế độ 3: gỡ rối thẳng node với debug node/
Hai chế độ trên gỡ rối trong pod. Chế độ thứ ba nhắm vào node: khi nghi vấn nằm ở host như đầy disk, kernel log, cấu hình kubelet, file CNI (những thứ ta tự tay dựng ở Part I). kubectl debug node/<tên-node> tạo một pod debug đặc quyền trên đúng node đó, và mount toàn bộ filesystem của host vào /host:
kubectl debug node/worker-0 -it=false --image=busybox:1.36 --container=nodedbg -- ls /host
Creating debugging pod node-debugger-worker-0-nns48 with container nodedbg on node worker-0.
bin
boot
dev
etc
home
lib
lib64
lost+found
...
/host chính là / của node worker-0. Chứng minh dứt khoát bằng cách đọc hostname của host rồi đối chiếu với hostname thật qua SSH:
kubectl debug node/worker-0 -it=false --image=busybox:1.36 --container=nodedbg -- cat /host/etc/hostname
# worker-0
ssh worker-0 hostname
# worker-0
Trùng khớp: pod debug đọc đúng /etc/hostname trên rootfs của node. Từ đây có thể chroot /host để dùng luôn công cụ của host, xem /host/var/log, /host/etc/kubernetes, kiểm tra /host/etc/cni/net.d (file CNI Bài 14)... mà không cần khóa SSH. Đây là cách gỡ rối node "từ trong cluster", tiện khi không có sẵn đường SSH nhưng có quyền kubectl. Đổi lại, pod này đặc quyền cao, dùng xong nên xóa ngay (nó là pod node-debugger-* ở namespace default).
Ba chế độ cạnh nhau
kubectl debug <pod> --target=C ── gắn ephemeral container VÀO pod đang chạy
(không restart, không sửa image; --target = chia sẻ
process namespace để soi tiến trình container C)
kubectl debug <pod> --copy-to=<sao> ── NHÂN BẢN pod rồi tùy biến trên bản sao
(đổi image/command, --share-processes; pod gốc nguyên vẹn)
kubectl debug node/<node> ── pod đặc quyền trên NODE, host fs mount tại /host
(gỡ rối host: log, cấu hình, disk)
🧹 Dọn dẹp
kubectl delete pod noshell noshell-dbg --now
# xóa pod debug node (tự sinh ở namespace default):
kubectl delete pod -l '!app' --field-selector=status.phase=Succeeded --now
kubectl get pods -o name | grep node-debugger | xargs -r kubectl delete
Lưu ý: ephemeral container debugger không xóa riêng được — nó gắn chặt vào pod noshell, nên xóa pod là xong. Cụm trở lại chỉ còn hai pod CoreDNS. Manifest ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 21-ephemeral-debug.
Tổng kết
Khi kubectl exec bất lực vì container không có shell hoặc đã crash, ephemeral container là đường vào: một container công cụ gắn tạm vào pod đang chạy qua subresource ephemeralcontainers, không restart pod, không sửa image, không bao giờ tự restart, và không gỡ ra được sau khi gắn. kubectl debug gói lại ba chế độ: gắn ephemeral container vào pod đang chạy (--target bật chia sẻ process namespace để soi tiến trình container đích, ta đã thấy /pause của image không-shell); nhân bản pod với --copy-to để đổi image/command mà giữ nguyên bản gốc (--share-processes); và debug node/ tạo pod đặc quyền mount rootfs node tại /host để gỡ rối host (ta đã đọc đúng /host/etc/hostname = worker-0). Ba chế độ phủ ba tầng: trong container, trong pod, và dưới node.
Part III tới đây đã đi hết vòng đời và cách quan sát/gỡ rối một pod. Bài 22 chuyển sang phần tài nguyên: requests/limits, ba lớp QoS (Guaranteed/Burstable/BestEffort) quyết định pod nào bị giết trước khi node cạn bộ nhớ, và Downward API để pod tự biết thông tin về chính nó.