0

Kubernetes Production checklist -Checklist khi triển khai ứng dụng trên Kubernetes

"Chạy được trên Kubernetes" và "sẵn sàng lên production trên Kubernetes" là hai chuyện hoàn toàn khác nhau. Một Deployment với replicas: 3 vẫn có thể sập toàn bộ khi mất 1 node, một Pod "healthy" vẫn có thể đang trả 500 cho user vì readiness probe không phản ánh đúng trạng thái thật.

Bài này mình tổng hợp lại checklist mình dùng khi review một workload trước khi cho lên production, chia làm 2 tầng:

  • Cluster Level: những thứ thuộc về hạ tầng/platform team, gồm control plane, node, network, security policy ở cấp cluster.
  • Application Level: những thứ nằm trong tầm kiểm soát của dev/DevOps khi viết manifest cho từng workload.

Mỗi mục mình sẽ giải thích ngắn gọn "tại sao cần" và kèm ví dụ Bad/Good để dễ hình dung.

Lưu ý: Đây là checklist tổng hợp từ kinh nghiệm thực tế và nhiều nguồn tham khảo khác nhau, không phải mục nào cũng bắt buộc áp dụng 100%, tùy quy mô hệ thống mà chọn lọc cho phù hợp.

Bắt đầu thôi 🚀


Phần 1: Cluster Level

1. Control Plane High Availability

Control plane gồm API server, scheduler, controller-manager, etcd. Nếu chỉ chạy 1 control plane node, cả cluster sẽ "đứng hình" (không scale, không deploy, không tạo pod mới) khi node đó chết, dù các Pod đang chạy vẫn tiếp tục phục vụ traffic bình thường (đây là điểm hay bị hiểu nhầm: mất control plane không đồng nghĩa mất traffic ngay lập tức, nhưng bạn sẽ không tự hồi phục được khi có sự cố tiếp theo).

  • Bad: 1 control plane node duy nhất, không có load balancer phía trước API server.
  • Good: Tối thiểu 3 control plane node (số lẻ để đảm bảo quorum), đặt sau 1 Load Balancer.
# kubeadm init config - ví dụ trỏ API server qua LB
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
controlPlaneEndpoint: "k8s-api.internal:6443" # LB endpoint, không trỏ thẳng vào 1 node
etcd:
  local:
    dataDir: /var/lib/etcd

Nếu dùng managed control plane (EKS/GKE/AKS), phần này cloud lo cho bạn, nhưng vẫn nên hiểu rõ để biết điều gì cloud provider không cover (ví dụ: backup etcd thường vẫn là trách nhiệm của bạn trên self-managed cluster).

2. etcd Quorum & Backup/Restore

etcd dùng thuật toán Raft, cần quorum (đa số thành viên còn sống) để hoạt động. Với số node lẻ, khả năng chịu lỗi như sau:

Số etcd member Chịu được mất tối đa
3 1
5 2
7 3

etcd lưu gì? Toàn bộ object bạn kubectl get được (Pod, Deployment, Service, ConfigMap, Secret, RBAC, PV/PVC...) dưới dạng key-value. Không lưu: image container, log Pod, dữ liệu thật trong PV/PVC, metrics. Chỉ API server nói chuyện trực tiếp với etcd, nên mất quorum không làm sập Pod đang chạy, chỉ chặn thao tác ghi state mới (deploy, scale...).

  • Bad: Chạy 2 hoặc 4 etcd member (số chẵn không tăng khả năng chịu lỗi so với số lẻ liền trước, chỉ tốn thêm tài nguyên).
  • Good: 3 hoặc 5 member, và luôn có backup định kỳ. HA không thay thế được backup, mất quorum vẫn có thể phục hồi từ snapshot.
# Backup etcd snapshot
etcdctl snapshot save /backup/etcd-$(date +%Y%m%d).db \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key

# Restore khi cần
etcdctl snapshot restore /backup/etcd-20260718.db \
  --data-dir=/var/lib/etcd-restore

3. Worker Node Phân Bổ Theo Zone

Nếu toàn bộ worker node nằm trong 1 zone (availability zone), một sự cố zone đó là mất luôn cả ứng dụng, bất kể bạn có bao nhiêu replicas.

  • Bad: Tất cả node trong ap-southeast-1a.
  • Good: Node pool trải đều qua nhiều zone, có label topology.kubernetes.io/zone để scheduler biết mà spread Pod ra.
kubectl get nodes -L topology.kubernetes.io/zone
# NAME       STATUS   ZONE
# node-1     Ready    ap-southeast-1a
# node-2     Ready    ap-southeast-1b
# node-3     Ready    ap-southeast-1c

Chỉ có label zone thôi chưa đủ. Phần Application Level (mục 4) sẽ nói cách thực sự tận dụng nó bằng Topology Spread Constraints.

4. Namespace Separation & Resource Quota

Không giới hạn tài nguyên theo namespace, một team/app "ăn tham" (memory leak, load test quên tắt) có thể chiếm hết resource của cả cluster, kéo theo app của team khác bị OOM hoặc Pending vô thời hạn.

  • Bad: Namespace dùng chung, không ResourceQuota, không LimitRange.
  • Good: Mỗi team/môi trường một namespace riêng, có quota rõ ràng.
apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-a-quota
  namespace: team-a
spec:
  hard:
    requests.cpu: "20"
    requests.memory: 40Gi
    limits.cpu: "40"
    limits.memory: 80Gi
    pods: "50"
---
apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
  namespace: team-a
spec:
  limits:
    - default:
        cpu: 500m
        memory: 512Mi
      defaultRequest:
        cpu: 100m
        memory: 128Mi
      type: Container

LimitRange đảm bảo container nào quên khai báo resources vẫn có giá trị mặc định, tránh trường hợp 1 Pod không giới hạn "nuốt" hết node.

5. RBAC: Nguyên Tắc Least Privilege

Mỗi workload chỉ nên có đúng quyền nó cần, không hơn. cluster-admin cho mọi ServiceAccount là cách nhanh nhất để 1 lỗ hổng nhỏ trong app biến thành chiếm quyền cả cluster.

  • Bad: Gán ClusterRoleBinding với cluster-admin cho ServiceAccount của app, "cho chắc khỏi lỗi quyền".
  • Good: Tạo Role/RoleBinding riêng, giới hạn đúng namespace và đúng resource cần dùng.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: shop-api-role
  namespace: production
rules:
  - apiGroups: [""]
    resources: ["configmaps", "secrets"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: shop-api-binding
  namespace: production
subjects:
  - kind: ServiceAccount
    name: shop-api-sa
    namespace: production
roleRef:
  kind: Role
  name: shop-api-role
  apiGroup: rbac.authorization.k8s.io

Nhớ audit định kỳ, RBAC hay bị "phình" dần theo thời gian vì không ai dọn quyền cũ khi không còn dùng.

6. NetworkPolicy

Mặc định, mọi Pod trong cluster đều nói chuyện được với nhau, kể cả Pod của 2 team hoàn toàn không liên quan. Nếu 1 Pod bị compromise, attacker có thể "đi ngang" (lateral movement) sang toàn bộ cluster.

  • Bad: Không có NetworkPolicy nào, mọi traffic đông-tây (east-west) đều được phép.
  • Good: Default-deny, chỉ mở đúng luồng traffic cần thiết.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes: ["Ingress", "Egress"]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-shop-api-from-ingress
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: shop-api
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ingress-nginx
      ports:
        - port: 8080
  policyTypes: ["Ingress"]

7. Pod Security Standards

Namespace nào cũng nên bắt buộc Pod chạy đúng chuẩn bảo mật tối thiểu, không container nào được chạy privileged, chạy root, hay mount host path tùy tiện.

  • Bad: Không enforce gì, Pod nào cũng deploy được kể cả privileged: true.
  • Good: Gắn label pod-security.kubernetes.io ở namespace để enforce chuẩn restricted hoặc baseline.
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

8. Image Supply Chain & Admission Control

Một image dính CVE nghiêm trọng hoặc kéo từ registry không đáng tin là cửa ngõ tấn công phổ biến nhất vào production.

  • Bad: Pull image trực tiếp từ Docker Hub public, không scan, không kiểm soát nguồn gốc.
  • Good: CI/CD scan image (Trivy, Grype...) trước khi push, chỉ cho phép pull từ registry nội bộ đã whitelist, dùng admission controller (Kyverno, OPA Gatekeeper) để chặn deploy image chưa qua scan.
# Scan trong CI trước khi push
trivy image --severity HIGH,CRITICAL --exit-code 1 myregistry.io/shop-api:1.4.0
# Ví dụ policy Kyverno: chỉ cho phép image từ registry nội bộ
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: restrict-image-registries
spec:
  validationFailureAction: Enforce
  rules:
    - name: allowed-registries
      match:
        resources:
          kinds: ["Pod"]
      validate:
        message: "Chỉ được dùng image từ myregistry.io"
        pattern:
          spec:
            containers:
              - image: "myregistry.io/*"

9. Cluster/Node Autoscaling

Application autoscale (HPA) mà node không đủ chỗ chứa Pod mới thì HPA cũng vô nghĩa, Pod sẽ kẹt ở trạng thái Pending.

  • Bad: Node pool cố định, HPA scale lên 10 replicas nhưng cluster chỉ đủ chỗ cho 6.
  • Good: Bật Cluster Autoscaler hoặc Karpenter, có headroom (overprovisioning bằng pause pod priority thấp) để giảm độ trễ khi cần scale gấp.
# Pause pod overprovisioning: chiếm chỗ trước, nhường ngay khi cần
apiVersion: apps/v1
kind: Deployment
metadata:
  name: overprovisioning
spec:
  replicas: 3
  selector:
    matchLabels: { app: overprovisioning }
  template:
    metadata:
      labels: { app: overprovisioning }
    spec:
      priorityClassName: overprovisioning-low-priority
      containers:
        - name: pause
          image: registry.k8s.io/pause:3.9
          resources:
            requests: { cpu: 1, memory: 2Gi }

Lưu ý: scale-down của Cluster Autoscaler phải tôn trọng PodDisruptionBudget. Nếu không cấu hình đúng, việc rút node có thể bị "kẹt" vô thời hạn (liên quan tới mục Application Level #3).

10. Ingress HA + TLS

Ingress controller là single point of failure kinh điển nếu chỉ chạy 1 replica, chết 1 pod là toàn bộ traffic từ ngoài vào không tới được cluster.

  • Bad: Ingress controller 1 replica, không TLS (traffic đi HTTP thuần).
  • Good: Ingress controller ≥ 2 replicas với anti-affinity + PDB, TLS bắt buộc qua cert-manager.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop-api
  namespace: production
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
    - hosts: ["api.example.com"]
      secretName: shop-api-tls
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: shop-api
                port: { number: 80 }

11. Observability Stack

Không có metrics/log tập trung, bạn sẽ không biết hệ thống "ốm" cho tới khi user report, lúc đó đã quá trễ.

  • Bad: kubectl logs thủ công từng Pod mỗi khi có sự cố, không có alert nào.
  • Good: Prometheus + Grafana (hoặc managed alternative) thu thập metrics, logging tập trung (Loki/ELK), alert dựa trên SLO burn rate chứ không chỉ CPU/memory thô.
# ServiceMonitor để Prometheus tự discover metrics endpoint
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: shop-api
  namespace: production
spec:
  selector:
    matchLabels: { app: shop-api }
  endpoints:
    - port: metrics
      interval: 30s
      path: /metrics

12. StorageClass & Backup

Với stateful workload, mất dữ liệu vì reclaim policy sai hoặc chưa test restore lần nào là rủi ro production nghiêm trọng nhất, nghiêm trọng hơn cả downtime.

  • Bad: reclaimPolicy: Delete cho volume chứa dữ liệu quan trọng, backup "có cấu hình" nhưng chưa restore thử lần nào.
  • Good: reclaimPolicy: Retain cho volume quan trọng, backup định kỳ bằng Velero và restore thử định kỳ để xác nhận backup thật sự dùng được.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: production-ssd
provisioner: ebs.csi.aws.com
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
# Velero: backup và test restore
velero backup create db-backup-$(date +%Y%m%d) --include-namespaces production
velero restore create --from-backup db-backup-20260718 --namespace-mappings production:production-restore-test

Phần 2: Application Level

1. Readiness / Liveness / Startup Probes

Đây là bộ 3 quan trọng nhất và cũng dễ cấu hình sai nhất:

  • Readiness: "Đừng gửi traffic cho tôi lúc này". Không đạt thì Pod bị rút khỏi Endpoints, không nhận traffic mới, nhưng không bị restart.

  • Liveness: "Tôi bị treo rồi, restart tôi đi". Không đạt thì container bị kill và tạo lại.

  • Startup: Dành cho app khởi động chậm. Trong lúc startup probe chưa pass, liveness/readiness probe tạm thời bị vô hiệu hoá, tránh app bị kill oan khi đang boot.

  • Bad: Liveness probe cấu hình quá gắt (failureThreshold: 1, timeoutSeconds: 1) trên endpoint nặng. App đang xử lý load cao bị hiểu nhầm là "chết", Kubernetes restart liên tục tạo thành restart storm, càng làm tải nặng hơn.

  • Good: Liveness đơn giản, chỉ check process còn sống (không gọi DB, không gọi service ngoài). Readiness mới là nơi check các dependency thật sự (DB, cache...). Startup probe riêng cho app khởi động lâu.

livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 10
  failureThreshold: 3
readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  periodSeconds: 5
  failureThreshold: 3
startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  failureThreshold: 30   # cho phép tối đa 30 x 10s = 5 phút để app boot xong
  periodSeconds: 10

Note quan trọng cho app khởi động chậm: ngoài startupProbe, đừng quên minReadySeconds ở cấp Deployment (không phải field của probe, mà nằm ở spec.minReadySeconds). Mặc định giá trị này là 0, nghĩa là ngay khi readiness probe pass lần đầu, Pod được tính là "available" và rollout tiếp tục ngay lập tức sang Pod kế tiếp. Với app khởi động chậm hoặc cần thời gian "warm up" thật sự ổn định (nạp cache, kết nối connection pool...) sau khi readiness đã pass, việc rollout đi quá nhanh có thể khiến bạn thay hết cả 3 Pod cùng lúc trước khi phát hiện ra Pod mới có vấn đề ẩn (ví dụ leak connection sau vài chục giây). Set minReadySeconds một khoảng hợp lý (ví dụ 30-60s) để rollout "chờ thêm" và quan sát Pod ổn định thật sự trước khi coi là thành công, giảm rủi ro rollout cả loạt Pod lỗi liên tiếp.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: shop-api
spec:
  replicas: 3
  minReadySeconds: 30   # chờ 30s sau khi Ready mới tính là "available", hữu ích với app boot chậm
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    spec:
      containers:
        - name: shop-api
          startupProbe:
            httpGet: { path: /healthz, port: 8080 }
            failureThreshold: 30
            periodSeconds: 10

2. Resource Requests & Limits

Không khai báo resources, Pod của bạn thuộc QoS class BestEffort, là ứng viên đầu tiên bị evict khi node thiếu tài nguyên, bất kể app có quan trọng cỡ nào.

  • Bad: Không set requests/limits.
  • Good: Set cả hai, dựa trên số liệu sử dụng thực tế (không đoán mò) để đạt QoS class Guaranteed cho workload quan trọng.
resources:
  requests:
    cpu: 250m
    memory: 256Mi
  limits:
    cpu: 500m
    memory: 512Mi

Mẹo: dùng Vertical Pod Autoscaler ở chế độ Off (chỉ recommend, không tự apply) trong vài ngày để lấy số liệu request/limit hợp lý thay vì đoán.

3. Pod Disruption Budget

PodDisruptionBudget (PDB) bảo vệ bạn khỏi voluntary disruption: node drain khi upgrade, cluster autoscaler scale-down. Nó không bảo vệ bạn khỏi kubectl delete thủ công hay node chết đột ngột (involuntary disruption).

  • Bad: Không có PDB, team hạ tầng drain node để patch, vô tình đá bay hết Pod của bạn cùng lúc.
  • Bad khác (cấu hình sai theo hướng ngược lại): minAvailable: 3 trong khi replicas: 3. Về lý thuyết đúng nhưng khiến kubectl drain bị treo vĩnh viễn, không node nào có thể maintenance được.
  • Good: Set số dư hợp lý, ví dụ cho phép mất tối đa 1 Pod cùng lúc.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: shop-api-pdb
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: shop-api

4. Pod (Anti-)Affinity & Topology Spread Constraints

Có 3 replicas nhưng cả 3 đều bị scheduler xếp chung 1 node/1 zone thì khác gì có 1 replica, node/zone đó chết là mất sạch.

  • Bad: Không khai báo gì, để scheduler tự quyết định (thường sẽ dồn Pod vào node còn nhiều tài nguyên nhất, dễ trùng node).
  • Good: Dùng podAntiAffinity để tránh 2 Pod cùng app nằm chung node, và topologySpreadConstraints để trải Pod đều qua các zone.
affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels: { app: shop-api }
        topologyKey: kubernetes.io/hostname
topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels: { app: shop-api }

5. HPA / VPA

HPA scale theo số lượng Pod (horizontal), VPA điều chỉnh request/limit của từng Pod (vertical). Chạy cả hai cùng lúc trên cùng 1 metric (thường là CPU) sẽ khiến chúng "đá" nhau: VPA đổi request, HPA lại tính toán lại dựa trên request mới, tạo vòng lặp không ổn định.

  • Bad: Bật cả HPA và VPA ở chế độ Auto, cùng theo dõi CPU.
  • Good: HPA quản lý CPU/số replicas, VPA chỉ chạy chế độ Off (Initial hoặc chỉ recommend) để tham khảo, hoặc VPA quản lý các resource khác (vd: memory) tách biệt với HPA.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: shop-api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: shop-api
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target: { type: Utilization, averageUtilization: 60 }
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
        - type: Percent
          value: 100
          periodSeconds: 15
    scaleDown:
      stabilizationWindowSeconds: 300   # scale down chậm, tránh flapping
      policies:
        - type: Pods
          value: 1
          periodSeconds: 60

6. Graceful Shutdown

Nguyên nhân số 1 gây mất request khi rollout: việc gỡ Pod khỏi Endpoints và gửi SIGTERM xảy ra song song, không tuần tự. Có một khoảng thời gian ngắn Pod đã nhận SIGTERM (và có thể đã bắt đầu tắt) nhưng vẫn còn nhận traffic vì chưa kịp gỡ khỏi Endpoints ở phía kube-proxy/load balancer.

  • Bad: App thoát ngay khi nhận SIGTERM, không có preStop, mỗi lần rollout đều rớt vài request.
  • Good: App xử lý SIGTERM để drain kết nối đang dở, kèm preStop sleep để "câu giờ" cho việc gỡ Endpoints kịp lan truyền.
lifecycle:
  preStop:
    exec:
      command: ["sh", "-c", "sleep 5"]
terminationGracePeriodSeconds: 30
// Ví dụ xử lý SIGTERM trong app (Go)
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM)
<-sigCh
server.Shutdown(context.Background()) // drain connection đang xử lý dở
os.Exit(0)

7. Deployment Strategy

Chiến lược rollout mặc định (RollingUpdate) không phải lúc nào cũng an toàn nếu để nguyên giá trị mặc định, và với thay đổi rủi ro cao, rolling update thuần cũng không đủ để phát hiện sớm.

  • Bad: Dùng mặc định maxUnavailable: 25% cho service quan trọng. Có thể mất tới 1/4 capacity trong lúc rollout ở giờ cao điểm.
  • Good: Với service quan trọng, set maxUnavailable: 0 để không bao giờ giảm capacity, dùng maxSurge để tạo Pod mới trước khi tắt Pod cũ. Với thay đổi rủi ro cao, cân nhắc canary/blue-green.
strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 1
    maxUnavailable: 0
# Canary đơn giản bằng cách chạy song song 2 Deployment, chia traffic qua Service weight
# (hoặc dùng Argo Rollouts / Flagger cho canary tự động theo % traffic)

8. Secrets Handling

Secret dạng biến môi trường (env) dễ bị lộ qua kubectl describe pod, log crash dump, hay child process kế thừa toàn bộ biến môi trường của tiến trình cha.

  • Bad:
env:
  - name: DB_PASSWORD
    value: "supersecret123"   # hardcode thẳng trong manifest
  • Good: Mount secret dưới dạng volume (không lộ khi liệt kê biến môi trường), và với hệ thống lớn, dùng external secret store (Vault, AWS Secrets Manager) qua External Secrets Operator thay vì Kubernetes Secret thuần (Secret mặc định chỉ base64, không mã hoá thật sự nếu chưa bật encryption at rest).
volumes:
  - name: db-secret
    secret:
      secretName: db-credentials
containers:
  - name: shop-api
    volumeMounts:
      - name: db-secret
        mountPath: /etc/secrets
        readOnly: true

9. Labels & Metadata

Không có labeling nhất quán, việc troubleshoot, tính cost theo team, hay viết selector chính xác đều trở nên khó khăn khi cluster lớn dần.

  • Bad: Label tùy hứng, mỗi service một kiểu đặt tên.
  • Good: Theo chuẩn recommended labels của Kubernetes.
metadata:
  labels:
    app.kubernetes.io/name: shop-api
    app.kubernetes.io/instance: shop-api-production
    app.kubernetes.io/version: "1.4.0"
    app.kubernetes.io/component: backend
    app.kubernetes.io/part-of: shop-platform
    app.kubernetes.io/managed-by: helm

10. Image Tag & Pull Policy

:latest là "tag không có nghĩa": không ai biết chắc chắn phiên bản nào đang chạy, rollback gần như bất khả thi vì không biết "phiên bản trước" là gì.

  • Bad: image: myregistry.io/shop-api:latest
  • Good: Pin tag cụ thể (bản thân CI/CD build ra), với hệ thống yêu cầu cao hơn có thể pin theo digest sha256 để đảm bảo bất biến.
containers:
  - name: shop-api
    image: myregistry.io/shop-api:1.4.0
    imagePullPolicy: IfNotPresent   # hoặc Always nếu tag có thể bị ghi đè (không nên, nhưng nếu có thì bắt buộc Always)

11. ConfigMap & Reload Strategy

Sửa ConfigMap không tự động khiến Pod đang chạy nhận giá trị mới. Nếu app chỉ đọc config lúc khởi động, bạn sẽ "sửa xong tưởng xong" nhưng thực tế Pod vẫn chạy config cũ.

  • Bad: Sửa ConfigMap, tưởng là xong, không biết Pod cần restart mới nhận giá trị mới.
  • Good: Có chiến lược rõ ràng: hoặc app tự watch file config và reload, hoặc dùng annotation checksum để buộc rollout mới mỗi khi ConfigMap đổi (cách phổ biến khi dùng Helm).
# Helm: checksum annotation buộc Pod restart khi ConfigMap thay đổi
spec:
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}

Vì sao annotation phải nằm ở spec.template.metadata? Deployment có 2 khối metadata: một ở cấp Deployment, một trong spec.template (mô tả Pod). Controller chỉ rollout khi spec.template đổi, vì đó là phần được hash thành pod-template-hash. Đặt annotation ở metadata ngoài cùng sẽ không kích hoạt gì cả. checksum/config không có ý nghĩa đặc biệt với Kubernetes, chỉ là quy ước Helm để buộc giá trị annotation đổi theo nội dung ConfigMap.

Gợi ý: dùng Stakater Reloader thay vì tự viết checksum vào từng chart. Reloader tự theo dõi ConfigMap/Secret và patch annotation tương tự khi phát hiện thay đổi, không cần sửa chart:

metadata:
  annotations:
    reloader.stakater.com/auto: "true"

Lưu ý: nếu dùng ArgoCD với selfHeal: true, patch trực tiếp của Reloader có thể bị ArgoCD revert vì coi là drift so với Git. Trường hợp đó nên ưu tiên cách checksum annotation ở trên.


Phần 3: Going Live - Kiểm Tra Cuối Cùng

Trước khi bấm nút release thật sự, review nhanh 4 câu hỏi sau:

  • Đã load-test đường scale chưa?: Cấu hình HPA/Cluster Autoscaler xong không có nghĩa là nó hoạt động đúng như kỳ vọng dưới tải thật. Test thử với traffic tăng đột biến 5-10 lần.
  • Rollback hay roll-forward?: Quyết định trước, đừng để tới lúc sự cố mới họp bàn. kubectl rollout undo có sẵn sàng dùng ngay không, hay quy trình là fix-forward?
  • Runbook đã có chưa?: Khi Pod crash loop, khi node chết, on-call biết làm gì đầu tiên không cần đoán?
  • Đã review cost & right-size chưa?: requests quá cao so với usage thực tế là lãng phí âm thầm, cộng dồn theo tháng có thể là con số không nhỏ.

Kết luận

Checklist này không phải để áp dụng máy móc 100% mọi mục cho mọi hệ thống, một side project cá nhân không cần etcd 5-member hay canary deployment. Nhưng càng gần production thật, càng nhiều user thật, thì càng nên rà lại từng mục một cách có chủ đích thay vì "chạy được là xong".

Hy vọng checklist này giúp bạn tự tin hơn khi đưa workload lên Kubernetes production. Lưu lại và áp dụng dần cho hệ thống của bạn nhé!

Happy Coding! 👨‍💻

Nếu bạn đang gặp khó khăn về hệ thống, cần hỗ trợ về DevOps tools, hoặc định hướng sự nghiệp, mình tin là có thể giúp được. Liên hệ mình qua hoangviet.io.vn, rất vui được trò chuyện và hợp tác cùng bạn.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí