Bài 02
Bài 02 — Pod & kubectl: đơn vị nhỏ nhất & cây gậy điều khiển
Bài 01 bạn có bản đồ: chỉ mặt được API Server, etcd, scheduler, kubelet. Giờ ta hạ độ cao, đáp xuống tầng thấp nhất mà bạn sẽ làm việc hằng ngày — Pod.
Ở chặng Docker, đơn vị nhỏ nhất là container: docker run nginx là xong. K8s thì khác — đơn vị nhỏ nhất nó biết tới không phải container, mà là Pod. Bài này trả lời: Pod là gì, vì sao K8s chèn thêm một lớp "vỏ" quanh container, và dùng kubectl để tạo/soi/debug Pod như bạn từng làm với docker.
Mục tiêu: tạo được Pod đầu tiên từ YAML, vào shell, đọc log, gắn health check, và debug khi nó đỏ (CrashLoopBackOff, ImagePullBackOff...). Đây là kỹ năng nền — mọi thứ ở Bài 03, 04, 05 đều quy về "nó tạo ra Pod, và bạn debug Pod".
Vấn đề: vì sao không chạy thẳng container?
Bạn sẽ nghĩ: "K8s chạy container, vậy cho tôi kubectl run-container nginx đi." Nhưng K8s cố tình không cho bạn quản trực tiếp container. Vì sao?
Hãy nhìn một ca thực tế. Web app cần một container phụ chạy kèm để gom log đẩy về trung tâm. Hai container này:
Nếu quản RỜI từng container:
┌─────────┐ ┌──────────┐
│ web │ ??? │ log-agent│
│ IP: A │ ───── │ IP: B │
└─────────┘ └──────────┘
• IP khác nhau → phải config network để nói chuyện
• Có thể bị xếp lên 2 máy khác nhau → log-agent không thấy file của web
• Scale/dời chỗ → phải đồng bộ 2 cái thủ côngĐây đúng kiểu rắc rối "thứ tự, network, đồng bộ" mà chặng Compose từng kêu. K8s giải bằng cách gói chúng vào một Pod — một "vỏ bọc" đảm bảo nhóm container luôn dính nhau như keo: cùng máy, cùng IP, cùng vòng đời.
Pod là gì
Pod là đơn vị triển khai nhỏ nhất trong Kubernetes — một "vỏ bọc" chứa một hoặc nhiều container chạy cùng nhau trên cùng một node.
Các container trong cùng một Pod:
- Chia sẻ network namespace → cùng một địa chỉ IP, gọi nhau qua
localhost. - Chia sẻ storage volume (nếu khai báo) → đọc/ghi chung thư mục.
- Cùng sinh, cùng tử → Pod được xếp nguyên khối lên 1 node, scale/dời chỗ cả cụm.
┌──────── NODE ────────┐
│ │
│ ┌──── POD ─────┐ │
│ │ IP: 10.244.. │ │
│ │ ┌────┐ ┌────┐ │ │
│ │ │web │ │log │ │ │ ← 2 container
│ │ │:80 │ │agent│ │ │ cùng IP,
│ │ └─┬──┘ └──┬─┘ │ │ gọi nhau qua
│ │ └ localhost┘ │ │ localhost
│ │ [ shared volume ] │
│ └───────────────┘ │
└───────────────────────┘Vài quy tắc cốt lõi:
- Mỗi Pod có 1 IP duy nhất trong cluster (cấp bởi CNI từ Bài 01).
- Các Pod giao tiếp với nhau qua Service hoặc DNS nội bộ (Bài 04), không gọi thẳng IP Pod.
- Container trong cùng Pod thì gọi nhau qua
localhost. - Pod có thể gắn Persistent Volume để lưu dữ liệu sống lâu hơn Pod (Bài 05).
Bao giờ thì 1 Pod nhiều container?
Mặc định: 1 Pod = 1 container chính. Nhồi nhiều app không liên quan vào 1 Pod là sai (chúng nên là Pod riêng để scale độc lập). Chỉ gộp khi các container thật sự là một khối logic:
- Sidecar — container phụ hỗ trợ container chính: gom log, proxy mạng (service mesh), đồng bộ file. Chạy song song suốt đời Pod.
- Init container — container chạy trước và xong rồi mới tới container chính: ví dụ đợi DB sẵn sàng, tải config, chạy migration. Chạy tuần tự, xong là tắt.
spec:
initContainers: # chạy TRƯỚC, xong mới tới containers
- name: wait-db
image: busybox
command: ['sh', '-c', 'until nc -z db 3306; do sleep 2; done']
containers: # container chính
- name: app
image: myapp💡
initContainerschính là lời giải đẹp cho nỗi đau "depends_onkhông đợi app ready" ở chặng Compose. Thay vì hy vọng DB lên kịp, ta chặn app cho tới khi DB thật sự nghe cổng.
Imperative vs Declarative — hai cách tạo Pod
Nhớ tư duy "khai báo" ở Bài 01 chứ? Nó thể hiện ngay ở 2 cách tạo Pod:
# IMPERATIVE — ra lệnh trực tiếp, nhanh, hợp để thử nghiệm
kubectl run my-pod --image=nginx
# DECLARATIVE — mô tả trạng thái trong file, rồi apply (CÁCH NÊN DÙNG)
kubectl apply -f pod.yaml Imperative Declarative
────────── ───────────
"tạo Pod nginx ngay" "tôi muốn có Pod như file này"
Nhanh, gõ tay Lưu file, commit git
Khó tái lập Tái lập 100%, version control
Hợp: thử, debug Hợp: mọi thứ nghiêm túc→ Giống hệt bài học docker run (mệnh lệnh) vs docker-compose.yml (khai báo) ở chặng trước. Ngoài đời gần như luôn declarative: file YAML vừa là cấu hình, vừa là tài liệu, vừa commit được. Imperative chỉ để nghịch nhanh.
YAML một Pod
apiVersion: v1 # nhóm API — Pod thuộc core "v1"
kind: Pod # loại tài nguyên
metadata:
name: my-pod
labels: # "nhãn dán" — cực quan trọng từ Bài 03
app: my-app
spec:
containers:
- name: my-container
image: nginx:1.25
ports:
- containerPort: 80⚠️ YAML nhạy indent y như chặng Compose: space, không tab; 2 space mỗi cấp. Sai indent = parse lỗi khó hiểu.
kubectl apply -f pod.yaml # tạo / cập nhậtkubectl — cây gậy điều khiển
Đây là bộ lệnh bạn sẽ gõ cả ngày. Đối chiếu thẳng với docker cho dễ nhớ:
# XEM
kubectl get pods # ~ docker ps
kubectl get pods -o wide # thêm IP, node đang chạy
kubectl get pods --watch # theo dõi trạng thái liên tục
kubectl get pod my-pod -o yaml # dump toàn bộ cấu hình + trạng thái
# SOI CHI TIẾT (lệnh debug số 1)
kubectl describe pod my-pod # events, lý do lỗi, mount, probe...
# LOG (~ docker logs)
kubectl logs my-pod
kubectl logs my-pod -f # follow
kubectl logs my-pod -c log-agent # Pod nhiều container → chỉ định -c
kubectl logs my-pod --previous # log của lần chạy TRƯỚC (vàng khi CrashLoop)
# VÀO SHELL (~ docker exec -it)
kubectl exec -it my-pod -- /bin/sh
kubectl exec my-pod -- env # chạy 1 lệnh rồi thoát
# CHỌC CỔNG RA NGOÀI (test nhanh, không cần Service)
kubectl port-forward pod/my-pod 8080:80
# → mở http://localhost:8080 trên máy bạn, tunnel vào cổng 80 của Pod
# XÓA (~ docker rm)
kubectl delete pod my-pod
kubectl delete -f pod.yaml💡
kubectl describevàkubectl logs --previouslà cặp bài trùng để debug. 90% sự cố Pod được giải bằng hai lệnh này. Học thuộc.
Vòng đời Pod — Pod đang ở "phase" nào?
Mỗi Pod có một phase tổng quát (xem ở cột STATUS):
Pending ──► Running ──┬──► Succeeded (chạy xong, thoát code 0)
│ │
│ └──► Failed (thoát code != 0)
│
(chưa xếp được lên node, hoặc đang kéo image)
Unknown = mất liên lạc với node (node có thể đã chết)- Pending — Pod đã nhận nhưng chưa chạy: chưa xếp được lên node (hết tài nguyên), đang kéo image, hoặc chờ volume.
- Running — đã lên node, ít nhất 1 container đang chạy.
- Succeeded / Failed — Pod chạy xong hẳn (hợp với Job, không hợp với web server vốn phải chạy mãi).
- Unknown — control plane mất liên lạc với node.
⚠️ Đừng nhầm phase (tổng quát) với container state chi tiết (Waiting, Running, Terminated) hiện trong kubectl describe. Khi debug, state container mới cho lý do thật (vd Waiting: ImagePullBackOff).
Restart policy
spec:
restartPolicy: Always # mặc định: container chết → khởi lại (hợp web)
# restartPolicy: OnFailure # chỉ khởi lại khi lỗi (hợp Job)
# restartPolicy: Never # không khởi lạiGiống restart: ở Compose, nhưng K8s gắn nó với cơ chế self-healing mạnh hơn nhiều — ta đào ở Bài 03.
Health check — Probes (phần tài liệu gốc bỏ trống, nhưng quan trọng bậc nhất)
Ở chặng Compose ta học healthcheck. K8s nâng nó lên 3 loại probe, và đây là thứ khiến app thực sự đáng tin:
livenessProbe → "App còn SỐNG không?" Fail → K8s RESTART container.
readinessProbe → "App SẴN SÀNG nhận
request chưa?" Fail → CẮT traffic (không xóa).
startupProbe → "App KHỞI ĐỘNG xong
chưa?" (app ì ạch) Bảo vệ app boot lâu khỏi bị
liveness giết oan lúc đang lên.Phân biệt sống còn giữa liveness và readiness:
- App treo (deadlock) nhưng tiến trình chưa chết → liveness phát hiện, restart.
- App đang khởi động / đang nạp cache, chưa phục vụ được → readiness tạm cắt traffic cho tới khi sẵn sàng, không restart.
spec:
containers:
- name: web
image: nginx:1.25
livenessProbe:
httpGet: { path: /healthz, port: 80 }
initialDelaySeconds: 10 # đợi 10s sau khi start mới bắt đầu check
periodSeconds: 5 # check mỗi 5s
failureThreshold: 3 # fail 3 lần liên tiếp mới coi là chết
readinessProbe:
httpGet: { path: /ready, port: 80 }
initialDelaySeconds: 5
periodSeconds: 3💡 Readiness probe là "người gác cổng zero-downtime" ở Bài 03: trong lúc rolling update, K8s chỉ rót traffic vào Pod đã readiness PASS. Không có readiness → traffic đổ vào Pod chưa sẵn sàng → khách thấy lỗi giữa lúc deploy.
Tài nguyên — requests & limits
K8s cho bạn ghi rõ mỗi container cần bao nhiêu (việc Compose làm yếu):
resources:
requests: # TỐI THIỂU — scheduler dùng để chọn node
cpu: "100m" # 100 milli-CPU = 0.1 core
memory: "128Mi"
limits: # TRẦN — vượt là bị bóp/giết
cpu: "500m"
memory: "256Mi"requests= "tôi cần ít nhất từng này" → scheduler dựa vào đây xếp Pod (nhớ Bài 01).limits= trần cứng → vượt RAM limit, container bị OOMKilled (giết vì hết bộ nhớ); vượt CPU thì bị bóp (throttle).
⚠️ Không đặt requests → scheduler "mù", dễ nhồi quá tải 1 node. Không đặt limits → 1 Pod lỗi có thể ăn sạch RAM, kéo sập cả node.
Debug Pod khi nó đỏ — bảng tra cứu
Đây là phần bạn sẽ quay lại nhiều nhất. Quy trình bất biến: describe xem events → logs --previous xem app nói gì.
kubectl describe pod <pod> # cuộn xuống mục Events ở cuối
kubectl logs <pod> --previous # log lần crash gần nhất STATUS Nghĩa là Soi gì trước
────── ──────── ────────────
ImagePullBackOff / Kéo image thất bại: sai tên, describe → mục Events:
ErrImagePull sai tag, registry private tên image, lỗi auth
CrashLoopBackOff Container cứ start rồi chết, logs --previous:
K8s khởi lại dồn dập app crash vì lý do gì
(thiếu env, sai config, port)
Pending Chưa xếp được lên node describe → Events:
hết CPU/RAM? PVC chưa bind?
taint chặn?
ContainerCreating Đang kéo image / gắn volume đợi; nếu kẹt lâu →
(kẹt lâu = volume/CNI lỗi) describe xem mount/CNI
OOMKilled Vượt memory limit, bị giết tăng limit hoặc fix rò RAM
Completed Chạy xong, thoát 0 (Job) bình thường với Job,
bất thường với web server🚀 Lab — Pod từ A tới Z
Dùng lại cluster kind từ Bài 01 (hoặc kind create cluster --name lab).
1. Tạo Pod khai báo
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: web
labels: { app: web }
spec:
containers:
- name: nginx
image: nginx:1.25
ports: [{ containerPort: 80 }]
readinessProbe:
httpGet: { path: /, port: 80 }
initialDelaySeconds: 2
periodSeconds: 3
EOF
kubectl get pods --watch # nhìn nó đi Pending → ContainerCreating → Running2. Soi, log, vào shell, chọc cổng
kubectl describe pod web # đọc Events, probe, IP
kubectl logs web -f # log nginx
kubectl exec -it web -- /bin/sh # vào trong, gõ: ls /usr/share/nginx/html; exit
kubectl port-forward pod/web 8080:80 # mở tab khác: curl http://localhost:80803. Cố tình phá để học debug
# Sai tên image → xem ImagePullBackOff
kubectl run broken --image=nginx:khong-ton-tai
kubectl get pods # broken: ImagePullBackOff
kubectl describe pod broken # Events nói rõ: pull image thất bại
kubectl delete pod broken
# Container chết ngay → xem CrashLoopBackOff
kubectl run crasher --image=busybox -- sh -c "echo bye; exit 1"
kubectl get pods --watch # crasher: Running → CrashLoopBackOff (khởi lại dồn dập)
kubectl logs crasher --previous # thấy "bye" — bằng chứng nó chạy rồi tự thoát
kubectl delete pod crasher4. Dọn
kubectl delete pod web→ Khoảnh khắc "à há": bạn vừa làm với K8s đúng những việc quen thuộc ở Docker (run, logs, exec), nhưng bằng tư duy khai báo — và lần đầu đọc được lý do một Pod đỏ thay vì đoán mò.
Câu chuyện thực tế
Tuần đầu chuyển sang K8s ở startup cũ, một junior nhắn tôi hoảng: "Anh ơi Pod cứ CrashLoopBackOff, em apply lại 5 lần vẫn đỏ!". Cậu ấy delete rồi apply lại, mỗi lần đợi, mỗi lần đỏ — lặp 5 lần, mất gần 1 tiếng.
Tôi gõ đúng một lệnh:
kubectl logs <pod> --previousLog hiện ra: app chết vì thiếu biến DB_HOST. Cậu ấy quên không gắn env. Fix trong 2 phút.
Bài học cậu ấy rút ra (và tôi muốn bạn nhớ sớm hơn): đừng restart trong vô vọng — hãy ĐỌC. apply lại 5 lần không cho thêm thông tin gì; Pod vẫn chết vì cùng một lý do. Trong khi describe + logs --previous cho biết chính xác lý do ngay lần đầu.
Một lần khác đắt hơn. Team đặt một app boot mất ~40 giây (nạp cache lớn). Họ gắn livenessProbe với initialDelaySeconds: 10. Kết quả: cứ 10 giây liveness check fail (app chưa boot xong) → K8s giết và khởi lại → app lại boot từ đầu → CrashLoop vĩnh viễn, không bao giờ lên nổi. Mất nửa buổi mới nhận ra thủ phạm là probe tự bắn vào chân mình. Lời giải đúng: startupProbe để che chở giai đoạn boot dài, rồi mới tới liveness.
Bài học:
describe+logs --previoustrước, mọi thứ khác sau. Đây là phản xạ số 1. Restart mù không phải debug.CrashLoopBackOff hầu như luôn là lỗi APP, không phải lỗi K8s. Thiếu env, sai config, sai port, exit code != 0. Log của app nói hết.
Probe sai còn hại hơn không có probe.
initialDelaySecondsngắn hơn thời gian boot = tự giết app. App boot lâu → dùngstartupProbe.Phân biệt liveness vs readiness cho đúng. Treo thì restart (liveness); chưa sẵn sàng thì cắt traffic (readiness). Nhầm hai cái gây ra đủ kiểu bug khó hiểu.
Pod là vật thể mau hỏng — đừng bám vào nó. IP đổi, tên đổi, nó bị xóa và tạo lại liên tục. Đây chính là lý do tồn tại Service ở Bài 04.
Pitfalls
Chạy bare Pod trong production. Pod trần chết là chết luôn, không ai hồi sinh. Production luôn để Deployment quản (Bài 03). Bare Pod chỉ hợp để thử.
Bám vào IP của Pod. IP Pod ephemeral — redeploy là đổi. Hardcode IP Pod = bom hẹn giờ. Dùng Service + DNS (Bài 04).
Quên
-c <container>khi Pod nhiều container.kubectl logsmặc định lấy container đầu; Pod nhiều container phải chỉ định, không thì xem nhầm log.Quên
--previouskhi CrashLoop.kubectl logs <pod>cho log lần chạy hiện tại (vừa start, thường rỗng). Lý do crash nằm ở lần trước → phải--previous.initialDelaySecondsquá ngắn cho app boot lâu → liveness giết app đang lên → CrashLoop vĩnh viễn. DùngstartupProbe.Không đặt requests/limits. Scheduler xếp mù, 1 Pod lỗi ăn sạch RAM node. Luôn đặt ít nhất
requests.Nhồi nhiều app không liên quan vào 1 Pod. Chúng nên là Pod riêng để scale độc lập. Chỉ gộp khi thật sự là sidecar/init.
Tab thay space trong YAML → parse lỗi. 2 space mỗi cấp.
Tưởng
Completedluôn tốt. Với Job thì đúng; với web server thì sai — server phảiRunningmãi,Completednghĩa là nó tự thoát (lỗi config).Dùng
kubectl run(imperative) cho mọi thứ rồi không lưu lại được gì. Nghịch thì ok, nhưng việc thật phải có file YAML để commit và tái lập.
Tóm tắt
- Pod = đơn vị nhỏ nhất của K8s — vỏ bọc quanh 1 (hoặc nhiều) container, cùng IP, cùng vòng đời, gọi nhau qua
localhost. - Mặc định 1 Pod = 1 container. Nhiều container chỉ khi là sidecar (chạy kèm) hoặc initContainer (chạy trước — lời giải đẹp cho
depends_oncủa Compose). - Declarative > imperative:
kubectl apply -f(file, commit được) thắngkubectl run(gõ tay). - kubectl ↔ docker:
get podsps,logslogs,exec -it -- sh~`exec. Thêmdescribevàlogs --previous` để debug. - Phase Pod: Pending → Running → Succeeded/Failed; Unknown = mất liên lạc node.
- 3 probe: liveness (chết → restart), readiness (chưa sẵn sàng → cắt traffic), startup (che chở app boot lâu).
- requests/limits: requests để scheduler chọn node, limits là trần cứng (vượt RAM = OOMKilled).
- Debug:
describe(Events) +logs --previous. Thuộc bảng tra: ImagePullBackOff / CrashLoopBackOff / Pending / OOMKilled. - Pod mau hỏng — đừng bám IP, đừng chạy bare Pod ở prod.
Bài sau (Bài 03 — ReplicaSet & Deployment) ta sửa điểm yếu chí tử vừa lộ ra: bare Pod chết là chết luôn. Ta sẽ bọc Pod trong ReplicaSet (giữ đủ số bản, tự hồi sinh) rồi Deployment (cập nhật version không downtime, rollback khi lỗi, tự co giãn) — đây mới là cách bạn chạy app thật trên K8s.
Nếu hôm nay bạn gõ kubectl logs --previous ra phản xạ thay vì apply lại trong vô vọng — bạn đã biết debug K8s. Phần còn lại là dựng lên trên nền đó.
— Minh Hưng