KUBERNETES MASTERCLASS CHO DEVELOPER: TỪ CƠ BẢN ĐẾN THỰC CHIẾN CHUYÊN SÂU
MỤC LỤC
PHẦN 1: KIẾN TRÚC ỨNG DỤNG NÂNG CAO TRÊN K8S (Developer Perspective) 1.1. Pod Lifecycle & Graceful Shutdown (Kinh nghiệm xương máu) 1.2. Init Containers & Sidecar Pattern (Use-case thực tế) 1.3. Nắm trọn Probes: Liveness, Readiness, Startup (Đừng để K8s giết app của bạn) 1.4. Quản lý Resource (Requests/Limits) & Auto-scaling (HPA, VPA, KEDA)
PHẦN 2: HELM & KUSTOMIZE MASTERCLASS (Templating System) 2.1. Tại sao Developer cần Helm? Tư duy đóng gói ứng dụng. 2.2. Cấu trúc Helm Chart chuyên sâu & Best Practices. 2.3. Helm Templating Engine: Control Structures, Functions, Pipelines. 2.4. Quản lý Dependencies & Subcharts. 2.5. Kustomize: Khi nào dùng Kustomize, khi nào dùng Helm? Kết hợp cả hai.
PHẦN 3: ADVANCED DEPLOYMENT STRATEGIES (Không downtime) 3.1. Rolling Update: Nắm rõ maxSurge và maxUnavailable. 3.2. Blue-Green Deployment (Thực hành với Native K8s và Ingress). 3.3. Canary Release với Argo Rollouts (Phân tích traffic).
PHẦN 4: CONFIGURATION & SECRETS MANAGEMENT CHO DEV 4.1. ConfigMap nâng cao: Checksum annotation để auto-reload Pod. 4.2. Secrets: Vấn đề mã hóa và giải pháp External Secrets Operator (AWS SM, HashiCorp Vault). 4.3. Quản lý biến môi trường động cho nhiều môi trường (Dev, Staging, Prod).
PHẦN 5: NETWORKING & TRAFFIC ROUTING CHO MICROSERVICES 5.1. Dịch vụ (Service): ClusterIP, NodePort, LoadBalancer (Hiểu đúng luồng đi của gói tin). 5.2. Ingress & Ingress Controller (Nginx, Traefik). 5.3. Giới thiệu Gateway API (Tương lai của K8s Networking). 5.4. Service Mesh (Istio/Linkerd) dưới góc nhìn của Developer (Circuit Breaker, Retries, Mutual TLS).
PHẦN 6: STATEFUL WORKLOADS & STORAGE CHO DEV 6.1. Khi nào Developer cần StatefulSet? (Database, Cache, Message Queue). 6.2. Persistent Volumes (PV), Persistent Volume Claims (PVC) & Storage Classes. 6.3. Ephemeral Storage và EmptyDir (Use case share data giữa các containers).
PHẦN 7: OBSERVABILITY & DEBUGGING (Bí kíp sinh tồn) 7.1. Đọc Logs nâng cao và Centralized Logging (ELK/EFK, Loki). 7.2. Distributed Tracing cho Microservices (Jaeger, OpenTelemetry). 7.3. Kỹ thuật Debugging thực chiến: Ephemeral Containers (kubectl debug), Port-forwarding. 7.4. Xử lý các lỗi kinh điển: CrashLoopBackOff, OOMKilled, ImagePullBackOff, 502/504 Bad Gateway.
PHẦN 8: GITOPS & CI/CD TÍCH HỢP 8.1. Tư duy GitOps với ArgoCD / FluxCD. 8.2. CI/CD Pipeline (GitLab CI / GitHub Actions) build và push ảnh, trigger ArgoCD.
NỘI DUNG CHI TIẾT (PHẦN 1 & 2)
PHẦN 1: KIẾN TRÚC ỨNG DỤNG NÂNG CAO TRÊN K8S (Developer Perspective)
Rất nhiều Developer nghĩ rằng: "Tôi chỉ cần viết code chạy được trên máy tôi (hoặc Docker), ném cho DevOps viết file YAML là xong". Sai lầm! K8s là một môi trường phân tán (distributed environment) cực kỳ khắc nghiệt. Pod của bạn có thể bị kill bất cứ lúc nào (Node scale down, OOM, Evicted). Nếu bạn code ứng dụng không có tư duy "Cloud-Native", hệ thống của bạn sẽ vỡ nát khi lên Production.
1.1. Pod Lifecycle & Graceful Shutdown (Kinh nghiệm xương máu)
Vấn đề: Bạn đang deploy một bản update mới. K8s bắt đầu quá trình Rolling Update. Nó gửi tín hiệu tắt Pod cũ. Lúc này, Pod cũ đang xử lý dở 50 request thanh toán của khách hàng. Pod bị tắt cái rụp! Giao dịch lỗi, data in-consistent, khách hàng chửi rủa.
Nguyên nhân gốc rễ: K8s tắt Pod bằng cách gửi tín hiệu SIGTERM (Signal Terminate) tới tiến trình PID 1 trong container của bạn. Mặc định, nó cho bạn 30 giây (terminationGracePeriodSeconds). Sau 30 giây, nếu ứng dụng chưa tắt, nó gửi tiếp SIGKILL (giết ngay lập tức). Nhiều framework (như Spring Boot đời cũ, Node.js mặc định, Go không bắt signal) KHÔNG tự động xử lý SIGTERM. Chúng nhận SIGTERM và tắt ngay lập tức, cắt đứt mọi kết nối đang xử lý dở.
Giải pháp của Developer (Graceful Shutdown): Bạn phải code ứng dụng để khi nhận được SIGTERM, nó thực hiện 3 bước:
- Dừng nhận request mới (Báo cho Ingress/Service biết tao sắp chết).
- Hoàn thành nốt các request đang xử lý dở (Drain requests).
- Đóng kết nối Database an toàn.
- Tự thoát với exit code 0.
Usecase thực tế: Hook preStop Đôi khi, ứng dụng của bạn là một legacy code (code cũ), bạn không thể sửa source code để bắt SIGTERM. Lúc này, K8s cung cấp cho Developer một vũ khí: preStop hook.
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
spec:
replicas: 3
selector:
matchLabels:
app: payment
template:
metadata:
labels:
app: payment
spec:
terminationGracePeriodSeconds: 60 # Tăng thời gian chờ lên 60s
containers:
- name: payment-api
image: my-company/payment:v2
lifecycle:
preStop:
exec:
# K8s sẽ chạy lệnh này TRƯỚC KHI gửi SIGTERM
# Sleep 15s để Service/Ingress kịp cập nhật Endpoint,
# không route traffic mới vào Pod này nữa.
command: ["/bin/sh", "-c", "sleep 15"]Phân tích chuyên sâu: Tại sao lại phải sleep 15? Khi K8s tắt Pod, nó làm 2 việc song song:
- Gửi lệnh xóa Pod khỏi Endpoints của Service (báo mạng ngừng gửi traffic). Việc này có độ trễ (do phải propagate qua kube-proxy/Ingress).
- Gửi
SIGTERMvào Pod. Nếu Pod tắt quá nhanh, trong khi Ingress chưa kịp cập nhật, Ingress vẫn gửi traffic vào Pod đã chết -> Lỗi 502 Bad Gateway. Lệnhsleep 15câu giờ để K8s kịp update Network Topology! Đây là câu hỏi tôi thường xuyên dùng để đánh trượt ứng viên Senior.
1.2. Nắm trọn Probes: Liveness, Readiness, Startup (Đừng để K8s giết app của bạn)
K8s không biết ứng dụng bên trong container của bạn chạy web (NodeJS), AI model (Python) hay worker (Go). Nó chỉ biết container đang chạy (Running). Nhưng Running không có nghĩa là "Sẵn sàng phục vụ" (Ready).
A. Readiness Probe (Khám điền thổ - Sẵn sàng nhận khách chưa?)
- Mục đích: K8s hỏi ứng dụng: "Mày đã khởi tạo xong Database connection, load xong cache chưa? Tao gửi request của khách vào nhé?"
- Nếu Fail: K8s KHÔNG giết Pod. K8s chỉ gỡ IP của Pod này ra khỏi Service. Traffic không vào đây nữa.
- Use case cho Dev: Viết một API
/health/ready. Trả về 200 OK nếu app có thể connect DB. Trả về 503 nếu mất kết nối DB.
B. Liveness Probe (Khám nhịp tim - Còn sống hay đã chết lâm sàng?)
- Mục đích: K8s hỏi: "App có đang bị deadlock không? Có bị OutOfMemory treo vòng lặp vô hạn không?"
- Nếu Fail: K8s GIẾT Pod (Restart lại container).
- Sai lầm tử huyệt của Dev: Rất nhiều bạn setup Liveness Probe gọi chung vào hàm check Database! Điều gì xảy ra khi Database bị chậm (slow query)? -> Hàm check DB timeout. -> K8s tưởng app chết (Liveness fail). -> K8s restart Pod. -> Hàng chục Pod bị restart cùng lúc vì DB chậm. -> Hệ thống sập toàn tập (Cascading Failure).
- Best Practice: Liveness Probe CHỈ NÊN trả về 200 OK một cách đơn giản nhất (ví dụ: trả về string "pong"). Chỉ check xem thread HTTP server còn phản hồi hay không, TUYỆT ĐỐI không check external dependencies (DB, Redis) trong Liveness.
C. Startup Probe (Bảo vệ các cụ già khởi động chậm)
- Use case: Ứng dụng Java Spring Boot Monolithic khổng lồ, mất 3 phút mới khởi động xong. Nếu bạn để Liveness check mỗi 10s, nó sẽ báo lỗi và kill Pod trước cả khi Spring Boot kịp khởi động!
- Cách hoạt động: Startup Probe chạy trước. Khi nào Startup Probe pass, Liveness và Readiness mới được phép chạy.
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
startupProbe:
httpGet:
path: /health/start
port: 8080
failureThreshold: 30
periodSeconds: 10
# Tổng thời gian cho phép khởi động = 30 * 10 = 300 giây (5 phút)1.3. Init Containers & Sidecar Pattern
Đây là lúc Developer thể hiện đẳng cấp thiết kế hệ thống. K8s Pod không chỉ chứa 1 container, nó có thể chứa nhiều container chạy chung mạng (localhost), chung ổ cứng (Volume).
A. Init Container (Người mở đường) Chạy TRƯỚC container chính. Phải chạy xong (exit 0) thì container chính mới được khởi động.
- Use case: Ứng dụng web cần connect vào DB. Nếu DB chưa chạy, app sẽ crash. Bạn dùng Init Container để
pinghoặcnc -ztới DB, bao giờ DB mở port thì Init Container mới exit 0, nhường chỗ cho Web App chạy. - Use case: Run database migrations (Flyway, Liquibase). Bạn không nên gọi script migration ở code app (vì nếu có 5 Pod scale lên, 5 app cùng chạy migration sẽ gây lock DB). Hãy dùng 1 Kubernetes
JobhoặcInit Container(có lock cơ chế) để chạy.
B. Sidecar Pattern (Người phụ tá) Chạy SONG SONG với container chính.
- Use case 1 (Logging): Container chính (Nginx) ghi log ra file
/var/log/nginx/access.log. Sidecar container (Fluentbit) mount chung thư mục này, đọc log và đẩy lên Elasticsearch. Nginx không cần biết Elasticsearch là gì. Tách biệt trách nhiệm (Separation of Concerns). - Use case 2 (Proxy/Mesh): Bạn gọi API
http://payment-service. Thực chất request chui qua một container Proxy (Envoy) nằm chung Pod (Sidecar). Envoy lo việc mTLS (mã hóa), retry, circuit breaker. Code của bạn hoàn toàn sạch sẽ, không cần thư viện retry phức tạp.
PHẦN 2: HELM & KUSTOMIZE MASTERCLASS (Dành cho Developer)
Khi mới học K8s, bạn dùng file YAML thuần. Để deploy lên Dev, bạn viết 1 bộ YAML. Lên Staging, bạn copy paste bộ YAML đó ra thư mục khác, đổi biến môi trường, đổi số Replicas. Lên Prod, bạn lại copy. Đây là "copy-paste driven development", cực kỳ dễ sai sót và khó bảo trì.
Đó là lúc ta cần công cụ Templating. Hai vị vua trong mảng này là Helm và Kustomize.
2.1. Tại sao Developer cần Helm?
Helm được ví như apt, yum hoặc npm của Kubernetes. Helm cho phép bạn đóng gói hàng chục file YAML của một microservice thành một bản mẫu (Template). Bạn truyền các giá trị thay đổi (Values) vào mẫu này để tạo ra YAML cuối cùng (Manifest) và đẩy lên K8s.
Một Helm Chart tiêu chuẩn có cấu trúc:
my-microservice/
├── Chart.yaml # Khai báo tên chart, version
├── values.yaml # Các giá trị mặc định (cho Dev)
├── values-prod.yaml # Các giá trị override cho Prod
├── charts/ # Chứa các chart phụ thuộc (Redis, DB...)
└── templates/ # Nơi chứa các file YAML đã được template hóa
├── deployment.yaml
├── service.yaml
├── ingress.yaml
└── _helpers.tpl # Chứa các hàm/biến dùng chung2.2. Helm Templating Engine Chuyên Sâu (The Hardcore Stuff)
Helm sử dụng ngôn ngữ Go Template. Đây là thứ khiến nhiều Dev ghét Helm vì cú pháp kỳ quặc (ngoặc nhọn { { } }). Nhưng nếu bạn hiểu nó, nó rất mạnh.
A. Pipeline và Function cơ bản Trong file values.yaml:
image:
repository: "my-registry/my-app"
tag: "1.0.0"Trong deployment.yaml:
image: "{ { .Values.image.repository } }:{ { .Values.image.tag | default "latest" } }"Giải thích: Dấu . ở đầu đại diện cho Context hiện tại (Root). Dấu | là pipeline (giống Linux pipe). default "latest" là một function: nếu user không truyền tag, nó sẽ dùng "latest".
B. Control Structures (If/Else và Vòng lặp Range)
Use case: Bạn chỉ muốn tạo Ingress ở môi trường Prod, không tạo ở Dev.
# templates/ingress.yaml
{ {- if .Values.ingress.enabled } }
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: { { include "my-chart.fullname" . } }
# ... cấu hình ingress ...
{ {- else } }
# Ingress bị disable môi trường này.
{ {- end } }Lưu ý từ chuyên gia: Bạn có thấy dấu gạch ngang { {- không? Nó gọi là "White-space champing". Go Template sinh ra rất nhiều dòng trống thừa nếu bạn dùng { { } } thường, khiến file YAML bị lỗi cú pháp. Dấu - sẽ gọt bỏ mọi dấu cách/xuống dòng ở bên trái hoặc phải của block template. Đây là lỗi YAML parsing mà 90% người mới học Helm gặp phải!
Use case: Inject động các biến môi trường từ values.yaml vào Pod.
# values.yaml
envVars:
LOG_LEVEL: "debug"
DB_HOST: "mysql.db.svc.cluster.local"
API_TIMEOUT: "30"# deployment.yaml
env:
{ {- range $key, $val := .Values.envVars } }
- name: { { $key } }
value: { { $val | quote } }
{ {- end } }Hàm quote là cực kỳ quan trọng. Nếu $val là số 30, YAML có thể hiểu lầm kiểu dữ liệu. quote ép nó thành chuỗi "30", tránh lỗi K8s API schema validation.
C. Sức mạnh của _helpers.tpl và hàm include Bạn không muốn lặp đi lặp lại việc định nghĩa Label cho mọi resource (Deployment, Service, Ingress...). Bạn định nghĩa nó 1 lần ở _helpers.tpl.
{ {/*
Tạo các labels dùng chung.
*/} }
{ {- define "my-chart.labels" -} }
app.kubernetes.io/name: { { .Chart.Name } }
app.kubernetes.io/instance: { { .Release.Name } }
app.kubernetes.io/version: { { .Chart.AppVersion | quote } }
app.kubernetes.io/managed-by: { { .Release.Service } }
{ {- end -} }Ở deployment.yaml, bạn gọi lại nó:
metadata:
name: my-app
labels:
{ {- include "my-chart.labels" . | nindent 4 } }Phân tích: nindent 4 là gì? Khi đoạn template my-chart.labels được render, nó sẽ canh lề sát mép trái. Nếu nhét thẳng vào dưới chữ labels:, nó sẽ sai thụt lề (indentation) của YAML. nindent 4 ép chuỗi kết quả lùi vào 4 space (và thêm 1 dòng trống ở đầu), đảm bảo YAML syntax hợp lệ. Nghệ thuật viết Helm Chart nằm ở việc kiểm soát thụt lề!
2.3. Helm Hooks: Xử lý vòng đời Deployment
Bạn cần chạy Database Migration (Tạo bảng) TRƯỚC KHI bản code mới của ứng dụng được Deploy. Bạn làm thế nào với Helm? Helm Hooks!
Bạn viết một Job K8s, nhưng thêm Annotation đặc biệt của Helm:
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration-{ { .Release.Revision } }
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": hook-succeeded
spec:
template:
spec:
containers:
- name: migration
image: my-app-migration:v2
command: ["npm", "run", "db:migrate"]
restartPolicy: Neverpre-install,pre-upgrade: Helm sẽ deploy Job này, đợi nó chạy xong (Completed) rồi mới bắt đầu apply Deployment của ứng dụng.hook-delete-policy: hook-succeeded: Khi Job chạy xong thành công, Helm sẽ tự động xóa Job đi cho sạch rác. Nếu fail, nó giữ lại để bạn đọc logs (kubectl logs).
2.4. Kustomize: Kẻ đối đầu hay bạn đồng hành?
Nếu Helm là "Template Engine" (giống render HTML), thì Kustomize là "Overlay Engine" (giống đắp các layer lên nhau). Kustomize đã được tích hợp sẵn vào trong kubectl (lệnh kubectl apply -k).
Triết lý của Kustomize: Không có { { } } lằng nhằng. Bạn có một bộ YAML gốc chuẩn (Base). Với mỗi môi trường (Overlay), bạn định nghĩa các file thay đổi (patch).
kustomize-app/
├── base/
│ ├── kustomization.yaml
│ ├── deployment.yaml
│ └── service.yaml
└── overlays/
├── dev/
│ ├── kustomization.yaml
│ └── patch-replicas.yaml
└── prod/
├── kustomization.yaml
└── patch-resources.yamlKhi bạn chạy kubectl apply -k overlays/prod, Kustomize sẽ lấy Base, đè patch-resources.yaml (tăng CPU, RAM) lên, và push lên cụm K8s.
Khi nào Dev nên dùng gì?
- Dùng Helm: Khi ứng dụng của bạn rất phức tạp, cần chia sẻ cho công ty khác, cài đặt bằng 1 lệnh, nhiều logic if/else động. (Ví dụ: Bạn viết ra Redis, Kafka).
- Dùng Kustomize: Khi bạn quản lý các microservice nội bộ của công ty. Kustomize giữ file YAML ở dạng thuần (dễ đọc hơn), ít học thuật (learning curve) hơn Helm template.
- Pro-Tip (Kết hợp cả 2): Rất nhiều công ty lớn dùng Helm để tạo template chung, sau đó dùng Kustomize để patch những thông số đặc thù của từng cụm (Cluster) mà file Helm values không lường trước được (Post-rendering). ArgoCD hỗ trợ việc này cực tốt.
PHẦN 3: ADVANCED DEPLOYMENT STRATEGIES (CHIẾN LƯỢC TRIỂN KHAI KHÔNG DOWNTIME)
Một trong những lời hứa hẹn lớn nhất của Kubernetes là "Zero Downtime Deployment" (Triển khai không gián đoạn). Nhưng sự thật phũ phàng là: Nếu Developer chỉ dùng kubectl apply -f deployment.yaml một cách mù quáng, hệ thống CHẮC CHẮN sẽ rớt vài chục đến vài trăm request trong lúc update.
Để đạt được Zero Downtime thực sự, Developer phải kết hợp nhuần nhuyễn giữa K8s Deployment Controller, Probes (đã học ở Phần 1), và chiến lược Routing.
3.1. Rolling Update: Bí ẩn đằng sau maxSurge và maxUnavailable
Đây là chiến lược mặc định của Kubernetes. K8s sẽ dần dần tắt Pod phiên bản cũ (v1) và tạo Pod phiên bản mới (v2). Thuật toán này được điều khiển bởi 2 tham số cực kỳ quan trọng trong spec của Deployment: maxSurge và maxUnavailable.
Nhiều Dev để mặc định, nhưng khi scale hệ thống lớn, bạn phải tính toán toán học cho 2 tham số này.
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-service
spec:
replicas: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25% # Hoặc số nguyên (vd: 3)
maxUnavailable: 25% # Hoặc số nguyên (vd: 2)Phân tích chuyên sâu (Toán học trong K8s): Giả sử bạn có replicas: 10.
maxSurge: 25%(Tối đa dôi dư): Nghĩa là trong quá trình update, tổng số Pod (cả cũ và mới) được phép tồn tại tối đa là10 + 25% = 12.5(làm tròn lên là 13 Pod). K8s được phép mượn thêm tài nguyên (CPU/RAM của Node) để spawn ra tối đa 3 Pod v2 ngay lập tức. Tâm sự chuyên gia: Nếu Cluster của bạn đang chạy sát trần (Full Resource), không còn chỗ trống trên các Worker Node để nhét thêm Pod, quá trình tạo Pod mới sẽ bị treo ở trạng tháiPending. Rolling Update sẽ bị kẹt mãi mãi! Lúc này, bạn PHẢI cấu hìnhmaxSurge: 0(không tạo dôi ra) và hy sinh một chút bằng cách cho phépmaxUnavailable.maxUnavailable: 25%(Tối đa sập tiệm): Nghĩa là trong quá trình update, số lượng Pod "Sẵn sàng phục vụ" (Ready) không bao giờ được phép tụt xuống dưới10 - 25% = 7.5(làm tròn xuống là 7 Pod). K8s được phép lập tức tắt (kill) tối đa 3 Pod v1 để nhường chỗ trống cho Pod v2. Tâm sự chuyên gia: Nếu bạn thiết kế ứng dụng có quá trình khởi động cực lâu (như Java Spring cần 2 phút để warmup cache), và bạn đểmaxUnavailablequá cao (vd 50%). Hệ thống của bạn tụt từ 10 Pod xuống còn 5 Pod. 5 Pod này phải gánh 100% traffic của khách hàng. Chúng sẽ bị quá tải (CPU Spike, OOM) và sập toàn tập. Hiệu ứng domino xảy ra.
Best Practice cho Developer:
- Ứng dụng Critical (Thanh toán, Core Banking): Đảm bảo tuyệt đối không rớt capacity. Cấu hình
maxUnavailable: 0vàmaxSurge: 20%. K8s sẽ phải tạo Pod mới, đợi Pod mới BÁO CÁO READY (thông qua Readiness Probe), thì nó mới rón rén tắt 1 Pod cũ. Quá trình deploy sẽ chậm hơn, nhưng cực kỳ an toàn. - Bắt buộc đi kèm Readiness Probe: Nếu bạn không viết Readiness Probe, K8s mặc định cứ container chạy (Running) là nó coi như sẵn sàng. Nó sẽ đẻ ra 10 Pod v2, tắt 10 Pod v1 trong chớp mắt. Nhưng Pod v2 chưa kết nối xong DB, trả về toàn lỗi 500. Downtime xảy ra! Rolling Update vô dụng nếu thiếu Readiness Probe.
3.2. Blue-Green Deployment (Tráo đổi danh tính)
Rolling Update có một điểm yếu chết người đối với Developer: Trạng thái hỗn loạn phiên bản (Version Inconsistency). Trong khoảng thời gian (có thể là vài phút) diễn ra Rolling Update, khách hàng A có thể được route vào Pod v1, khách hàng B lại được route vào Pod v2. Nếu v2 trả về cấu trúc JSON khác v1 (Front-end v1 không đọc được API v2), ứng dụng client (React/Vue/Mobile) sẽ bị crash ngẫu nhiên.
Blue-Green giải quyết triệt để chuyện này bằng cách: Môi trường cũ (Blue) giữ nguyên. Dựng một môi trường mới hoàn toàn (Green) song song. Đợi Green chạy ổn định 100%, gạt công tắc, toàn bộ 100% traffic chuyển từ Blue sang Green trong 1 phần nghìn giây.
Cách thực thi thuần túy (Native K8s) dưới góc nhìn Developer: Chúng ta lợi dụng cơ chế Label Selector của K8s Service. K8s Service không quan tâm Pod tên gì, nó chỉ gom traffic vào các Pod có Label khớp với nó.
Bước 1: Bạn đang có Deployment-v1 (Blue) chạy với label app: my-app, version: v1. Service đang trỏ vào version: v1.
# service.yaml hiện tại
apiVersion: v1
kind: Service
metadata:
name: my-app-svc
spec:
selector:
app: my-app
version: v1 # Đang trỏ vào BlueBước 2: Bạn viết thêm file deployment-v2.yaml (Green) với label app: my-app, version: v2. Chạy kubectl apply -f deployment-v2.yaml. Lúc này, v1 và v2 chạy song song. Nhưng khách hàng vẫn chỉ gọi vào v1 (vì Service vẫn đang trỏ v1). Nhóm QA/Tester của bạn sẽ port-forward trực tiếp vào Pod v2 để test ngầm trên Production.
Bước 3 (Gạt công tắc): Sửa file service.yaml, đổi chữ v1 thành v2.
# service.yaml (Cập nhật)
selector:
app: my-app
version: v2 # Tráo đổi sang Green!Chạy kubectl apply -f service.yaml. K8s sẽ update Endpoint, toàn bộ traffic lập tức dồn sang v2. v1 không bị xóa, nó nằm đó làm "Backup". Nếu v2 có bug nghiêm trọng, bạn chỉ cần sửa lại version: v1 trong Service, rollback ngay lập tức (Thời gian phục hồi < 1 giây).
Cảnh báo tử huyệt của Blue-Green: CƠ SỞ DỮ LIỆU (DATABASE) Hàng ngàn Developer đã khóc hận với Blue-Green vì quên mất Database. Bạn có v1 và v2 chạy song song. Bản v2 có chứa script Migration: Xóa cột user_name, tách thành 2 cột first_name và last_name. Lúc v2 vừa khởi động xong (chưa chuyển traffic), script DB đã chạy xong. Cột user_name bị xóa. Lúc này v1 (đang nhận 100% traffic) gửi lệnh SELECT user_name FROM users. Database báo lỗi "Column not found". Toàn bộ Production SẬP TỨC KHẮC!
Giải pháp cho Dev: Với Blue-Green, mọi thay đổi Database phải có tính Backward Compatible (Tương thích ngược) gồm 2 phase:
- Phase 1: Deploy v2. DB thêm cột
first_name,last_name, nhưng KHÔNG XÓAuser_name. Code v2 phải biết ghi song song vào cả cột cũ và mới. - Phase 2: Chạy vài ngày ổn định, code phase tiếp theo để drop hẳn cột
user_name. Làm Developer cho K8s là phải thay đổi hoàn toàn tư duy thiết kế luồng dữ liệu.
3.3. Canary Release với Argo Rollouts (Thả chim Yến vào hầm mỏ)
Thuật ngữ "Canary" bắt nguồn từ việc thợ mỏ xưa mang theo một con chim Yến xuống hầm mỏ. Nếu có khí độc, con chim sẽ chết trước, thợ mỏ biết đường chạy. Trong K8s, Canary Release nghĩa là: Thay vì chuyển 100% traffic, ta rẽ 5% traffic thực tế của khách hàng vào phiên bản v2. 95% vẫn dùng v1. Theo dõi hệ thống phân tích (Prometheus/Datadog), nếu 5% này tỷ lệ lỗi HTTP 500 tăng cao, hệ thống tự động cuộn lại (rollback). Nếu ổn, tăng từ từ lên 20% -> 50% -> 100%.
K8s Native (Deployment mặc định) KHÔNG LÀM ĐƯỢC Canary một cách tử tế theo phần trăm traffic. Bạn phải dùng Custom Resource. Phổ biến nhất và được yêu thích nhất hiện nay là Argo Rollouts.
Argo Rollouts cung cấp một resource tên là Rollout (thay thế gần như y hệt Deployment).
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: e-commerce-api
spec:
replicas: 10
selector:
matchLabels:
app: api
template:
# ... giống hệt Deployment template ...
strategy:
canary:
# Tích hợp với Ingress Controller (VD: NGINX) để chia traffic chính xác
trafficRouting:
nginx:
stableIngress: api-ingress-stable
steps:
- setWeight: 5 # Chuyển 5% traffic sang bản mới (Chim Yến)
- pause: {duration: 10m} # Dừng lại 10 phút để theo dõi
- setWeight: 20 # Tăng lên 20%
# Tự động phân tích logs/metrics thông qua Prometheus
- analysis:
templates:
- templateName: success-rate-check
- setWeight: 50
- pause: {} # Dừng VÔ THỜI HẠN, đợi con người (DevOps/QA) bấm nút PromoteSự kết hợp hoàn hảo giữa Developer và DevOps: Developer viết code, push lên Git. CI/CD tự đổi tag Image. ArgoCD/Flux apply file Rollout này lên. Hệ thống sẽ tự động tách 5% khách hàng (hoặc dựa trên HTTP Header, ví dụ chỉ những request có header X-Tester: true mới vào v2 - Dark Launch). Developer viết các câu truy vấn PromQL (Prometheus Query Language) trong AnalysisTemplate để K8s tự động đánh giá sức khỏe của ứng dụng. Ví dụ: "Nếu tỷ lệ lỗi API /checkout vượt quá 1%, lập tức Abort Rollout". Đây chính là đỉnh cao của tự động hóa vận hành phần mềm mà mọi tập đoàn lớn đang áp dụng.
PHẦN 4: CONFIGURATION & SECRETS MANAGEMENT CHO DEV (QUẢN TRỊ CẤU HÌNH BẢO MẬT)
Nguyên tắc thứ 3 trong "The Twelve-Factor App" (Ứng dụng 12 yếu tố) là: Lưu trữ cấu hình trong môi trường (Store config in the environment). Code của bạn (Docker Image) phải là duy nhất. Chạy Image đó ở Dev, Staging hay Prod chỉ khác nhau cái Config (URL Database, API Keys). Kubernetes quản lý việc này qua ConfigMap và Secret.
Nhưng cách sử dụng chúng ẩn chứa cực kỳ nhiều cạm bẫy đối với Developer.
4.1. ConfigMap Nâng Cao & Thảm họa Auto-Reload
Có 2 cách để đưa ConfigMap vào Pod: Variable (Biến môi trường) và Volume Mount (Gắn như một file).
Cách 1: Inject qua Environment Variables Phù hợp cho các cấu hình đơn giản, chuỗi ngắn.
env:
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: database_urlCách 2: Inject qua Volume Mounts Phù hợp khi ứng dụng của bạn cần đọc một file cấu hình phức tạp (VD: application.yml của Spring, nginx.conf, file JSON cấu hình rule).
volumes:
- name: config-volume
configMap:
name: app-config
containers:
- name: my-app
volumeMounts:
- name: config-volume
mountPath: /app/config/application.yml
subPath: application.ymlGhi chú chuyên gia: Chú ý tham số subPath. Nếu bạn không dùng subPath, K8s sẽ map TOÀN BỘ thư mục /app/config/ thành nội dung của ConfigMap. Mọi file gốc (nếu có) trong thư mục đó của Docker Image sẽ bị xóa trắng! Dùng subPath để K8s chỉ đè duy nhất 1 file được chỉ định, giữ nguyên các file khác trong thư mục.
Thảm họa "Auto-Reload" (Khi Developer chửi hệ thống): Tình huống: Dev sửa file ConfigMap, lưu lại trên K8s (kubectl apply). Chờ 10 phút, ứng dụng vẫn đọc cấu hình cũ! App không nhận URL Database mới. Tại sao?
- Nếu bạn truyền qua biến môi trường (
env): Biến môi trường chỉ được nạp vào RAM duy nhất 1 lần khi tiến trình (process) trong container khởi động (PID 1). Bạn đổi ConfigMap, biến môi trường của container đang chạy KHÔNG BAO GIỜ THAY ĐỔI. - Nếu bạn truyền qua Volume: Kubelet (chạy ngầm trên Node) quét thay đổi ConfigMap theo chu kỳ (mặc định khoảng 1-2 phút). File trong container sẽ được cập nhật. NHƯNG! Ứng dụng của bạn (VD: Node.js, Java) có chịu đọc lại file đó từ ổ cứng không? Đa số framework đọc file cấu hình 1 lần lúc start rồi lưu vào RAM. File đổi, RAM không đổi.
Giải pháp Thực chiến: Bạn CẦN phải Restart lại Pod để nó nhận ConfigMap mới. Nhưng bằng cách nào để tự động hóa (chứ không phải kubectl delete pod thủ công)?
Bí kíp Helm (Checksum Annotation): Developer dùng Helm có thể đặt một cái "bẫy" vào trong Deployment. Bạn tính mã băm (SHA256) của nội dung ConfigMap và gán vào một Annotation của Pod Template.
kind: Deployment
spec:
template:
metadata:
annotations:
# Mỗi khi nội dung configmap.yaml thay đổi, mã băm này sẽ thay đổi
checksum/config: { { include (print $.Template.BasePath "/configmap.yaml") . | sha256sum } }Cơ chế: Helm render file YAML -> Thay đổi nội dung ConfigMap -> Hàm sha256sum sinh ra chuỗi hash mới (vd: abc123...) -> Pod Template trong Deployment bị thay đổi (do annotation thay đổi) -> K8s Controller phát hiện Pod Template bị đổi -> TỰ ĐỘNG TRIGGER ROLLING UPDATE. Đây là "bài tủ" của dân làm Helm Chart chuyên nghiệp!
Công cụ bên thứ 3: Nếu không dùng Helm, bạn có thể cài một operator nhỏ tên là Reloader (của Stakater) vào Cluster. Chỉ cần thêm annotation reloader.stakater.com/auto: "true" vào Deployment, nó sẽ tự lùng sục mọi ConfigMap/Secret liên quan, thấy thay đổi là nó tự restart Pod giùm bạn.
4.2. K8s Secrets: Trò bịp bợm Base64 và Bí quyết Bảo mật thật sự
Đây là kiến thức phỏng vấn tôi hay hỏi nhất: "K8s Secret mã hóa dữ liệu như thế nào?" Nếu ứng viên trả lời: "Nó dùng Base64" -> Trượt ngay lập tức. Base64 KHÔNG PHẢI là mã hóa (Encryption). Nó chỉ là mã hóa định dạng (Encoding). Bất kỳ ai có lệnh echo "chuỗi-base64" | base64 --decode đều đọc được mật khẩu Database của bạn. K8s Secret mặc định lưu dạng text thô (plaintext) trong database nội bộ của K8s (etcd).
Là một Developer, bạn đang viết code. Bạn không được phép lưu mật khẩu Database, API Token của Stripe/AWS trực tiếp vào file secret.yaml và đẩy lên GitHub. Bị lộ code là mất sạch tiền. (Nhiều con bot rà quét GitHub, lộ AWS Key chỉ 5 phút sau là có hacker vào spawn máy đào coin bay ngay vài ngàn USD).
Giải pháp Tối thượng: External Secrets Operator (ESO) Đừng để K8s tự quản lý Secret. Hãy dùng các dịch vụ chuyên nghiệp như AWS Secrets Manager, Google Secret Manager, Azure Key Vault, hoặc HashiCorp Vault. K8s sẽ là kẻ "đi vay mượn" secret.
Developer chỉ cần đẩy lên Git một file định nghĩa (Manifest) vô hại như sau:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
spec:
refreshInterval: "1h"
secretStoreRef:
name: aws-secretsmanager-store # Trỏ tới cấu hình AWS
kind: ClusterSecretStore
target:
name: my-k8s-db-secret # Tên K8s Secret SẼ ĐƯỢC sinh ra
creationPolicy: Owner
data:
- secretKey: db-password # Key trong K8s Secret
remoteRef:
key: prod/ecommerce/database # Tên Secret thực tế trên hệ thống AWS
property: password # Key trong chuỗi JSON của AWSLuồng hoạt động (Workflow an toàn tuyệt đối):
- DevOps team tạo Secret trên AWS console (Bảo mật, phân quyền IAM chặt chẽ).
- Developer viết file
ExternalSecretnhư trên, lưu vào GitHub (file này không hề chứa mật khẩu, chỉ chứa ĐƯỜNG DẪN đến mật khẩu). - ESO chạy trong K8s, đọc file này, dùng quyền (Role) của Node để gọi lên AWS API.
- Kéo mật khẩu về và tự động vắt vào memory (tạo thành resource
Secretchuẩn của K8s). - Pod của Developer mount cái K8s Secret đó vào và dùng bình thường. Nếu mật khẩu trên AWS bị đổi, ESO sẽ tự động đồng bộ (refresh) sau mỗi 1 giờ. An toàn, chuẩn Compliance (ISO 27001/PCI-DSS).
PHẦN 5: NETWORKING & TRAFFIC ROUTING CHO MICROSERVICES (MÊ CUNG MẠNG)
Phần này là nơi Developer hay "bị hành" nhất. "Tại sao ở local gọi localhost được mà lên K8s gọi tên service lại timeout? Tại sao API bị lỗi CORS? Tại sao Nginx trả về 404?"
Hãy xóa bỏ tư duy IP tĩnh. Trong K8s, IP của Pod thay đổi liên tục (Mỗi lần deploy, mỗi lần crash, Pod sinh ra đều mang IP mới tinh của dải mạng ảo Flannel/Calico). Bạn KHÔNG BAO GIỜ kết nối vào IP của Pod. Bạn giao tiếp thông qua Service.
5.1. K8s Service: Cỗ máy định tuyến ngầm (The Load Balancer in the shadow)
Service là một khái niệm trừu tượng (Abstract), được hiện thực hóa bởi một thành phần chạy ngầm trên TẤT CẢ các Node gọi là kube-proxy. Mặc định, kube-proxy can thiệp sâu vào nhân Linux (iptables hoặc IPVS) để điều hướng các gói tin TCP/UDP ở tầng L4 (Transport Layer).
A. ClusterIP (Giao tiếp nội bộ - Internal) Dành cho các Microservice gọi nhau.
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
type: ClusterIP # Mặc định
ports:
- port: 80 # Port của Service (Người gọi sẽ dùng port này)
targetPort: 8080 # Port thực tế ứng dụng đang chạy bên trong Pod
selector:
app: orderDeveloper Usecase: Từ container của payment-service, bạn muốn gọi sang order, bạn chỉ cần gọi HTTP request tới URL: http://order-service:80. Hệ thống DNS nội bộ của K8s (CoreDNS) sẽ phân giải chữ order-service thành 1 IP ảo (VD: 10.96.0.10). Khi gói tin chạm vào IP ảo này, luật iptables trên Node sẽ lập tức "bóp méo" gói tin, đổi IP đích (DNAT) thành IP thật của 1 trong 3 Pod của order-service (VD: 192.168.1.5:8080) một cách ngẫu nhiên. Đây là cơ chế Client-side Load Balancing cấp thấp.
Kiến thức nâng cao: Endpoints Service tìm Pod thông qua selector. Khi tìm thấy, K8s tự động tạo ra một đối tượng đi kèm gọi là Endpoints. Nếu bạn tạo Service mà gõ sai tên Label ở phần selector, K8s vẫn báo "Service created successfully". Nhưng khi gọi thì timeout. Bài học Debugging cho Dev: Hãy luôn kiểm tra kubectl get endpoints order-service. Nếu mục ENDPOINTS trống trơn (không có IP nào), nghĩa là selector của bạn viết sai, hoặc toàn bộ Pod đang bị Crash/Readiness Probe Fail! Service có tồn tại cũng vô nghĩa.
B. NodePort (Cổng mở ra thế giới thực - Mức thô sơ) Nếu ClusterIP chỉ gọi được nội bộ, làm sao Frontend (trình duyệt của khách) truy cập được? NodePort mở một cổng cụ thể (bắt buộc trong dải 30000 - 32767) trên TẤT CẢ các máy chủ vật lý (Node) của Cluster. Nếu bạn cấu hình NodePort là 30080. Khách hàng gõ http://IP-Node-Bất-Kỳ:30080, traffic sẽ chui vào Service và ném cho Pod. Nhược điểm: Phải mở port tường lửa quá nhiều, khách hàng không thích gõ URL có thêm :30080, dễ bị xung đột port nếu công ty có nhiều team. Thường chỉ dùng cho Dev/Test cục bộ.
5.2. Ingress & Ingress Controller: Người gác cổng (The API Gateway)
Để giải quyết nhược điểm của NodePort, ta dùng Ingress. Ingress hoạt động ở tầng L7 (Application Layer), hiểu được giao thức HTTP/HTTPS. Ingress cung cấp:
- Routing theo tên miền (Domain-based routing:
api.company.comvsweb.company.com). - Routing theo đường dẫn (Path-based routing:
/api/usersgọi service user,/api/ordersgọi service order). - SSL/TLS Termination (Gắn chứng chỉ HTTPS).
Lưu ý sinh tử: K8s cung cấp đối tượng Ingress (chỉ là tờ giấy ghi luật), nhưng để luật hoạt động, bạn phải có Ingress Controller (một phần mềm thực thi, như NGINX, Traefik, HAProxy, Envoy) cài sẵn trong Cluster.
Ví dụ kinh điển làm khó Developer: Path Rewriting (Viết lại URL)
Giả sử Microservice User-API của bạn code bằng Spring Boot. Trong code, tất cả các route đều bắt đầu bằng /. (VD: @GetMapping("/list")). Nhưng trên Ingress (chặn cổng K8s), bạn muốn phân loại traffic bằng path /user-api để không đụng hàng với service khác. (Khách gọi: https://company.com/user-api/list).
Nếu bạn chỉ viết Ingress bình thường, NGINX sẽ đẩy NGUYÊN SI chuỗi /user-api/list vào Pod. Spring Boot nhận được, thấy không khớp với /list -> Trả về lỗi 404 Not Found. Dev hì hục debug cả ngày không hiểu vì sao ở local chạy ngon mà lên K8s lại 404!
Giải pháp: Dùng Annotation để gọt (Rewrite) URL trước khi đẩy vào Pod.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: user-ingress
annotations:
# Báo K8s biết tao dùng NGINX
kubernetes.io/ingress.class: "nginx"
# Lệnh cắt bỏ phần bị Regex (định nghĩa ở dưới), chỉ truyền phần sau ( $2 ) vào Pod
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- host: api.company.com
http:
paths:
- pathType: Prefix
# Cú pháp Regex của NGINX: Gom "/user-api" thành nhóm 1, phần còn lại thành nhóm 2
path: /user-api(/|$)(.*)
backend:
service:
name: user-service
port:
number: 80Phân tích luồng đi:
- Client gọi:
https://api.company.com/user-api/list - NGINX Ingress nhận request. Chớp lấy regex: Nhóm 1 là
/user-api/, Nhóm 2 làlist. - NGINX Ingress áp dụng luật
rewrite-target: /$2-> Chuỗi biến thành/list. - NGINX forward chuỗi
/listtớiuser-service. - Spring Boot nhận
/list. Cười mãn nguyện, trả về 200 OK. Chỉ một annotation nhỏ bé nhưng là mồ hôi nước mắt của vô số System/Dev.
Quản trị chứng chỉ tự động (Cert-Manager) Developer không cần mua SSL hay cài đặt bằng tay nữa. Cài cert-manager vào cluster. Viết thêm 3 dòng vào Ingress:
tls:
- hosts:
- api.company.com
secretName: api-tls-cert # Cert-manager sẽ tự động sinh file nàyNó sẽ tự động giao tiếp với Let's Encrypt qua HTTP-01 challenge, sinh ra chứng chỉ SSL miễn phí, lưu vào K8s Secret, và tự động cấu hình Nginx sang HTTPS. Khi gần hết hạn (90 ngày), nó tự động gia hạn. Zero-touch operation!
5.3. Service Mesh: Lưới dịch vụ (Vũ khí tối thượng của Microservices)
Khi số lượng microservices vượt quá 10, 20 hoặc 100... một mớ hỗn độn mạng nhện xuất hiện. Service A gọi B, B gọi C, C gọi lại A. Khi một request bị chậm lại 5 giây, làm sao Developer biết nó tắc ở mắt xích nào? Nếu Service C sập, Service B cứ cố gọi rồi timeout, gây treo toàn bộ tài nguyên của B. Làm sao mã hóa dữ liệu truyền nội bộ (mTLS) giữa A và B để hacker không sniff (bắt) được gói tin?
Bắt Developer tự code tính năng Retry, Timeout, Circuit Breaker (Cầu dao tự ngắt), Distributed Tracing vào TỪNG service bằng thư viện (như Resilience4j, Hystrix) là một cực hình. Và nếu các ngôn ngữ khác nhau (Go, Java, Nodejs), việc duy trì thư viện đồng nhất là ác mộng.
Service Mesh (như Istio, Linkerd) ra đời để giải thoát Developer.
Triết lý của Service Mesh: Tách biệt hoàn toàn Logic Mạng (Network Logic) ra khỏi Logic Nghiệp Vụ (Business Logic).
Cách hoạt động (Sự vĩ đại của Sidecar Pattern): Khi bạn bật Istio, K8s sẽ tự động tiêm (inject) một container Proxy (tên là Envoy) chạy song song vào MỌI Pod của bạn. Envoy can thiệp vào iptables cục bộ của Pod. Khi code ứng dụng của bạn (ví dụ App A) thực hiện lệnh: curl http://service-b.
- Lệnh này không bao giờ ra khỏi Pod trực tiếp. Nó bị Envoy của App A bắt lại (Intercept).
- Envoy A đóng gói, mã hóa bằng chứng chỉ (mTLS).
- Envoy A truyền qua mạng tới Envoy B (trong Pod của Service B).
- Envoy B giải mã, kiểm tra quyền (Authorization), rồi chuyển cho App B qua
localhost.
Developer làm được gì với sức mạnh của Istio Service Mesh?
1. Circuit Breaking (Cầu dao điện): Bảo vệ hệ thống khỏi Cascading Failure. Nếu Service B quá tải. Envoy tự động theo dõi, thấy B trả về 500 liên tục 5 lần. Envoy sẽ "sập cầu dao" (Open Circuit). Trong 30 giây tiếp theo, mọi request từ A gọi sang B, Envoy A sẽ LẬP TỨC từ chối và trả về lỗi 503 ngay lập tức cho A, không mất công chờ đợi (timeout) mạng tới B nữa, giảm tải cho B để B có cơ hội hồi phục.
# Istio DestinationRule cấu hình cho Dev
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: service-b-circuit-breaker
spec:
host: service-b
trafficPolicy:
outlierDetection:
consecutive5xxErrors: 5 # Lỗi 5 lần
interval: 10s # Quét mỗi 10s
baseEjectionTime: 30s # Cắt mạng 30s (Cách ly Pod bệnh)
maxEjectionPercent: 100 # Cho phép cắt tối đa 100% Pod nếu tất cả đều bệnh2. Automatic Retries (Thử lại tự động trong im lặng): Bạn không cần viết vòng lặp try...catch...sleep...retry trong code nữa.
# Istio VirtualService
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: service-b-retry
spec:
hosts:
- service-b
http:
- route:
- destination:
host: service-b
retries:
attempts: 3 # Thử gọi lại 3 lần nếu lỗi mạng
perTryTimeout: 2s # Mỗi lần chờ tối đa 2s
retryOn: connect-failure,5xx3. Fault Injection (Bơm lỗi - Chaos Engineering): Kiểm thử tính kiên cường của hệ thống. Bạn muốn test xem Frontend sẽ hiện thông báo gì nếu API giỏ hàng chậm 5 giây. Không cần sửa code Backend, chỉ cần bảo Envoy: "Hãy làm chậm 10% các gói tin bay vào Giỏ Hàng thêm 5s".
fault:
delay:
percentage:
value: 10.0
fixedDelay: 5sSức mạnh của Service Mesh biến Developer thành một kiến trúc sư thao túng toàn bộ chiều không gian và thời gian của hệ thống mạng.
PHẦN 6: STATEFUL WORKLOADS & STORAGE CHO DEV (KHI ỨNG DỤNG CÓ KÝ ỨC)
Trong thế giới Cloud-Native nguyên thủy, mọi người rỉ tai nhau một nguyên tắc vàng: "Hãy giữ cho ứng dụng của bạn Stateless (Phi trạng thái)". Nghĩa là Pod chỉ là nơi xử lý logic, tính toán xong thì ghi kết quả ra một Database bên ngoài (RDS, DynamoDB). Pod chết đi, Pod mới sinh ra không mang theo bất kỳ dữ liệu cũ nào. Deployment sinh ra để phục vụ tư duy này.
Nhưng thực tế không như mơ. Nếu bạn muốn tự chạy MongoDB, Kafka, Redis Cluster, hay ElasticSearch ngay bên TRONG cụm Kubernetes thì sao? Những ứng dụng này CÓ TRẠNG THÁI (Stateful). Chúng cần lưu dữ liệu vào ổ cứng, và chúng cần định danh cố định (Thằng Master phải biết thằng Slave tên là gì để đồng bộ dữ liệu). Nếu bạn dùng Deployment để chạy Database, đó là hành động tự sát hệ thống.
Đó là lý do Kubernetes tạo ra StatefulSet và hệ thống Storage.
6.1. Khi nào Developer cần StatefulSet? Sự khác biệt tử huyệt so với Deployment
Nếu bạn scale (mở rộng) một Deployment lên 3 Replicas, K8s sẽ đẻ ra 3 Pod với cái tên ngẫu nhiên: web-7b9c5d-x2pf, web-7b9c5d-m8kl, web-7b9c5d-q1zw. Chúng sinh ra đồng loạt, và khi scale down, K8s giết chúng một cách ngẫu nhiên.
Nhưng với StatefulSet, K8s cung cấp 4 đặc quyền tối thượng mà các hệ cơ sở dữ liệu phân tán khao khát:
1. Định danh mạng cố định và dễ đoán (Sticky Network Identity): Khi bạn tạo StatefulSet tên là mongo với 3 Replicas, K8s sẽ đặt tên chính xác là: mongo-0, mongo-1, mongo-2. Tên này vĩnh viễn không đổi. Nếu mongo-1 chết, K8s sẽ tạo lại một Pod mới thay thế vẫn mang tên chính xác là mongo-1.
2. Triển khai theo thứ tự (Ordered Deployment): K8s sẽ tạo mongo-0 trước. Đợi mongo-0 trạng thái Ready hoàn toàn, nó mới tạo tiếp mongo-1. Nó tạo mongo-1 xong mới tạo mongo-2. Usecase của Dev: Trong Database Replication, thằng mongo-0 thường được cấu hình làm Primary (Master). Khi mongo-1 (Secondary) sinh ra, nó có thể tìm ngay đến mongo-0 để xin đồng bộ dữ liệu. Nếu sinh ra lộn xộn cùng lúc (như Deployment), chúng sẽ tranh nhau làm Master (hiện tượng Split-Brain - Não chia cắt).
3. Xóa theo thứ tự ngược lại (Ordered Termination): Khi bạn scale từ 3 xuống 1, K8s sẽ giết mongo-2 trước. Xong mới đến mongo-1.
4. Ổ cứng gắn liền với định danh (Stable Storage): Đây là phép thuật lớn nhất. Khi mongo-1 chết, ổ cứng của nó (Persistent Volume) KHÔNG bị xóa. Khi K8s sinh lại Pod mongo-1 ở một máy chủ (Node) khác, nó sẽ ra lệnh cho hạ tầng Cloud (AWS/GCP) tháo ổ cứng đó ra và gắn lại chính xác vào con Pod mongo-1 mới. Dữ liệu không hề suy suyển!
Headless Service: Mảnh ghép bắt buộc của StatefulSet Khi dùng StatefulSet, bạn phải tạo một loại Service đặc biệt gọi là Headless Service (Service không có IP ảo).
apiVersion: v1
kind: Service
metadata:
name: mongo-headless
spec:
clusterIP: None # ĐÂY CHÍNH LÀ ĐIỂM QUYẾT ĐỊNH (HEADLESS)
selector:
app: mongo
ports:
- port: 27017Phân tích cốt lõi (Kiến thức Senior): Tại sao lại là clusterIP: None? Nếu Service bình thường có IP ảo, khi bạn gọi mongo-service, K8s sẽ Load-Balance ngẫu nhiên gói tin của bạn vào 1 trong 3 Pod. Nhưng khi bạn lập trình, thao tác GHI (WRITE) chỉ được phép chạy vào con Master (mongo-0), thao tác ĐỌC (READ) được phân bổ cho Slaves (mongo-1, mongo-2). Nếu gọi ngẫu nhiên thì dữ liệu nát bét! Khi đặt clusterIP: None, K8s DNS sẽ tự động tạo ra các bản ghi DNS định tuyến trực tiếp đến từng Pod. Lúc này, trong code của bạn, chuỗi kết nối Database (Connection String) sẽ được viết rõ ràng: mongodb://mongo-0.mongo-headless.default.svc.cluster.local:27017,mongo-1.mongo-headless... Chỉ có StatefulSet kết hợp Headless Service mới làm được trò này.
6.2. PV, PVC và Storage Classes (Quản lý ổ cứng như một lập trình viên)
Kubernetes có một thiết kế cực kỳ thông minh nhằm tách biệt trách nhiệm giữa Kỹ sư hạ tầng (Sysadmin) và Lập trình viên (Developer).
- Persistent Volume (PV - Ổ cứng vật lý): Do Sysadmin quản lý. Có dung lượng thật (100GB), loại thật (AWS EBS, NFS, Ceph).
- Persistent Volume Claim (PVC - Phiếu yêu cầu ổ cứng): Do Developer viết. Chỉ chứa "mong muốn" (Tôi cần ổ cứng 10GB, tốc độ cao, chỉ cần đọc ghi bởi 1 Node).
Là Developer, bạn CHỈ CẦN QUAN TÂM ĐẾN PVC.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-data-pvc
spec:
accessModes:
- ReadWriteOnce # Chế độ truy cập
storageClassName: aws-ebs-gp3 # Xưởng sản xuất ổ cứng
resources:
requests:
storage: 50Gi # Xin 50GBPhân tích chuyên sâu về AccessModes (Chỗ Developer hay ngã ngựa nhất): K8s định nghĩa 3 chế độ tiếp cận ổ cứng. Bạn chọn sai, hệ thống treo ngay lập tức.
- ReadWriteOnce (RWO): Ổ cứng này chỉ được phép gắn (mount) vào MỘT máy chủ vật lý (Node) duy nhất tại một thời điểm. Mọi Pod nằm trên Node đó đều có thể đọc/ghi. Nhưng Pod ở Node khác tuyệt đối không chạm vào được. Usecase: Database (MySQL, Postgres), ổ cứng AWS EBS block storage.
- ReadWriteMany (RWX): Ổ cứng này có thể gắn vào NHIỀU máy chủ vật lý (Node) cùng một lúc. Hàng chục Pod rải rác trên toàn cụm có thể đọc/ghi chung một file. Usecase: Thư mục chứa hình ảnh upload của người dùng (Shared Media files), File server. Bắt buộc hạ tầng phải dùng Network File System như AWS EFS, NFS, CephFS.
- ReadOnlyMany (ROX): Nhiều Node cùng gắn vào nhưng chỉ được đọc.
VolumeClaimTemplates: Bí thuật của StatefulSet Trong Deployment, nếu bạn mount 1 PVC cho 3 Replicas. 3 Pod đó sẽ tranh nhau cào xé chung 1 ổ cứng (và sẽ lỗi ngay nếu dùng RWO). Trong StatefulSet, bạn không khai báo PVC tĩnh, bạn khai báo Khuôn đúc PVC (volumeClaimTemplates).
apiVersion: apps/v1
kind: StatefulSet
# ... các config khác ...
volumeClaimTemplates:
- metadata:
name: data-dir
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "fast-storage"
resources:
requests:
storage: 100GiMỗi khi K8s sinh ra mongo-0, nó tự động đúc ra một ổ cứng data-dir-mongo-0 100GB gắn vào. Khi sinh mongo-1, nó đúc ra data-dir-mongo-1 100GB riêng biệt. Mỗi Pod sở hữu một vương quốc dữ liệu độc lập. Đây là thiết kế tuyệt hảo.
6.3. Ephemeral Storage và EmptyDir (Bộ nhớ tạm thời)
Đôi khi, ứng dụng của bạn cần một chỗ để ghi file tạm, file cache, hoặc đơn giản là để 2 container trong CÙNG MỘT POD trao đổi dữ liệu với nhau. Bạn không cần lưu dữ liệu này khi Pod chết. Bạn cần một ổ cứng vòng đời ngắn (Ephemeral). Đó là emptyDir.
volumes:
- name: shared-cache
emptyDir: {} # Tạo một thư mục rỗng trên đĩa của Worker NodeKỹ thuật nâng cao: emptyDir backed by RAM (Tăng tốc I/O cực đại) Nếu ứng dụng của bạn (ví dụ AI Model cần xử lý data tốc độ cực cao, hoặc Memcached) cần một nơi ghi file nhưng tốc độ đọc/ghi phải nhanh bằng tốc độ RAM, hãy lợi dụng tính năng này của K8s:
volumes:
- name: high-speed-cache
emptyDir:
medium: Memory # Sử dụng RAM làm ổ cứng (tmpfs)Cảnh báo xương máu cho Dev: Khi bạn thiết lập medium: Memory, thư mục này sẽ sử dụng RAM của máy chủ. Bạn ghi 5GB file vào thư mục đó, K8s sẽ tính là Pod của bạn đang tiêu thụ 5GB RAM! Nếu vượt quá resources.limits.memory của Pod, Pod sẽ bị K8s thảm sát ngay lập tức bằng án tử hình OOMKilled. Luôn cẩn trọng khi dùng tmpfs.
PHẦN 7: OBSERVABILITY & DEBUGGING (BÍ KÍP SINH TỒN VÀ GỠ LỖI)
Code chạy ở máy Local bằng lệnh npm start hay go run thì bạn nhìn thấy lỗi in ra màn hình ngay. Trên K8s, một cụm có 50 máy chủ, 500 Pod, hàng nghìn container chạy ẩn. Khi hệ thống báo lỗi 502 Bad Gateway từ phía khách hàng, bạn làm gì? Cuống cuồng lên? Hay gọi điện cho DevOps lúc 2 giờ sáng? Là một Developer làm chủ K8s, bạn phải nắm vững hệ sinh thái Observability (Khả năng quan sát) và các tuyệt kỹ gỡ lỗi trực tiếp.
7.1. Đọc Logs Nâng cao trong Kubernetes
Lệnh vỡ lòng ai cũng biết: kubectl logs <tên-pod>. Nhưng như thế là chưa đủ cho môi trường Production.
Tuyệt chiêu 1: Đọc log của Pod vừa chết (Cực kỳ quan trọng) Pod của bạn tự nhiên Crash và bị K8s khởi động lại (Restart count tăng lên 1). Bạn chạy kubectl logs thì chỉ thấy log của cái Pod MỚI vừa sinh ra (trống trơn). Bạn muốn biết TẠI SAO nó chết ở kiếp trước? Dùng cờ -p (hoặc --previous): kubectl logs <tên-pod> -p Lệnh này lôi từ dưới mồ lên những dòng log cuối cùng của container trước khi nó trút hơi thở cuối cùng. Rất thường xuyên, bạn sẽ thấy dòng chữ: Exception: NullPointerException hoặc Connection refused to database.
Tuyệt chiêu 2: Đọc log của nhiều Pod cùng lúc (Tailing by Label) Microservice của bạn có 5 Replicas (Pod). Bạn không thể gõ lệnh đọc log cho từng Pod được. Hãy dùng tính năng gom log theo Label (Giống y hệt nguyên lý của Service): kubectl logs -f -l app=payment-service --max-log-requests=10 Nó sẽ stream log trực tiếp (live) từ CẢ 5 POD trộn lại vào màn hình console của bạn. Bạn sẽ thấy luồng xử lý tổng thể của toàn bộ microservice.
Kiến trúc Centralized Logging (ELK/EFK/Loki) Là Developer, bạn hãy tuân thủ nghiêm ngặt nguyên tắc 12-Factor App: Ứng dụng chỉ ghi log ra tiêu chuẩn đầu ra (stdout / stderr). Đừng viết code bắt ứng dụng tạo file C:\logs\app.log hay /var/log/app/payment.log bên trong container. Nếu bạn ghi ra stdout (như hàm console.log() trong JS, System.out.println() trong Java), K8s engine (Kubelet/Containerd) sẽ tự động bắt lấy dòng log đó, đóng gói kèm theo thông tin (Tên Pod, Tên Node, Timestamp) và một tác nhân (Agent) chạy ngầm như Fluent-bit, Promtail sẽ tự động hút đống log đó đẩy về trung tâm Elasticsearch hoặc Grafana Loki. Nhiệm vụ của bạn là vào Grafana, gõ câu query tìm kiếm (ví dụ LogQL): {app="payment-service"} |= "NullPointerException" Mọi thứ hiện ra trong vòng 1 giây.
7.2. Distributed Tracing: Tìm cây kim đáy bể trong Microservices
Tình huống sinh tử: Khách hàng than phiền: "Bấm nút Thanh Toán mất 15 giây mới hiện thành công". Hệ thống của bạn có luồng đi như sau: Frontend -> Nginx Ingress -> API Gateway -> Order Service -> Payment Service -> Inventory Service -> Gửi Email Service. Log của mỗi service đều ghi "Xử lý thành công". Vậy 15 giây đó tắc ở khâu nào? Tắc ở mạng? Hay do Inventory query DB chậm? Đọc log không bao giờ giải quyết được bài toán này.
Giải pháp: Distributed Tracing (OpenTelemetry / Jaeger) Bạn cần một thứ "phát sáng" bám theo request của khách hàng đi xuyên qua tất cả các Microservices. Đó gọi là Trace.
Cơ chế hoạt động dành cho Developer:
- Khi request đầu tiên chạm vào Nginx Ingress, Ingress sẽ tự động sinh ra một mã duy nhất gọi là
X-Request-IDhoặcX-B3-TraceId(VD:abc-123) và gắn vào HTTP Header. - Request bay vào API Gateway. Gateway ghi nhận: "Tôi bắt đầu xử lý
abc-123lúc 10:00:00, xong lúc 10:00:02 (mất 2s)". Khoảng thời gian này gọi là một Span. - Gateway chuyển tiếp (forward) request sang Order Service. TRÁCH NHIỆM BẮT BUỘC CỦA CODE DEVELOPER: Code của bạn phải lấy cái chuỗi
abc-123từ header request nhận được, và BƠM NGƯỢC nó vào header của request gọi đi Order Service. Việc này gọi là Context Propagation (Truyền bối cảnh). - Order Service nhận được
abc-123, lại tính thời gian xử lý của nó (Span) rồi báo cáo về máy chủ trung tâm Jaeger. - Cứ thế lặp lại.
Khi bạn mở giao diện Jaeger UI, bạn gõ mã abc-123, một biểu đồ thác nước (Waterfall chart) hiện ra:
- Tổng thời gian: 15s.
- Gateway: 2s
- Order: 1s
- Payment: 11.5s (Kéo dài màu đỏ chót) -> Bắt được thủ phạm!
- Inventory: 0.5s Nếu bạn kết hợp Service Mesh (như Istio ở Phần 5), Mesh sẽ tự động lo khâu gửi báo cáo thời gian về Jaeger, code của Dev chỉ cần đảm bảo 1 việc duy nhất: Nhận Header vào, và truyền Header ra.
7.3. Các kỹ thuật Debugging Thực chiến Đỉnh cao
A. Kỹ thuật Port-Forwarding (Đâm xuyên tường hỏa) Database MongoDB của bạn nằm gọn trong K8s, không có NodePort, không có Ingress, hoàn toàn cách ly với Internet. Làm sao Dev ở nhà có thể dùng phần mềm DBeaver/Robo3T trên laptop để kết nối vào xem data? Dùng port-forward - Nó tạo ra một đường hầm bảo mật trực tiếp từ K8s API xuống laptop của bạn. kubectl port-forward svc/mongo-headless 27017:27017 Ngay lập tức, cổng 27017 trên vùng localhost của máy tính bạn biến thành cổng 27017 của Database trên K8s. Bạn kết nối vào localhost:27017 và xem data như thể nó chạy trên máy mình. Tuyệt kỹ này giúp Developer test mọi thứ cục bộ mà không cần mở Port nguy hiểm ra ngoài.
B. Kỹ thuật Ephemeral Containers (kubectl debug) - Bác sĩ phẫu thuật Một xu hướng bảo mật hiện nay là Developer phải build các Docker Image siêu nhẹ và siêu an toàn, gọi là Distroless Image (như gcr.io/distroless/static) hoặc Scratch. Những image này CHỈ chứa duy nhất file thực thi nhị phân của app (VD file chạy của Go hoặc C++). Nó KHÔNG CÓ bash, sh, curl, ping, ls, wget. Không có bất kỳ lệnh Linux cơ bản nào!
Tình huống: App chạy bị lỗi không gọi được API bên ngoài. Bạn muốn chui vào trong container để curl thử xem có phải bị chặn tường lửa không. Bạn gõ: kubectl exec -it <tên-pod> -- sh K8s trả lời: OCI runtime exec failed: exec failed: container_linux.go: process: exec: "sh": executable file not found in $PATH (Lỗi kinh điển: Không tìm thấy shell). Bạn bất lực! Vì bên trong không có gì để debug.
Đừng lo, từ Kubernetes v1.25+, tính năng Ephemeral Containers đã ra mắt. Nó cho phép bạn "bắn" một container chứa đầy đồ nghề debug (như Ubuntu/Alpine đầy đủ) VÀO CHUNG POD đang chạy của ứng dụng, chia sẻ TẤT CẢ Namespace (mạng, tiến trình) với ứng dụng.
kubectl debug -it <tên-pod> --image=alpine --target=<tên-container-ứng-dụng> Lúc này, bạn đứng bên trong container Alpine (có đủ curl, ping), nhưng bạn đang dùng chung Network card và không gian mạng của container Distroless kia. Bạn tha hồ gõ curl test kết nối, thậm chí dùng strace để bắt các system call của app đang chạy. Đây là trình độ của một Senior/Staff Engineer!
7.4. Phân tích Tứ Đại Tử Huyệt (The Big Four K8s Errors)
Đây là 4 lỗi mà bất kỳ ứng dụng nào đưa lên Production cũng sẽ gặp. Hiểu sâu bản chất sẽ giúp bạn khắc phục trong vòng 1 nốt nhạc.
1. CrashLoopBackOff (Vòng lặp cái chết)
- Biểu hiện: Pod cứ Running được vài giây rồi Crash. K8s khởi động lại. Lại Crash. Sau mỗi lần, thời gian chờ để K8s thử lại tăng lên theo cấp số nhân (10s, 20s, 40s... tối đa 5 phút).
- Nguyên nhân gốc (Góc nhìn Dev): Có 2 lý do chính.
- Thứ nhất: Container chạy xong tác vụ của nó và THOÁT ra tự nhiên (Exit code 0). Nhưng K8s Deployment yêu cầu một tiến trình chạy ngầm vô hạn (daemon). Ví dụ: Bạn dùng image Ubuntu, không có lệnh CMD chạy vô hạn nào. Ubuntu start lên, xong hết việc, tắt. K8s tưởng lỗi, bật lại.
- Thứ hai: Ứng dụng khởi động thất bại (Exit code 1). Thiếu biến môi trường bắt buộc, file cấu hình sai định dạng, không kết nối được Database ở vòng đời khởi động, hoặc có một tiến trình khác đang chiếm cổng (Port in use).
- Cách trị: Đọc log lùi (
kubectl logs -p), hoặc đọc mô tả sự kiện (kubectl describe pod <tên-pod>).
2. OOMKilled (Exit Code 137) - Kẻ giết người thầm lặng OOM là Out Of Memory.
- Bản chất nhân Linux: K8s sử dụng công nghệ
cgroups(Control Groups) của nhân Linux để giới hạn RAM của container. Khi Developer cấu hìnhresources.limits.memory: "512Mi", K8s báo với Linux Kernel rằng: "Nếu tiến trình trong container này tiêu thụ vượt 512MB RAM, hãy GIẾT nó ngay lập tức bằng tín hiệu SIGKILL (137)". - Thảm họa Java: Nếu bạn chạy Java Spring Boot cũ (Java 8). Máy chủ Node có 32GB RAM. Java tự động nhìn thấy 32GB RAM (chứ không phải 512MB limit kia, vì JVM đời cũ bị mù với cgroups). Nó hân hoan thiết lập kích thước Heap size mặc định là 8GB (1/4 tổng RAM). Khi ứng dụng chạy, nó ăn dần lên 600MB. CẢNH SÁT LINUX (OOM Killer) vung gươm chém đứt đầu tiến trình Java ngay lập tức. Lỗi hiện ra
OOMKilled. - Cách trị:
- Cấu hình lại ứng dụng để hiểu giới hạn memory (Với Java 11+, dùng cờ
-XX:+UseContainerSupport). - Nâng mức Limit lên đủ lớn.
- TUYỆT ĐỐI QUAN TRỌNG: Rò rỉ bộ nhớ (Memory Leak) trong code của Dev. Nếu code có memory leak, không có limit nào là đủ. Nó sẽ từ từ ăn đến chết. Dùng công cụ phân tích Heap Dump để sửa code.
- Cấu hình lại ứng dụng để hiểu giới hạn memory (Với Java 11+, dùng cờ
3. ImagePullBackOff / ErrImagePull (Không kéo được hàng)
- Nguyên nhân 1: Bạn gõ sai tên Image, hoặc gõ tag không tồn tại (Ví dụ dev đẩy tag
v1.2, bạn deploy file YAML tagv1.2.0). Kubelet báo lỗi 404 Not Found từ Docker Hub. - Nguyên nhân 2 (Rất hay gặp): Bạn kéo Image từ một Private Registry (kho chứa bí mật của công ty như AWS ECR, GitLab Registry). Nhưng bạn QUÊN không cung cấp chìa khóa cho K8s.
- Cách trị: Phải tạo một K8s Secret loại
docker-registry. Sau đó trong Deployment, khai báo thuộc tínhimagePullSecretstrỏ tới Secret đó.
4. HTTP 502 / 504 Bad Gateway (Cơn ác mộng từ Ingress) Khách hàng không bao giờ nhìn thấy OOMKilled, họ chỉ nhìn thấy màn hình trắng xóa báo 502 hoặc 504.
- 502 Bad Gateway (Cửa đóng then cài): Ingress nhận request, nó tìm đường gửi vào Pod. Nhưng Pod từ chối kết nối (Connection Refused) hoặc Pod ĐANG CHẾT, hoặc K8s Service định tuyến nhầm vào port sai (Khai báo
targetPorttrong Service không khớp với port app đang chạy trong Pod). - 504 Gateway Timeout (Đợi mỏi mòn): Ingress kết nối được vào Pod. Gửi gói tin đi. Và Ingress ngồi chờ... chờ... Ingress mặc định Nginx có timeout là 60 giây. Nếu sau 60s ứng dụng của bạn chưa trả lời (vì bị deadlock, vì kẹt query DB chậm, hoặc bạn đang viết API trích xuất báo cáo Excel quá nặng), Nginx sẽ cắt kết nối và quăng 504.
- Cách trị 504: Dev cần phân rã các API chạy lâu thành luồng bất đồng bộ (Asynchronous) dùng Message Queue (Kafka/RabbitMQ) + Webhooks/WebSockets. Không được ép HTTP request đồng bộ chạy quá dài. Nếu ép buộc, phải tăng cấu hình timeout của Ingress qua Annotation:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600".
PHẦN 8: GITOPS & CI/CD TÍCH HỢP (TƯ DUY TRIỂN KHAI THẾ HỆ MỚI)
Nhiều Developer quen với tư duy dùng Jenkins hoặc GitLab CI cũ: Code xong -> Jenkins chạy test -> Jenkins build Docker Image -> Jenkins chạy lệnh kubectl apply -f manifest.yaml thẳng vào cụm K8s. Tư duy đó gọi là Push Model (Mô hình đẩy). Nó đang bị giới DevOps hiện đại coi là "Anti-pattern" (Mẫu thiết kế sai lầm) vì một lý do chết người: Bảo mật. Để Jenkins chạy được kubectl apply, bạn phải cấp cho Jenkins một chiếc chìa khóa vạn năng (Kubeconfig với quyền ClusterAdmin) có thể can thiệp mọi thứ trên cụm K8s. Nếu hacker chiếm được Jenkins, cụm K8s của công ty coi như bay màu hoàn toàn.
Hơn nữa, nếu có một Sysadmin táy máy, nửa đêm vào thẳng server gõ lệnh kubectl edit deployment để sửa số Replica từ 3 lên 5. Sáng hôm sau, file YAML trên Git của bạn ghi số 3, hệ thống chạy số 5. Không ai biết sự thật nằm ở đâu (Configuration Drift).
Đó là lý do GITOPS ra đời. Và ông vua của GitOps là ArgoCD (hoặc FluxCD).
8.1. Tư duy GitOps với ArgoCD (Pull Model)
Tư duy cốt lõi của GitOps: Kho chứa Git (Git Repository) là "Nguồn chân lý duy nhất" (Single Source of Truth). Những gì nằm trên Git PHẢI khớp 100% với những gì đang chạy trên K8s.
Thay vì Jenkins ĐẨY vào K8s. Ta cài đặt phần mềm ArgoCD chạy TRONG LÒNG cụm K8s. ArgoCD hoạt động theo mô hình KÉO (Pull Model). Nó liên tục kết nối tới kho chứa Git của bạn (nơi chứa Helm chart hoặc Kustomize YAML). Cứ 3 phút nó nhìn (watch) Git một lần.
- Nếu Git báo có code mới được merge (Ví dụ:
image: my-app:v2), ArgoCD phát hiện ra sự khác biệt (Out of Sync). - ArgoCD sẽ TỰ ĐỘNG áp dụng bản quyền của chính nó bên trong K8s để đưa trạng thái hệ thống từ v1 lên v2.
- Nếu gã Sysadmin táy máy kia sửa Replicas thành 5, ArgoCD lập tức phát hiện: "Ê, trên Git tao thấy ghi số 3, tại sao mày tự chạy 5? Tao sẽ đè lại thành 3 ngay lập tức". Tính năng này gọi là Auto-Healing (Tự phục hồi hệ thống theo khuôn mẫu Git). Hệ thống không bao giờ bị trôi dạt cấu hình.
8.2. CI/CD Pipeline Thực chiến (Quy trình khép kín Developer - K8s)
Hãy hình dung bạn là một Developer. Luồng làm việc (Workflow) chuyên nghiệp tại các công ty Tech lớn sẽ diễn ra tự động 100% như sau, không cần một thao tác tay nào.
Bước 1: Lập trình viên (Developer) Bạn sửa file payment.js, commit và push lên nhánh main của repo mã nguồn (Ví dụ: Repo payment-app-source).
Bước 2: Continuous Integration (CI - Tích hợp liên tục bằng GitHub Actions / GitLab CI)
- GitHub Actions tự động kích hoạt. Nó kéo code xuống.
- Nó chạy Unit Tests (
npm test). Nếu test lỗi, pipeline đỏ, gửi thông báo Slack về mắng Dev. Dừng toàn bộ. - Nếu test pass, nó chạy
docker build -t my-registry.com/payment:abc1234(trong đóabc1234là mã SHA của commit git hiện tại. Lời khuyên: LUÔN sử dụng Git commit SHA làm Image Tag, TUYỆT ĐỐI KHÔNG dùng taglatest. Nếu dùnglatest, K8s không phát hiện được sự thay đổi Image, nó sẽ không bao giờ kéo lại bản mới về). - Nó dùng lệnh
docker pushđẩy image vừa build lên kho lưu trữ (Docker Hub, AWS ECR).
Bước 3: Giao điểm CI và CD (Sự kỳ diệu nằm ở đây) Pipiline CI chưa kết thúc. Bước cuối cùng của nó là gì? Công ty phải có một repo Git THỨ HAI. Tên là kubernetes-manifests (chứa toàn file YAML và cấu hình Helm/Kustomize, không chứa code logic). Trong repo thứ 2 này, file kustomization.yaml hoặc values.yaml đang ghi: imageTag: abc1230. Pipeline CI ở Bước 2 dùng một con bot (Git Bot) tự động nhân bản (clone) cái repo thứ 2 về, tìm đúng dòng chữ imageTag, dùng regex hoặc yq CLI để SỬA thành tag mới abc1234. Sau đó con bot TỰ ĐỘNG COMMIT VÀ PUSH ngược lên cái repo kubernetes-manifests. Đến đây, trách nhiệm của CI kết thúc hoàn toàn. CI không biết K8s là gì.
Bước 4: Continuous Deployment (CD - Triển khai liên tục với ArgoCD)
- ArgoCD ngồi im trong K8s, phát hiện repo
kubernetes-manifestsvừa có commit mới từ con bot CI. - Nó thấy
imageTagđổi từabc1230sangabc1234. - Nó so sánh file YAML render ra với trạng thái K8s hiện tại.
- Nó báo cáo trạng thái
OutOfSync. - Quá trình Sync bắt đầu: ArgoCD thông báo cho K8s API server thực hiện một đợt Rolling Update (hoặc gọi sang Argo Rollouts để chạy Canary theo kịch bản ở Phần 3).
- Các Pod mới tag
abc1234được sinh ra, Readiness Probe pass, Pod cũ bị tắt.
Kết quả cuối cùng: Từ lúc Lập trình viên gõ dòng lệnh git push origin main cho đến khi tính năng hiển thị trực tiếp cho khách hàng trên Production, mọi rủi ro bảo mật được chặn đứng, mọi phiên bản được lưu trữ vết (Audit) rành mạch trên Git. Nếu code mới lỗi? Bạn chỉ cần bấm nút "Revert Commit" trên GitHub của repo kubernetes-manifests, ArgoCD sẽ ngay lập tức rollback toàn bộ hạ tầng K8s về giây phút bình yên trước đó.
Đó chính là Cảnh Giới Tối Cao của Kubernetes và DevOps. Và với tư cách là một Lập trình viên hiện đại, khi bạn thấu hiểu toàn bộ quy trình này, từ vòng đời sống chết của một hạt nhân Pod (Lifecycle), đến nghệ thuật uốn nắn cấu hình YAML (Helm), len lỏi qua mạng nhện giao tiếp (Ingress, Service Mesh), bảo vệ dữ liệu (StatefulSet) và nắm quyền năng sinh sát tự động hóa (GitOps)... Bạn không còn là một Coder bình thường nữa. Bạn là một Thợ Săn Kiến Trúc (Architect) thực thụ trong kỷ nguyên Cloud-Native.