# 🧱 DaemonSet, Job, CronJob

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

Sau 25 phút này, bạn phải làm được 4 việc sau mà không cần tra tài liệu:

- Nhìn yêu cầu đề bài và chọn đúng loại workload: DaemonSet, Job hay CronJob.
- Viết được YAML DaemonSet chạy trên **mọi** node, kể cả control plane.
- Cấu hình Job với `completions`, `parallelism`, `backoffLimit` và kiểm tra nó hoàn thành.
- Viết CronJob với `schedule`, `concurrencyPolicy`, history limits, và test nó mà không phải ngồi chờ.

---

## Ôn nhanh bài trước

Bài trước bạn đã nắm Deployment: nó quản lý ReplicaSet và đảm bảo **luôn có N pod chạy mãi**, có rolling update và rollback. Nhưng Deployment không trả lời được 3 dạng yêu cầu quen thuộc trong đề CKA:

- "chạy đúng một pod trên **mỗi** node" → DaemonSet
- "chạy cho đến khi **hoàn thành** rồi dừng" → Job
- "chạy **lặp lại theo lịch** mỗi X phút" → CronJob

| Tiêu chí | Deployment | DaemonSet | Job | CronJob |
| --- | --- | --- | --- | --- |
| Số pod | theo `replicas` | 1 pod mỗi node | theo `completions` | tạo Job theo lịch |
| Pod có kết thúc không | Không | Không | Có | Có |
| Đơn vị điều khiển | ReplicaSet | node | số lần hoàn thành | `schedule` |
| Dùng cho | app stateless | agent, log, CNI | batch, migration, backup một lần | backup, report định kỳ |
| Lệnh imperative | `kubectl create deployment` | **không có** | `kubectl create job` | `kubectl create cronjob` |

```mermaid
flowchart TD
    Start["Đọc yêu cầu đề"] --> Q1{"Pod phải chạy<br/>trên mọi node?"}
    Q1 -->|Có| DS["🖥️ DaemonSet"]
    Q1 -->|Không| Q2{"Pod phải chạy<br/>đến khi xong?"}
    Q2 -->|Không| DEP["♻️ Deployment"]
    Q2 -->|Có| Q3{"Lặp lại<br/>theo lịch?"}
    Q3 -->|Có| CJ["⏰ CronJob"]
    Q3 -->|Không| JOB["✅ Job"]
```

> [!NOTE]
> Ba từ khóa quyết định trong đề: "every node"/"each node" → DaemonSet, "completion"/"process"/"runs N times" → Job, "every minute"/"schedule"/"daily" → CronJob.

---

## Ví dụ mẫu

### Ví dụ 1: DaemonSet chạy trên mọi node, kể cả control plane

Đề kiểu: *"Create a DaemonSet named `log-agent` using image `busybox:1.36` that runs on all nodes including the control plane. Command: `sh -c "while true; do echo log; sleep 30; done"`."*

Điểm mấu chốt: **không có `kubectl create daemonset`**. Mẹo nhanh nhất là sinh YAML từ Deployment rồi sửa.

```steps
1. Sinh YAML nháp: `kubectl create deployment log-agent --image=busybox:1.36 --dry-run=client -o yaml > ds.yaml`
2. Sửa file: đổi `kind: Deployment` thành `kind: DaemonSet`, xoá `spec.replicas`, xoá `spec.strategy`, xoá `status: {}`.
3. Thêm `tolerations` cho taint của control plane vào `spec.template.spec`.
4. Apply: `kubectl apply -f ds.yaml`
5. Kiểm tra: `kubectl get ds log-agent` với cột DESIRED phải bằng số node, rồi `kubectl get pods -o wide -l app=log-agent`
```

Kết quả YAML hoàn chỉnh:

```yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: log-agent
spec:
  selector:
    matchLabels:
      app: log-agent
  template:
    metadata:
      labels:
        app: log-agent
    spec:
      tolerations:
      - key: node-role.kubernetes.io/control-plane
        operator: Exists
        effect: NoSchedule
      containers:
      - name: log-agent
        image: busybox:1.36
        command: ["sh", "-c", "while true; do echo log; sleep 30; done"]
```

Điều quan trọng nhất của bài này: DaemonSet **tự động** thêm toleration cho các taint hệ thống (`node.kubernetes.io/not-ready`, `unreachable`, `disk-pressure`, `memory-pressure`, `pid-pressure`, và `unschedulable`), nhưng ==nó không tự tolerate taint `node-role.kubernetes.io/control-plane`==. Đề nào yêu cầu "including control plane" thì bạn phải tự thêm toleration như trên.

### Ví dụ 2: Job có nhiều lần hoàn thành

Đề kiểu: *"Create a Job named `report` that completes 4 times, running 2 pods in parallel, image `busybox:1.36`, restartPolicy Never, backoffLimit 3."*

```steps
1. Sinh YAML nháp: `kubectl create job report --image=busybox:1.36 --dry-run=client -o yaml -- sh -c "echo processing; sleep 5" > job.yaml`
2. Thêm `completions: 4`, `parallelism: 2`, `backoffLimit: 3` vào `spec`.
3. Apply: `kubectl apply -f job.yaml`
4. Theo dõi: `kubectl get job report -w` cho tới khi COMPLETIONS hiện `4/4`
```

```yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: report
spec:
  completions: 4
  parallelism: 2
  backoffLimit: 3
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: report
        image: busybox:1.36
        command: ["sh", "-c", "echo processing; sleep 5"]
```

Lưu ý: `kubectl create job` đã tự set `restartPolicy: Never` cho bạn, không cần sửa. Nhưng nếu bạn tự viết YAML từ đầu thì `restartPolicy` **bắt buộc** là `Never` hoặc `OnFailure`, đặt `Always` sẽ bị API server từ chối.

### Ví dụ 3: CronJob chạy mỗi 2 phút

```steps
1. Sinh YAML nháp: `kubectl create cronjob backup --image=busybox:1.36 --schedule="*/2 * * * *" --dry-run=client -o yaml -- sh -c "date; echo backup done" > cj.yaml`
2. Thêm `concurrencyPolicy: Forbid` và `successfulJobsHistoryLimit: 2`.
3. Apply: `kubectl apply -f cj.yaml`
4. Test ngay không cần chờ: `kubectl create job --from=cronjob/backup manual-run-1`
5. Kiểm tra: `kubectl get cj`, `kubectl get jobs`, `kubectl get pods`
```

```yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: backup
spec:
  schedule: "*/2 * * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 2
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      backoffLimit: 2
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - name: backup
            image: busybox:1.36
            command: ["sh", "-c", "date; echo backup done"]
```

Schedule dùng **5 field**: `phút giờ ngày-trong-tháng tháng ngày-trong-tuần`. Mặc định tính theo **UTC**. Nếu đề cho giờ Việt Nam và cluster là bản mới, dùng thêm `timeZone: "Asia/Ho_Chi_Minh"`.

---

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

Làm từng bài trong lab, tự bấm giờ khoảng 2-3 phút mỗi bài. Chỉ mở đáp án sau khi đã thử.

**Bài 1.** Tạo DaemonSet tên `node-monitor`, image `busybox:1.36`, command `["sh", "-c", "while true; do date; sleep 20; done"]`, chạy trên **tất cả** node gồm cả control plane. Kiểm tra cột DESIRED.

<details><summary>Đáp án gợi ý</summary>

```bash
kubectl create deployment node-monitor --image=busybox:1.36 --dry-run=client -o yaml > ds.yaml
# sửa: kind: DaemonSet, xoá replicas/strategy, thêm tolerations
kubectl apply -f ds.yaml
kubectl get ds node-monitor
kubectl get pods -o wide -l app=node-monitor
```

`DESIRED` phải bằng tổng số node trong `kubectl get nodes`. Nếu thiếu control plane thì bạn quên toleration `node-role.kubernetes.io/control-plane`.

</details>

**Bài 2.** Đổi image của `node-monitor` sang `busybox:1.37` và theo dõi rollout.

<details><summary>Đáp án gợi ý</summary>

```bash
kubectl set image ds/node-monitor node-monitor=busybox:1.37
kubectl rollout status ds/node-monitor
kubectl get ds node-monitor -o wide
```

DaemonSet cũng hỗ trợ `rollout status` và `rollout undo`. Nếu `updateStrategy` là `OnDelete`, đổi image sẽ **không** tự thay pod, bạn phải tự xoá từng pod.

</details>

**Bài 3.** Tạo Job `countdown` image `busybox:1.36`, command in ra số rồi sleep, `completions: 5`, `parallelism: 2`, `backoffLimit: 4`, `restartPolicy: Never`. Chờ đến khi xong và đọc log của một pod.

<details><summary>Đáp án gợi ý</summary>

```bash
kubectl create job countdown --image=busybox:1.36 --dry-run=client -o yaml \
  -- sh -c "echo running; sleep 5" > job.yaml
# thêm completions: 5, parallelism: 2, backoffLimit: 4
kubectl apply -f job.yaml
kubectl get job countdown -w
kubectl logs job/countdown
kubectl get pods -l job-name=countdown
```

Sau khi đạt `5/5`, Job giữ nguyên các pod đã Completed. Xoá bằng `kubectl delete job countdown`.

</details>

**Bài 4.** Tạo CronJob `hello` chạy mỗi phút một lần, image `busybox:1.36`, command `echo hello from cronjob`, `concurrencyPolicy: Replace`. Test ngay bằng cách trigger thủ công thay vì chờ.

<details><summary>Đáp án gợi ý</summary>

```bash
kubectl create cronjob hello --image=busybox:1.36 --schedule="*/1 * * * *" \
  --dry-run=client -o yaml -- sh -c "echo hello from cronjob" > cj.yaml
# thêm concurrencyPolicy: Replace
kubectl apply -f cj.yaml
kubectl create job --from=cronjob/hello hello-manual-1
kubectl get jobs
kubectl logs job/hello-manual-1
```

`--from=cronjob/...` là mẹo ăn điểm: bạn kiểm chứng jobTemplate chạy đúng mà không phải chờ đến phút tiếp theo.

</details>

**Bài 5.** Một CronJob có `schedule: "*/1 * * * *"` nhưng 5 phút trôi qua vẫn không thấy Job mới nào. Nêu 4 nguyên nhân cần kiểm tra, theo thứ tự.

<details><summary>Đáp án gợi ý</summary>

```bash
kubectl get cj <name> -o yaml
kubectl describe cj <name>
```

1. `suspend: true` trong spec.
2. `concurrencyPolicy: Forbid` và Job của lần chạy trước vẫn đang chạy, nên lần mới bị bỏ qua.
3. Schedule sai định dạng (6 field kiểu có seconds, hoặc gõ `* /1` có dấu cách).
4. `startingDeadlineSeconds` quá nhỏ, controller bỏ qua vì trễ hạn; hoặc controller-manager trên control plane không chạy.

</details>

---

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

| Triệu chứng | Nguyên nhân thường gặp | Cách xử lý |
| --- | --- | --- |
| `kubectl get ds` có DESIRED nhỏ hơn số node | Thiếu toleration cho taint của control plane hoặc taint riêng của node | Thêm `tolerations` với `operator: Exists`, kiểm tra `kubectl describe node` phần Taints |
| Apply DaemonSet báo lỗi field lạ | Còn sót `spec.replicas` hoặc `spec.strategy` do copy từ Deployment | Xoá hai field đó khỏi YAML |
| Job báo `valid values: OnFailure, Never` | `restartPolicy: Always` do tự viết YAML hoặc sửa từ Pod template | Đổi sang `Never` hoặc `OnFailure` |
| Job không bao giờ đạt completions, pod Failed liên tục | Command trả exit code khác 0, vượt `backoffLimit` (mặc định 6) | Sửa command, hoặc tăng `backoffLimit`, xem `kubectl logs` và `kubectl describe pod` |
| `kubectl apply` sửa Job báo `field is immutable` | `spec.template` của Job không sửa được sau khi tạo | `kubectl delete job <name>` rồi apply lại |
| Xoá pod của DaemonSet nhưng pod quay lại | Đúng thiết kế, DaemonSet luôn đảm bảo 1 pod mỗi node | Không cần xử lý, đó là hành vi mong đợi |
| CronJob không tạo Job | `suspend: true`, `Forbid` đang chặn, hoặc schedule sai | Xem Bài 5 ở trên |
| Giờ chạy lệch 7 tiếng | CronJob mặc định dùng UTC | Thêm `timeZone: "Asia/Ho_Chi_Minh"` (cần cluster 1.27+) hoặc tự quy đổi giờ |

> [!WARNING]
> Đừng set `ttlSecondsAfterFinished` bừa trong lúc luyện thi. Nếu đề yêu cầu kiểm tra log hoặc trạng thái Job sau khi hoàn thành, Job bị tự xoá sẽ khiến bạn mất điểm oan.

---

## Góc kinh nghiệm thi

- **Không có lệnh imperative cho DaemonSet.** Đây là bẫy tiết kiệm thời gian lớn nhất: `kubectl create deployment ... --dry-run=client -o yaml` rồi sửa `kind` luôn nhanh hơn gõ YAML tay. Sửa bằng `vim` vài dòng là xong.
- **Có lệnh cho Job và CronJob**, dùng luôn: `kubectl create job`, `kubectl create cronjob`. Nhớ đặt `--dry-run=client -o yaml > file.yaml` để có base rồi mới thêm `completions`, `parallelism`, `concurrencyPolicy`.
- **Luôn kiểm tra sau khi apply** bằng đúng cột đề quan tâm: `kubectl get ds` (DESIRED), `kubectl get job` (COMPLETIONS), `kubectl get cj` (SCHEDULE, SUSPEND, ACTIVE). Giám khảo chấm trạng thái object, không chấm file YAML của bạn.
- **Test CronJob bằng `--from=cronjob/<name>`** thay vì chờ. Mỗi phút chờ là một phút mất điểm ở bài khác.
- **Rule of thumb đọc đề**: thấy "on every node" hoặc "agent" thì nghĩ DaemonSet; thấy "process", "compute", "runs to completion", "successful" thì nghĩ Job; thấy "every N minutes", "daily", "schedule" thì nghĩ CronJob.
- **Schedule hay gõ sai**: `*/5 * * * *` là mỗi 5 phút. `0 */2 * * *` là mỗi 2 giờ. Luôn đặt trong dấu ngoặc kép để shell không nuốt dấu `*`.
- **Nhớ `kubectl explain`** khi quên tên field: `kubectl explain cronjob.spec.concurrencyPolicy`, `kubectl explain daemonset.spec.template.spec.tolerations`.

---

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

Bốn điều cần mang ra khỏi bài này:

1. DaemonSet = 1 pod mỗi node, không có `replicas`, **không có lệnh imperative**, và không tự tolerate taint của control plane.
2. Job = chạy đến khi đủ `completions`, điều tiết bằng `parallelism`, giới hạn retry bằng `backoffLimit` (mặc định 6), `restartPolicy` chỉ được `Never` hoặc `OnFailure`.
3. CronJob = schedule 5 field theo UTC, `concurrencyPolicy` mặc định `Allow`, giữ 3 Job thành công và 1 Job thất bại gần nhất.
4. Job là immutable: muốn đổi pod template thì xoá rồi tạo lại.

Tự kiểm tra nhanh bằng các câu điền khuyết sau:

```cloze
1. DaemonSet đảm bảo có đúng {{c1::một pod}} trên mỗi node, nên nó không có trường replicas.
2. Job chỉ chấp nhận restartPolicy là {{c2::Never hoặc OnFailure}}, đặt Always sẽ bị API server từ chối.
3. CronJob mặc định tính giờ theo múi {{c3::UTC}}, nên schedule giờ Việt Nam cần thêm timeZone.
4. Đặt concurrencyPolicy: {{c4::Forbid}} để CronJob bỏ qua lần chạy mới khi Job trước còn chạy.
5. Không có lệnh imperative nào tạo được {{c5::DaemonSet}}, phải viết YAML hoặc sửa từ Deployment.
6. updateStrategy: {{c6::OnDelete}} khiến DaemonSet chỉ thay pod khi bạn tự xoá pod.
```

Bộ flashcard để ôn lại sau:

```flashcard
[
  { "front": "DaemonSet đảm bảo điều gì?", "back": "Đúng một pod chạy trên mỗi node (hoặc mỗi node khớp selector)." },
  { "front": "DaemonSet có trường replicas không?", "back": "Không. Số pod do số node quyết định." },
  { "front": "DaemonSet tự tolerate những taint hệ thống nào?", "back": "not-ready, unreachable, disk-pressure, memory-pressure, pid-pressure và unschedulable." },
  { "front": "DaemonSet có tự tolerate taint node-role.kubernetes.io/control-plane không?", "back": "Không. Muốn chạy trên control plane phải tự thêm toleration với operator Exists." },
  { "front": "Có lệnh kubectl create daemonset không?", "back": "Không có. Phải viết YAML hoặc sửa từ Deployment dry-run." },
  { "front": "updateStrategy nào khiến DaemonSet chỉ thay pod khi bạn xoá pod?", "back": "OnDelete (mặc định là RollingUpdate)." },
  { "front": "Job chấp nhận restartPolicy nào?", "back": "Chỉ Never hoặc OnFailure." },
  { "front": "Giá trị mặc định của completions và parallelism trong Job?", "back": "Cả hai đều mặc định là 1." },
  { "front": "backoffLimit mặc định của Job là bao nhiêu?", "back": "6." },
  { "front": "Sửa spec.template của Job đã tồn tại thì sao?", "back": "Báo lỗi field is immutable, phải xoá Job rồi tạo lại." },
  { "front": "CronJob dùng định dạng schedule mấy field và múi giờ nào?", "back": "5 field (phút giờ ngày tháng thứ), mặc định UTC." },
  { "front": "Ba giá trị của concurrencyPolicy trong CronJob?", "back": "Allow (mặc định), Forbid, Replace." },
  { "front": "Giá trị mặc định của successfulJobsHistoryLimit và failedJobsHistoryLimit?", "back": "3 và 1." },
  { "front": "Làm sao test CronJob ngay mà không chờ lịch?", "back": "kubectl create job <ten> --from=cronjob/<ten-cronjob>." },
  { "front": "Field nào tạm dừng một CronJob?", "back": "spec.suspend: true." }
]
```

Bài tiếp theo chuyển sang **Resource requests, limits và QoS class**: cách khai báo tài nguyên cho các pod bạn vừa tạo, cách scheduler dùng requests để xếp pod, và vì sao pod của bạn bị `Pending` hoặc bị OOMKilled khi thiếu limits.
