Kubernetes workloads คือ controller ที่สร้างและดูแล Pod แทนคุณ ในงานจริงเราแทบไม่สร้าง Pod ด้วยมือเลย แต่จะบอกว่า process ของเราเป็นแบบไหน แล้วให้ controller ที่เหมาะสมคอยรักษาให้มี Pod ที่ถูกต้องรันอยู่เสมอ การเลือกให้ถูกตัวแทบทั้งหมดขึ้นอยู่กับคำถามเดียว: สำเนาแต่ละตัวของ process นี้ควรมีพฤติกรรมแบบไหน
- สำเนาที่เหมือนกันทุกตัว สลับกันได้ และต้องรันตลอด: Deployment
- สำเนาที่แต่ละตัวต้องมีชื่อคงที่และ disk ของตัวเอง: StatefulSet
- สำเนาหนึ่งตัวบนทุก node: DaemonSet
- งานที่รันจนเสร็จแล้วจบ: Job หรือ CronJob ถ้าต้องรันตามตารางเวลา
บทความนี้อธิบายทีละตัว พร้อม YAML ที่ใช้งานได้จริง และข้อผิดพลาดที่เจอบ่อย
ภาพรวม Kubernetes workloads
| Workload | ใช้กับ | ชื่อ Pod | Storage | ตัวอย่าง |
|---|---|---|---|---|
| Deployment | Service แบบ stateless ที่รันยาว | สุ่ม (web-7c9f-x2kq8) | ใช้ร่วมกันหรือไม่มี | Web server, API, worker |
| StatefulSet | ซอฟต์แวร์ stateful ที่ทำงานเป็นคลัสเตอร์ | คงที่ (db-0, db-1) | PVC แยกต่อ Pod | Database, Kafka, etcd |
| DaemonSet | Agent หนึ่งตัวต่อ node | ตัวเดียวต่อ node | ส่วนใหญ่ใช้ host path | Log collector, monitoring agent, CNI |
| Job | งาน batch ที่มีจุดจบ | สุ่ม | แล้วแต่งาน | Migration, import, backfill |
| CronJob | งาน batch ตามตารางเวลา | สุ่ม | แล้วแต่งาน | รายงานรายคืน, cleanup, backup |
Pod: หน่วยที่ทุก workload ดูแล
Pod คือหน่วยเล็กที่สุดที่ Kubernetes ใช้ schedule ข้างในมี container ตั้งแต่หนึ่งตัวขึ้นไปที่ใช้ network namespace ร่วมกัน (คุยกันผ่าน localhost ได้) และแชร์ volume กันได้ Pod เป็นของใช้แล้วทิ้ง เมื่อตัวหนึ่งตาย มันจะไม่ถูกซ่อม แต่ถูกแทนที่ด้วย Pod ใหม่ที่มีชื่อใหม่และ IP ใหม่
นี่คือเหตุผลที่ไม่ควรรัน Pod เปล่า ๆ บน production ถ้า node ที่รัน Pod เปล่าอยู่พัง จะไม่มีใครสร้างมันขึ้นมาใหม่ workload controller มีไว้เพื่อคอยดูและแทนที่ Pod ให้
ReplicaSet: รักษาจำนวน Pod ให้ครบ
ReplicaSet ประกอบด้วย label selector, Pod template และจำนวน replica controller ของมันจะรักษาให้มี Pod ที่ match อยู่ตามจำนวนนั้นพอดี ถ้าหายไปหนึ่งตัวก็สร้างเพิ่ม ถ้าเกินก็ลบทิ้ง
แทบไม่มีใครเขียน ReplicaSet เอง เพราะ Deployment จะสร้างและจัดการให้ โดยมีหนึ่ง ReplicaSet ต่อหนึ่งเวอร์ชันของ Pod template ReplicaSet เก่าที่ถูก scale เหลือศูนย์ซึ่งเห็นได้จาก kubectl get rs คือกลไกที่ทำให้ rollback ได้
Deployment: ตัวเลือกหลักของแอป stateless
Deployment ดูแล ReplicaSet และเพิ่มความสามารถ rolling update กับ rollback เข้าไป ใช้กับทุกอย่างที่ Pod ทุกตัวเหมือนกันและแทนกันได้ เช่น web server, API หรือ consumer ของ queue
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
revisionHistoryLimit: 5
selector:
matchLabels:
app: web
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: ghcr.io/example/web:1.8.2
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /healthz
port: 8080
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Miคำสั่งที่ใช้ประจำ:
kubectl set image deployment/web web=ghcr.io/example/web:1.9.0
kubectl rollout status deployment/web
kubectl rollout history deployment/web
kubectl rollout undo deployment/web
kubectl scale deployment/web --replicas=5รายละเอียดที่ควรรู้:
selectorแก้ไขไม่ได้ในapps/v1เลือก label ที่มั่นใจว่าจะไม่อยากเปลี่ยนภายหลังRollingUpdateทยอยแทนที่ Pod ทีละส่วน ส่วนRecreateหยุด Pod เก่าทั้งหมดก่อนแล้วค่อยเปิดตัวใหม่ เหมาะกับแอปที่รันสองเวอร์ชันพร้อมกันไม่ได้ แต่แลกมากับ downtime- การ rollout แบบไม่มี downtime ยังต้องอาศัย readiness probe และ graceful shutdown ด้วย ดูรายละเอียดได้ใน Kubernetes rolling update แบบไม่มี downtime
StatefulSet: identity และ storage ที่คงที่
ซอฟต์แวร์บางตัวต้องแยกแยะได้ว่า replica ไหนเป็นตัวไหน database ต้องรู้ว่า member ไหนเป็น primary, Kafka broker ต้องมี ID คงที่และต้องกลับไปใช้ log directory เดิมของตัวเองหลัง restart StatefulSet จึงรับประกันสามอย่างให้แต่ละ Pod:
- ชื่อคงที่ ตามลำดับเลข:
db-0,db-1,db-2ถ้าdb-1ถูกย้ายไป node อื่น มันก็กลับมาในชื่อdb-1เหมือนเดิม - ชื่อ DNS คงที่ ผ่าน headless Service เช่น
db-1.db.default.svc.cluster.local - PersistentVolumeClaim ของตัวเอง ที่สร้างจาก
volumeClaimTemplatesโดยdb-1จะผูกกับdata-db-1เสมอ
โดยค่าเริ่มต้น Pod ยังถูกสร้างตามลำดับ (db-0 ต้อง Ready ก่อน db-1 จึงจะเริ่ม) และถูกลบในลำดับย้อนกลับ
apiVersion: v1
kind: Service
metadata:
name: db
spec:
clusterIP: None # headless: DNS returns Pod IPs
selector:
app: db
ports:
- name: postgres
port: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
spec:
serviceName: db
replicas: 3
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: postgres
image: postgres:16
envFrom:
- secretRef:
name: db-credentials # contains POSTGRES_PASSWORD
env:
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
ports:
- name: postgres
containerPort: 5432
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 20Giต้องเข้าใจให้ชัดว่า manifest นี้ไม่ได้ทำอะไร มันรัน PostgreSQL สามตัวที่ แยกขาดจากกัน StatefulSet ให้แค่ identity กับ storage ส่วน replication, การเลือก leader และ failover เป็นหน้าที่ของตัวแอปหรือ operator ในทางปฏิบัติทีมส่วนใหญ่จึงรัน database ผ่าน operator หรือใช้ managed service ข้อดีข้อเสียของแต่ละทางอยู่ในบทความ การรัน database บน production
พฤติกรรมอื่นของ StatefulSet:
- PVC อยู่นานกว่า Pod เมื่อ scale จาก 3 เหลือ 2,
db-2ถูกลบแต่data-db-2ยังอยู่ พอ scale กลับขึ้นมาก็ได้ข้อมูลเดิมคืน ปรับพฤติกรรมนี้ได้ด้วยpersistentVolumeClaimRetentionPolicy - Update ไล่จากเลขมากไปเลขน้อย ถ้าตั้ง
updateStrategy.rollingUpdate.partition: 2จะอัปเดตแค่db-2ก่อน ใช้เป็น canary ได้ podManagementPolicy: Parallelข้ามการเรียงลำดับ เมื่อซอฟต์แวร์ไม่ต้องการลำดับ ทำให้ scale ได้เร็วขึ้น
Headless Service และ DNS ราย Pod อธิบายเพิ่มเติมไว้ใน Kubernetes Service types
DaemonSet: หนึ่ง Pod ต่อหนึ่ง node
DaemonSet รัน Pod หนึ่งตัวบนทุก node หรือทุก node ที่ตรงกับ selector เมื่อมี node ใหม่เข้าคลัสเตอร์ DaemonSet จะวาง Pod ลงไปให้เอง เมื่อ node ออกไป Pod ก็ออกไปด้วย งานโครงสร้างพื้นฐานระดับ node รันแบบนี้ทั้งหมด เช่น log collector, monitoring agent อย่าง node-exporter หรือ OpenTelemetry Collector แบบ agent, CNI plugin, CSI node driver และตัว kube-proxy เอง
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: log-agent
namespace: logging
spec:
selector:
matchLabels:
app: log-agent
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
template:
metadata:
labels:
app: log-agent
spec:
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
containers:
- name: fluent-bit
image: fluent/fluent-bit:3.1
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 128Mi
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
volumes:
- name: varlog
hostPath:
path: /var/logToleration ทำให้ agent ลงไปรันบน node ของ control plane ที่มี taint NoSchedule ได้ด้วย ถ้าต้องการเฉพาะบาง node เช่นเฉพาะ node ที่มี GPU ให้เพิ่ม nodeSelector หรือ node affinity ใน Pod template
ข้อผิดพลาดที่เจอบ่อยคือใช้ Deployment แล้วตั้ง replicas เท่ากับจำนวน node ซึ่งไม่มีอะไรรับประกันว่าจะได้หนึ่ง Pod ต่อ node สอง replica อาจไปกองอยู่ node เดียวกันได้ และจำนวนก็ไม่ปรับตามเมื่อมีการเพิ่มหรือลด node ถ้าโจทย์คือ "ต้องมีบนทุก node" คำตอบคือ DaemonSet
Job: รันจนเสร็จแล้วจบ
Job รัน Pod จนกว่าจะมีจำนวนที่สำเร็จครบตามที่กำหนด ใช้กับ database migration, การ import ข้อมูล หรือ backfill ครั้งเดียว
apiVersion: batch/v1
kind: Job
metadata:
name: migrate-2026-09
spec:
backoffLimit: 3 # retries before the Job is marked failed
activeDeadlineSeconds: 600 # hard time limit for the whole Job
ttlSecondsAfterFinished: 3600 # clean up an hour after it finishes
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: ghcr.io/example/web:1.9.0
command: ["./migrate", "up"]restartPolicy ต้องเป็น Never หรือ OnFailure ห้ามเป็น Always ถ้าต้องการรันแบบขนาน ให้ตั้ง completions (ต้องสำเร็จกี่ Pod) และ parallelism (รันพร้อมกันกี่ตัว) เนื่องจากมีการ retry งานเดียวกันจึงอาจรันซ้ำได้ ต้องออกแบบให้ idempotent
CronJob: Job ตามตารางเวลา
CronJob สร้าง Job ตาม cron schedule เหมือน cron บนเครื่อง Linux
apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-report
spec:
schedule: "0 2 * * *"
timeZone: "Asia/Bangkok"
concurrencyPolicy: Forbid # skip a run if the last one is still going
startingDeadlineSeconds: 300
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
jobTemplate:
spec:
backoffLimit: 2
template:
spec:
restartPolicy: OnFailure
containers:
- name: report
image: ghcr.io/example/reports:2.3.0
args: ["--date", "yesterday"]ถ้าไม่ใส่ timeZone schedule จะอิงเวลาของ kube-controller-manager ซึ่งส่วนใหญ่เป็น UTC ทำให้งานตี 2 กลายเป็น 9 โมงเช้าเวลาไทย concurrencyPolicy ตั้งได้เป็น Allow (ค่าเริ่มต้น), Forbid หรือ Replace และ CronJob controller มีโอกาสพลาดรอบหรือสร้าง Job ซ้ำได้บ้าง งานตามตารางเวลาจึงต้อง idempotent เช่นกัน
เลือก Kubernetes workload ให้ถูกตัว
ไล่ตามลำดับนี้:
- Process นี้มีจุดจบไหม ถ้ามี ใช้ Job หรือ CronJob ถ้าต้องรันซ้ำตามเวลา
- ต้องรันบนทุก node หรือทุก node บางประเภทไหม ใช้ DaemonSet
- แต่ละ replica ต้องมี identity คงที่หรือ disk ถาวรของตัวเองไหม ใช้ StatefulSet
- ถ้าไม่ใช่ทั้งหมด ใช้ Deployment
แอปส่วนใหญ่จะจบที่ Deployment ถ้ากำลังจะใช้ StatefulSet กับ service ที่เขียนเอง ลองถามก่อนว่า state นั้นย้ายไปอยู่ใน database หรือ object storage ได้ไหม เพราะ service แบบ stateless scale และ rollout ง่ายกว่ามาก ส่วนเรื่อง storage ของ StatefulSet อ่านต่อได้ที่ Kubernetes Persistent Volume, PVC และ StorageClass
คำถามที่พบบ่อย
Deployment กับ StatefulSet ต่างกันอย่างไร
Pod ของ Deployment แทนกันได้ ชื่อสุ่ม และใช้ storage ร่วมกันหรือไม่มีเลย ส่วน Pod ของ StatefulSet มีชื่อคงที่, DNS คงที่ และ PVC ของตัวเอง โดยถูกสร้างและอัปเดตตามลำดับที่กำหนด
ควรสร้าง ReplicaSet เองไหม
แทบไม่เคยต้องทำ ให้สร้าง Deployment แล้วปล่อยให้มันจัดการ ReplicaSet เอง จะได้ rolling update และ rollback มาโดยไม่ต้องทำอะไรเพิ่ม
Deployment ใช้ PersistentVolume ได้ไหม
ได้ แต่ทุก replica จะ mount claim เดียวกัน ถ้าเป็น volume แบบ ReadWriteOnce จะใช้ได้ก็ต่อเมื่อทุก replica อยู่บน node เดียวกัน ทางเลือกคือใช้ volume แบบ ReadWriteMany, ใช้ replica เดียว หรือเปลี่ยนไปใช้ StatefulSet
จะรัน Pod บนทุก node ใน Kubernetes ได้อย่างไร
ใช้ DaemonSet เพิ่ม toleration ถ้าต้องรันบน node ที่มี taint อย่าง control plane และเพิ่ม node selector ถ้าต้องการจำกัดเฉพาะบาง node
Volume ของ StatefulSet จะเป็นอย่างไรเมื่อ scale down
โดยค่าเริ่มต้น PVC และข้อมูลจะยังอยู่ เมื่อ scale กลับขึ้นมาก็ผูกกลับเข้าที่เดิม ถ้าอยากให้ลบทิ้งให้ตั้ง persistentVolumeClaimRetentionPolicy
Checklist สำหรับ workload
- ไม่มี Pod เปล่าบน production ทุก Pod ต้องมี controller ดูแล
- Service แบบ stateless รันเป็น Deployment พร้อม readiness probe และ resource request
- Database และ broker ใช้ StatefulSet โดยมีตัวซอฟต์แวร์หรือ operator ดูแล replication
- Agent ระดับ node รันเป็น DaemonSet พร้อม toleration ที่จำเป็น
- Job และ CronJob ออกแบบให้ idempotent และตั้ง
backoffLimit, deadline กับttlSecondsAfterFinishedไว้
ถ้าอยากลองใช้ workload เหล่านี้บนคลัสเตอร์จริง คอร์ส DevOps ฟรีของ Vectorkub มีให้ลงมือทำทีละขั้น
