K8s 5 — Pod, Deployment & các workload

13 thg 7, 2026 3 lượt xem
#deployment
#kubernetes
#devops
#job
#pod
#statefulset

Từ kiến trúc đến "chạy ứng dụng"

Ở bài Kiến trúc Kubernetes, ta đã thấy bộ não (control plane) và cơ bắp (node), cùng tư tưởng nền tảng: bạn khai báo trạng thái mong muốn (desired state) và các controller chạy vòng reconciliation để kéo thực tế về khớp. Bài này trả lời câu hỏi kế tiếp: cụ thể bạn khai báo cái gì để ứng dụng của mình chạy?

Câu trả lời là các workload — nhóm đối tượng API mô tả "tôi muốn chạy phần mềm này, theo kiểu này": Pod, ReplicaSet, Deployment, StatefulSet, DaemonSet, Job, CronJob. Mỗi khuôn phù hợp một tính chất ứng dụng khác nhau; chọn đúng khuôn cho việc gì là kỹ năng cốt lõi khi vận hành K8s.

Lưu ý về môi trường: sandbox của loạt bài này chỉ có PostgreSQL, không có cụm Kubernetes. Vì vậy mọi khối kubectl/YAML dưới đây đều là (minh hoạ) — dùng để đọc hiểu và mang đi áp dụng trên cụm thật của bạn, không chạy trực tiếp ở đây.


Pod — đơn vị nhỏ nhất

Kubernetes không chạy container trực tiếp. Đơn vị triển khai nhỏ nhất mà bạn khai báo là Pod. Một Pod bọc một hoặc nhiều container cùng chạy chung và chia sẻ:

  • Network namespace: mọi container trong một Pod dùng chung một địa chỉ IP và không gian cổng. Chúng gọi nhau qua localhost. Hai container trong cùng Pod không thể cùng nghe một cổng.
  • Volume: các container có thể mount chung một volume để trao đổi file.
  • Vòng đời: chúng được xếp lịch (schedule) cùng nhau lên một node, sinh và tử gần như cùng lúc.

Trường hợp phổ biến nhất là một Pod một container — Pod chỉ là lớp bọc mỏng. Nhưng mô hình đa container mở ra pattern sidecar: container phụ chạy cạnh container chính để bổ trợ, ví dụ một sidecar thu thập log rồi đẩy đi, hoặc một proxy mạng (service mesh) chặn lưu lượng vào/ra. Sidecar dùng chung network và volume với container chính nên phối hợp rất chặt.

init container

Ngoài container chính và sidecar, Pod còn có thể khai báo init container: các container chạy tuần tự tới khi hoàn thành trước khi container ứng dụng khởi động. Dùng để làm việc chuẩn bị: chờ một dịch vụ phụ thuộc sẵn sàng, chạy migration schema, tải cấu hình/khoá xuống volume chung. Nếu một init container thất bại, Kubernetes khởi động lại Pod cho tới khi bước init thành công (theo restartPolicy). Chỉ khi tất cả init container xong, container chính mới chạy.

Pod là ephemeral (phù du)

Điểm cực kỳ quan trọng: Pod không bền. Pod có định danh gắn liền một lần tạo; khi nó chết (crash, node hỏng, bị dời), Kubernetes không "hồi sinh" đúng Pod đó — mà tạo một Pod mới với tên khác và IP khác. Không có gì đảm bảo Pod mới nằm trên node cũ.

Hệ quả thực tế:

  • Đừng nhớ IP của Pod hay gọi Pod trực tiếp — IP sẽ đổi. Đây là lý do tồn tại Service (bài Networking & Services) làm điểm truy cập ổn định.
  • Đừng lưu dữ liệu quan trọng trong Pod — filesystem của container mất khi Pod chết. Dữ liệu cần bền phải nằm trên volume/PersistentVolume (bài Storage & Config).
  • Hiếm khi bạn tạo Pod "trần" trong sản xuất. Pod trần không tự khởi động lại khi node chết. Bạn hầu như luôn để một controller cấp cao (Deployment, StatefulSet…) tạo và quản lý Pod hộ bạn.

ReplicaSet — giữ đúng số bản sao

ReplicaSet là controller có một nhiệm vụ duy nhất và làm nó rất tốt: đảm bảo luôn có đúng N bản sao (replica) của một Pod đang chạy. Nó khai báo replicas: N, một template để tạo Pod, và một selector để nhận diện Pod thuộc quyền quản lý của mình.

Vòng reconciliation của nó cực đơn giản: đếm số Pod khớp selector, thiếu thì tạo thêm, thừa thì xoá bớt, cho tới khi bằng N. Một Pod crash → ReplicaSet tạo Pod thay thế — chính là self-healing ở cấp bản sao.

Trên thực tế bạn hiếm khi tạo ReplicaSet trực tiếp, vì nó chỉ biết giữ số lượng chứ không biết cập nhật phiên bản an toàn (đổi image mà không gián đoạn). Việc đó cần một tầng cao hơn — Deployment — và Deployment sẽ tự tạo/quản ReplicaSet giúp bạn. Hiểu ReplicaSet để đọc được chuyện gì xảy ra bên dưới một Deployment.


Deployment — workload phổ biến nhất cho stateless

Deployment là đối tượng bạn dùng nhiều nhất. Nó quản lý ReplicaSet thay bạn, và bổ sung phần khó nhất: cập nhật phiên bản có kiểm soát (rolling update) và quay lui (rollback).

Quan hệ ba tầng:

Bạn nói chuyện với Deployment; Deployment điều khiển ReplicaSet; ReplicaSet giữ Pod. Bạn gần như không bao giờ chạm tay vào ReplicaSet hay Pod trực tiếp.

Ví dụ Deployment đầy đủ

# web-deployment.yaml  (minh hoạ — không chạy trong sandbox)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 3                     # trạng thái mong muốn: 3 bản sao
  selector:
    matchLabels:
      app: web                    # Deployment "sở hữu" Pod có nhãn app=web
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1                 # tạo tối đa +1 Pod so với 3 khi update
      maxUnavailable: 0           # không cho tụt xuống dưới 3 Pod sẵn sàng
  template:                       # khuôn tạo Pod
    metadata:
      labels:
        app: web                  # nhãn phải khớp selector ở trên
    spec:
      containers:
        - name: web
          image: myregistry/web:v2
          ports:
            - containerPort: 8080
          readinessProbe:         # Pod chỉ "sẵn sàng" khi probe này pass
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"

Đọc từ trên xuống: replicas: 3 là mong muốn; selector.matchLabels cho biết Deployment quản Pod nào; strategy quy định cách thay Pod khi update; template là khuôn tạo Pod (bao gồm nhãn — phải khớp selector — và spec container). Áp dụng bằng:

# (minh hoạ)
kubectl apply -f web-deployment.yaml
kubectl get deployment web
kubectl rollout status deployment/web

Rolling update: đổi phiên bản không gián đoạn

Khi bạn đổi image: web:v2 thành web:v3 rồi kubectl apply lại (hoặc kubectl set image deployment/web web=myregistry/web:v3), Deployment không giết hết Pod cũ rồi mới dựng Pod mới. Nó thực hiện rolling update:

  1. Tạo một ReplicaSet mới cho phiên bản v3.
  2. Từ từ tăng số Pod v3 và giảm số Pod v2, mỗi bước một chút.
  3. Chỉ đưa Pod mới vào phục vụ khi nó sẵn sàng (pass readinessProbe).
  4. Khi ReplicaSet v3 đủ số bản sao và ReplicaSet v2 về 0, quá trình kết thúc.

Hai "cần điều khiển" quyết định độ mượt:

  • maxSurge: số Pod được phép tạo vượt mức mong muốn trong lúc update. maxSurge: 1 với replicas: 3 → trong lúc rollout có thể có tới 4 Pod. Surge cao hơn = update nhanh hơn nhưng tốn tài nguyên hơn.
  • maxUnavailable: số Pod được phép thiếu so với mức mong muốn. maxUnavailable: 0 → luôn có đủ 3 Pod sẵn sàng phục vụ, không hụt công suất một giây nào (đổi lại rollout cần surge để có chỗ xoay).

Cặp maxSurge/maxUnavailable cho bạn cân bằng giữa tốc độ, tính sẵn sàngtài nguyên. Cấu hình maxUnavailable: 0 phù hợp dịch vụ nhạy cảm về downtime.

Strategy: RollingUpdate vs Recreate

Deployment có hai chiến lược:

  • RollingUpdate (mặc định): thay Pod dần dần như trên. Trong thời gian rollout, cả phiên bản cũ và mới cùng tồn tại. Đây là lựa chọn tiêu chuẩn cho dịch vụ web stateless. Lưu ý: vì hai phiên bản chạy song song, ứng dụng cần tương thích ngược trong lúc chuyển giao (ví dụ schema DB không được đổi kiểu phá vỡ v-cũ giữa chừng).
  • Recreate: giết toàn bộ Pod cũ rồi mới tạo Pod mới. Có downtime giữa hai pha. Dùng khi hai phiên bản không thể cùng chạy (ví dụ giữ một khoá độc quyền, hoặc migration không tương thích ngược), và bạn chấp nhận gián đoạn ngắn.

Rollback: quay lui khi hỏng

Deployment giữ lại các ReplicaSet cũ (số lượng theo revisionHistoryLimit). Nhờ vậy nếu v3 lỗi, bạn quay lui gần như tức thì:

# (minh hoạ)
kubectl rollout history deployment/web        # xem các bản đã triển khai
kubectl rollout undo deployment/web           # quay về revision liền trước
kubectl rollout undo deployment/web --to-revision=2

rollout undo thực chất là scale ReplicaSet cũ lên và ReplicaSet lỗi về 0 — cũng là một rolling update, theo chiều ngược. Đây là mạng lưới an toàn khiến Deployment trở thành workload mặc định cho ứng dụng stateless (không giữ trạng thái cục bộ): web server, API, worker không lưu dữ liệu.


StatefulSet — cho ứng dụng có trạng thái

Deployment coi mọi Pod là có thể thay thế cho nhau (interchangeable) — Pod nào cũng như Pod nào, tên và storage không quan trọng. Điều đó sai với các hệ có trạng thái như cơ sở dữ liệu, Kafka, ZooKeeper, Elasticsearch: ở đó mỗi node là một cá thể với dữ liệu riêng và vai trò riêng (primary/replica).

StatefulSet sinh ra cho lớp bài toán này, cung cấp ba bảo đảm mà Deployment không có:

  • Định danh mạng ổn định: Pod được đặt tên có thứ tự <tên>-0, <tên>-1, <tên>-2 thay vì hậu tố ngẫu nhiên. Khi một Pod chết và được tạo lại, nó giữ nguyên tên (ví dụ postgres-0). Kết hợp với một headless Service, mỗi Pod có một DNS ổn định — thứ mà một cụm DB cần để các node biết nhau.
  • Storage riêng, gắn liền theo Pod: qua volumeClaimTemplates, mỗi Pod được cấp một PersistentVolume riêng đi theo định danh của nó. postgres-0 luôn được gắn lại đúng volume dữ liệu của postgres-0, kể cả sau khi Pod tái tạo. Storage không bị chia sẻ hay xáo trộn giữa các bản sao.
  • Thứ tự triển khai và scale: Pod được tạo/xoá tuần tự theo thứ tự (0 trước, rồi 1, rồi 2; xoá thì ngược lại). Nhiều hệ phân tán yêu cầu khởi động có thứ tự (node 0 lên trước để bootstrap cụm) — StatefulSet tôn trọng điều đó.

Đổi lại, StatefulSet phức tạp hơnupdate chậm/cẩn trọng hơn Deployment. Với các dịch vụ được quản lý (managed DB trên cloud), nhiều đội chọn dùng dịch vụ ngoài cụm thay vì tự vận hành StatefulSet — nhưng khi cần chạy trạng thái trong cụm, StatefulSet là công cụ đúng.


DaemonSet — một Pod trên mỗi node

DaemonSet đảm bảo mỗi node trong cụm chạy đúng một bản sao của một Pod. Thêm node mới vào cụm → DaemonSet tự đặt một Pod lên node đó; gỡ node → Pod tương ứng biến mất. Bạn không khai báo replicas — số lượng bằng số node (có thể giới hạn bằng nodeSelector/affinity).

Đây là khuôn dành cho agent chạy trên mọi máy, chứ không phải ứng dụng của người dùng:

  • Thu thập log trên từng node (ví dụ Fluentd / Fluent Bit đọc log của mọi container trên node).
  • Thu thập metrics ở cấp node (ví dụ node-exporter cho Prometheus).
  • Plugin mạng/lưu trữ (CNI/CSI) cần hiện diện trên mọi node.

Quy tắc nhận biết: nếu tác vụ cần "có mặt trên từng máy" thì đó là DaemonSet, không phải Deployment.


Job & CronJob — chạy tới hoàn thành / theo lịch

Deployment, StatefulSet, DaemonSet đều dành cho tiến trình chạy mãi (long-running service). Nhưng nhiều việc là hữu hạn: xử lý một lô dữ liệu, chạy một migration, gửi một đợt báo cáo. Cho lớp này có JobCronJob.

  • Job: chạy một hoặc nhiều Pod cho tới khi hoàn thành thành công (tiến trình exit code 0), rồi dừng. Job theo dõi số lần hoàn thành; nếu Pod thất bại nó có thể thử lại tới backoffLimit. Có thể chạy song song (parallelism) và yêu cầu nhiều lần hoàn thành (completions). Đây là khuôn cho tác vụ batch một lần.
  • CronJob: tạo một Job theo lịch kiểu cron. Ví dụ "2 giờ sáng mỗi ngày chạy job dọn dữ liệu cũ". CronJob quản việc "đến giờ thì đẻ ra một Job".
# cronjob.yaml  (minh hoạ)
apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-report
spec:
  schedule: "0 2 * * *"           # 02:00 mỗi ngày
  jobTemplate:
    spec:
      backoffLimit: 3
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: report
              image: myregistry/reporter:1.0
              args: ["--range", "yesterday"]

Điểm nối với thực tế: nếu bạn đã dùng Airflow để điều phối pipeline dữ liệu, một pattern rất phổ biến là để Airflow kích hoạt Job/Pod trên Kubernetes (qua các operator tương ứng) — Airflow lo phần điều phối/lịch/phụ thuộc, còn Kubernetes lo phần chạy tác vụ cách ly, có tài nguyên và tự dọn dẹp. Job/CronJob là cầu nối tự nhiên giữa hai thế giới đó.


Labels & selectors — chất keo nối mọi thứ

Đây là cơ chế nền tảng khiến mọi workload ở trên hoạt động, dù nó dễ bị bỏ qua.

Label là các cặp key: value bạn gắn lên đối tượng (Pod, Deployment…), ví dụ app: web, tier: frontend, env: prod. Label không dùng để định danh duy nhất — nhiều đối tượng có thể chung một label. Chúng là cách phân loại đối tượng theo ý nghĩa nghiệp vụ.

Selector là cách chọn ra một tập đối tượng theo label. Ví dụ matchLabels: {app: web} chọn mọi Pod mang nhãn app=web.

Đây chính là cơ chế "liên kết" trong Kubernetes — không có con trỏ cứng giữa các đối tượng, mà là khớp nhãn động:

  • Deployment/ReplicaSet biết Pod nào là "của mình" nhờ selector khớp label trong template (nhớ: trong ví dụ trên, selector.matchLabels.app=web phải khớp template.metadata.labels.app=web — lệch là Deployment không quản được Pod).
  • Service (bài Networking & Services) tìm các Pod để định tuyến lưu lượng cũng bằng selector theo label, không cần biết tên hay IP từng Pod. Khi rolling update thay Pod v2 bằng v3 nhưng giữ nhãn app=web, Service tự nhiên trỏ sang Pod mới — không cần cấu hình lại.

Vì thế label là nền tảng để các đối tượng tìm nhau, và là công cụ vận hành hằng ngày:

# (minh hoạ)
kubectl get pods -l app=web              # lọc theo nhãn
kubectl get pods -l 'env in (prod,staging)'

Một bộ nhãn có kỷ luật (app, env, version, team…) giúp bạn truy vấn, gom nhóm và vá lỗi cụm dễ hơn rất nhiều.


Chọn đúng workload theo tính chất ứng dụng

Bảng quyết định nhanh:

Tính chất ứng dụngWorkload nên dùng
Web/API/worker stateless, cần scale & rolling updateDeployment
trạng thái, cần định danh + storage riêng theo pod (DB, Kafka…)StatefulSet
Agent phải chạy trên mọi node (log/metrics/mạng)DaemonSet
Tác vụ batch chạy tới xong một lầnJob
Tác vụ chạy theo lịchCronJob
(Gần như không bao giờ) một Pod thử nghiệm nhanhPod trần — không tự phục hồi khi node chết

Câu hỏi tự vấn khi chọn: Ứng dụng có giữ trạng thái cục bộ không? Nó chạy mãi hay chạy tới hoàn thành? Nó cần có mặt trên mọi máy không? Ba câu này gần như quyết định luôn workload.


Use case thực tế

Bối cảnh: một hệ thống web bán hàng gồm hai phần: frontend/API statelesscơ sở dữ liệu PostgreSQL có trạng thái.

1. Web app stateless → Deployment + rolling update. Frontend/API chạy dưới một Deployment với replicas: 3, strategy: RollingUpdate, maxUnavailable: 0, maxSurge: 1. Mỗi lần ra bản mới, đội chỉ đổi tag image và kubectl apply. Kubernetes dựng dần Pod mới, chỉ đưa vào phục vụ khi readinessProbe pass, và thu Pod cũ — người dùng không thấy downtime. Nếu bản mới lỗi (5xx tăng), một lệnh kubectl rollout undo deployment/web đưa hệ về phiên bản trước trong vài chục giây. Đây đúng là lý do stateless service mặc định dùng Deployment.

2. Database có trạng thái → StatefulSet. PostgreSQL không đặt vào Deployment: mỗi replica DB cần định danh ổn định (postgres-0, postgres-1) và volume dữ liệu riêng đi theo Pod. Đội dùng StatefulSet với volumeClaimTemplates (mỗi Pod một PersistentVolume) và một headless Service để các node biết nhau qua DNS ổn định. Update DB làm cẩn trọng, tuần tự — không "cuốn chiếu vô tư" như web. (Nhiều đội còn chọn managed DB ngoài cụm; nhưng nếu chạy trong cụm thì StatefulSet là đúng khuôn.)

3. Phần hỗ trợ. Một DaemonSet chạy agent thu log trên mọi node; một CronJob chạy job dọn dữ liệu và xuất báo cáo lúc 2 giờ sáng — có thể do Airflow kích hoạt. Tất cả nối với nhau nhờ label/selector: Service của web trỏ tới Pod app=web bất kể chúng bị thay bao nhiêu lần trong các đợt rollout.


Ghi nhớ

  • Pod là đơn vị nhỏ nhất: một/nhiều container chia sẻ network + volume + vòng đời; pattern sidecarinit container đều dựa vào đó. Pod ephemeral — chết là thay bằng Pod mới, IP mới; đừng nhớ IP, đừng lưu dữ liệu bền trong Pod, đừng chạy Pod trần trong sản xuất.
  • ReplicaSet chỉ giữ đúng số bản sao; bạn hầu như không tạo trực tiếp mà để Deployment quản.
  • Deployment = workload phổ biến nhất cho stateless: quản ReplicaSet, làm rolling update (điều chỉnh bằng maxSurge/maxUnavailable), chọn RollingUpdate (mặc định, hai phiên bản cùng chạy) vs Recreate (có downtime), và rollback nhanh nhờ giữ ReplicaSet cũ.
  • StatefulSet cho ứng dụng có trạng thái: định danh ổn định (-0, -1…), storage riêng theo Pod (volumeClaimTemplates), thứ tự triển khai — hợp DB/Kafka.
  • DaemonSet: một Pod trên mỗi node — agent log/metrics/mạng.
  • Job/CronJob: chạy tới hoàn thành / theo lịch — batch, migration, nối được với Airflow.
  • Label & selector là chất keo: Deployment và Service tìm Pod qua nhãn, không qua tên/IP — nền tảng để mọi thứ liên kết động và sống sót qua rolling update.
  • Chọn workload theo tính chất app: có trạng thái không? chạy mãi hay tới xong? cần trên mọi node không?

Tiếp theo: Networking & Services (làm sao các Pod phù du này có địa chỉ ổn định để gọi nhau và ra ngoài), rồi Storage & ConfigProduction & vận hành.


Nguồn tham khảo: tài liệu chính thức kubernetes.io — Workloads, Deployments, StatefulSets, DaemonSet, JobsLabels and Selectors.

Bài viết liên quan

Continuous Integration/Delivery/Deployment, cấu trúc pipeline, ví dụ GitHub Actions và chiến lược release.

13 thg 7, 2026 4

Container vs máy ảo, image/layer, Dockerfile, volume, network, Docker Compose và best practices.

13 thg 7, 2026 3

Vì sao cần orchestration; Pod, Deployment, Service, Ingress; scaling, self-healing và cấu hình.

13 thg 7, 2026 3

Bản chất DevOps: phá bỏ rào cản Dev–Ops, vòng lặp vô tận, CALMS, và vì sao tự động hoá.

13 thg 7, 2026 3