Init Container và Sidecar Container
Bài 18 để lại hai condition đáng đào tiếp: Initialized và Ready. Bài này lấy Initialized, condition phản ánh việc các init container đã chạy xong hay chưa, và nhân tiện bàn luôn người anh em gần của nó: sidecar container. Cả hai đều là container phụ trong pod, nhưng phục vụ hai nhu cầu trái ngược: init container chạy trước rồi biến mất, sidecar chạy song song và ở lại. (Ready và cơ chế probe để dành Bài 20.)
Init container: chuẩn bị xong rồi mới nhường chỗ
Init container là container chạy tới khi hoàn tất trước khi container ứng dụng được khởi động. Tài liệu nói gọn: "Init containers always run to completion" và "Each init container must complete successfully before the next one starts". Chúng khai trong spec.initContainers, và nếu có nhiều, chúng chạy lần lượt theo thứ tự — không song song.
Dùng init container khi có việc phải làm một lần, trước khi app chạy: chờ một dịch vụ phụ thuộc sẵn sàng, tải/chuẩn bị cấu hình hay dữ liệu vào một volume mà app sẽ đọc, chạy migration database, hay đặt quyền file. Tách những việc này ra khỏi container chính giữ cho image app gọn và trách nhiệm rõ ràng.
Một điểm khác biệt quan trọng so với container thường, trích thẳng tài liệu: "Regular init containers ... do not support the lifecycle, livenessProbe, readinessProbe, or startupProbe fields." Một thứ chạy rồi kết thúc thì không cần probe sống/sẵn-sàng. Nhưng nó vẫn hỗ trợ volume, resource limit, security context như container thường.
Thử một init container thật
Pod sau có một init container ghi dữ liệu vào một emptyDir dùng chung, rồi container app đọc lại — mô hình "init chuẩn bị, app tiêu thụ" điển hình:
apiVersion: v1
kind: Pod
metadata: {name: init-demo}
spec:
volumes:
- {name: shared, emptyDir: {}}
initContainers:
- name: init-prep
image: busybox:1.36
command: ["sh","-c","echo 'du-lieu-do-init-chuan-bi' > /work/data.txt; sleep 15"]
volumeMounts: [{name: shared, mountPath: /work}]
containers:
- name: app
image: busybox:1.36
command: ["sh","-c","cat /work/data.txt; sleep 3600"]
volumeMounts: [{name: shared, mountPath: /work}]
Chụp pod trong lúc init đang chạy:
kubectl get pod init-demo
kubectl get pod init-demo -o jsonpath='Initialized={..conditions[?(@.type=="Initialized")].status}{"\n"}app={.status.containerStatuses[0].state}{"\n"}'
NAME READY STATUS RESTARTS AGE
init-demo 0/1 Init:0/1 0 4s
Initialized=False
app={"waiting":{"reason":"PodInitializing"}}
STATUS: Init:0/1 (xong 0 trong 1 init container), Initialized=False, và container app chưa chạy — nó Waiting với lý do PodInitializing. Đây là điều init container đảm bảo: app không khởi động khi việc chuẩn bị chưa xong. Đợi init hoàn tất rồi xem lại:
kubectl get pod init-demo
kubectl get pod init-demo -o jsonpath='Initialized={..conditions[?(@.type=="Initialized")].status}{"\n"}init={.status.initContainerStatuses[0].state.terminated.reason}{"\n"}'
kubectl logs init-demo -c app
NAME READY STATUS RESTARTS AGE
init-demo 1/1 Running 0 20s
Initialized=True
init=Completed
du-lieu-do-init-chuan-bi
Init container Completed, Initialized lật sang True, app khởi động và đọc được đúng dữ liệu init vừa ghi qua volume chung. Condition Initialized của Bài 18 giờ đã thấy thứ điều khiển nó.
Khi init container lỗi
Init container fail thì sao? Tài liệu: "If a Pod's init container fails, the kubelet repeatedly restarts that init container until it succeeds. However, if the Pod has a restartPolicy of Never ... Kubernetes treats the overall Pod as failed." Thử với một init thoát mã 1 (pod để restartPolicy mặc định Always):
kubectl get pod init-fail
kubectl get pod init-fail -o jsonpath='initRestartCount={.status.initContainerStatuses[0].restartCount}{"\n"}app={.status.containerStatuses[0].state.waiting.reason}{"\n"}'
NAME READY STATUS RESTARTS AGE
init-fail 0/1 Init:Error 4 (66s ago) 100s
initRestartCount=4
app=PodInitializing
Init container chết rồi bị restart lặp lại (restartCount tăng dần), và container app vẫn kẹt ở PodInitializing: pod không nhúc nhích cho tới khi init thành công. Nếu init cứ chết, nó vào backoff lũy thừa y như Bài 18 và STATUS chuyển thành Init:CrashLoopBackOff. Bài học vận hành: một pod kẹt mãi ở Init:... nghĩa là việc chuẩn bị đang hỏng, hãy đọc kubectl logs init-fail -c init-bad để biết vì sao, đừng nhìn container app (nó còn chưa được chạy).
Sidecar container: chạy kèm app, ở lại suốt đời pod
Init container biến mất trước khi app chạy. Nhưng nhiều khi ta cần một container chạy kèm app suốt đời pod: một agent đẩy log, một proxy mạng, một bộ đồng bộ cấu hình. Đó là sidecar.
Trước đây người ta làm sidecar bằng cách thêm một container nữa vào spec.containers, vẫn dùng được khi các container "ngang hàng" và bạn không cần kiểm soát thứ tự. Nhưng cách đó có điểm yếu: không đảm bảo sidecar khởi động trước app, và khi pod tắt thì thứ tự tắt không kiểm soát được (proxy có thể chết trước khi app kịp đóng kết nối).
Kubernetes giải bài này bằng native sidecar, định nghĩa của nó là một init container có restartPolicy: Always. Theo tài liệu, tính năng này (feature gate SidecarContainers) bật mặc định từ v1.29 và GA/stable từ v1.33; cụm của ta chạy v1.36.1 nên dùng được ngay, không phải bật gì.
apiVersion: v1
kind: Pod
metadata: {name: sidecar-demo}
spec:
initContainers:
- name: logshipper
image: busybox:1.36
restartPolicy: Always # <-- dòng này biến init container thành sidecar
command: ["sh","-c","while true; do echo shipping logs; sleep 10; done"]
containers:
- name: app
image: busybox:1.36
command: ["sh","-c","echo app dang chay; sleep 3600"]
Vì sao đặt sidecar trong initContainers mà nó lại không chặn app như init thường? Mấu chốt ở chỗ sidecar chỉ cần bắt đầu, không cần hoàn tất. Tài liệu: "After a sidecar-style init container is running (the kubelet has set the started status for that init container to true), the kubelet then starts the next init container." Tức là khi sidecar đã started, kubelet cho qua luôn, không đợi nó kết thúc (mà nó cũng không bao giờ kết thúc). Kết quả là sidecar khởi động trước, rồi app chạy song song:
kubectl get pod sidecar-demo
kubectl get pod sidecar-demo -o jsonpath='sidecar={.status.initContainerStatuses[0].name}:{.status.initContainerStatuses[0].state}{"\n"}app={.status.containerStatuses[0].name}:{.status.containerStatuses[0].state}{"\n"}'
NAME READY STATUS RESTARTS AGE
sidecar-demo 2/2 Running 0 21s
sidecar=logshipper:{"running":{"startedAt":"2026-05-23T15:19:51Z"}}
app=app:{"running":{"startedAt":"2026-05-23T15:19:51Z"}}
READY 2/2: pod tính cả sidecar lẫn app, và cả hai cùng running. Để ý logshipper nằm trong initContainerStatuses (nó là init container về mặt khai báo) nhưng hành xử như một container thường trú: chạy mãi, restart độc lập nếu chết, và khác init thường ở chỗ được phép có probe.
So hai mô hình khởi động cạnh nhau cho rõ:
INIT thường (chặn, chạy tới khi xong):
|== init-1 ==|== init-2 ==|========= app =========>
↑ Initialized=True, app mới chạy
SIDECAR (chỉ cần "started", rồi chạy song song):
|== sidecar started ==|···· sidecar chạy tiếp ····|
|========= app ============>|
↑ app chạy ngay khi sidecar started
Thứ tự tắt, và vì sao điều đó quý
Điểm tinh tế nhất của native sidecar nằm ở lúc tắt pod. Tài liệu: "the kubelet postpones terminating sidecar containers until the main application container has fully stopped. The sidecar containers are then shut down in the opposite order of their appearance." Nghĩa là khi pod bị xóa, app dừng trước, sidecar dừng sau và theo thứ tự ngược. Đây đúng thứ ta muốn: một proxy mạng hay agent đẩy log phải còn sống cho tới khi app đóng nốt kết nối/đẩy nốt log cuối, chứ không phải chết trước rồi app gửi vào hư không.
Một hệ quả nữa, quan trọng với Job (Bài 27): sidecar khai theo kiểu này không chặn Job hoàn tất. Khi container chính của Job xong, Job được coi là xong dù sidecar vẫn đang chạy, kubelet sẽ tự tắt sidecar. Sidecar khai theo kiểu cũ (thêm vào containers) thì sẽ khiến Job treo mãi vì sidecar không bao giờ thoát.
🧹 Dọn dẹp
kubectl delete pod init-demo init-fail sidecar-demo
Tất cả là object trong cluster, xóa là sạch. Manifest ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 19-init-sidecar.
Tổng kết
Init container và sidecar là hai cách thêm container phụ vào pod cho hai nhu cầu trái ngược. Init container chạy tuần tự tới khi hoàn tất trước khi app khởi động, điều khiển condition Initialized, lỗi thì kubelet restart tới khi thành công (hoặc làm pod Failed nếu restartPolicy: Never). Sidecar là init container gắn restartPolicy: Always: nó chỉ cần started là app được chạy song song, ở lại suốt đời pod, và khi tắt thì tắt sau app theo thứ tự ngược, đúng thứ tự an toàn cho proxy/agent. Khác biệt "chỉ cần started, không cần completed" giải thích vì sao một thứ khai trong initContainers lại chạy kèm app.
Bài 20 lấy nốt condition còn lại của Bài 18 là Ready, cùng cơ chế đứng sau nó: probe. Ta sẽ xem ba loại probe (liveness, readiness, startup) quyết định khi nào container được coi là sống, khi nào sẵn sàng nhận lưu lượng, và khi nào kubelet nên giết rồi khởi động lại nó.