🚀 DevOps✍️ Khoa📅 19/04/2026☕ 19 phút đọc

DevOps: Docker + Kubernetes Core Concepts (Intermediate++)

Bài này dành cho mức intermediate++: bạn đã quen với Docker và Kubernetes cơ bản, nhưng còn nhiều câu hỏi dạng "tại sao container bị OOMKill?", "iptables liên quan gì đến Service?", "tại sao pod Pending dù node còn memory?".

Mục tiêu: hiểu đủ sâu để vận hành hệ thống production, debug sự cố thực tế, và giải thích các quyết định thiết kế khi phỏng vấn senior level.

Mental model: Docker là công nghệ đóng gói — biến app và dependencies thành một đơn vị di động, chạy nhất quán mọi môi trường. Kubernetes là hệ thống điều phối — quản lý vòng đời các container đó ở quy mô lớn, tự động hoá recovery, scaling, và rollout.


1. Docker internals sâu hơn

1.1 Container không phải mini-VM — kernel namespaces là cốt lõi

Container không virtualise hardware. Chúng dùng tính năng của Linux kernel để tạo môi trường ảo hoá nhẹ:

flowchart TB
  subgraph VM["VM"]
    V1["Hardware"] --> V2["Hypervisor"] --> V3["Guest OS (kernel riêng)"] --> V4["App"]
    V4 -.-> V5["Overhead: ~100–500ms boot, ~128MB RAM/VM"]
  end
  subgraph CT["Container"]
    C1["Hardware"] --> C2["Host OS kernel"] --> C3["containerd/runc"] --> C4["App (process biệt lập)"]
    C4 -.-> C5["Overhead: ~10–50ms start, ~1–5MB"]
  end
VM:
  Hardware ──► Hypervisor ──► Guest OS (kernel riêng) ──► App
  Tốn: ~100–500ms boot, ~128MB RAM/VM overhead

Container:
  Hardware ──► Host OS kernel ──► containerd/runc ──► App (process biệt lập)
  Tốn: ~10–50ms start, ~1–5MB overhead

Linux namespaces — 7 loại cách ly môi trường:

Namespace Cách ly cái gì Ý nghĩa thực tế
pid Process ID tree Container chỉ thấy process của mình, PID 1 là process chính
net Network stack Interface, routing table, firewall riêng
mnt Filesystem mounts Cây thư mục riêng, không thấy /proc host
ipc System V IPC, POSIX MQ Shared memory riêng biệt
uts Hostname, domain name hostname trong container độc lập
user UID/GID mapping Root trong container ≠ root trên host
cgroup cgroup tree Container thấy giới hạn resource của mình

cgroups (control groups) — giới hạn và đo lường tài nguyên:

# Xem cgroup của container đang chạy
cat /sys/fs/cgroup/memory/docker/<container_id>/memory.limit_in_bytes

# Nếu không set limit → unlimited → OOM killer sẽ ngẫu nhiên kill process trên host

Hai phiên bản: cgroups v1 (systemd/docker cũ) và cgroups v2 (unified hierarchy, mặc định từ kernel 5.x, Docker 20.10+). cgroups v2 quan trọng vì:

  • Memory accounting chính xác hơn (tính cả page cache).
  • PSI (Pressure Stall Information) — đo áp lực memory/CPU/IO.
  • Hỗ trợ tốt hơn cho Kubernetes QoS classes.

1.2 Union filesystem và overlay2

Image Docker là stack các layer read-only. Khi container chạy, engine thêm một writable layer (container layer) ở trên cùng. Đây là Union Filesystem — nhiều layer hợp thành một filesystem duy nhất.

Container writable layer  ← ghi ở đây, bị xoá khi container chết
─────────────────────────
Layer 3: COPY . .         (code app)
Layer 2: RUN npm ci       (node_modules)
Layer 1: COPY package.json .
Layer 0: node:20-alpine   (base image)
─────────────────────────
Host filesystem

Overlay2 (driver mặc định trên Linux) dùng cơ chế Copy-on-Write (CoW):

  • Đọc file từ layer thấp hơn → không copy.
  • Ghi file từ layer thấp hơn lần đầu → copy lên writable layer rồi ghi.
  • Hệ quả: write-heavy workload (database) không nên chạy trên writable layer — dùng volume thay thế.

Tối ưu Dockerfile để tận dụng layer cache:

# ❌ Tệ: thay đổi code → rebuild toàn bộ (npm ci lại từ đầu)
FROM node:20-alpine
WORKDIR /app
COPY . .          # copy tất cả — cache miss mọi lúc
RUN npm ci
RUN npm run build

# ✅ Tốt: layer ổn định ở dưới, layer hay thay đổi ở trên
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./        # Layer này cache đến khi package.json đổi
RUN npm ci                   # Chỉ chạy lại khi dependencies đổi
COPY . .                     # Code thay đổi thường xuyên → đặt sau
RUN npm run build

# Multi-stage: final image không chứa build tools
FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

BuildKit (bật từ Docker 23+, mặc định) mở thêm:

  • --mount=type=cache: cache npm/go module giữa các build, không copy vào layer.
  • Build song song các stage.
  • Không build stage không cần thiết (lazy evaluation).
# BuildKit cache mount — npm cache không đi vào image
RUN --mount=type=cache,target=/root/.npm \
    npm ci --prefer-offline

1.3 OCI, containerd, và runc

Chuỗi runtime thực tế trên một K8s node:

flowchart LR
  K["kubelet"] --> CD["containerd (CRI endpoint)"]
  CD --> SH["containerd-shim-runc-v2"]
  SH --> R["runc (clone, pivot_root, ...)"]
kubelet
  └── containerd (CRI endpoint: /run/containerd/containerd.sock)
        └── containerd-shim-runc-v2
              └── runc (tạo container thực sự bằng syscall clone, pivot_root, ...)
  • OCI (Open Container Initiative): chuẩn image format và runtime spec — đảm bảo image build bằng Docker chạy được trên Podman, containerd, hay bất kỳ runtime nào.
  • containerd: daemon quản lý lifecycle container, image pull, snapshot.
  • runc: công cụ CLI tạo container theo OCI runtime spec — gọi clone(), pivot_root(), seccomp, capabilities của kernel.

Vì sao quan trọng: khi debug ở node level, bạn dùng crictl thay vì docker:

# Liệt kê container đang chạy (trên node K8s)
crictl ps

# Xem logs
crictl logs <container_id>

# Inspect container
crictl inspect <container_id>

2. Kubernetes architecture — sâu hơn một lớp

2.1 Control plane: cách các component phối hợp

┌─────────────────────────────────────────────────────────────────┐
│                         CONTROL PLANE                           │
│                                                                 │
│  kubectl / API ──► kube-apiserver ──► etcd (source of truth)   │
│                          │                                      │
│                    ┌─────┴──────┐                               │
│                    │            │                               │
│              scheduler    controller-manager                    │
│              (chọn node)  (reconciliation loops)                │
└─────────────────────────────────────────────────────────────────┘
         │ watch & notify                 │ watch & notify
         ▼                               ▼
┌────────────────┐              ┌────────────────┐
│  Worker Node 1  │              │  Worker Node 2  │
│  kubelet        │              │  kubelet        │
│  kube-proxy     │              │  kube-proxy     │
│  containerd     │              │  containerd     │
└────────────────┘              └────────────────┘

etcd — distributed key-value store dùng Raft consensus:

  • Toàn bộ trạng thái cluster lưu ở đây: pod specs, secrets, configmaps, service endpoints.
  • etcd yếu → toàn bộ control plane tê liệt (worker nodes vẫn chạy pod cũ, nhưng không có gì mới được schedule).
  • Khuyến nghị production: 3 hoặc 5 etcd member (chịu được 1 hoặc 2 node failure).

kube-apiserver — mọi thứ đi qua đây:

  • Authenticate → Authorize (RBAC) → Admission (Mutating → Validating) → Persist vào etcd.
  • Admission controllers: OPA Gatekeeper, Kyverno, ImagePolicyWebhook... chạy ở đây.

kube-scheduler — quy trình chọn node:

  1. Filtering (predicates): loại bỏ node không đủ điều kiện (insufficient CPU, taint không match, affinity rules).
  2. Scoring (priorities): chấm điểm các node còn lại (spread score, resource balance, topology spread).
  3. Chọn node điểm cao nhất, ghi vào PodSpec.

controller-manager — chứa hàng chục controller chạy song song:

  • ReplicaSet controller: đếm pod đang chạy, tạo/xoá để match replicas.
  • Deployment controller: quản lý ReplicaSet, thực hiện rolling update.
  • EndpointSlice controller: cập nhật danh sách IP:port của pods vào Service.
  • Job controller, CronJob controller, HPA controller...

2.2 Reconciliation loop — trái tim của K8s

Pattern này là lý do K8s self-healing:

while true:
    desired_state  = etcd.read(resource)
    current_state  = observe_real_world()
    if current_state != desired_state:
        take_action_to_converge()
    sleep(a_bit)

Khi bạn kubectl apply -f deployment.yaml:

  1. apiserver lưu desired state vào etcd.
  2. Deployment controller detect thay đổi → tạo ReplicaSet mới.
  3. ReplicaSet controller detect pods thiếu → tạo Pod objects.
  4. Scheduler detect pod chưa có nodeName → gắn node.
  5. kubelet trên node detect pod được gán cho mình → pull image, chạy container.

2.3 kubelet — agent trên worker node

kubelet làm nhiệm vụ:

  • Watch apiserver, lấy pod spec được gán cho node mình.
  • Giao tiếp với containerd qua CRI (Container Runtime Interface).
  • Chạy probe (liveness, readiness, startup) theo schedule.
  • Báo cáo node status, pod status lên apiserver.
  • Evict pod khi node bị áp lực memory (dựa trên requests, không phải limits).

3. Resource management — chi tiết production

3.1 Requests, Limits và QoS Classes

resources:
  requests:
    cpu: "250m"       # 0.25 core — scheduler dùng để chọn node
    memory: "256Mi"   # kubelet dùng để tính eviction priority
  limits:
    cpu: "1000m"      # throttled nếu vượt (CPU compressible)
    memory: "1Gi"     # OOMKilled nếu vượt (memory incompressible)

Kubernetes chia pod thành 3 QoS class dựa vào cách set requests/limits:

QoS Class Điều kiện Eviction priority
Guaranteed requests == limits (cả CPU và memory) Bị evict sau cùng
Burstable requests < limits hoặc chỉ set một trong hai Trung gian
BestEffort Không set requests lẫn limits Bị evict đầu tiên

Bài học thực chiến: Pod không set resources = BestEffort = bị kill đầu tiên khi node áp lực. Đây là lý do hay gặp pod bị terminate bất ngờ trong cluster dùng chung.

CPU throttling — khác với OOMKill:

  • Khi container dùng CPU vượt limits.cpu, CFS (Completely Fair Scheduler) throttle process.
  • Container không bị kill, nhưng mọi syscall bị làm chậm → latency tăng đột biến theo chu kỳ.
  • Dấu hiệu: container_cpu_cfs_throttled_seconds_total tăng trong Prometheus.
  • Fix: tăng limits.cpu hoặc giảm requests.cpu để phân bổ công bằng hơn.

Memory và OOMKill:

  • Khi container vượt limits.memory, kernel OOM killer kill process ngay lập tức.
  • K8s ghi lại lý do trong pod events: OOMKilled.
  • Nguyên nhân phổ biến: memory leak, JVM heap không được giới hạn, load spike đột ngột.

3.2 Scheduler: node affinity và topology spread

Node Affinity — ràng buộc cứng và mềm:

affinity:
  nodeAffinity:
    # Cứng: phải chạy trên node có SSD
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
        - matchExpressions:
            - key: disk-type
              operator: In
              values: ["ssd"]
    # Mềm: ưu tiên node ở zone us-east-1a
    preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 80
        preference:
          matchExpressions:
            - key: topology.kubernetes.io/zone
              operator: In
              values: ["us-east-1a"]

Topology Spread Constraints — phân tán pod đều các zone (K8s 1.19+):

topologySpreadConstraints:
  - maxSkew: 1                              # Zone A và B chênh nhau tối đa 1 pod
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule        # Không schedule nếu vi phạm
    labelSelector:
      matchLabels:
        app: payment-api

Đây là cách đúng để đảm bảo HA đa-zone — tốt hơn podAntiAffinity vì hoạt động theo số lượng, không chỉ theo "không trùng node".

Taints & Tolerations — dùng để tách workload:

# Taint node: chỉ pod có toleration mới vào được
kubectl taint nodes node1 dedicated=gpu:NoSchedule

# Toleration trong pod spec
tolerations:
  - key: "dedicated"
    operator: "Equal"
    value: "gpu"
    effect: "NoSchedule"

Dùng phổ biến: node GPU cho ML workload, node spot/preemptible, node cho system daemonsets.


4. Networking — hiểu tường tận

4.1 Mô hình mạng K8s và CNI

K8s yêu cầu:

  1. Mọi Pod có thể nói chuyện với mọi Pod khác (không NAT) — kể cả khác node.
  2. Node có thể nói chuyện với Pod.
  3. Pod thấy IP của chính mình đúng như IP mà node khác thấy.

CNI plugin implement model này. Cách hoạt động:

Pod A (node 1, 10.244.1.5)  ──►  veth pair  ──►  bridge cbr0  ──►  node routing ──►  Pod B (node 2, 10.244.2.8)
  • Calico: dùng BGP để quảng bá route giữa các node. Performance tốt, hỗ trợ NetworkPolicy đầy đủ.
  • Cilium: dùng eBPF thay vì iptables — performance cao hơn, observability tốt hơn (L7 metrics), native mTLS.
  • Flannel: đơn giản, chỉ overlay, không hỗ trợ NetworkPolicy.

4.2 Service và kube-proxy — traffic routing thực sự

Khi bạn tạo Service, K8s không tạo một process riêng để proxy traffic. Thay vào đó, kube-proxy cài iptables rules (hoặc ipvs/eBPF) trên mọi node:

Client Pod ──► iptables PREROUTING ──► DNAT đến một pod IP ngẫu nhiên (load balance)

Ví dụ chain iptables cho Service:

KUBE-SERVICES → KUBE-SVC-XXXXX (selector: app=payment) 
  → KUBE-SEP-AAA (50%) → 10.244.1.5:8080
  → KUBE-SEP-BBB (50%) → 10.244.2.8:8080

Hệ quả quan trọng:

  • Service IP (ClusterIP) là virtual IP — không route được mà không qua iptables.
  • Khi pod mới join hoặc rời, EndpointSlice controller cập nhật, kube-proxy sync lại rules.
  • Delay giữa pod ready và traffic được route: readinessProbe + terminationGracePeriodSeconds phải được set đúng để tránh connection drop khi deploy.

DNS trong cluster — CoreDNS:

# Service "payment-api" trong namespace "payments":
payment-api.payments.svc.cluster.local → ClusterIP của service

# Từ cùng namespace: có thể dùng short form
payment-api → resolve đến payment-api.payments.svc.cluster.local

kube-proxy modes (theo thứ tự hiệu năng tăng dần):

  • iptables (mặc định): O(n) lookup, chậm khi cluster > vài nghìn services.
  • ipvs: O(1) hash-based, hỗ trợ nhiều load balancing algorithm.
  • eBPF (Cilium): bypass iptables hoàn toàn, highest performance.

4.3 NetworkPolicy — zero-trust networking

Mặc định K8s cho phép all-to-all. NetworkPolicy là cách enforce least-privilege:

# Chỉ cho phép payment-api gọi vào postgres, port 5432
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: postgres-ingress
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: postgres
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: payment-api
      ports:
        - protocol: TCP
          port: 5432

Nguyên tắc áp dụng:

  • Default deny all: tạo NetworkPolicy rỗng cho namespace → block tất cả, rồi mở từng rule.
  • Ingress và Egress là độc lập — phải khai báo cả hai chiều nếu cần.
  • NetworkPolicy chỉ hiệu lực khi CNI plugin hỗ trợ (Calico, Cilium có; Flannel không có).

5. Health checks — semantic đúng, không phải đặt cho có

5.1 Ba loại probe và khi nào dùng gì

spec:
  containers:
    - name: payment-api
      
      # startupProbe: chỉ chạy khi container start
      # Tránh liveness kill container đang khởi động
      startupProbe:
        httpGet:
          path: /healthz/startup
          port: 8080
        failureThreshold: 30   # Cho phép fail 30 lần × 10s = 5 phút để start
        periodSeconds: 10

      # livenessProbe: container có đang "sống" không?
      # Fail → kubelet RESTART container (không xoá pod)
      livenessProbe:
        httpGet:
          path: /healthz/live
          port: 8080
        initialDelaySeconds: 5
        periodSeconds: 10
        failureThreshold: 3    # Fail 3 lần liên tiếp → restart

      # readinessProbe: container có sẵn sàng nhận traffic không?
      # Fail → pod bị rút khỏi Service endpoints (không restart)
      readinessProbe:
        httpGet:
          path: /healthz/ready
          port: 8080
        periodSeconds: 5
        failureThreshold: 2    # Fail nhanh hơn liveness

Semantics của từng probe — quan trọng để implement đúng:

Probe Câu hỏi Nên check gì KHÔNG nên check gì
liveness Process còn "sống" không? Deadlock, goroutine leak DB connection (DB down → không nên restart container)
readiness Sẵn sàng nhận request? DB connection, cache warm Dependency ngoài tầm kiểm soát của pod
startup Đã khởi động xong chưa? Chỉ cần return 200 khi init xong

Anti-pattern phổ biến: liveness và readiness cùng endpoint /health check DB connection → DB flap → toàn bộ pod bị restart liên tục → thundering herd.

5.2 Deployment strategy nâng cao

Rolling Update (mặc định):

strategy:
  type: RollingUpdate
  rollingUpdate:
    maxUnavailable: 0   # Không giảm capacity (zero-downtime)
    maxSurge: 1         # Tạo thêm 1 pod mới trước khi kill pod cũ

Blue/Green qua cách đổi selector:

# Đang chạy version blue
kubectl patch service payment-api -p '{"spec":{"selector":{"version":"blue"}}}'

# Deploy green
kubectl apply -f deployment-green.yaml

# Kiểm tra green ổn
kubectl patch service payment-api -p '{"spec":{"selector":{"version":"green"}}}'

# Rollback tức thì nếu lỗi
kubectl patch service payment-api -p '{"spec":{"selector":{"version":"blue"}}}'

Canary bằng cách điều chỉnh replica ratio:

# deployment-stable.yaml: replicas: 9
# deployment-canary.yaml: replicas: 1
# → 10% traffic đến canary (vì Service load balance đều theo số pod)

PodDisruptionBudget — đảm bảo không down quá nhiều pod khi drain node:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: payment-api-pdb
spec:
  minAvailable: 2   # Phải còn ít nhất 2 pod "Available" mọi lúc
  selector:
    matchLabels:
      app: payment-api

6. Stateful workloads — StatefulSet, PV/PVC, StorageClass

6.1 StatefulSet — khi identity quan trọng

StatefulSet đảm bảo:

  • Pod name ổn định: postgres-0, postgres-1, postgres-2.
  • Thứ tự scale up/down: deploy từ 0 → n, scale down từ n → 0.
  • Mỗi pod có PVC riêng (qua volumeClaimTemplates), PVC không bị xoá khi pod restart.
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres-headless  # Headless service cho DNS per-pod
  replicas: 3
  selector:
    matchLabels:
      app: postgres
  template:
    spec:
      containers:
        - name: postgres
          image: postgres:16
          env:
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:            # Mỗi pod tự động tạo PVC riêng
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 100Gi

Headless Service (clusterIP: None) cho phép DNS resolution per-pod:

postgres-0.postgres-headless.default.svc.cluster.local → IP của postgres-0
postgres-1.postgres-headless.default.svc.cluster.local → IP của postgres-1

Đây là cách replica set của PostgreSQL/Redis/Elasticsearch biết địa chỉ của nhau.

6.2 PersistentVolume, StorageClass và dynamic provisioning

StorageClass (template)
  └── PVC (yêu cầu storage)
        └── PV (storage thực tế — EBS, GCE PD, NFS...)
              └── Mount vào Pod

Dynamic provisioning — PVC tự động tạo PV:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: ebs.csi.aws.com      # AWS EBS CSI driver
parameters:
  type: gp3
  iops: "3000"
  throughput: "125"
reclaimPolicy: Retain              # Giữ EBS volume khi PVC xoá (an toàn hơn Delete)
volumeBindingMode: WaitForFirstConsumer  # Tạo EBS cùng AZ với pod

Access Modes:

  • ReadWriteOnce (RWO): mount bởi một node (EBS, GCE PD).
  • ReadOnlyMany (ROX): nhiều node đọc (NFS, EFS).
  • ReadWriteMany (RWX): nhiều node đọc và ghi (NFS, EFS, CephFS).

7. Security — production-grade

7.1 securityContext — hardening container

spec:
  securityContext:               # Pod-level
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000                # Files tạo bởi process có group 2000
    seccompProfile:
      type: RuntimeDefault       # Chặn syscall nguy hiểm theo default profile

  containers:
    - name: app
      securityContext:           # Container-level (override pod-level)
        allowPrivilegeEscalation: false   # Chặn setuid, sudo
        readOnlyRootFilesystem: true      # / không ghi được → attacker không thể modify files
        capabilities:
          drop: ["ALL"]                   # Drop tất cả Linux capabilities
          add: ["NET_BIND_SERVICE"]       # Chỉ thêm lại cái thực sự cần

7.2 RBAC — phân quyền đúng

# ServiceAccount cho pod
apiVersion: v1
kind: ServiceAccount
metadata:
  name: payment-api
  namespace: payments

---
# Role: chỉ đọc secrets trong namespace payments
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: secret-reader
  namespace: payments
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames: ["payment-api-secret"]   # Chỉ secret cụ thể, không phải tất cả
    verbs: ["get"]

---
# Bind role vào service account
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: payment-api-secret-reader
  namespace: payments
subjects:
  - kind: ServiceAccount
    name: payment-api
    namespace: payments
roleRef:
  kind: Role
  name: secret-reader
  apiGroup: rbac.authorization.k8s.io

Kiểm tra permission:

kubectl auth can-i get secrets --as=system:serviceaccount:payments:payment-api -n payments

7.3 Pod Security Standards (PSS)

Thay thế PodSecurityPolicy (deprecated K8s 1.21, removed 1.25). Áp dụng qua namespace label:

# Enforce: vi phạm → pod bị reject
# Warn: vi phạm → cảnh báo nhưng vẫn cho qua
# Audit: vi phạm → ghi vào audit log
apiVersion: v1
kind: Namespace
metadata:
  name: payments
  labels:
    pod-security.kubernetes.io/enforce: restricted  # Chế độ chặt nhất
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/audit: restricted

Levels: privileged (không giới hạn) → baseline (ngăn escalation rõ ràng) → restricted (best practice đầy đủ).

7.4 Secrets management

Kubernetes Secret mặc định chỉ base64 encoded, không encrypted. Production cần thêm:

  • Encryption at rest: cấu hình EncryptionConfiguration với KMS provider (AWS KMS, GCP KMS).
  • External secret store: External Secrets Operator đồng bộ từ AWS Secrets Manager / GCP Secret Manager / HashiCorp Vault.
  • Không dùng env vars cho secrets (xuất hiện trong ps aux, /proc, logs): dùng file mount từ secret volume + readOnlyRootFilesystem.

8. Autoscaling

8.1 HPA — Horizontal Pod Autoscaler

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: payment-api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: payment-api
  minReplicas: 3
  maxReplicas: 50
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60     # Scale khi CPU > 60% của requests
    - type: Resource
      resource:
        name: memory
        target:
          type: AverageValue
          averageValue: 800Mi
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second  # Custom metric từ Prometheus
        target:
          type: AverageValue
          averageValue: "1000"

HPA + PDB phối hợp: HPA scale down có thể vi phạm PDB → K8s sẽ throttle scale-down để đảm bảo minAvailable.

8.2 VPA và Cluster Autoscaler

VPA (Vertical Pod Autoscaler) — tự động điều chỉnh requests/limits:

  • Mode Off: chỉ recommend (dùng để calibrate giá trị ban đầu).
  • Mode Auto: tự update requests và restart pod.
  • Chú ý: VPA restart pod để apply resource thay đổi → không nên dùng cùng HPA trên cùng metric.

Cluster Autoscaler — thêm/xoá node tự động:

  • Scale up: khi pod Pending vì không đủ resource trên node hiện tại.
  • Scale down: khi node utilization thấp và tất cả pod có thể chạy trên node khác.
  • Yêu cầu requests được set đúng — CA dùng requests để tính, không phải actual usage.

9. Observability — biết hệ thống đang làm gì

9.1 Golden signals (Google SRE)

Signal Metric ví dụ Alert khi nào
Latency http_request_duration_seconds p99 > SLO threshold
Traffic http_requests_total rate Anomaly (quá cao hoặc quá thấp)
Errors http_request_errors_total rate Error rate > 1%
Saturation CPU throttling, memory usage, queue depth Approaching limits

9.2 Structured logging và distributed tracing

// Structured log — machine-readable, dễ filter
log.Info("payment processed",
    "order_id", orderID,
    "amount", amount,
    "duration_ms", elapsed.Milliseconds(),
    "trace_id", span.SpanContext().TraceID(),
)
# Fluentd/Fluent Bit DaemonSet: collect logs từ mọi node, forward đến Elasticsearch/Loki
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentbit
spec:
  selector:
    matchLabels:
      app: fluentbit
  template:
    spec:
      containers:
        - name: fluentbit
          image: fluent/fluent-bit:2.2
          volumeMounts:
            - name: varlog
              mountPath: /var/log
      volumes:
        - name: varlog
          hostPath:
            path: /var/log

10. Anti-patterns phổ biến ở production

Anti-pattern Hệ quả Cách đúng
Dùng latest tag Không reproducible, deploy không biết version gì Tag bằng git SHA hoặc semver
Không set resource requests Pod là BestEffort, bị evict đầu tiên; CA không scale up đúng Luôn set requests
Requests == Limits Guaranteed nhưng lãng phí; JVM/Node không thể burst Burstable: requests < limits
Liveness probe check DB DB flap → toàn bộ pod restart liên tục Liveness chỉ check internal health
Không có PDB Node drain kill > 1 pod cùng lúc → downtime Tạo PDB cho tất cả production service
Secrets trong env vars Lộ qua /proc, logs, kubectl describe Volume mount secrets
Không có NetworkPolicy Blast radius không giới hạn khi container bị compromise Default-deny, mở từng rule
Không test restore Backup có nhưng restore lỗi thì backup vô nghĩa Quarterly restore drill

Bước tiếp theo