Skip to content

บทที่ 08 — Workload Types: StatefulSet, DaemonSet, Job, CronJob

← บทที่ 07 | สารบัญ | บทที่ 09: Helm + Deployment →

จนถึงตอนนี้เราใช้ 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แอป statelessweb, API
StatefulSetแอป stateful — pod ต้องมี identity + storagedatabase, Kafka
DaemonSetรัน 1 pod ทุก nodelog 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) — pod db-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-1db-2
  • ลบ: db-2 ก่อน → db-1db-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):

bash
kubectl create secret generic db-secret --from-literal=password=mysecretpw

ถ้าไม่สร้าง pod จะ stuck ที่ CreateContainerConfigError

อธิบายส่วนพิเศษ:

  • serviceName: db — ชี้ไป Headless Service ชื่อ db
  • volumeClaimTemplates — แม่แบบ 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 taint node-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 ตัว/node

3.3 DaemonSet เฉพาะบาง node

ถ้าอยากให้รันเฉพาะบาง node — ใช้ nodeSelector:

yaml
spec:
  template:
    spec:
      nodeSelector:
        disktype: ssd                  # รันเฉพาะ node ที่มี label disktype=ssd

Part 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_INDEX env)
  • podFailurePolicy — granular retry logic เช่น "fail ทันทีถ้า exit code = 42" ไม่ต้องรอ backoffLimit
  • successPolicy (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-migration

COMPLETIONS 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 ผ่าน PGPASSWORD env 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 — กระจายข้าม node
  • topology.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: Never

8.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 ไหนก็ได้:

yaml
apiVersion: 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 -w

Lab 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 ต่อ node

Lab 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

  1. Deployment เหมาะกับ stateless หรือ stateful?
  2. StatefulSet ให้อะไรที่ Deployment ไม่มี (4 ข้อ)?
  3. volumeClaimTemplates ทำอะไร?
  4. DaemonSet ต่าง Deployment ยังไง? ทำไมไม่มี replicas?
  5. ทำไมใช้ Deployment กับ migration ไม่ได้?
  6. Job restartPolicy ต้องเป็นอะไร? ทำไม?
  7. backoffLimit คืออะไร?
  8. CronJob ต่าง Job ยังไง?
  9. concurrencyPolicy: Forbid ทำอะไร?
  10. Taint + Toleration ใช้ทำอะไร?

Part 10.5: ตารางสรุป — Workload + Scheduling

Workload types:

Englishไทยใช้กับจำง่าย
Deploymentดีพลอย-เม้นต์แอป statelessweb, API
StatefulSetสเตจฟูล-เซตแอปที่ pod ต้องมี identity + storagedatabase, Kafka
DaemonSetเดมอน-เซตงานที่ต้องรัน 1 ตัว/nodelog/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