โหมดมืด
บทที่ 08 — Workload Types: StatefulSet, DaemonSet, Job, CronJob
จนถึงตอนนี้เราใช้ Deployment อย่างเดียว — แต่ Deployment เหมาะกับ "แอป stateless" เท่านั้น. บทนี้สอน workload type อื่น ๆ สำหรับงานที่ Deployment ทำไม่ได้
อ่านจบจะ:
- เข้าใจว่าเมื่อไหร่ใช้ workload ชนิดไหน
- StatefulSet — สำหรับแอปที่มี state (database)
- DaemonSet — รัน 1 pod ต่อ 1 node
- Job / CronJob — งานที่ทำครั้งเดียว / ตามเวลา
ใช้เวลา 2-3 ชั่วโมง
Part 1: ภาพรวม Workload Types
K8s มี workload หลายชนิด — แต่ละชนิดเหมาะกับงานต่างกัน:
| Workload | ใช้กับ | ตัวอย่าง |
|---|---|---|
| Deployment | แอป stateless | web, API |
| StatefulSet | แอป stateful — pod ต้องมี identity + storage | database, Kafka |
| DaemonSet | รัน 1 pod ทุก node | log agent, monitoring |
| Job | งานที่ทำครั้งเดียวจบ | migration, batch |
| CronJob | งานตามเวลา | backup รายวัน, report |
| ReplicaSet | (Deployment สร้างให้ — ไม่ใช้ตรง ๆ) | — |
Part 2: StatefulSet — สำหรับแอปที่มี State
2.1 ทำไม Deployment ไม่พอ
จากบท 02 + 05 — Deployment มอง pod ทุกตัว "เหมือนกันหมด สลับกันได้":
- ชื่อ pod สุ่ม (
myapp-7d4b8c9f8-abc12) - pod ตัวไหนก็เหมือนกัน — traffic ไปตัวไหนก็ได้
- ทุก pod แชร์ PVC เดียวกัน (หรือไม่มี state)
แต่ database cluster (เช่น PostgreSQL replica, Kafka, MongoDB) ต้องการตรงข้าม:
- แต่ละ pod ต้องมี identity ที่แน่นอน (identity = ตัวตน/เอกลักษณ์ที่ระบุตัวได้ เช่น
db-0,db-1,db-2) — poddb-0คือ primary, อื่นเป็น replica - แต่ละ pod ต้องมี storage ของตัวเอง (db-0 มี disk ของ db-0)
- pod ต้องเริ่ม ตามลำดับ (db-0 ก่อน แล้ว db-1)
- pod ตัวเดิมตาย สร้างใหม่ → ต้องได้ ชื่อเดิม + disk เดิม
→ Deployment ทำสิ่งเหล่านี้ไม่ได้ — ต้องใช้ StatefulSet
2.2 StatefulSet ให้อะไร
1. ชื่อ pod ที่แน่นอน เรียงเลข
StatefulSet ชื่อ db ที่มี 3 replica → ได้ pod:
db-0
db-1
db-2ชื่อแน่นอน เรียงเลข — ไม่สุ่ม
2. เริ่ม/ลบ ตามลำดับ
- เริ่ม:
db-0พร้อมก่อน →db-1→db-2 - ลบ:
db-2ก่อน →db-1→db-0(ย้อนกลับ)
3. storage ของตัวเอง (volumeClaimTemplates)
แต่ละ pod ได้ PVC ของตัวเอง (จาก Storage บท 05) — db-0 ได้ data-db-0, db-1 ได้ data-db-1
4. ชื่อ DNS ที่แน่นอน (ใช้คู่ Headless Service)
แต่ละ pod เรียกได้ด้วยชื่อเฉพาะ: db-0.db, db-1.db
รูปแบบ:
[pod-name].[service-name]—db-0.dbหมายถึง pod ชื่อdb-0ที่ resolve ผ่าน Headless Service ชื่อdb→ ได้ IP ของ pod ตรง ๆ (ไม่ผ่าน load balance)
2.3 StatefulSet Manifest
yaml
# Headless Service (จากบท 03) — จำเป็นสำหรับ StatefulSet
apiVersion: v1
kind: Service
metadata:
name: db
spec:
clusterIP: None # ← headless
selector:
app: db
ports:
- port: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
spec:
serviceName: db # ← ชี้ไป headless service
replicas: 3
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
# ⚠️ production: เพิ่ม resources + securityContext เสมอ — ดูบท 07 + บท 10 §7.3
# resources:
# requests: { cpu: 250m, memory: 256Mi }
# limits: { memory: 1Gi } # CPU limit ระวัง: throttle database ได้
# securityContext:
# runAsUser: 999 # postgres user
# runAsNonRoot: true
# allowPrivilegeEscalation: false
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
# ← สร้าง PVC ของตัวเองให้แต่ละ pod
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOncePod"] # RWOP (GA ใน K8s 1.27) — ปลอดภัยกว่า RWO สำหรับ database: ป้องกัน 2 pod mount disk เดียวกันพร้อมกัน แม้อยู่บน node เดียวกัน
resources:
requests:
storage: 20Gi📝 ก่อน apply manifest นี้ ต้องสร้าง Secret
db-secretก่อน (manifest อ้างถึงในsecretKeyRef):bashkubectl create secret generic db-secret --from-literal=password=mysecretpwถ้าไม่สร้าง pod จะ stuck ที่
CreateContainerConfigError
อธิบายส่วนพิเศษ:
serviceName: db— ชี้ไป Headless Service ชื่อdbvolumeClaimTemplates— แม่แบบ PVC. StatefulSet สร้าง PVC แยกให้แต่ละ pod:data-db-0,data-db-1,data-db-2
2.4 พฤติกรรมพิเศษ
bash
kubectl apply -f statefulset.yaml
kubectl get pods
# db-0 Running
# db-1 Running ← เริ่มทีหลัง db-0
# db-2 Running ← เริ่มทีหลัง db-1
kubectl get pvc
# data-db-0 Bound
# data-db-1 Bound
# data-db-2 Bound ← PVC แยกของแต่ละ podถ้า db-1 ตาย → K8s สร้าง pod ใหม่ที่ชื่อ db-1 เหมือนเดิม + ได้ PVC data-db-1 ตัวเดิมกลับมา (ข้อมูลเดิม)
💡 Operator (ออ-เปอ-เรเตอร์) ในบริบท K8s = controller พิเศษที่รู้จัก domain ของแอป (เช่น database) และจัดการ lifecycle ให้อัตโนมัติ — ห่อ StatefulSet ไว้แล้วจัดการงานเฉพาะทางของ database ให้ เช่น setup replication, failover, backup — ทำตัวเหมือน "ผู้ดูแล database อัตโนมัติ"
⚠️ หมายเหตุสำคัญ: StatefulSet ช่วยเรื่อง "identity + storage" — แต่ไม่ได้ทำให้ database เป็น cluster ให้ (replication, failover ของ database เอง ยังต้อง config) — งานจริงส่วนใหญ่ใช้ Operator เช่น:
- CloudNativePG — operator สำหรับ PostgreSQL (จัดการ replication, failover, backup ของ Postgres)
- Strimzi — operator สำหรับ Apache Kafka
หา Operator ตัวอื่นได้ที่ operatorhub.io (registry กลางของ Operator) — และ OLM (Operator Lifecycle Manager = โอ-แอล-เอ็ม = ตัวจัดการ Operator ที่ช่วยติดตั้ง/อัปเดต Operator ได้ง่ายขึ้น) ช่วยจัดการ install/upgrade Operator แบบ declarative
Part 3: DaemonSet — รัน 1 Pod ต่อ 1 Node
3.1 ปัญหา
บางงานต้องรัน "1 ตัวบนทุกเครื่อง (node)":
- Log agent — เก็บ log ของทุก container บน node นั้น (เช่น Fluent Bit)
- Monitoring agent — เก็บ metric ของ node (เช่น Node Exporter)
- Network plugin — CNI ที่ต้องรันทุก node
- Storage plugin
Deployment ทำไม่ได้ — Deployment บอกแค่ "อยากได้ N ตัว" ไม่ได้บอก "1 ตัวต่อ node"
3.2 DaemonSet
DaemonSet = รับประกันว่า ทุก node มี pod นี้ 1 ตัว:
- มี node 5 → DaemonSet สร้าง 5 pod (node ละ 1)
- เพิ่ม node ใหม่ → DaemonSet สร้าง pod บน node นั้นอัตโนมัติ
- ลบ node → pod บน node นั้นหายไป
yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: log-agent
spec:
selector:
matchLabels:
app: log-agent
template:
metadata:
labels:
app: log-agent
spec:
tolerations:
- operator: Exists # tolerate ทุก taint — รวม control-plane node
containers:
- name: fluent-bit
image: fluent/fluent-bit:3.2
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
volumes:
- name: varlog
hostPath: # อ่าน log ของ node
path: /var/log📝 ตัวอย่างนี้ย่อมาก — งานจริง log shipper (เช่น Fluent Bit, Vector) ต้อง mount หลาย path เพื่อเก็บ container log ครบ:
/var/log/pods— log ของ pod (รูปแบบใหม่ใช้กับ containerd/CRI-O)/var/log/containers— symlink ของ container log/var/lib/docker/containers— เฉพาะ cluster ที่ยังใช้ Docker runtimeและต้องมี
tolerationsที่ tolerate taintnode-role.kubernetes.io/control-planeด้วย (ใช้operator: Existsแบบในตัวอย่าง = tolerate ทุก taint) — ไม่งั้น DaemonSet จะไม่ลง control-plane node ทำให้เก็บ log/metric ของ node เหล่านั้นไม่ได้
สังเกต: DaemonSet ไม่มี replicas — เพราะจำนวน = จำนวน node เสมอ
bash
kubectl get daemonset
kubectl get pods -o wide # เห็น pod กระจาย 1 ตัว/node3.3 DaemonSet เฉพาะบาง node
ถ้าอยากให้รันเฉพาะบาง node — ใช้ nodeSelector:
yaml
spec:
template:
spec:
nodeSelector:
disktype: ssd # รันเฉพาะ node ที่มี label disktype=ssdPart 4: Job — งานที่ทำครั้งเดียวจบ
4.1 ปัญหา
Deployment/StatefulSet = สำหรับงานที่ "รันตลอด" (web server รอ request ไม่จบ)
แต่บางงาน "ทำครั้งเดียวแล้วจบ":
- database migration
- ประมวลผล batch
- ส่ง email จำนวนมาก
- import ข้อมูล
ถ้าใช้ Deployment กับงานพวกนี้ → งานเสร็จ pod จบ → Deployment เห็นว่า "pod ตาย" → สร้างใหม่ → รันงานซ้ำไม่จบ ❌
4.2 Job
Job = รัน pod จนทำงานเสร็จ (exit 0) แล้วจบ — ไม่ restart วน
yaml
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
spec:
template:
spec:
containers:
- name: migrate
image: node:20-alpine # ใช้ image จริงที่ pull ได้
command: ["sh", "-c", "echo running migration; sleep 5; echo done"]
restartPolicy: OnFailure # ถ้า fail → ลองใหม่ / Never → ไม่ลอง
backoffLimit: 4 # ลองใหม่ได้ 4 ครั้งถ้า fail
activeDeadlineSeconds: 1800 # จำกัดเวลารวมของ Job ทั้งหมด (30 นาที)อธิบาย:
restartPolicy: OnFailure— ถ้า container fail → restart (Job ต้องเป็นOnFailureหรือNeverห้ามAlways)backoffLimit: 4— ลองใหม่ทั้งหมดได้ 4 ครั้ง — เกินนั้นถือว่า Job ล้มเหลวactiveDeadlineSeconds: 1800— จำกัดเวลารวมของ Job (ครอบ retry ทั้งหมด) — ต่างจากbackoffLimitที่นับ "จำนวนครั้ง" ตัวนี้นับ "เวลา" ป้องกัน Job ค้างวนไม่จบ
🔧 Job features ใหม่ (K8s ล่าสุด 2026):
suspend: true— pause/resume Job แบบ declarative- Indexed Job (
completionMode: Indexed) — parallel processing แบ่ง work item ตาม index (JOB_COMPLETION_INDEXenv)podFailurePolicy— granular retry logic เช่น "fail ทันทีถ้า exit code = 42" ไม่ต้องรอ backoffLimitsuccessPolicy(K8s 1.31 beta) — กำหนดเงื่อนไข success แบบยืดหยุ่นสำหรับ Indexed Job
bash
kubectl apply -f job.yaml
kubectl get jobs
# NAME COMPLETIONS DURATION AGE
# db-migration 1/1 25s 1m
kubectl logs job/db-migrationCOMPLETIONS 1/1 = เสร็จ 1 จาก 1 ที่ต้องการ
4.3 Job แบบขนาน
yaml
spec:
completions: 10 # ต้องสำเร็จทั้งหมด 10 ครั้ง
parallelism: 3 # รันพร้อมกันได้สูงสุด 3 podใช้กับงานที่แบ่งเป็นชิ้น ๆ ได้ — เช่น process 10 ไฟล์ ทำทีละ 3
4.4 ลบ Job อัตโนมัติ
yaml
spec:
ttlSecondsAfterFinished: 3600 # ลบ Job อัตโนมัติ 1 ชม. หลังเสร็จไม่งั้น Job ที่เสร็จแล้วค้างเต็มไปหมด
Part 5: CronJob — งานตามเวลา
5.1 CronJob
CronJob = Job ที่รันตามตารางเวลา (เหมือน cron ของ Linux)
yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-backup
spec:
schedule: "0 2 * * *" # ตี 2 ทุกวัน
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: postgres:16
env:
- name: PGPASSWORD
valueFrom:
secretKeyRef: { name: db-secret, key: password }
command:
- sh
- -c
- "pg_dump -h db -U postgres mydb > /backup/dump.sql"
restartPolicy: OnFailure📝
...ในตัวอย่างเก่าเป็น placeholder — ตัวอย่างจริงต้องระบุ host (-h db), user (-U postgres), database name, และต้องส่ง password ผ่านPGPASSWORDenv var (ดึงจาก Secret)
CronJob = "ตัวตั้งเวลา" ที่สร้าง Job ขึ้นมาตามเวลาที่กำหนด
5.2 Cron Schedule Syntax
schedule เขียนด้วย cron syntax 5 ช่อง (นาที ชั่วโมง วันที่ เดือน วันของสัปดาห์) — มาตรฐานเดียวกับ cron บน Linux:
┌─ นาที (0-59)
│ ┌─ ชั่วโมง (0-23)
│ │ ┌─ วันที่ของเดือน (1-31)
│ │ │ ┌─ เดือน (1-12)
│ │ │ │ ┌─ วันของสัปดาห์ (0-6, 0=อาทิตย์)
│ │ │ │ │
* * * * *ตัวอย่าง:
0 2 * * * — ตี 2 ทุกวัน
*/15 * * * * — ทุก 15 นาที
0 * * * * — ทุกชั่วโมง (นาทีที่ 0)
0 9 * * 1-5 — 9 โมงเช้า จันทร์-ศุกร์
0 0 1 * * — เที่ยงคืน วันที่ 1 ของเดือน5.3 CronJob Options
yaml
spec:
schedule: "0 2 * * *"
timeZone: "Asia/Bangkok" # timezone (GA ตั้งแต่ K8s 1.27 — ปัจจุบันใช้ได้ทุก cluster ที่ supported)
concurrencyPolicy: Forbid # ถ้า job เก่ายังไม่เสร็จ
successfulJobsHistoryLimit: 3 # เก็บประวัติ job ที่สำเร็จ 3 อัน
failedJobsHistoryLimit: 1
startingDeadlineSeconds: 300 # ถ้าเริ่มช้าเกิน 300 วิ → ข้ามconcurrencyPolicy:
Allow(default) — รันทับกันได้Forbid— ถ้า job เก่ายังไม่เสร็จ → ข้ามรอบนี้Replace— ยกเลิก job เก่า เริ่มใหม่
bash
kubectl get cronjobs
kubectl get jobs # เห็น job ที่ cronjob สร้าง
kubectl create job --from=cronjob/daily-backup manual-run # รันเอง (test)Part 6: เลือก Workload ยังไง — Flowchart
จำง่าย ๆ:
- Deployment — แอป stateless ทั่วไป (web, API)
- StatefulSet — database, แอปที่ pod ต้องมี identity
- DaemonSet — agent ที่ต้องรันทุก node
- Job — งานครั้งเดียว
- CronJob — งานตามเวลา
Part 7: Pod Scheduling — ควบคุมว่า pod ลง node ไหน
บางครั้งเราอยากคุมว่า pod ไปรันที่ node ไหน — K8s มีหลายกลไก. ขอแนะนำให้รู้จักศัพท์ก่อน (เป็นกลุ่มที่ผู้เริ่มต้นมักสับสน):
- nodeSelector — กลไกง่ายสุด ระบุ label ที่ node ต้องมี
- affinity (อะ-ฟิน-นิ-ที่ = ความใกล้ชิด/ผูกพัน) — ระบุเงื่อนไขแบบยืดหยุ่นว่า pod "อยากอยู่" ใกล้ใคร/บน node แบบไหน
- anti-affinity — ตรงข้ามกับ affinity = "ไม่อยากอยู่" ใกล้ใคร (กระจาย pod เพื่อ HA)
- taint (อ่าน "เทนต์" = ป้ายห้ามเข้า) — ติดบน node เพื่อ "ไล่" pod
- toleration (อ่าน "ทอล-เลอ-เร-ชั่น" = ใบผ่านทาง) — ติดบน pod เพื่อ "ทน" taint ของ node ได้
จะอธิบายทีละตัวด้านล่าง — ถ้ายังไม่ต้องการตอนนี้ ข้ามไปอ่าน Part 8 ได้ แล้วค่อยกลับมาอ่านเมื่อเจอ use case จริง (ดูเพิ่มเติมที่ K8s official docs: Assigning Pods to Nodes)
7.1 nodeSelector — ง่ายสุด
nodeSelector แค่ระบุ label ที่ต้องการ — K8s จะเลือกเฉพาะ node ที่มี label นั้น เหมาะกับเงื่อนไขแบบ "ตรงกันเป๊ะ":
yaml
spec:
template:
spec:
nodeSelector:
disktype: ssd→ pod รันเฉพาะ node ที่มี label disktype=ssd
(ตั้ง label ให้ node: kubectl label node node1 disktype=ssd)
7.2 Affinity / Anti-Affinity — ยืดหยุ่นกว่า
Node Affinity — "อยากอยู่ node แบบนี้" (ยืดหยุ่นกว่า nodeSelector):
yaml
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution: # ← กฎ "บังคับ" (hard)
nodeSelectorTerms:
- matchExpressions:
- key: disktype
operator: In
values: [ssd]📝
requiredDuringSchedulingIgnoredDuringExecution= ชื่อยาวมาก จำไม่ต้องจำชื่อเต็ม — แค่รู้ว่าrequired= บังคับ,Ignored= ละเว้น ตอนรัน (ดูkubectl explain pods.spec.affinityขณะเขียน YAML):
required...Scheduling— บังคับ ตอน schedule (ถ้าไม่มี node ตรงเงื่อนไข → pod จะ Pending ไม่ขึ้น)Ignored...Execution— แต่ถ้า pod รันอยู่แล้วและ label เปลี่ยน → ไม่ evict (ปล่อยรันต่อ)- มีอีกแบบคือ
preferredDuringSchedulingIgnoredDuringExecution= แค่อยาก (soft) — ถ้าไม่มี node ตรง ก็ยังลง node อื่นได้
Pod Anti-Affinity — "อย่าให้ pod ของฉันอยู่ node เดียวกัน" (กระจายเพื่อ HA):
yaml
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: myapp
topologyKey: kubernetes.io/hostname # ← กระจายข้าม node→ K8s พยายามกระจาย pod ของ myapp ไปคนละ node — ถ้า node หนึ่งล่ม ก็ยังมี pod บน node อื่น
📝
topologyKey= บอก "ระดับการกระจาย" โดยใช้ label ของ node:
kubernetes.io/hostname— กระจายข้าม nodetopology.kubernetes.io/zone— กระจายข้าม availability zone (สำคัญใน cloud — ถ้า zone หนึ่งล่ม pod ใน zone อื่นยังรอด)topology.kubernetes.io/region— กระจายข้าม region
7.2.1 topologySpreadConstraints — แนวทางใหม่กว่า
ตั้งแต่ K8s 1.19 (GA) — K8s มีกลไกใหม่ชื่อ topologySpreadConstraints ที่ flexible กว่า podAntiAffinity และอ่านง่ายกว่า. ปี 2026 production cluster ส่วนใหญ่ใช้ตัวนี้แทน anti-affinity แล้ว:
yaml
spec:
template:
spec:
topologySpreadConstraints:
- maxSkew: 1 # ผลต่างจำนวน pod ระหว่าง zone ห้ามเกิน 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule # ถ้ากระจายไม่ลง → Pending
labelSelector:
matchLabels:
app: myapp→ K8s กระจาย pod ของ myapp เท่า ๆ กันข้าม zone (ผลต่างไม่เกิน 1)
7.3 Taints + Tolerations — "node กันคนเข้า"
Taint = "ป้ายห้ามเข้า" ที่ติดบน node:
bash
kubectl taint node node1 dedicated=gpu:NoSchedule📝 แยกส่วน
dedicated=gpu:NoSchedule:
dedicated= key (ตั้งชื่อเองได้)gpu= value (ตั้งชื่อเองได้)NoSchedule= effect — แปลว่า "ห้าม schedule pod ใหม่ลง node นี้" (effect อื่น:PreferNoSchedule= แค่หลีกเลี่ยง,NoExecute= ไล่ pod ที่รันอยู่ออกด้วย)
→ node1 จะ "ปฏิเสธ" pod ทุกตัว ยกเว้น pod ที่มี toleration ตรงกัน
Toleration = "ใบผ่านทาง" ที่ติดบน pod:
yaml
spec:
tolerations:
- key: dedicated
operator: Equal
value: gpu
effect: NoSchedule→ pod นี้ "ทน" taint ของ node1 ได้ → ลงได้
ใช้ทำ: กันไม่ให้ pod ทั่วไปลง node พิเศษ (เช่น node ที่มี GPU แพง ๆ — ให้เฉพาะ pod ที่ต้องการ GPU เท่านั้นลงได้)
⚠️
operator: Existsโดยไม่ระบุkey= ยอมรับ ทุก taint บน cluster — รวม system taint เช่นnode.kubernetes.io/not-ready,node.kubernetes.io/unreachable, และ taint ของ control-plane node. เหมาะสำหรับ DaemonSet ที่ต้องรันทุก node (เช่น log agent) แต่ ระวัง อย่าใช้กับ workload ทั่วไปที่ไม่ควรรันบน node พิเศษ (เช่น GPU node, spot node)
⚠️ ผู้เริ่มต้น: ถ้ารู้สึกว่าศัพท์ taint/toleration/affinity เยอะเกินไปในรอบเดียว — ไม่ต้องจำหมดตอนนี้. ใช้แค่
nodeSelectorก็พอครอบคลุม 80% ของงานจริง. กลับมาอ่านส่วน affinity + taint ตอนเจอ use case จริงในบท 10
Part 8: ตัวอย่างจริง
8.1 PostgreSQL ด้วย StatefulSet
(manifest เต็มดูที่ Part 2.3)
(ดู Part 2.3 — manifest เต็ม)
8.2 Log agent ด้วย DaemonSet
(รายละเอียดดู Part 3.2)
(ดู Part 3.2)
8.3 Migration ด้วย Job
ตัวอย่างนี้ตั้ง backoffLimit ให้ retry และ ttlSecondsAfterFinished ให้ลบตัวเองทิ้งหลังเสร็จ:
yaml
apiVersion: batch/v1
kind: Job
metadata:
name: migrate-v2
spec:
ttlSecondsAfterFinished: 600
backoffLimit: 3
template:
spec:
containers:
- name: migrate
image: myapp:v2
command: ["./migrate", "up"]
restartPolicy: Never8.4 Backup ด้วย CronJob
ตัวอย่างนี้ตั้ง concurrencyPolicy: Forbid กันไม่ให้ backup รอบใหม่ทับรอบเก่าที่ยังไม่เสร็จ:
yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: db-backup
spec:
schedule: "0 3 * * *"
timeZone: "Asia/Bangkok"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 7
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: postgres:16
command:
- sh
- -c
- "pg_dump -h db -U postgres mydb | gzip > /backup/$(date +%F).sql.gz"
volumeMounts:
- name: backup
mountPath: /backup
volumes:
- name: backup
persistentVolumeClaim:
claimName: backup-pvc
restartPolicy: OnFailure📝 ต้องสร้าง PVC
backup-pvcก่อน apply CronJob นี้ — และเพราะแต่ละรอบ CronJob อาจถูก schedule ลง node ต่างกัน PVC ตัวนี้ควรมีaccessModes: ["ReadWriteMany"](RWX) เพื่อ mount จาก node ไหนก็ได้:yamlapiVersion: v1 kind: PersistentVolumeClaim metadata: { name: backup-pvc } spec: accessModes: ["ReadWriteMany"] # RWX — รองรับ multi-node resources: { requests: { storage: 50Gi } } storageClassName: nfs # ต้องใช้ storage ที่รองรับ RWX เช่น NFS, CephFS, EFSถ้าใช้
ReadWriteOnce(RWO) ปกติ — แต่ละรอบจะติดMulti-Attach errorถ้าไป node ใหม่
Part 9: Lab
Lab 1: StatefulSet
ลองดูว่าเมื่อลบ pod กลางทิ้ง มันสร้างใหม่ด้วยชื่อเดิมและผูก storage เดิมกลับมาไหม:
bash
# ใช้ manifest จาก Part 2.3 (ตัด env secret ออกหรือสร้าง secret ก่อน)
kubectl apply -f statefulset.yaml
kubectl get pods # db-0, db-1, db-2 (เรียงเลข)
kubectl get pvc # data-db-0, data-db-1, data-db-2
# ลบ db-1 — สังเกตว่ามันสร้างใหม่ชื่อเดิม
kubectl delete pod db-1
kubectl get pods -wLab 2: DaemonSet
ลองดูว่า pod กระจายครบทุก node หรือเปล่า:
yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: demo-ds
spec:
selector:
matchLabels: { app: demo-ds }
template:
metadata:
labels: { app: demo-ds }
spec:
containers:
- name: busybox
image: busybox
command: ['sh', '-c', 'while true; do sleep 3600; done']bash
kubectl apply -f ds.yaml
kubectl get pods -o wide # 1 pod ต่อ nodeLab 3: Job + CronJob
bash
# Job
kubectl create job pi --image=perl -- perl -Mbignum=bpi -wle 'print bpi(100)'
kubectl logs job/pi
# CronJob — ทุก 1 นาที
kubectl create cronjob hello --image=busybox --schedule="* * * * *" -- echo "Hello"
kubectl get jobs -w # เห็น job เกิดทุกนาทีPart 10: Checkpoint
- Deployment เหมาะกับ stateless หรือ stateful?
- StatefulSet ให้อะไรที่ Deployment ไม่มี (4 ข้อ)?
volumeClaimTemplatesทำอะไร?- DaemonSet ต่าง Deployment ยังไง? ทำไมไม่มี
replicas? - ทำไมใช้ Deployment กับ migration ไม่ได้?
- Job
restartPolicyต้องเป็นอะไร? ทำไม? backoffLimitคืออะไร?- CronJob ต่าง Job ยังไง?
concurrencyPolicy: Forbidทำอะไร?- Taint + Toleration ใช้ทำอะไร?
Part 10.5: ตารางสรุป — Workload + Scheduling
Workload types:
| English | ไทย | ใช้กับ | จำง่าย |
|---|---|---|---|
| Deployment | ดีพลอย-เม้นต์ | แอป stateless | web, API |
| StatefulSet | สเตจฟูล-เซต | แอปที่ pod ต้องมี identity + storage | database, Kafka |
| DaemonSet | เดมอน-เซต | งานที่ต้องรัน 1 ตัว/node | log/monitoring agent |
| Job | จ๊อบ | งานที่ทำครั้งเดียวจบ | migration, batch |
| CronJob | ครอน-จ๊อบ | งานตามเวลา | backup รายวัน |
Scheduling mechanisms:
| English | คำแปล | ใช้ทำ |
|---|---|---|
| nodeSelector | ตัวเลือก node ตาม label | บังคับ pod ลง node ที่มี label ตรง |
| Node Affinity | ความผูกพันกับ node | บังคับ/แนะนำ pod ลง node ตามเงื่อนไข (ยืดหยุ่นกว่า nodeSelector) |
| Pod Anti-Affinity | ตรงข้ามกับ affinity ระดับ pod | กระจาย pod ห้ามอยู่ที่เดียวกัน (เพื่อ HA) |
| topologySpreadConstraints | กฎกระจายตาม topology | กระจาย pod เท่า ๆ กันข้าม zone/node (modern) |
| Taint | ป้ายห้ามเข้า (บน node) | ไล่ pod ทั่วไปออกจาก node พิเศษ |
| Toleration | ใบผ่านทาง (บน pod) | ทน taint ของ node ได้ → ลงได้ |
Part 11: สรุปบทนี้
- Deployment — stateless (web, API)
- StatefulSet — stateful (database) — ชื่อ pod แน่นอน, storage แยก, เริ่มตามลำดับ
- DaemonSet — 1 pod ต่อ 1 node (log/monitoring agent)
- Job — งานครั้งเดียวจบ (migration, batch) —
restartPolicy: OnFailure/Never - CronJob — งานตามเวลา (backup, report) — สร้าง Job ตามตาราง
- Scheduling: nodeSelector / Affinity / Taint+Toleration คุมว่า pod ลง node ไหน
- StatefulSet ช่วย identity+storage — database cluster จริงส่วนใหญ่ใช้ Operator
บทต่อไป — Helm + Deployment Strategy
← บทที่ 07 | สารบัญ | บทที่ 09: Helm + Deployment →
Glossary: ../glossary.md · Style guide: ../CONTRIBUTING.md last_verified: 2026-06-12 · review report: ../REVIEW-2026-06-03.md