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ẻ:
- Network namespace — một IP cho mỗi Pod; container gọi nhau qua
localhostvà không được đụng độ port. - Storage volume — volume khai báo trên Pod có thể mount vào bất kỳ container nào của nó, đây là cách chúng chia sẻ file.
- Vòng đời — chúng được schedule, khởi động, và (thường là) dừng cùng nhau.
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:
- Sidecar — một helper chạy dài bên cạnh container chính: một log shipper, một proxy (data plane của service-mesh), một metrics exporter, một agent đồng bộ file. Từ Kubernetes v1.28+ sidecar có hỗ trợ hạng nhất (khai báo dưới dạng
initContainervớirestartPolicy: Always), sửa được thứ tự khởi động và shutdown để sidecar khởi động trước và dừng sau app. - Init container — chạy tới khi hoàn thành theo thứ tự, trước khi các container app khởi động. Dùng cho việc setup phải xong trước: chờ một dependency, chạy migration schema, tải cấu hình, đặt quyền file. Nếu một init container lỗi, Pod restart nó (theo restart policy) và các container app không bao giờ khởi động.
- Ambassador / Adapter — các biến thể của sidecar proxy kết nối đi ra hoặc định hình lại output của app cho thế giới bên ngoài.
Đừ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) |
| Succeeded | Mọi container kết thúc thành công và sẽ không restart (Job) |
| Failed | Mọi container kết thúc, ít nhất một cái lỗi, và sẽ không restart |
| Unknown | Khô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):
- Always (mặc định) — restart container mỗi khi nó dừng, dù thành công hay lỗi. Dùng bởi Deployment/StatefulSet/DaemonSet — dịch vụ chạy dài nên luôn được duy trì.
- OnFailure — chỉ restart khi exit khác 0. Dùng bởi Job cần retry tới khi thành công.
- Never — không restart. Dùng bởi Job nơi một lần thử lỗi cần được ghi nhận, không phải retry tại chỗ.
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):
- readinessProbe — “tôi có thể phục vụ traffic ngay bây giờ không?” Fail sẽ loại Pod khỏi endpoint của Service mà không giết nó; đây là thứ làm cho rolling update an toàn.
- livenessProbe — “tôi có bị kẹt không?” Fail sẽ restart container. Đừng bao giờ trỏ nó vào một dependency phía dưới.
- startupProbe — bảo vệ các app khởi động chậm để một lần boot dài không bị nhầm là treo.
Khái niệm chính
Các workload controller nhìn tổng quan
| Controller | Giữ | Danh tính | Thứ tự | Storage | Dùng điển hình |
|---|---|---|---|---|---|
| ReplicaSet | N Pod giống hệt | Thay thế được | Không | Ephemeral | Tầng thấp; được quản lý bởi Deployment |
| Deployment | N Pod qua ReplicaSet | Thay thế được | Không | Ephemeral | App stateless, API, web frontend |
| StatefulSet | N Pod, tên ổn định | Ổn định, đánh số | Có thứ tự | Persistent theo Pod | Database, Kafka, ZooKeeper, hệ quorum |
| DaemonSet | Một Pod mỗi node | Theo node | Không | Thường là host path | Agent log/metric, CNI, CSI, tuning node |
| Job | Pod tới khi hoàn thành | Thay thế được | Song song hoặc tuần tự | Ephemeral | Tác vụ batch, migration, xử lý một lần |
| CronJob | Job theo lịch | — | — | — | Backup, 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:
- RollingUpdate (mặc định) — thay thế dần Pod cũ bằng Pod mới, điều khiển bởi
maxSurge(được phép có bao nhiêu Pod dư trên số mong muốn trong lúc roll) vàmaxUnavailable(được phép thiếu bao nhiêu dưới số mong muốn). Điều này cho deploy không downtime khi có readiness probe.maxSurge: 1, maxUnavailable: 0là một mặc định an toàn, thận trọng: luôn thêm một Pod mới và chờ nó Ready trước khi gỡ một Pod cũ. - Recreate — kết thúc toàn bộ Pod cũ, rồi tạo Pod mới. Gây downtime, nhưng cần thiết khi phiên bản cũ và mới không thể chạy đồng thời (ví dụ một lock độc quyền, hoặc thay đổi schema không tương thích).
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:
- Tên ordinal ổn định —
web-0,web-1,web-2, không phải hash ngẫu nhiên. Một Pod chết sẽ quay lại với cùng tên. - Storage ổn định theo từng Pod — qua
volumeClaimTemplates, mỗi Pod nhận PersistentVolumeClaim riêng (data-web-0,data-web-1, …) đi theo nó qua các lần reschedule. Xóa StatefulSet không xóa các PVC này, bảo vệ dữ liệu. - Triển khai và scale có thứ tự, nhẹ nhàng — Pod được tạo theo thứ tự (0, rồi 1, rồi 2), mỗi cái chờ cái trước Ready, và kết thúc theo thứ tự ngược. Điều này quan trọng với các hệ clustered bầu leader hoặc join thành viên từng cái một.
- Danh tính mạng ổn định qua một headless Service (
clusterIP: None), cấp cho mỗi Pod một tên DNS riêng:web-0.web.default.svc.cluster.local. Client (và các peer) gọi từng replica trực tiếp thay vì một VIP được load-balance.
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
- App stateless (web server, API, worker không giữ state cục bộ) → Deployment.
- Stateful, cần danh tính/storage ổn định (database, message broker, hệ quorum) → StatefulSet (hoặc một Operator quản lý nó).
- Chạy trên mọi node (agent, driver, collector) → DaemonSet.
- Tác vụ hữu hạn phải hoàn thành (migration, job batch) → Job.
- Chạy theo lịch → CronJob.
- Chỉ cần N bản, không cần logic rollout → vẫn là Deployment (không bao giờ dùng ReplicaSet trần).
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
- 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.
- 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.
- Đặ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.
- Đị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.
- Mặc định dùng
RollingUpdatevớimaxUnavailable: 0, và chỉ dùngRecreatekhi các phiên bản thực sự không thể cùng tồn tại. GiữrevisionHistoryLimithợp lý để rollback được nhưng ReplicaSet cũ không tích tụ mãi. - 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.
- Dùng init container cho setup có thứ tự, sidecar cho helper gắn kết — và ưu tiên sidecar native (
initContainervớirestartPolicy: 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. - 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. - Xử lý
SIGTERMvà tinh chỉnhterminationGracePeriodSecondsvới mộtpreStophook để request đang xử lý được drain trong lúc rollout và drain thay vì bị cắt đột ngột. - Đặt label nhất quán theo quy ước
app.kubernetes.io/*, và coispec.selectorcủa controller là bất biến — chọn có chủ đích, vì đổi nó nghĩa là tạo lại object. - 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.
- Với Job, đặt
backoffLimitvàactiveDeadlineSeconds; với CronJob, đặtconcurrencyPolicyvà history limit để retry có giới hạn và các lần chạy chậm không chồng đống. - 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.
- 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.
- Ư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.
- Xem
CrashLoopBackOfflà triệu chứng, không phải nguyên nhân. Lý do thật nằm trongkubectl logs --previousvà events củakubectl 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
- Pods · Pod lifecycle
- Init containers · Sidecar containers
- Workloads overview
- ReplicaSet
- Deployments
- StatefulSets · StatefulSet basics tutorial
- DaemonSet
- Jobs · CronJob
- Labels and selectors · Recommended labels · Annotations
- Pod Disruption Budgets · Specifying a Disruption Budget
- Configure liveness, readiness and startup probes
- Horizontal Pod Autoscaling
- Sách: Kubernetes Up & Running (Hightower, Burns, Beda — O’Reilly)
- roadmap.sh — Kubernetes
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:
- The network namespace — one IP per Pod; containers reach each other over
localhostand must not collide on ports. - Storage volumes — volumes declared on the Pod can be mounted into any of its containers, which is how they share files.
- Lifecycle — they’re scheduled, started, and (usually) stopped together.
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:
- Sidecar — a long-running helper alongside the main container: a log shipper, a proxy (service-mesh data plane), a metrics exporter, a file-sync agent. As of Kubernetes v1.28+ sidecars have first-class support (declared as an
initContainerwithrestartPolicy: Always), which fixes startup ordering and shutdown so the sidecar starts before and stops after the app. - Init containers — run to completion in order, before the app containers start. Use them for setup that must finish first: waiting for a dependency, running a schema migration, fetching config, setting file permissions. If an init container fails, the Pod restarts it (subject to the restart policy) and the app containers never start.
- Ambassador / Adapter — variants of the sidecar that proxy outbound connections or reshape the app’s output for the outside world.
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):
| Phase | Meaning |
|---|---|
| Pending | Accepted by the cluster but not yet running — waiting to be scheduled, or pulling images |
| Running | Bound to a node; at least one container is running (or starting/restarting) |
| Succeeded | All containers terminated successfully and won’t restart (Jobs) |
| Failed | All containers terminated, at least one in failure, and won’t restart |
| Unknown | The 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:
- Always (default) — restart the container whenever it stops, success or failure. Used by Deployments/StatefulSets/DaemonSets — long-running services should always be up.
- OnFailure — restart only on non-zero exit. Used by Jobs that should retry until they succeed.
- Never — don’t restart. Used by Jobs where a failed attempt should be recorded, not retried in place.
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):
- readinessProbe — “can I serve traffic now?” Failing removes the Pod from Service endpoints without killing it; this is what makes rolling updates safe.
- livenessProbe — “am I wedged?” Failing restarts the container. Never point it at a downstream dependency.
- startupProbe — protects slow starters so a long boot isn’t mistaken for a hang.
Key Concepts
Workload controllers at a glance
| Controller | Keeps | Identity | Ordering | Storage | Typical use |
|---|---|---|---|---|---|
| ReplicaSet | N identical Pods | Interchangeable | None | Ephemeral | Low-level; managed by Deployments |
| Deployment | N Pods via ReplicaSets | Interchangeable | None | Ephemeral | Stateless apps, APIs, web frontends |
| StatefulSet | N Pods, stable names | Stable, numbered | Ordered | Per-Pod persistent | Databases, Kafka, ZooKeeper, quorum systems |
| DaemonSet | One Pod per node | Per-node | None | Usually host paths | Log/metric agents, CNI, CSI, node tuning |
| Job | Pods to completion | Interchangeable | Parallel or serial | Ephemeral | Batch tasks, migrations, one-off processing |
| CronJob | Jobs on a schedule | — | — | — | Backups, 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:
- RollingUpdate (default) — gradually replace old Pods with new ones, controlled by
maxSurge(how many extra Pods above desired count may exist during the roll) andmaxUnavailable(how many below desired may be missing). This gives zero-downtime deploys when readiness probes are set.maxSurge: 1, maxUnavailable: 0is a safe, conservative default: always add one new Pod and wait for it to be Ready before removing an old one. - Recreate — terminate all old Pods, then create new ones. Causes downtime, but is required when old and new versions can’t run simultaneously (e.g. an exclusive lock, or an incompatible schema change).
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:
- Stable, ordinal names —
web-0,web-1,web-2, not random hashes. A Pod that dies comes back with the same name. - Stable per-Pod storage — via
volumeClaimTemplates, each Pod gets its own PersistentVolumeClaim (data-web-0,data-web-1, …) that follows it across reschedules. Deleting the StatefulSet does not delete these PVCs, protecting the data. - Ordered, graceful deployment and scaling — Pods are created in order (0, then 1, then 2), each waiting for the previous to be Ready, and terminated in reverse order. This matters for clustered systems that elect leaders or join members one at a time.
- A stable network identity via a headless Service (
clusterIP: None), which gives each Pod its own DNS name:web-0.web.default.svc.cluster.local. Clients (and peers) address individual replicas directly rather than a load-balanced VIP.
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
- Stateless app (web server, API, worker that keeps no local state) → Deployment.
- Stateful, needs stable identity/storage (database, message broker, quorum system) → StatefulSet (or an Operator that manages one).
- Runs on every node (agent, driver, collector) → DaemonSet.
- Finite task that must complete (migration, batch job) → Job.
- Runs on a schedule → CronJob.
- Just need N copies, no rollout logic → still a Deployment (never a bare ReplicaSet).
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
- 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.
- 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.
- 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.
- 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.
- Default to
RollingUpdatewithmaxUnavailable: 0, and only useRecreatewhen versions genuinely can’t coexist. KeeprevisionHistoryLimitsane so rollback works but old ReplicaSets don’t accumulate forever. - 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.
- Use init containers for ordered setup, sidecars for coupled helpers — and prefer the native sidecar (
initContainerwithrestartPolicy: Always) so startup/shutdown ordering is correct. Don’t co-locate unrelated services in one Pod. - 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. - Handle
SIGTERMand tuneterminationGracePeriodSecondswith apreStophook so in-flight requests drain during rollouts and drains instead of being cut off. - Label consistently with the
app.kubernetes.io/*convention, and treat controllerspec.selectoras immutable — choose it deliberately, because changing it means recreating the object. - Use annotations, not labels, for non-selector metadata (build info, config hashes, tool settings); reserve labels for things you actually select or group by.
- For Jobs, set
backoffLimitandactiveDeadlineSeconds; for CronJobs, setconcurrencyPolicyand history limits so retries are bounded and slow runs don’t pile up. - 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.
- Review DaemonSets as privileged infrastructure. They often tolerate taints and mount host paths on every node; a bad one has cluster-wide reach.
- 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.
- Watch
CrashLoopBackOffas a symptom, not a cause. The real reason is inkubectl logs --previousandkubectl describeevents (bad config, missing dependency, OOM) — fix that, not the restart loop.
References
- Pods · Pod lifecycle
- Init containers · Sidecar containers
- Workloads overview
- ReplicaSet
- Deployments
- StatefulSets · StatefulSet basics tutorial
- DaemonSet
- Jobs · CronJob
- Labels and selectors · Recommended labels · Annotations
- Pod Disruption Budgets · Specifying a Disruption Budget
- Configure liveness, readiness and startup probes
- Horizontal Pod Autoscaling
- Book: Kubernetes Up & Running (Hightower, Burns, Beda — O’Reilly)
- roadmap.sh — Kubernetes