Bài 02

09 tháng 7, 2026 · 30 phút đọc

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 qun RI tng container:

   ┌─────────┐        ┌──────────┐
   │  web    │  ???   │ log-agent│
   │  IP: A  │ ─────  │  IP: B   │
   └─────────┘        └──────────┘
   • IP khác nhauphi config network để nói chuynCó thbxếp lên 2 máy khác nhaulog-agent không thy file ca webScale/di chỗ → phi đồng b2 cái thcô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ướcxong 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

💡 initContainers chính là lời giải đẹp cho nỗi đau "depends_on khô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ậpi 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ật

kubectl — 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 describekubectl logs --previouscặ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ại

Giố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 livenessreadiness:

  • 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 tht bi: sai tên,  describemc Events:
   ErrImagePull       sai tag, registry private      tên image, li auth

   CrashLoopBackOff   Container cstart ri chết,    logs --previous:
                      K8s khi li dn dp            app crash vì lý do gì
                      (thiếu env, sai config, port)

   Pending            Chưa xếp được lên node          describeEvents:
                                                      hết CPU/RAM? PVC chưa bind?
                                                      taint chn?

   ContainerCreating  Đang kéo image / gn volume     đợi; nếu kt lâu →
                      (kẹt lâu = volume/CNI lỗi)      describe xem mount/CNI

   OOMKilled          Vượt memory limit, bgiết      tăng limit hoc fix rò RAM

   Completed          Chy xong, thoát 0 (Job)        bình thường vi Job,
                                                      bt thường vi 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 → Running

2. 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:8080

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

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

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

  1. describe + logs --previous trước, mọi thứ khác sau. Đây là phản xạ số 1. Restart mù không phải debug.

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

  3. Probe sai còn hại hơn không có probe. initialDelaySeconds ngắn hơn thời gian boot = tự giết app. App boot lâu → dùng startupProbe.

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

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

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

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

  3. Quên -c <container> khi Pod nhiều container. kubectl logs mặc định lấy container đầu; Pod nhiều container phải chỉ định, không thì xem nhầm log.

  4. Quên --previous khi 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.

  5. initialDelaySeconds quá ngắn cho app boot lâu → liveness giết app đang lên → CrashLoop vĩnh viễn. Dùng startupProbe.

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

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

  8. Tab thay space trong YAML → parse lỗi. 2 space mỗi cấp.

  9. Tưởng Completed luôn tốt. Với Job thì đúng; với web server thì sai — server phải Running mãi, Completed nghĩa là nó tự thoát (lỗi config).

  10. 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_on của Compose).
  • Declarative > imperative: kubectl apply -f (file, commit được) thắng kubectl run (gõ tay).
  • kubectl ↔ docker: get podsps, logslogs, exec -it -- sh~`exec. Thêm describelogs --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

← Bài trước
Bài 01
Bài tiếp theo →
Bài 03
Zalo tư vấn