# 🚨 Troubleshoot pod: CrashLoopBackOff, ImagePullBackOff, Pending

## Mục tiêu bài học

Sau bài này, bạn phải làm được:

- Đọc đúng status của pod và biết ngay lệnh tiếp theo cần chạy.
- Phân biệt ba nhóm lỗi: Pending (chưa được schedule), ImagePullBackOff (không lấy được image), CrashLoopBackOff (container chạy rồi thoát).
- Sửa manifest tương ứng và xác nhận pod lên Running trong vài phút, đủ nhanh cho một câu hỏi CKA.

> [!NOTE]
> Nhắc lại bài trước: bài 6.2 dạy bạn đọc node NotReady bằng `kubectl describe node` và `journalctl -u kubelet`. Nguyên tắc cốt lõi vẫn giữ nguyên: luôn đọc **Events** trước khi đoán mò. Bài này áp dụng đúng nguyên tắc đó nhưng ở phạm vi pod.

Ba trạng thái này chiếm phần lớn câu hỏi troubleshooting trong đề CKA, nên hãy học theo phản xạ: thấy status nào thì chạy lệnh đó ngay, không suy nghĩ lâu.

| Status | Nghĩa là gì | Lệnh đầu tiên | Nhóm nguyên nhân |
| --- | --- | --- | --- |
| Pending | Chưa được scheduler gán vào node | `kubectl describe pod` | Tài nguyên, nodeSelector/affinity, taint, PVC |
| ImagePullBackOff | Không pull được image | `kubectl describe pod` | Sai tên/tag, registry private thiếu secret |
| CrashLoopBackOff | Container chạy rồi thoát, bị restart liên tục | `kubectl logs --previous` | Command sai, app lỗi, probe sai, OOMKilled |

```mermaid
flowchart TD
    Start["🔍<br/>Pod không Running"] --> Q1{"Status là gì?"}
    Q1 -->|"Pending"| P["📦<br/>Chưa schedule"]
    Q1 -->|"ImagePullBackOff"| I["🖼️<br/>Không pull được image"]
    Q1 -->|"CrashLoopBackOff"| C["💥<br/>Container chạy rồi thoát"]
    P --> PD["kubectl describe pod<br/>đọc Events"]
    I --> ID["kubectl describe pod<br/>kiểm tra image và imagePullSecrets"]
    C --> CL["kubectl logs --previous<br/>xem lastState và exit code"]
```

## Ví dụ có hướng dẫn: từ CrashLoopBackOff đến Running

Bối cảnh: bạn apply một Deployment và pod cứ restart liên tục. Đây là manifest gốc (có lỗi cố ý):

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        command: ["nginx"]
        args: ["-g", "daemon off;", "--invalid-flag"]
```

**Bước 1: Xác nhận triệu chứng.**

```bash
kubectl get pods -o wide
```

Kết quả mẫu:

```
NAME                   READY   STATUS             RESTARTS      AGE
web-7d9c8f5b6c-2xk4l   0/1     CrashLoopBackOff   5 (40s ago)   4m
```

`RESTARTS` tăng liên tục là dấu hiệu chắc chắn của CrashLoopBackOff. Con số `(40s ago)` là thời gian backoff hiện tại, nó tăng dần theo cấp số nhân: 10s, 20s, 40s, 80s, 160s, tối đa 300s.

**Bước 2: Đọc describe, tập trung vào hai chỗ.**

```bash
kubectl describe pod web-7d9c8f5b6c-2xk4l
```

Chỗ thứ nhất là `Last State`:

```
    Last State:     Terminated
      Reason:       Error
      Exit Code:    1
```

Chỗ thứ hai là `Events` ở cuối output. Với lỗi này bạn sẽ thấy các dòng `Started container nginx` rồi `Back-off restarting failed container`, tức là container đã thực sự chạy và thoát, không phải lỗi image hay lỗi schedule.

**Bước 3: Xem log của lần chạy trước.**

```bash
kubectl logs web-7d9c8f5b6c-2xk4l --previous
```

Nếu quên `--previous`, bạn sẽ nhận `Error from server (BadRequest): container ... is waiting to start: CrashLoopBackOff`. Đây là lỗi thao tác phổ biến nhất của người mới.

Output mẫu:

```
nginx: invalid option: "--invalid-flag"
```

**Bước 4: Kết luận nguyên nhân.** Flag `--invalid-flag` không tồn tại, nginx thoát với exit code 1, kubelet restart, lặp mãi thành CrashLoopBackOff. Vấn đề nằm ở `args` trong manifest, không phải ở image.

**Bước 5: Sửa manifest.**

```bash
kubectl edit deployment web
```

Xoá `--invalid-flag` khỏi `args`, lưu lại. Hoặc dùng cách nhanh hơn nếu chỉ cần đổi image:

```bash
kubectl set image deployment/web nginx=nginx:1.25
```

**Bước 6: Xác nhận.**

```bash
kubectl rollout status deployment/web
kubectl get pods -l app=web
```

Pod mới phải ở trạng thái `Running` với `RESTARTS` bằng 0 (hoặc không tăng nữa). Trong đề thi, bước xác nhận này là bắt buộc, không được bỏ.

## Luyện tập có hướng dẫn

Mỗi bài dưới đây là một manifest lỗi. Cách làm: apply manifest, chạy các lệnh chẩn đoán, viết ra nguyên nhân bằng một câu, rồi mới mở phần đáp án để đối chiếu. Đừng mở đáp án trước.

### Bài tập 1: Pod Pending vì tài nguyên

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: big-mem
spec:
  containers:
  - name: app
    image: nginx:1.25
    resources:
      requests:
        memory: "64Gi"
        cpu: "8"
```

Việc cần làm:

1. `kubectl apply -f big-mem.yaml`
2. `kubectl get pod big-mem` và ghi lại status.
3. `kubectl describe pod big-mem`, đọc dòng cuối phần Events.
4. Trả lời: pod có được gán vào node chưa, và vì sao?

<details><summary>Xem đáp án</summary>

Status là `Pending`. Event điển hình:

```
Warning  FailedScheduling  0/3 nodes are available: 3 Insufficient cpu, 3 Insufficient memory.
```

Pod chưa được schedule vì không node nào đáp ứng `requests`. Lưu ý: `requests` là mức cam kết, scheduler dùng nó để lọc node, không liên quan đến việc app có dùng hết hay không.

Cách sửa: giảm `requests` xuống mức thực tế (ví dụ `cpu: "100m"`, `memory: "128Mi"`), rồi xoá và tạo lại pod. Với Pod trần, `resources` không sửa được tại chỗ, phải `kubectl delete pod big-mem` rồi apply lại.

</details>

### Bài tập 2: Pod Pending vì nodeSelector

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: ssd-only
spec:
  nodeSelector:
    disktype: ssd
  containers:
  - name: app
    image: nginx:1.25
```

Việc cần làm:

1. Apply manifest, xem status.
2. `kubectl get nodes --show-labels` để kiểm tra nhãn `disktype`.
3. Đọc Events và trả lời: sửa nhãn node hay sửa manifest thì nhanh hơn trong phòng thi?

<details><summary>Xem đáp án</summary>

Event điển hình:

```
Warning  FailedScheduling  0/3 nodes are available: 3 node(s) didn't match Pod's node affinity/selector.
```

Không node nào có nhãn `disktype=ssd`, nên pod không thể schedule.

Hai cách sửa, chọn theo đề bài:

- Đề cho phép đổi node: `kubectl label node <node-name> disktype=ssd`
- Đề yêu cầu pod chạy được mà không đụng vào node: xoá `nodeSelector` khỏi manifest, delete pod rồi apply lại.

Trong phòng thi, nếu đề không nói rõ phải giữ `nodeSelector`, cách nhanh và an toàn nhất là bỏ selector. Nhưng nếu đề ghi "pod phải chạy trên node có nhãn X" thì phải gán nhãn cho node.

</details>

### Bài tập 3: ImagePullBackOff

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: bad-image
spec:
  containers:
  - name: app
    image: nginx:1.25.99
```

Việc cần làm:

1. Apply manifest, đợi khoảng 20 giây.
2. `kubectl get pod bad-image`, quan sát status thay đổi từ `ErrImagePull` sang `ImagePullBackOff`.
3. Đọc Events, tìm chính xác tag nào không tồn tại.
4. Sửa image mà không cần xoá pod, sau đó kiểm tra lại.

<details><summary>Xem đáp án</summary>

Event điển hình:

```
Warning  Failed   Failed to pull image "nginx:1.25.99": ... manifest unknown
Warning  Failed   Error: ErrImagePull
Normal   BackOff  Back-off pulling image "nginx:1.25.99"
Warning  Failed   Error: ImagePullBackOff
```

`ErrImagePull` là lần thử đầu tiên thất bại, `ImagePullBackOff` là trạng thái kubelet đang chờ backoff trước khi thử lại. Bản chất là cùng một lỗi, chỉ khác giai đoạn.

Cách sửa nhanh, field `image` của Pod là field được phép sửa tại chỗ:

```bash
kubectl set image pod/bad-image app=nginx:1.25
kubectl get pod bad-image -w
```

Nếu là Deployment thì dùng `kubectl set image deployment/<name> <container>=<image>`.

</details>

### Bài tập 4: CrashLoopBackOff vì liveness probe

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: probe-fail
spec:
  containers:
  - name: app
    image: nginx:1.25
    ports:
    - containerPort: 80
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080
      initialDelaySeconds: 2
      periodSeconds: 5
```

Việc cần làm:

1. Apply manifest, đợi khoảng 30 giây rồi xem status và số lần RESTARTS.
2. Đọc Events, tìm dòng có chữ `Liveness probe failed`.
3. Chạy `kubectl logs probe-fail` và `kubectl logs probe-fail --previous`. So sánh hai output, giải thích vì sao log thường không có lỗi gì.
4. Sửa manifest và xác nhận pod ổn định.

<details><summary>Xem đáp án</summary>

Event điển hình:

```
Warning  Unhealthy  Liveness probe failed: Get "http://10.244.0.5:8080/healthz": dial tcp ...: connect: connection refused
Normal   Killing    Container app failed liveness probe, will be restarted
```

Container chạy bình thường và phục vụ ở cổng 80, nhưng probe lại gọi cổng 8080 và path `/healthz`. Probe thất bại 3 lần liên tiếp nên kubelet giết container và khởi động lại, tạo ra vòng lặp.

Log có thể hoàn toàn sạch vì bản thân nginx không hề lỗi. Đây là lý do phải đọc `describe pod` chứ không chỉ đọc `logs`.

Cách sửa: đổi `port: 8080` thành `port: 80` và `path: /healthz` thành `path: /`. Sau khi sửa, `kubectl delete pod probe-fail` rồi apply lại, vì `livenessProbe` không sửa được tại chỗ trên Pod đang chạy.

Bài học: khi thấy CrashLoopBackOff mà log sạch, hãy nghĩ ngay đến probe.

</details>

> [!TIP]
> Bài tập 1 và 2 đều là Pending nhưng nguyên nhân khác nhau hoàn toàn. Điểm chung duy nhất là cả hai đều nằm trong phần Events của `describe pod`. Đừng đoán theo status, hãy đọc Events.

## Lỗi thường gặp và cách xử lý

Đây là những chỗ tôi từng mất điểm hoặc mất thời gian, và tôi thấy người mới lặp lại rất nhiều:

- **Chỉ nhìn status mà không đọc Events.** Status chỉ nói nhóm lỗi, Events nói nguyên nhân cụ thể. Luôn cuộn xuống cuối output của `describe pod`.
- **Quên `--previous` khi pod đang restart.** `kubectl logs` thường trả về lỗi BadRequest vì container chưa sẵn sàng. Thêm `-p` là ra ngay.
- **Sửa trực tiếp trên Pod.** Rất nhiều field không mutable: `resources`, `command`, `nodeSelector`, `livenessProbe`. Sửa bằng `kubectl edit pod` sẽ bị từ chối, phải xoá và tạo lại. Riêng `image` thì sửa được.
- **Nhầm `CreateContainerConfigError` với CrashLoopBackOff.** Nếu Pod tham chiếu ConfigMap hoặc Secret không tồn tại qua `envFrom` hoặc `valueFrom`, status là `CreateContainerConfigError`, container chưa hề chạy nên không có log. Sửa bằng cách tạo ConfigMap/Secret còn thiếu hoặc sửa tên trong manifest.
- **Bỏ qua exit code.** Exit code 1 là app lỗi, 127 là không tìm thấy lệnh, 137 là bị SIGKILL gần như chắc chắn do OOMKilled (vượt memory limit), 143 là SIGTERM. Đọc `Last State` trong describe là ra.
- **Quên `-n`.** Pod không hiện ra vì bạn đang ở `default` namespace. Kiểm tra bằng `kubectl get pods -A | grep <tên>`.
- **Chờ backoff thay vì reset nó.** Khi backoff đã lên 5 phút, sửa manifest cũng không giúp pod chạy ngay. Xoá pod để workload tạo pod mới với backoff bằng 0.
- **Nghĩ CrashLoopBackOff là lỗi image.** Pod có status CrashLoopBackOff nghĩa là image đã pull thành công và container đã chạy. Nếu image sai, status đã là ImagePullBackOff.
- **Tin vào `status.phase`.** Pod CrashLoopBackOff vẫn có `status.phase` là `Running`, nên lọc `--field-selector status.phase!=Running` sẽ bỏ sót chúng.

## Expert Corner: mẹo phòng thi

Vài thói quen tôi mang từ phòng lab vào phòng thi và thấy hiệu quả rõ rệt:

- **Thứ tự lệnh cố định:** `kubectl get pod` → `kubectl describe pod` → `kubectl logs [-p]`. Ba lệnh này giải quyết khoảng 90% câu hỏi pod trong đề.
- **Alias tiết kiệm thời gian:** đặt `alias k=kubectl` và `export do="--dry-run=client -o yaml"` ngay khi vào đề.
- **Lấy exit code bằng jsonpath** khi describe quá dài:

```bash
kubectl get pod <pod> -o jsonpath='{.status.containerStatuses[0].lastState.terminated.exitCode}'
```

- **Xem event toàn namespace theo thời gian** khi Events của pod đã bị rotate:

```bash
kubectl get events -n <ns> --sort-by=.metadata.creationTimestamp | tail -20
```

- **Trần thời gian cho mỗi câu là 5 phút.** Nếu quá 5 phút mà chưa ra nguyên nhân, đánh dấu và đi tiếp. Một câu Pending khó không đáng để mất ba câu khác.
- **Khi sửa Deployment, luôn kết thúc bằng `kubectl rollout status`** để chắc chắn replica mới đã Available. Đề CKA hay chấm điểm trên trạng thái cuối cùng, không phải trên việc bạn đã gõ lệnh gì.
- **Đọc đề trước khi sửa:** đề có yêu cầu giữ nguyên node hay giữ nguyên resource không? Sửa đúng chỗ nhưng sai ràng buộc vẫn là 0 điểm.
- **Nhớ lại nhanh mối liên hệ với Module 5:** Pending cũng có thể do PVC chưa bind. Nếu Events ghi `pod has unbound immediate PersistentVolumeClaims`, nguyên nhân nằm ở storage, không phải ở pod.

## Tóm tắt và bước tiếp theo

Chốt lại trong bốn dòng, đúng như cách tôi tự nhắc mình trước giờ thi:

1. Pending: chưa schedule, đọc Events để biết là thiếu tài nguyên, sai selector, hay thiếu PVC.
2. ImagePullBackOff: sai tên/tag image hoặc thiếu `imagePullSecrets`, sửa được tại chỗ bằng `kubectl set image`.
3. CrashLoopBackOff: container chạy rồi thoát, đọc `Last State`, exit code và `logs --previous`.
4. Luôn kết thúc bằng bước xác nhận pod đã `Running` và `RESTARTS` không tăng.

Điền nhanh để tự kiểm tra:

```cloze
Pod ở trạng thái Pending nghĩa là nó chưa được {{c1::scheduler}} gán vào node nào.
Lệnh xem log của lần chạy trước của container: kubectl logs <pod> {{c2::--previous}}.
Exit code 137 thường là dấu hiệu container bị {{c3::OOMKilled}} do vượt memory limit.
Với registry private, pod cần {{c4::imagePullSecrets}} để pull được image.
CrashLoopBackOff có thời gian backoff tối đa là {{c5::300 giây}}, tức 5 phút.
```

Ôn lại bằng bộ thẻ dưới đây, mỗi thẻ một dữ kiện, cố gắng trả lời trước khi lật:

```flashcard
[
  { "front": "Pod ở trạng thái Pending nghĩa là gì?", "back": "Pod đã được API server nhận nhưng chưa được scheduler gán vào node nào." },
  { "front": "Lệnh đầu tiên nên chạy khi pod có status lạ?", "back": "kubectl describe pod <tên>, đọc kỹ phần Events ở cuối output." },
  { "front": "Xem log của lần chạy trước của container bị restart?", "back": "kubectl logs <pod> --previous (viết tắt -p)." },
  { "front": "Exit code 137 cho biết điều gì?", "back": "Container bị SIGKILL, gần như chắc chắn do OOMKilled hoặc vượt memory limit." },
  { "front": "ErrImagePull khác ImagePullBackOff ở điểm nào?", "back": "ErrImagePull là lần pull thất bại đầu tiên, ImagePullBackOff là giai đoạn kubelet chờ backoff để thử lại." },
  { "front": "Nguyên nhân phổ biến nhất của CrashLoopBackOff?", "back": "Container khởi động rồi thoát mã lỗi, do command/args sai, app lỗi, probe sai hoặc OOMKilled." },
  { "front": "Thời gian backoff tối đa của CrashLoopBackOff?", "back": "300 giây (5 phút), tăng dần từ 10 giây theo cấp số nhân." },
  { "front": "Event 'Insufficient cpu' khi pod Pending thì sửa gì?", "back": "Giảm resources.requests trong manifest hoặc giải phóng tài nguyên trên node." },
  { "front": "Event \"didn't match Pod's node affinity/selector\" nghĩa là gì?", "back": "nodeSelector hoặc affinity không khớp nhãn của node nào, sửa manifest hoặc gán nhãn cho node." },
  { "front": "Đổi image của một Pod đang lỗi mà không xoá pod?", "back": "kubectl set image pod/<tên> <container>=<image>, vì field image là mutable." },
  { "front": "Status CreateContainerConfigError nghĩa là gì?", "back": "Pod tham chiếu ConfigMap hoặc Secret không tồn tại, container chưa hề chạy nên không có log." },
  { "front": "Image có tag :latest mặc định dùng imagePullPolicy nào?", "back": "Always, nên luôn pull lại mỗi lần khởi động container." },
  { "front": "Làm sao reset backoff mà không phải chờ?", "back": "Xoá pod, workload sẽ tạo pod mới với backoff bắt đầu lại từ đầu." },
  { "front": "CrashLoopBackOff mà log hoàn toàn sạch thì nghi ngờ gì đầu tiên?", "back": "Liveness probe sai cấu hình, kiểm tra dòng 'Liveness probe failed' trong Events." }
]
```

**Bài tiếp theo:** bài 6.4 sẽ mở rộng sang lỗi kết nối: pod đã Running nhưng Service không trả lời, DNS trong cluster không phân giải được, và cách kiểm tra endpoints. Khi đó bạn sẽ dùng lại đúng quy trình `get` → `describe` → `log` vừa luyện, chỉ thêm một bước kiểm tra mạng.
