กลับไปยังบทความ

DevOps

Kubernetes Workloads: เลือกใช้ Deployment, StatefulSet และ DaemonSet ให้ถูกงาน

คู่มือเลือก Kubernetes workloads ให้ถูกงาน ว่าเมื่อไหร่ควรใช้ Deployment, StatefulSet, DaemonSet, Job หรือ CronJob พร้อมตัวอย่าง YAML ที่ใช้ได้จริงและข้อผิดพลาดที่เจอบ่อย

24 กันยายน 256910 นาทีในการอ่าน

kubernetes workloads deployment statefulset daemonset

Kubernetes workloads คือ controller ที่สร้างและดูแล Pod แทนคุณ ในงานจริงเราแทบไม่สร้าง Pod ด้วยมือเลย แต่จะบอกว่า process ของเราเป็นแบบไหน แล้วให้ controller ที่เหมาะสมคอยรักษาให้มี Pod ที่ถูกต้องรันอยู่เสมอ การเลือกให้ถูกตัวแทบทั้งหมดขึ้นอยู่กับคำถามเดียว: สำเนาแต่ละตัวของ process นี้ควรมีพฤติกรรมแบบไหน

  • สำเนาที่เหมือนกันทุกตัว สลับกันได้ และต้องรันตลอด: Deployment
  • สำเนาที่แต่ละตัวต้องมีชื่อคงที่และ disk ของตัวเอง: StatefulSet
  • สำเนาหนึ่งตัวบนทุก node: DaemonSet
  • งานที่รันจนเสร็จแล้วจบ: Job หรือ CronJob ถ้าต้องรันตามตารางเวลา

บทความนี้อธิบายทีละตัว พร้อม YAML ที่ใช้งานได้จริง และข้อผิดพลาดที่เจอบ่อย

ภาพรวม Kubernetes workloads

Workloadใช้กับชื่อ PodStorageตัวอย่าง
DeploymentService แบบ stateless ที่รันยาวสุ่ม (web-7c9f-x2kq8)ใช้ร่วมกันหรือไม่มีWeb server, API, worker
StatefulSetซอฟต์แวร์ stateful ที่ทำงานเป็นคลัสเตอร์คงที่ (db-0, db-1)PVC แยกต่อ PodDatabase, Kafka, etcd
DaemonSetAgent หนึ่งตัวต่อ nodeตัวเดียวต่อ nodeส่วนใหญ่ใช้ host pathLog 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

yaml
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

คำสั่งที่ใช้ประจำ:

bash
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:

  1. ชื่อคงที่ ตามลำดับเลข: db-0, db-1, db-2 ถ้า db-1 ถูกย้ายไป node อื่น มันก็กลับมาในชื่อ db-1 เหมือนเดิม
  2. ชื่อ DNS คงที่ ผ่าน headless Service เช่น db-1.db.default.svc.cluster.local
  3. PersistentVolumeClaim ของตัวเอง ที่สร้างจาก volumeClaimTemplates โดย db-1 จะผูกกับ data-db-1 เสมอ

โดยค่าเริ่มต้น Pod ยังถูกสร้างตามลำดับ (db-0 ต้อง Ready ก่อน db-1 จึงจะเริ่ม) และถูกลบในลำดับย้อนกลับ

yaml
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 เอง

yaml
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/log

Toleration ทำให้ 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 ครั้งเดียว

yaml
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

yaml
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 ให้ถูกตัว

ไล่ตามลำดับนี้:

  1. Process นี้มีจุดจบไหม ถ้ามี ใช้ Job หรือ CronJob ถ้าต้องรันซ้ำตามเวลา
  2. ต้องรันบนทุก node หรือทุก node บางประเภทไหม ใช้ DaemonSet
  3. แต่ละ replica ต้องมี identity คงที่หรือ disk ถาวรของตัวเองไหม ใช้ StatefulSet
  4. ถ้าไม่ใช่ทั้งหมด ใช้ 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 มีให้ลงมือทำทีละขั้น

พร้อมสร้างสิ่งที่ยิ่งใหญ่หรือยัง?

เล่าโปรเจคของคุณให้เราฟัง — เราจะพาคุณไปถึง

เลือกคุกกี้ที่เราใช้ได้ คุกกี้ที่จำเป็นช่วยให้เว็บไซต์ทำงานได้และเปิดใช้งานเสมอ