K8s 8 — Vận hành production: Helm, scaling, RBAC, observability
Từ "chạy được" đến "chạy được ở production"
Qua các bài trước của series, chúng ta đã dựng được một hệ thống Kubernetes hoạt động: Workloads (Deployment, StatefulSet, Job) tạo và duy trì Pod; Services & networking đưa lưu lượng vào; Storage & config gắn dữ liệu bền bỉ và cấu hình. Nhưng "một hệ thống chạy được trên cluster dev" và "một hệ thống đứng vững ở production lúc 3 giờ sáng khi tải tăng gấp mười" là hai chuyện rất khác nhau.
Bài này khép lại series bằng những thứ tách biệt "demo" khỏi "production": đóng gói ứng dụng bằng Helm, khai báo tài nguyên đúng cách để scheduler và autoscaler làm việc được, cấu hình health probes để cluster tự chữa lành, bật autoscaling, siết RBAC & bảo mật, và cuối cùng là observability để bạn biết chuyện gì đang xảy ra trước khi khách hàng biết.
Nhắc lại quy ước của series: sandbox chỉ có PostgreSQL, không có cluster Kubernetes. Mọi khối
yaml/bashdưới đây là (minh hoạ) — chúng đúng cú pháp và phản ánh thực tế, nhưng không được thực thi trong môi trường này. Hãy áp dụng trên cluster thật của bạn.
Helm: package manager cho Kubernetes
Khi ứng dụng lớn lên, bạn không còn quản lý 3 file YAML mà là 20–30 file: Deployment, Service, Ingress, ConfigMap, Secret, HPA, PDB, ServiceAccount... nhân với số môi trường (dev/staging/prod). Copy-paste rồi sửa tay là công thức của lỗi.
Helm giải quyết bằng cách đóng gói tất cả thành một chart — một thư mục có cấu trúc chuẩn:
Chart.yaml: metadata (tên, phiên bản chart, phiên bản app).values.yaml: các giá trị mặc định, có thể ghi đè.templates/: các file YAML dạng template (dùng cú pháp Go template), nội suy giá trị từvalues.
Khi bạn cài chart, Helm render template + values thành manifest cụ thể rồi apply lên cluster. Kết quả là một release — một lần cài đặt có tên, có lịch sử phiên bản. Đây là điểm mạnh nhất: Helm lưu lại từng revision, nên nâng cấp (helm upgrade) và rollback (helm rollback) trở nên tầm thường.
# templates/deployment.yaml — (minh hoạ) một phần chart
spec:
replicas: {{ .Values.replicaCount }}
template:
spec:
containers:
- name: app
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
# (minh hoạ) — không chạy trong sandbox
helm install my-api ./my-api-chart -n prod # cài lần đầu → tạo release "my-api"
helm upgrade my-api ./my-api-chart --set image.tag=1.4.2 # nâng cấp, ghi đè 1 giá trị
helm history my-api -n prod # xem lịch sử các revision
helm rollback my-api 3 -n prod # quay về revision 3 nếu 1.4.2 lỗi
helm repo add bitnami https://charts.bitnami.com/bitnami # dùng chart cộng đồng
Giá trị thực tế của Helm: bạn giữ một chart chung, và mỗi môi trường chỉ khác nhau ở file values-prod.yaml / values-staging.yaml. App phức tạp (có phụ thuộc như Redis, PostgreSQL) được khai báo dependency trong Chart.yaml và Helm kéo về giúp. Có công cụ khác cùng mục tiêu (Kustomize — cách "patch" YAML không dùng template; jsonnet...), nhưng Helm là chuẩn de-facto phổ biến nhất.
Resource requests & limits: nền tảng của scheduling ổn định
Đây là phần bị bỏ quên nhiều nhất và gây đau đớn nhiều nhất. Mỗi container nên khai báo:
- requests: lượng CPU/memory container cần đảm bảo. Scheduler dùng con số này để quyết định đặt Pod lên node nào — nó chỉ đặt Pod lên node còn đủ tổng requests. Đặt requests quá cao → lãng phí (node "đầy" dù thực tế nhàn); quá thấp → Pod bị dồn lên node và tranh chấp.
- limits: trần tuyệt đối container được dùng.
Điểm mấu chốt là CPU và memory bị chặn theo hai cơ chế hoàn toàn khác nhau:
- CPU limit là giới hạn có thể nén: khi vượt, kernel throttle (làm chậm) container chứ không giết. App vẫn sống nhưng chậm.
- Memory limit là giới hạn không thể nén: khi container xin vượt trần, kernel OOMKilled — giết tiến trình. Bạn sẽ thấy
OOMKilledtrong lý do restart. Đây là lý do việc đặt memory limit đúng cực kỳ quan trọng.
# (minh hoạ)
resources:
requests:
cpu: "250m" # 0.25 core được đảm bảo cho scheduling
memory: "256Mi"
limits:
cpu: "500m" # vượt → bị throttle (chậm), không chết
memory: "512Mi" # vượt → OOMKilled
QoS class: ai bị đuổi trước khi node cạn tài nguyên
Từ requests/limits, Kubernetes tự gán QoS class cho Pod, quyết định thứ tự bị evict khi node thiếu tài nguyên:
- Guaranteed: mọi container có requests = limits cho cả CPU và memory. Được bảo vệ tốt nhất, bị đuổi sau cùng.
- Burstable: có ít nhất một request được đặt, nhưng không thoả điều kiện Guaranteed. Được "bung" vượt requests khi node còn dư.
- BestEffort: không container nào đặt request/limit. Ưu tiên thấp nhất, bị đuổi đầu tiên khi node bị áp lực.
Bài học production: workload quan trọng nên nhắm tới Guaranteed (hoặc chí ít là Burstable với requests hợp lý). Đừng để service quan trọng chạy BestEffort.
Health probes: để cluster tự chữa lành
Kubernetes không đọc được suy nghĩ của app — bạn phải nói cho nó biết app khoẻ hay không qua ba loại probe, mỗi loại một vai trò khác nhau:
- liveness probe: "App có còn sống không?" Nếu fail → kubelet restart container. Dùng để thoát khỏi deadlock/treo.
- readiness probe: "App đã sẵn sàng nhận traffic chưa?" Nếu fail → Pod bị gỡ khỏi endpoints của Service (không nhận traffic) nhưng không bị restart. Đây là cơ chế gate traffic: trong lúc app đang khởi động / warm cache / mất kết nối DB tạm thời, nó ngừng nhận request thay vì trả lỗi cho người dùng.
- startup probe: "App khởi động xong chưa?" Dùng cho app khởi động chậm. Trong lúc startup probe chưa pass, liveness/readiness bị hoãn — tránh việc liveness giết nhầm một app đang boot lâu.
# (minh hoạ)
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet: { path: /ready, port: 8080 }
periodSeconds: 5
startupProbe:
httpGet: { path: /healthz, port: 8080 }
failureThreshold: 30 # cho tối đa 30 × 10s = 5 phút để boot
periodSeconds: 10
Sai lầm kinh điển: chỉ dùng liveness và trỏ nó vào endpoint kiểm tra DB. DB chậm một nhịp → liveness fail → restart → càng tải DB → restart hàng loạt. Endpoint liveness nên nhẹ và chỉ kiểm tra bản thân tiến trình; việc "phụ thuộc chưa sẵn sàng" thuộc về readiness.
Autoscaling: co giãn theo tải
Ba cơ chế bổ sung cho nhau:
-
HPA (Horizontal Pod Autoscaler): thêm/bớt số Pod theo metric (CPU utilization, memory, hoặc custom/external metric). Công thức lõi (theo tài liệu Kubernetes):
desiredReplicas = ceil[ currentReplicas × (currentMetric / desiredMetric) ]Ví dụ đang 4 Pod, CPU trung bình 160% so với mục tiêu 80% → cần
ceil(4 × 2) = 8Pod. HPA bỏ qua nếu tỉ lệ nằm trong dung sai mặc định 10%. HPA cần Metrics Server để lấy metric CPU/memory và yêu cầu Pod đã đặt resource requests (vì utilization tính theo requests). Lưu ý: HPA không áp dụng cho DaemonSet (thứ không scale được). -
VPA (Vertical Pod Autoscaler): điều chỉnh requests/limits của Pod (co giãn "theo chiều dọc"). Hữu ích để tìm đúng con số tài nguyên. Thường không dùng chung một metric CPU với HPA để tránh xung đột.
-
Cluster Autoscaler: khi Pod ở trạng thái
Pendingvì không node nào đủ chỗ, nó thêm node vào cluster (và gỡ node rảnh để tiết kiệm). Đây là mảnh ghép cuối: HPA tạo thêm Pod, nhưng nếu cluster hết chỗ, Cluster Autoscaler mới là thứ mở rộng hạ tầng.
# (minh hoạ) HPA co giãn 2→10 Pod, mục tiêu CPU 70%
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: my-api }
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: my-api }
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 70 }
Chiến lược triển khai
Deployment mặc định dùng rolling update: thay Pod cũ bằng Pod mới từng phần, không downtime (khi kết hợp readiness probe đúng). Hai chiến lược nâng cao thường cần công cụ bổ trợ:
- Blue-green: chạy song song hai môi trường (blue = hiện tại, green = mới), chuyển toàn bộ traffic sang green một phát khi đã kiểm tra xong; rollback = trỏ ngược lại blue.
- Canary: đẩy phiên bản mới cho một tỉ lệ nhỏ traffic (5%), quan sát metric, tăng dần nếu ổn.
Kubernetes thuần hỗ trợ rolling tốt; blue-green và canary tinh vi thường dùng thêm Argo Rollouts, Flagger, hoặc service mesh. Đây chỉ là phần nhắc đến — chọn công cụ theo nhu cầu.
RBAC: least privilege trong cluster
RBAC (Role-Based Access Control) trả lời câu hỏi "ai được làm gì với tài nguyên nào":
- Role: tập quyền (verbs như get/list/create/delete trên resources) trong một namespace.
- ClusterRole: tương tự nhưng phạm vi toàn cluster (hoặc cho resource cluster-scoped như nodes).
- RoleBinding / ClusterRoleBinding: gán Role/ClusterRole cho một subject — có thể là user, group, hoặc ServiceAccount.
- ServiceAccount: danh tính mà Pod dùng để gọi API server. Mỗi app nên có ServiceAccount riêng với đúng quyền cần thiết.
# (minh hoạ) chỉ cho phép đọc Pod trong namespace prod
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { namespace: prod, name: pod-reader }
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { namespace: prod, name: read-pods }
subjects:
- kind: ServiceAccount
name: my-api-sa
namespace: prod
roleRef: { kind: Role, name: pod-reader, apiGroup: rbac.authorization.k8s.io }
Nguyên tắc xuyên suốt là least privilege: cấp quyền tối thiểu đủ dùng. Đừng dùng ServiceAccount default, đừng gán cluster-admin cho app.
Bảo mật: siết bề mặt tấn công
Vài lớp phòng thủ cần có ở production:
- Pod Security Standards — ba profile chuẩn: Privileged (không giới hạn, chỉ cho workload hạ tầng tin cậy), Baseline (chặn các leo thang đặc quyền đã biết — cấm privileged container, cấm dùng host namespace), Restricted (siết mạnh theo best practice: bắt buộc chạy non-root, không cho privilege escalation, giới hạn loại volume). Áp Restricted cho ứng dụng thông thường.
- Không chạy root: đặt
runAsNonRoot: true,readOnlyRootFilesystem: true, bỏ mọi capability không cần trongsecurityContext. - NetworkPolicy: mặc định Pod nói chuyện với nhau tự do; NetworkPolicy khai báo ai được kết nối tới ai (deny-by-default rồi mở đúng luồng cần). Cần CNI hỗ trợ.
- Secret encryption at rest: Secret mặc định chỉ base64 (không phải mã hoá). Bật encryption-at-rest cho etcd, hoặc dùng KMS / external secret manager.
- Image scanning: quét image tìm CVE trong CI trước khi deploy, và dùng tag cố định (digest) thay vì
latest.
# (minh hoạ) securityContext siết chặt
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
Observability: biết chuyện gì đang xảy ra
Vận hành mù là vận hành chờ sự cố. Ba trụ cột (xem sâu ở DevOps — Observability):
- Metrics: chuẩn de-facto là Prometheus thu thập (scrape) số liệu + Grafana trực quan hoá. Đo tài nguyên node/Pod, latency, error rate, tình trạng HPA...
- Logs tập trung: gom log từ mọi Pod về một chỗ (ví dụ Loki, hoặc Elasticsearch/OpenSearch). Pod là ephemeral — Pod chết là log cục bộ mất; phải gom trước khi mất.
- Tracing (distributed tracing): theo dấu một request đi qua nhiều service (OpenTelemetry, Jaeger). Cần khi hệ phân tán và bạn hỏi "chậm ở đâu?".
Công cụ nhanh tại chỗ với kubectl:
# (minh hoạ)
kubectl top nodes # CPU/memory theo node (cần Metrics Server)
kubectl top pods -n prod # theo Pod
kubectl get events -n prod --sort-by=.lastTimestamp # sự kiện gần đây: OOMKilled, FailedScheduling...
kubectl describe pod my-api-xxxx -n prod # lý do restart, probe fail, events của Pod
kubectl get events là bạn thân khi debug: nó cho biết vì sao Pod Pending (thiếu tài nguyên?), vì sao restart (OOMKilled? probe fail?). Cuối cùng, alerting (Alertmanager / cảnh báo trong Grafana) mới là thứ đánh thức bạn đúng lúc — đặt cảnh báo trên error rate, latency p99, Pod restart liên tục, đĩa/etcd sắp đầy.
GitOps, namespace/quota và PodDisruptionBudget
Vài mảnh ghép production nữa, nêu ngắn:
- GitOps (Argo CD, Flux — nhắc đến): Git là nguồn chân lý duy nhất cho trạng thái cluster; một agent liên tục đồng bộ cluster về đúng như khai báo trong repo. Mọi thay đổi đi qua pull request — có review, có audit, rollback = git revert.
- Namespace & ResourceQuota: chia cluster theo team/môi trường bằng namespace, đặt ResourceQuota để một namespace không "ăn" hết tài nguyên cluster, và LimitRange để áp mặc định requests/limits.
- PodDisruptionBudget (PDB): đảm bảo trong lúc voluntary disruption (nâng cấp node, drain), luôn còn tối thiểu số Pod sẵn sàng (ví dụ
minAvailable: 2). Không có PDB, một lệnh drain node có thể tình cờ hạ toàn bộ replica của một service cùng lúc.
Use case thực tế: đưa một service lên K8s production
Giả sử bạn có my-api (đã hiểu kiến trúc cluster ở K8s — Kiến trúc) và cần đưa lên production đàng hoàng. Trình tự thực dụng:
- Đóng gói bằng Helm: một chart với
values-prod.yamlriêng. Deploy quahelm upgrade --installtrong pipeline. - Khai báo tài nguyên: đặt requests/limits cho mọi container. Nhắm QoS Burstable/Guaranteed cho service quan trọng — không để nó BestEffort.
- Health probes: readiness gate traffic (chỉ nhận request khi DB đã kết nối), liveness nhẹ (tránh restart dây chuyền), startup nếu app boot chậm.
- HPA:
minReplicas: 3(chịu mất 1 Pod vẫn ổn),maxReplicastheo ngân sách, mục tiêu CPU 70%. Kèm Cluster Autoscaler ở tầng node. - PDB
minAvailable: 2để nâng cấp node không gây gián đoạn. - RBAC: ServiceAccount riêng, chỉ đúng quyền cần;
securityContextnon-root, drop capabilities; áp Pod Security Restricted; NetworkPolicy chỉ mở luồng tới DB và ingress. - Giám sát: Prometheus scrape
/metrics, dashboard Grafana (latency, error rate, số Pod), log gom về Loki, alert trên p99 latency và tỉ lệ 5xx. - Triển khai: rolling update mặc định; nếu rủi ro cao, canary bằng Argo Rollouts.
Kết quả: service tự co giãn theo tải, tự khởi động lại khi treo, gỡ khỏi load balancer khi chưa sẵn sàng, không bị hạ oan khi bảo trì node, và bạn nhìn thấy nó qua dashboard + được đánh thức khi có sự cố. Đó là "production-ready" thật sự.
Ghi nhớ
- Helm đóng gói app thành chart (Chart.yaml + values + templates) → cài thành release có lịch sử;
upgrade/rollbacklà siêu năng lực của nó. - requests ảnh hưởng scheduling; limits giới hạn runtime. CPU vượt → throttle; memory vượt → OOMKilled.
- QoS: Guaranteed (requests=limits mọi container) > Burstable > BestEffort (không đặt gì, bị evict trước).
- Probes: liveness → restart; readiness → gate traffic (không restart); startup → cho app boot chậm. Đừng để liveness phụ thuộc DB.
- Autoscaling: HPA thêm/bớt Pod theo metric (cần requests + Metrics Server); VPA chỉnh requests; Cluster Autoscaler thêm node khi Pod Pending.
- RBAC = Role/ClusterRole + Binding gán cho ServiceAccount, theo least privilege. Đừng dùng SA
defaulthaycluster-admin. - Bảo mật: Pod Security Restricted, non-root, NetworkPolicy deny-by-default, mã hoá Secret at-rest, quét image.
- Observability: metrics (Prometheus + Grafana), log tập trung, tracing,
kubectl top/events, và alerting. - Đừng quên namespace + ResourceQuota, PodDisruptionBudget, và GitOps (Argo CD/Flux) để vận hành có kỷ luật.
Đây là bài khép lại series Docker & Kubernetes chuyên sâu. Từ một docker-compose.yml trên một máy, bạn đã đi tới một hệ thống tự co giãn, tự chữa lành, có kiểm soát truy cập và quan sát được — sẵn sàng cho production.
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.
Container vs máy ảo, image/layer, Dockerfile, volume, network, Docker Compose và best practices.
Vì sao cần orchestration; Pod, Deployment, Service, Ingress; scaling, self-healing và cấu hình.
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á.