← Kubernetes← Kubernetes
KubernetesKubernetes19 Th7, 2026Jul 19, 202619 phút đọc16 min read

Pod và WorkloadPods and Workloads

Thuộc bộ tài liệu kiến thức theo Kubernetes Roadmap.

Tổng quan

Pod là đơn vị triển khai nhỏ nhất trong Kubernetes — không phải một container, mà là một lớp bọc quanh một hoặc nhiều container gắn kết chặt chẽ, cùng chia sẻ một network namespace, một địa chỉ IP, và các storage volume. Tuy nhiên bạn hiếm khi tạo Pod trần. Trong thực tế bạn khai báo một workload controller ở tầng cao hơn — một Deployment, StatefulSet, DaemonSet, Job, hay CronJob — và để nó tạo và quản lý Pod hộ bạn. Controller là thứ cho bạn self-healing, scaling, và rolling update; Pod chỉ là thứ chạy.

Sự phân tầng này là có chủ đích. Một Pod đứng một mình thì mong manh và không được quản lý: nếu nó chết, nó cứ chết vậy, và nếu node của nó chết, nó đơn giản là biến mất. Controller thêm vòng lặp reconciliation quanh các Pod — “luôn giữ 3 cái này chạy”, “roll từ v1 sang v2 không downtime”, “chạy một cái trên mỗi node”, “chạy cái này tới khi hoàn thành” — biến các Pod đơn lẻ dễ vỡ thành một dịch vụ bền bỉ, tự bảo trì. Chọn đúng controller cho một workload là một trong những quyết định thiết kế hệ trọng nhất trong Kubernetes, vì nó quyết định app của bạn scale, update, sống sót qua lỗi, và giữ (hay không giữ) trạng thái như thế nào.

Một mô hình tư duy

Hãy nghĩ về Pod template như một con dấu và controller như cỗ máy cầm con dấu đó. Bạn khắc con dấu một lần (Pod spec: image, port, resource, probe) rồi giao cho một cỗ máy quyết định dập bao nhiêu bản, ở đâu, theo thứ tự nào, và làm gì khi một bản bị nhòe. Một Deployment dập ra các bản giống hệt nhau, thay thế được cho nhau. Một StatefulSet dập ra các bản đánh số giữ nguyên số của mình và giữ nguyên đĩa. Một DaemonSet dập đúng một bản trên mỗi node. Một Job dập ra các bản có ý định chạy xong rồi dừng. Cùng con dấu, những cỗ máy rất khác nhau — chọn cỗ máy khớp với nhu cầu của workload.

Kiến thức nền tảng

Pod

Một Pod gom nhóm các container phải chạy cùng nhau trên cùng một node và chia sẻ tài nguyên. Trong một Pod, các container chia sẻ:

Hầu hết Pod chạy một container ứng dụng duy nhất. Pod nhiều container tồn tại cho các pattern trợ giúp nơi container thứ hai thực sự gắn kết với container chính:

Đừng dùng Pod nhiều container để chạy các dịch vụ riêng biệt cùng nhau — đó là việc của các Deployment riêng. Chỉ đặt chung những gì chia sẻ vòng đời và số phận.

Vòng đời và phase của Pod

Một Pod đi qua một tập nhỏ các phase cấp cao (status.phase):

PhaseÝ nghĩa
PendingĐã được cluster chấp nhận nhưng chưa chạy — đang chờ schedule, hoặc đang pull image
RunningĐã gán vào một node; ít nhất một container đang chạy (hoặc đang khởi động/restart)
SucceededMọi container kết thúc thành công và sẽ không restart (Job)
FailedMọi container kết thúc, ít nhất một cái lỗi, và sẽ không restart
UnknownKhông lấy được trạng thái của node (thường là vấn đề node/mạng)

Phase là thô; chi tiết thực sự nằm ở trạng thái container (Waiting, Running, Terminated) và condition (PodScheduled, Initialized, ContainersReady, Ready). Ready là cái quyết định traffic — một Pod chỉ được thêm vào endpoint của một Service khi readiness probe của nó pass.

Restart policy

spec.restartPolicy của Pod điều khiển kubelet làm gì khi một container thoát (exit):

Restart dùng exponential backoff (chặn trần ở 5 phút). Một container kẹt trong vòng crash-restart-crash hiện lên là CrashLoopBackOff — một trạng thái, không phải một loại lỗi; nguyên nhân thật nằm trong log (--previous) và events. Restart policy áp dụng cho container bên trong một Pod; nó không hồi sinh một Pod đã bị xóa hoặc có node đã chết — đó là việc của controller.

Nhắc lại về probe

Ba loại probe định hình sự tham gia của một container vào dịch vụ (được nói kỹ ở file giới thiệu, tóm tắt ở đây vì chúng chi phối hành vi workload):

Khái niệm chính

Các workload controller nhìn tổng quan

ControllerGiữDanh tínhThứ tựStorageDùng điển hình
ReplicaSetN Pod giống hệtThay thế đượcKhôngEphemeralTầng thấp; được quản lý bởi Deployment
DeploymentN Pod qua ReplicaSetThay thế đượcKhôngEphemeralApp stateless, API, web frontend
StatefulSetN Pod, tên ổn địnhỔn định, đánh sốCó thứ tựPersistent theo PodDatabase, Kafka, ZooKeeper, hệ quorum
DaemonSetMột Pod mỗi nodeTheo nodeKhôngThường là host pathAgent log/metric, CNI, CSI, tuning node
JobPod tới khi hoàn thànhThay thế đượcSong song hoặc tuần tựEphemeralTác vụ batch, migration, xử lý một lần
CronJobJob theo lịchBackup, báo cáo, dọn dẹp định kỳ

ReplicaSet

Một ReplicaSet có một việc duy nhất: đảm bảo một số lượng Pod giống hệt nhau đã cho luôn chạy. Nó theo dõi các Pod khớp selector của nó, tạo thêm nếu quá ít, xóa bớt nếu quá nhiều. Bạn gần như không bao giờ tạo ReplicaSet trực tiếp, vì chúng không có update strategy — đổi Pod template của một ReplicaSet không roll ra các Pod đang tồn tại. Thay vào đó bạn dùng Deployment, nó quản lý ReplicaSet hộ bạn và thêm logic rollout ở trên.

Deployment — rollout, strategy, và rollback

Một Deployment là workload mặc định cho app stateless. Nó quản lý các ReplicaSet để cung cấp update khai báo: đổi Pod template (ví dụ bump image) và apply lại, và Deployment điều phối quá trình chuyển từ ReplicaSet cũ sang một cái mới.

Hai strategy:

Vì mỗi phiên bản là một ReplicaSet riêng biệt mà Deployment giữ lại (tới revisionHistoryLimit), rollback cực kỳ đơn giản:

kubectl rollout status deploy/web            # xem quá trình roll
kubectl rollout history deploy/web           # liệt kê các revision
kubectl rollout undo deploy/web              # về revision trước
kubectl rollout undo deploy/web --to-revision=3
kubectl rollout restart deploy/web           # roll lại (ví dụ để nhận Secret đã đổi)
kubectl rollout pause deploy/web             # tạm dừng giữa chừng (kiểm tra kiểu canary)

StatefulSet — danh tính ổn định và vòng đời có thứ tự

Một số workload không thể là những Pod thay thế được, dùng xong vứt — một database replica cần giữ cùng tên, cùng storage, và cùng danh tính mạng qua các lần restart và reschedule. Đó là StatefulSet. Nó cung cấp:

StatefulSet nặng về vận hành hơn Deployment; với các hệ stateful phức tạp, một Operator chuyên dụng (một CRD cộng một controller, ví dụ operator CloudNativePG, Strimzi/Kafka, hay Zalando Postgres) thường quản lý StatefulSet hộ bạn và lo backup, failover, và upgrade phiên bản.

DaemonSet

Một DaemonSet đảm bảo một bản của một Pod chạy trên mọi node (hoặc mọi node khớp một selector). Khi node join cluster, DaemonSet controller thêm Pod vào chúng; khi node rời đi, Pod của chúng bị garbage-collect. Đây là mô hình cho hạ tầng cấp node: bộ thu log (Fluent Bit), agent metric (node-exporter), CNI plugin, CSI driver, và agent bảo mật. Pod của DaemonSet thường cần tolerations để chạy trên các node bị taint (kể cả node control-plane) và hay mount host path, nên hãy coi chúng là hạ tầng đặc quyền và review kỹ.

Job và CronJob

Một Job chạy các Pod tới khi một số lượng đã cho hoàn thành thành công, rồi dừng — mô hình cho công việc hữu hạn, chạy-tới-khi-xong (một migration, một tính toán batch, một lần export dữ liệu). Các núm chính: completions (cần bao nhiêu lần chạy thành công), parallelism (bao nhiêu cái chạy cùng lúc), backoffLimit (số lần retry trước khi Job bị đánh Failed), và activeDeadlineSeconds (timeout theo thời gian thực). Kết hợp với restartPolicy: OnFailure hoặc Never.

Một CronJob tạo Job theo một lịch (cú pháp cron chuẩn), cho backup, báo cáo, và dọn dẹp định kỳ:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-backup
spec:
  schedule: "0 2 * * *"                 # 02:00 mỗi ngày
  concurrencyPolicy: Forbid             # không bắt đầu lần chạy mới nếu lần trước còn chạy
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      backoffLimit: 2
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: backup
              image: myorg/pgbackup:1.2
              args: ["--target", "s3://backups/db"]

Chú ý concurrencyPolicy (Allow/Forbid/Replace) để các lần chạy chậm không chồng đống, và startingDeadlineSeconds để một lịch bị lỡ (do controller downtime) không kích một cơn bão Job chạy bù.

Label, selector, và annotation

Label (key: value trên metadata) là cách controller claim Pod và Service tìm endpoint — chất keo loose-coupling. Hãy áp dụng bộ label khuyến nghị app.kubernetes.io/name, app.kubernetes.io/instance, app.kubernetes.io/version, và app.kubernetes.io/component để dashboard, công cụ chi phí, và selector đều nói cùng một ngôn ngữ. Selector có hai dạng: dựa trên đẳng thức (app=web) và dựa trên tập hợp (environment in (prod, staging)). spec.selector của một controller là bất biến trên Deployment/StatefulSet — chọn kỹ, vì bạn không thể đổi nó sau này mà không tạo lại object.

Annotation mang metadata không định danh mà công cụ và controller đọc — thông tin build, một config hash để buộc rollout, cấu hình ingress-controller, kubectl.kubernetes.io/last-applied-configuration. Bạn không bao giờ select theo annotation, và chúng có thể chứa giá trị lớn hoặc có cấu trúc mà label không chứa được.

Pod Disruption Budget

Disruption đến từ hai loại: không tự nguyện (involuntary) (node crash, lỗi phần cứng — bạn không ngăn được) và tự nguyện (voluntary) (drain node để upgrade, scale-down cluster, kubectl drain). Một PodDisruptionBudget (PDB) bảo vệ tính sẵn sàng trong lúc disruption tự nguyện bằng cách giới hạn bao nhiêu Pod của một tập được down cùng lúc:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web
spec:
  minAvailable: 2          # hoặc: maxUnavailable: 1
  selector:
    matchLabels:
      app: web

Với cái này, một lần drain node sẽ chặn việc evict một Pod web nếu làm vậy khiến số Pod sẵn sàng tụt xuống dưới 2, buộc drain phải chờ tới khi một Pod thay thế Ready ở nơi khác. Một PDB vô nghĩa với một replica duy nhất — bạn cần đủ replica để có dư địa. Lưu ý PDB không bảo vệ khỏi disruption không tự nguyện; nó chỉ khiến disruption tự nguyện tôn trọng ngưỡng sẵn sàng của bạn.

Chọn đúng loại workload

Lỗi thường gặp: nhồi một database vào một Deployment. Deployment coi Pod là thay thế được cho nhau và dùng storage chia sẻ hoặc ephemeral, nên hai Pod đang roll có thể tranh nhau cùng một volume hoặc mất dữ liệu. Dữ liệu stateful thuộc về một StatefulSet với PVC theo từng Pod, hoặc tốt hơn, một database managed.

Ví dụ có lời giải — Deployment và StatefulSet, giải thích

Một frontend stateless dưới dạng một Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: storefront
  labels:
    app.kubernetes.io/name: storefront
spec:
  replicas: 4
  selector:
    matchLabels:
      app.kubernetes.io/name: storefront
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1                 # thêm một Pod dư trong lúc roll
      maxUnavailable: 0           # không bao giờ tụt dưới capacity mong muốn
  template:
    metadata:
      labels:
        app.kubernetes.io/name: storefront
    spec:
      containers:
        - name: app
          image: ghcr.io/acme/storefront:2.5.1     # tag bất biến
          ports:
            - containerPort: 8080
          resources:
            requests: { cpu: 100m, memory: 128Mi } # chi phối schedule
            limits:   { cpu: 500m, memory: 256Mi } # chặn trần usage
          readinessProbe:                          # quyết định traffic trong lúc rollout
            httpGet: { path: /healthz, port: 8080 }
          livenessProbe:
            httpGet: { path: /livez, port: 8080 }
            initialDelaySeconds: 10

Vì sao chọn như vậy: replica thay thế được cho nhau → Deployment; maxUnavailable: 0 giữ đủ capacity trong lúc roll; readiness probe nghĩa là một Pod mới chỉ nhận traffic khi đã khỏe, và quá trình roll dừng lại nếu Pod mới không bao giờ Ready — một image lỗi không thể làm sập dịch vụ. Resource request cho phép scheduler đặt Pod và bật autoscaling.

Một replica set PostgreSQL dưới dạng một StatefulSet (sơ đồ — trong production dùng operator):

apiVersion: v1
kind: Service
metadata:
  name: pg                       # headless Service → DNS theo từng Pod
spec:
  clusterIP: None
  selector:
    app: pg
  ports:
    - port: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: pg
spec:
  serviceName: pg                # gắn StatefulSet với headless Service
  replicas: 3
  selector:
    matchLabels:
      app: pg
  template:
    metadata:
      labels:
        app: pg
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: postgres
          image: postgres:16.3
          ports:
            - containerPort: 5432
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
          readinessProbe:
            exec: { command: ["pg_isready", "-U", "postgres"] }
  volumeClaimTemplates:          # mỗi Pod nhận đĩa persistent RIÊNG của nó
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 20Gi

Vì sao chọn như vậy: các Pod không thay thế được cho nhau — pg-0, pg-1, pg-2 giữ nguyên tên và mỗi cái sở hữu một PVC riêng data-pg-N sống sót qua reschedule và cả khi xóa StatefulSet. Headless Service (clusterIP: None) cấp cho mỗi Pod một tên DNS ổn định (pg-0.pg...) để client và replica gọi được thành viên cụ thể — thiết yếu cho topology primary/replica. Pod lên theo thứ tự, nên pg-0 (giả sử là primary) Ready trước khi các replica join. Đây chính xác là thứ một Deployment không thể cung cấp.

Best Practices

  1. Không bao giờ chạy Pod trần trong production — luôn dùng một controller. Một Pod đơn độc không có self-healing; Deployment/StatefulSet/DaemonSet reschedule nó sau khi mất node.
  2. Khớp controller với workload. Stateless → Deployment, stateful → StatefulSet (hoặc Operator), theo node → DaemonSet, hữu hạn → Job, theo lịch → CronJob. Nhồi database vào Deployment có nguy cơ mất dữ liệu.
  3. Đặt resource request và limit trên mọi container. Request chi phối schedule và autoscaling; limit chặn phạm vi ảnh hưởng. Thiếu request khiến scheduler mù; thiếu limit để một Pod bỏ đói cả node.
  4. Định nghĩa readiness (và khi hữu ích, liveness/startup) probe. Readiness làm rolling update không downtime bằng cách giữ Pod chưa sẵn sàng ngoài load balancer; một rollout với probe tốt sẽ tự phanh khi gặp image lỗi.
  5. Mặc định dùng RollingUpdate với maxUnavailable: 0, và chỉ dùng Recreate khi các phiên bản thực sự không thể cùng tồn tại. Giữ revisionHistoryLimit hợp lý để rollback được nhưng ReplicaSet cũ không tích tụ mãi.
  6. Chạy ít nhất 2–3 replica và ghép với một PodDisruptionBudget. Điều này sống sót qua drain node và upgrade cluster mà không tụt về 0 — một PDB trên app một replica thì vô dụng.
  7. Dùng init container cho setup có thứ tự, sidecar cho helper gắn kết — và ưu tiên sidecar native (initContainer với restartPolicy: Always) để thứ tự khởi động/shutdown đúng. Đừng đặt chung các dịch vụ không liên quan vào một Pod.
  8. Cho StatefulSet một headless Service và volumeClaimTemplates. Đó là thứ mang lại danh tính ổn định và storage theo từng Pod; thiếu chúng bạn không có ngữ nghĩa StatefulSet.
  9. Xử lý SIGTERM và tinh chỉnh terminationGracePeriodSeconds với một preStop hook để request đang xử lý được drain trong lúc rollout và drain thay vì bị cắt đột ngột.
  10. Đặt label nhất quán theo quy ước app.kubernetes.io/*, và coi spec.selector của controller là bất biến — chọn có chủ đích, vì đổi nó nghĩa là tạo lại object.
  11. Dùng annotation, không phải label, cho metadata không dùng để select (thông tin build, config hash, cấu hình công cụ); để dành label cho những gì bạn thực sự select hoặc gom nhóm.
  12. Với Job, đặt backoffLimitactiveDeadlineSeconds; với CronJob, đặt concurrencyPolicy và history limit để retry có giới hạn và các lần chạy chậm không chồng đống.
  13. Scale app stateless bằng một HPA, không phải sửa replica bằng tay, và không bao giờ chạy HPA và VPA trên cùng một metric — chúng đánh nhau.
  14. Review DaemonSet như hạ tầng đặc quyền. Chúng thường tolerate taint và mount host path trên mọi node; một cái lỗi có tầm với toàn cluster.
  15. Ưu tiên một dịch vụ managed hoặc một Operator trưởng thành cho hệ stateful phức tạp. Tự làm failover database trên một StatefulSet trần tốn công hơn nhiều so với vẻ ngoài.
  16. Xem CrashLoopBackOff là triệu chứng, không phải nguyên nhân. Lý do thật nằm trong kubectl logs --previous và events của kubectl describe (cấu hình sai, thiếu dependency, OOM) — sửa cái đó, không phải vòng lặp restart.

Tài liệu tham khảo

Part of the Kubernetes Roadmap knowledge base.

Overview

A Pod is the smallest deployable unit in Kubernetes — not a container, but a wrapper around one or more tightly-coupled containers that share a network namespace, an IP address, and storage volumes. You rarely create bare Pods, though. In practice you declare a higher-level workload controller — a Deployment, StatefulSet, DaemonSet, Job, or CronJob — and let it create and manage Pods for you. The controller is what gives you self-healing, scaling, and rolling updates; the Pod is just the thing that runs.

This layering is deliberate. A Pod on its own is mortal and unmanaged: if it dies, it stays dead, and if its node dies, it’s simply gone. Controllers add the reconciliation loop around Pods — “always keep 3 of these running”, “roll from v1 to v2 without downtime”, “run one on every node”, “run this to completion” — turning fragile individual Pods into a durable, self-maintaining service. Choosing the right controller for a workload is one of the most consequential design decisions in Kubernetes, because it determines how your app scales, updates, survives failure, and holds (or doesn’t hold) state.

A mental model

Think of the Pod template as a stamp and the controller as the machine holding it. You engrave the stamp once (the Pod spec: image, ports, resources, probes) and hand it to a machine that decides how many impressions to make, where, in what order, and what to do when one smudges. A Deployment stamps identical, interchangeable copies. A StatefulSet stamps numbered copies that keep their number and their disk. A DaemonSet stamps exactly one per node. A Job stamps copies that are meant to finish and stop. Same stamp, very different machines — pick the machine to match the workload’s needs.

Fundamentals

The Pod

A Pod groups containers that must run together on the same node and share resources. Within a Pod, containers share:

Most Pods run a single application container. Multi-container Pods exist for helper patterns where a second container is genuinely coupled to the main one:

Don’t reach for multi-container Pods to run separate services together — that’s what separate Deployments are for. Co-locate only what shares a lifecycle and a fate.

Pod lifecycle and phases

A Pod moves through a small set of top-level phases (status.phase):

PhaseMeaning
PendingAccepted by the cluster but not yet running — waiting to be scheduled, or pulling images
RunningBound to a node; at least one container is running (or starting/restarting)
SucceededAll containers terminated successfully and won’t restart (Jobs)
FailedAll containers terminated, at least one in failure, and won’t restart
UnknownThe node’s state can’t be obtained (usually a node/network problem)

Phases are coarse; the real detail lives in container states (Waiting, Running, Terminated) and conditions (PodScheduled, Initialized, ContainersReady, Ready). Ready is the one that gates traffic — a Pod is added to a Service’s endpoints only when its readiness probe passes.

Restart policy

A Pod’s spec.restartPolicy governs what the kubelet does when a container exits:

Restarts use exponential backoff (capped at 5 minutes). A container stuck in a crash-restart-crash cycle shows up as CrashLoopBackOff — a status, not an error type; the real cause is in the logs (--previous) and events. Restart policy applies to containers within a Pod; it does not resurrect a Pod that was deleted or whose node died — that’s the controller’s job.

Probes recap

Three probe types shape a container’s participation in the service (covered in depth in the introduction file, summarized here because they drive workload behavior):

Key Concepts

Workload controllers at a glance

ControllerKeepsIdentityOrderingStorageTypical use
ReplicaSetN identical PodsInterchangeableNoneEphemeralLow-level; managed by Deployments
DeploymentN Pods via ReplicaSetsInterchangeableNoneEphemeralStateless apps, APIs, web frontends
StatefulSetN Pods, stable namesStable, numberedOrderedPer-Pod persistentDatabases, Kafka, ZooKeeper, quorum systems
DaemonSetOne Pod per nodePer-nodeNoneUsually host pathsLog/metric agents, CNI, CSI, node tuning
JobPods to completionInterchangeableParallel or serialEphemeralBatch tasks, migrations, one-off processing
CronJobJobs on a scheduleBackups, reports, periodic cleanup

ReplicaSet

A ReplicaSet has one job: ensure a specified number of identical Pods are running at all times. It watches Pods matching its selector, creates more if there are too few, deletes some if there are too many. You almost never create ReplicaSets directly, because they have no update strategy — changing the Pod template of a ReplicaSet does not roll out to existing Pods. Instead you use a Deployment, which manages ReplicaSets for you and adds rollout logic on top.

Deployment — rollouts, strategies, and rollback

A Deployment is the default workload for stateless apps. It manages ReplicaSets to provide declarative updates: change the Pod template (e.g. bump the image) and re-apply, and the Deployment orchestrates the transition from the old ReplicaSet to a new one.

Two strategies:

Because each version is a distinct ReplicaSet that the Deployment keeps around (up to revisionHistoryLimit), rollback is trivial:

kubectl rollout status deploy/web            # watch the roll
kubectl rollout history deploy/web           # list revisions
kubectl rollout undo deploy/web              # back to the previous revision
kubectl rollout undo deploy/web --to-revision=3
kubectl rollout restart deploy/web           # re-roll (e.g. to pick up a changed Secret)
kubectl rollout pause deploy/web             # pause mid-roll (for canary-style checks)

StatefulSet — stable identity and ordered lifecycle

Some workloads can’t be interchangeable, throwaway Pods — a database replica needs to keep the same name, the same storage, and the same network identity across restarts and reschedules. That’s a StatefulSet. It provides:

StatefulSets are more operationally involved than Deployments; for complex stateful systems, a purpose-built Operator (a CRD plus a controller, e.g. the CloudNativePG, Strimzi/Kafka, or Zalando Postgres operators) usually manages the StatefulSet for you and handles backups, failover, and version upgrades.

DaemonSet

A DaemonSet ensures one copy of a Pod runs on every node (or every node matching a selector). As nodes join the cluster, the DaemonSet controller adds the Pod to them; as nodes leave, their Pods are garbage-collected. This is the model for node-level infrastructure: log collectors (Fluent Bit), metrics agents (node-exporter), CNI plugins, CSI drivers, and security agents. DaemonSet Pods often need tolerations to run on tainted nodes (including control-plane nodes) and frequently mount host paths, so treat them as privileged infrastructure and review them carefully.

Job and CronJob

A Job runs Pods until a specified number complete successfully, then stops — the model for finite, run-to-completion work (a migration, a batch computation, a data export). Key knobs: completions (how many successful runs are needed), parallelism (how many run at once), backoffLimit (retries before the Job is marked Failed), and activeDeadlineSeconds (wall-clock timeout). Pair with restartPolicy: OnFailure or Never.

A CronJob creates Jobs on a schedule (standard cron syntax), for backups, reports, and periodic cleanup:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-backup
spec:
  schedule: "0 2 * * *"                 # 02:00 every day
  concurrencyPolicy: Forbid             # don't start a run if the last is still going
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      backoffLimit: 2
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: backup
              image: myorg/pgbackup:1.2
              args: ["--target", "s3://backups/db"]

Watch out for concurrencyPolicy (Allow/Forbid/Replace) so slow runs don’t pile up, and startingDeadlineSeconds so a missed schedule (controller downtime) doesn’t fire a storm of catch-up Jobs.

Labels, selectors, and annotations

Labels (key: value on metadata) are how controllers claim Pods and Services find endpoints — the loose-coupling glue. Adopt the recommended app.kubernetes.io/name, app.kubernetes.io/instance, app.kubernetes.io/version, and app.kubernetes.io/component labels so dashboards, cost tools, and selectors all speak the same language. Selectors come in two forms: equality-based (app=web) and set-based (environment in (prod, staging)). A controller’s spec.selector is immutable on Deployments/StatefulSets — pick it carefully, because you can’t change it later without recreating the object.

Annotations carry non-identifying metadata that tools and controllers read — build info, a config hash to force rollouts, ingress-controller settings, kubectl.kubernetes.io/last-applied-configuration. You never select on annotations, and they can hold large or structured values that labels can’t.

Pod Disruption Budgets

Disruptions come in two kinds: involuntary (node crash, hardware failure — you can’t prevent these) and voluntary (node drain for an upgrade, cluster scale-down, kubectl drain). A PodDisruptionBudget (PDB) protects availability during voluntary disruptions by capping how many Pods of a set may be down at once:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web
spec:
  minAvailable: 2          # or: maxUnavailable: 1
  selector:
    matchLabels:
      app: web

With this, a node drain will block evicting a web Pod if doing so would drop below 2 available, forcing the drain to wait until a replacement is Ready elsewhere. A PDB is meaningless with a single replica — you need enough replicas to have slack. Note a PDB does not protect against involuntary disruptions; it only makes voluntary ones respect your availability floor.

Choosing the right workload type

Common mistake: forcing a database into a Deployment. Deployments treat Pods as interchangeable and use shared or ephemeral storage, so two rolling Pods can end up fighting over the same volume or losing data. Stateful data belongs in a StatefulSet with per-Pod PVCs, or better, a managed database.

Worked example — Deployment and StatefulSet, explained

A stateless frontend as a Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: storefront
  labels:
    app.kubernetes.io/name: storefront
spec:
  replicas: 4
  selector:
    matchLabels:
      app.kubernetes.io/name: storefront
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1                 # add one extra Pod during a roll
      maxUnavailable: 0           # never drop below desired capacity
  template:
    metadata:
      labels:
        app.kubernetes.io/name: storefront
    spec:
      containers:
        - name: app
          image: ghcr.io/acme/storefront:2.5.1     # immutable tag
          ports:
            - containerPort: 8080
          resources:
            requests: { cpu: 100m, memory: 128Mi } # drives scheduling
            limits:   { cpu: 500m, memory: 256Mi } # caps usage
          readinessProbe:                          # gates traffic during rollouts
            httpGet: { path: /healthz, port: 8080 }
          livenessProbe:
            httpGet: { path: /livez, port: 8080 }
            initialDelaySeconds: 10

Why these choices: interchangeable replicas → Deployment; maxUnavailable: 0 keeps full capacity during the roll; the readiness probe means a new Pod receives traffic only once healthy, and the roll pauses if new Pods never become Ready — a broken image can’t take the service down. Resource requests let the scheduler place Pods and enable autoscaling.

A PostgreSQL replica set as a StatefulSet (schematic — use an operator in production):

apiVersion: v1
kind: Service
metadata:
  name: pg                       # headless Service → per-Pod DNS
spec:
  clusterIP: None
  selector:
    app: pg
  ports:
    - port: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: pg
spec:
  serviceName: pg                # ties the StatefulSet to the headless Service
  replicas: 3
  selector:
    matchLabels:
      app: pg
  template:
    metadata:
      labels:
        app: pg
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: postgres
          image: postgres:16.3
          ports:
            - containerPort: 5432
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
          readinessProbe:
            exec: { command: ["pg_isready", "-U", "postgres"] }
  volumeClaimTemplates:          # each Pod gets its OWN persistent disk
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 20Gi

Why these choices: the Pods are not interchangeable — pg-0, pg-1, pg-2 keep their names and each owns a private data-pg-N PVC that survives reschedules and even deletion of the StatefulSet. The headless Service (clusterIP: None) gives every Pod a stable DNS name (pg-0.pg...) so clients and replicas can address specific members — essential for primary/replica topologies. Pods come up in order, so pg-0 (say, the primary) is Ready before replicas join. This is exactly what a Deployment cannot provide.

Best Practices

  1. Never run bare Pods in production — always use a controller. A lone Pod has no self-healing; a Deployment/StatefulSet/DaemonSet reschedules it after node loss.
  2. Match the controller to the workload. Stateless → Deployment, stateful → StatefulSet (or Operator), per-node → DaemonSet, finite → Job, scheduled → CronJob. Forcing a database into a Deployment risks data loss.
  3. Set resource requests and limits on every container. Requests drive scheduling and autoscaling; limits cap blast radius. Missing requests make the scheduler blind; missing limits let one Pod starve a node.
  4. Define readiness (and where useful, liveness/startup) probes. Readiness makes rolling updates zero-downtime by keeping unready Pods out of the load balancer; a rollout with good probes self-arrests on a bad image.
  5. Default to RollingUpdate with maxUnavailable: 0, and only use Recreate when versions genuinely can’t coexist. Keep revisionHistoryLimit sane so rollback works but old ReplicaSets don’t accumulate forever.
  6. Run at least 2–3 replicas and pair them with a PodDisruptionBudget. This survives node drains and cluster upgrades without dropping to zero — a PDB on a single-replica app does nothing.
  7. Use init containers for ordered setup, sidecars for coupled helpers — and prefer the native sidecar (initContainer with restartPolicy: Always) so startup/shutdown ordering is correct. Don’t co-locate unrelated services in one Pod.
  8. Give StatefulSets a headless Service and volumeClaimTemplates. That’s what delivers stable identity and per-Pod storage; without them you don’t get StatefulSet semantics.
  9. Handle SIGTERM and tune terminationGracePeriodSeconds with a preStop hook so in-flight requests drain during rollouts and drains instead of being cut off.
  10. Label consistently with the app.kubernetes.io/* convention, and treat controller spec.selector as immutable — choose it deliberately, because changing it means recreating the object.
  11. Use annotations, not labels, for non-selector metadata (build info, config hashes, tool settings); reserve labels for things you actually select or group by.
  12. For Jobs, set backoffLimit and activeDeadlineSeconds; for CronJobs, set concurrencyPolicy and history limits so retries are bounded and slow runs don’t pile up.
  13. Scale stateless apps with an HPA, not by editing replicas by hand, and never run an HPA and VPA on the same metric — they fight.
  14. Review DaemonSets as privileged infrastructure. They often tolerate taints and mount host paths on every node; a bad one has cluster-wide reach.
  15. Prefer a managed service or a mature Operator for complex stateful systems. Hand-rolling database failover on a raw StatefulSet is far more work than it looks.
  16. Watch CrashLoopBackOff as a symptom, not a cause. The real reason is in kubectl logs --previous and kubectl describe events (bad config, missing dependency, OOM) — fix that, not the restart loop.

References