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

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:

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

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

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

Đáp án gợi ý
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.

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

Đáp án gợi ý
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.

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.

Đáp án gợi ý
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.

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

Đáp án gợi ý
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.

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

Đáp án gợi ý
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.

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:

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:

[
  { "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.