Quản lý Kubernetes trong môi trường production là một bài toán phức tạp chắc mọi người trải nghiệm đã đều hiểu rồi. Khi một cluster sập nguyên nhân thường không phải bug code mà lại là một lỗi YAML. Nó thường là những sai lầm cơ bản, những cái default không ngờ, hoặc những cấu hình tạm rồi chúng ta quên sửa.
Nên trong bài này tôi nói một chút về các lỗi mà mọi người có thể hay gặp nhé, mong giúp ích được cho bạn nào chưa có trải nghiệm, nếu nhiều người thấy hữu ích thì tôi sẽ làm vài phần nữa 😀

Resource Limits
Vấn đề phổ biến và gây ra nhiều hậu quả nghiêm trọng nhất là cấu hình requests và limits tài nguyên cẩu thả. Đây là nguyên nhân gây ra việc pod bị evict bất ngờ và hiệu năng hệ thống không thể dự đoán. Rất nhiều cấu hình Deployment mà tôi thấy đang chạy mà không có bất kỳ một khai báo tài nguyên nào.
# Cấu hình tôi thấy sai
apiVersion: apps/v1
kind: Deployment
metadata:
name: critical-app-no-limits
spec:
replicas: 3
template:
spec:
containers:
- name: app-container
image: my-service:1.2.0
# KHÔNG CÓ resource requests hay limits
# Đây là hành vi thiếu chuyên nghiệp trong production
Việc này tạo ra một quả bom nổ chậm. Kubernetes sẽ xếp các pod này vào loại QoS thấp nhất là BestEffort. Khi cluster bắt đầu cạn kiệt tài nguyên, đây là những pod đầu tiên bị Kubelet kill để bảo vệ các pod quan trọng hơn. Tệ hơn, một pod error có thể chiếm hết tài nguyên của Node và làm sập cả Node đó. Vậy xếp pod của mọi người vào loại QoS Guaranteed nếu requests bằng limits hoặc Burstable nếu requests nhỏ hơn limits, cả hai đều tốt hơn BestEffort.
# Cấu hình tôi nghĩ an toàn
apiVersion: apps/v1
kind: Deployment
metadata:
name: critical-app-with-limits
spec:
replicas: 3
template:
spec:
containers:
- name: app-container
image: my-service:1.2.0
resources:
requests: # Tối thiểu nó CẦN để chạy ổn định
memory: "512Mi"
cpu: "250m" # 0.25 Core
limits: # Tối đa nó ĐƯỢC PHÉP dùng
memory: "1Gi"
cpu: "1000m" # 1 Core
Đừng đoán mò các giá trị này. Hãy sử dụng các công cụ Prometheus, Grafana để theo dõi mức sử dụng thực tế. Một phương pháp phổ biến là đặt requests ở mức P50 và limits ở mức P95 sau một chu kỳ hoạt động thông thường.
Lạm dụng Liveness Probe
Một cấu hình Liveness Probe sai lầm có thể gây ra nhiều sự cố hơn bản thân ứng dụng. Cấu hình probe với các thông số too aggressive hoặc trỏ vào một endpoint phức tạp.
# Cấu hình gây restart liên hoàn
apiVersion: v1
kind: Pod
metadata:
name: probe-nightmare
spec:
containers:
- name: app
image: my-service:1.0
livenessProbe:
httpGet:
path: /api/health # Endpoint này check cả DB, Redis...?
port: 8080
initialDelaySeconds: 5 # App còn chưa kịp khởi động...
periodSeconds: 10
timeoutSeconds: 2 # Chỉ cho 2 giây để phản hồi?
failureThreshold: 2 # Lỗi 2 lần là restart ngay
Vấn đề ở đây là nếu endpoint /api/health cần check database, và database bị chậm, nó có thể phản hồi chậm hơn timeoutSeconds. Khi đó, Kubelet sẽ đánh dấu pod là unhealthy. Thất bại 2 lần trong khoảng 20 giây là pod bị restart. Điều này tạo ra một vòng lặp cascading restarts đúng vào lúc hệ thống đang chịu tải nặng nhất. Liveness Probe không phải để check ứng dụng có hoạt động hoàn hảo không, mà là để check process còn sống hay không.
# Cấu hình Liveness hợp lý
apiVersion: v1
kind: Pod
metadata:
name: probe-sanity
spec:
containers:
- name: app
image: my-service:1.0
livenessProbe: # Chỉ check xem process có chết không
tcpSocket: # Cách đơn giản và đáng tin cậy nhất
port: 8080
# Hoặc dùng 1 endpoint /healthz CỰC NHẸ, không check dependency
initialDelaySeconds: 30 # Cho nó đủ thời gian khởi động
periodSeconds: 20
timeoutSeconds: 3
failureThreshold: 3
Liveness Probe phải nhẹ, nhanh, và đáng tin cậy. Đừng bao giờ check các dependency bên ngoài DB, API khác trong Liveness Probe.
Ingress TLS
Một sai lầm phổ biến là nghĩ rằng chỉ cần có Ingress là traffic đã được mã hóa. Thực tế là, traffic có thể đang chạy plaintext bên trong cluster.
# Sai lầm về TLS
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tls-illusion
spec:
rules:
- host: secure.mydomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-web-app
port:
number: 80 # Traffic vào service ở cổng 80 (HTTP)
Với cấu hình này, traffic từ client đến Ingress Controller (ví dụ: NGINX) có thể là HTTPS nếu được cấu hình ở Load Balancer, nhưng từ Ingress Controller đi vào Service bên trong cluster là HTTP không mã hoá. Vậy cần phải khai báo tường minh việc sử dụng TLS trên Ingress và bắt buộc redirect.
# Cấu hình TLS đúng
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tls-correct
annotations:
# Bắt buộc redirect từ http sang https
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
tls: # PHẢI CÓ PHẦN NÀY
- hosts:
- secure.mydomain.com
secretName: my-domain-tls-secret # Tên của Secret chứa cert
rules:
- host: secure.mydomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-web-app
port:
number: 80
Luôn đảm bảo tls section được cấu hình, secretName trỏ đúng đến cert, và bật SSL redirect thường qua annotation để chặn truy cập không an toàn.
StatefulSet: ReclaimPolicy và Mất dữ liệu
Sử dụng StatefulSet cho các ứng dụng cần lưu trữ database, message queue tôi nghĩ phải hiểu về StorageClass.
Sử dụng storageClassName mặc định mà không kiểm tra chính sách của nó.
# Rủi ro mất dữ liệu
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: risky-database
spec:
# ... (phần còn lại)
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "standard" # Dùng storage class MẶC ĐỊNH
resources:
requests:
storage: 20Gi
Vấn đề là cái storageClassName: standard đó có ReclaimPolicy nếu chạy lệnh kubectl get storageclass và thấy nó là Delete, thì khi xoá Persistent Volume Claim hoặc xoá cả StatefulSet, cái Persistent Volume chứa dữ liệu cũng bị xoá theo. Không dùng storage class mặc định cho dữ liệu production quan trọng. Hãy tự tạo một StorageClass an toàn.
# StorageClass an toàn
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: retain-on-delete-storage
provisioner: kubernetes.io/aws-ebs # Hoặc gce-pd, azure-disk...
reclaimPolicy: Retain # Quan trọng là giữ lại Volume dù PVC bị xoá
volumeBindingMode: WaitForFirstConsumer
Sau đó, cập nhật StatefulSet để sử dụng storageClassName: "retain-on-delete-storage".
NetworkPolicy
Kubernetes cho phép mọi pod nói chuyện với mọi pod khác trong cluster, bất kể namespace. Đây có thể là lỗ hổng về bảo mật. Một cluster không có bất kỳ NetworkPolicy nào. Điều này cho phép một pod frontend bị xâm nhập có thể truy cập thẳng vào pod database hoặc auth-service. Hãy chặn tất cả, sau đó mở lại những gì cần thiết.
Đầu tiên, áp dụng policy chặn hết cả Ingress và Egress cho namespace:
# Code viết lại: Default Deny
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {} # Áp dụng cho TẤT CẢ pod
policyTypes:
- Ingress
- Egress
# Không có rule nào, nghĩa là CHẶN HẾT
Sau đó, mở từng đường một cách tường minh. Ví dụ, cho phép pod app: api-gateway gọi đến app: user-service ở cổng 8080:
# Code viết lại: Mở đường cho API Gateway gọi User Service
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-to-user-service
namespace: production
spec:
podSelector:
matchLabels:
app: user-service # Pod NHẬN
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: api-gateway # Pod GỌI
ports:
- protocol: TCP
port: 8080
Anti-Pattern về ‘Secrets’
Một sự thật cần phải nhắc lại: Base64 không phải là mã hoá, nó chỉ là encoding.
- Hardcode thẳng vào YAML:
env: - name: DB_PASSWORD value: "sieu-bi-mat-123" # Không bao giờ được làm điều này! - Dùng K8s Secret mặc định và nghĩ rằng nó an toàn:
apiVersion: v1 kind: Secret metadata: name: db-creds data: password: c2lldS1iaS1tYXQtMTIz # (Đây chỉ là 'sieu-bi-mat-123' encode base64)
Secret này được lưu dạng plaintext sau khi decode base64 trong etcd. Bất kỳ ai có quyền truy cập etcd hoặc quyền get secret trong namespace đều có thể đọc được nó.
Phải sử dụng một giải pháp quản lý bí mật thực thụ, tích hợp vào K8s:
- HashiCorp Vault (với Vault Agent Injector)
- External Secrets Operator (tích hợp với AWS/GCP/Azure Secrets Manager)
- Sealed Secrets (cho GitOps)
Ít nhất, hãy bật mã hoá etcd trên cluster của mọi người.
# Code viết lại: Ví dụ dùng External Secrets (với AWS Secrets Manager)
# Nó sẽ tự động đồng bộ secret từ AWS về K8s
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
spec:
secretStoreRef:
name: aws-secret-store # Đã cấu hình trước
kind: ClusterSecretStore
target:
name: db-secret-local # Tên Secret nó sẽ tạo ra trong K8s
data:
- secretKey: POSTGRES_PASSWORD # Key trong K8s Secret
remoteRef:
key: /production/database/password # Key trong AWS Secrets Manager
Thiếu Pod Disruption Budgets
PDB là một cơ chế an toàn quan trọng nhưng thường bị bỏ qua. Nó ngăn chặn các hành động tự nguyện như nâng cấp node làm sập dịch vụ của mọi người. Không có PDB. Khi cần bảo trì và drain một Node, K8s sẽ đẩy hết các pod trên Node đó đi. Nếu 3 replicas của service quan trọng của mọi người lỡ chạy trên cùng 1 Node đó thì Service của mọi người sập 100%. Nên hãy tạo PDB cho mọi service quan trọng.
# PDB cho service có 3 replicas
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: my-app-pdb
spec:
minAvailable: 2 # LUÔN LUÔN phải còn ít nhất 2 replicas chạy
# Hoặc dùng maxUnavailable: 1 (Cho phép sập tối đa 1)
selector:
matchLabels:
app: my-critical-app # Trỏ đến service của mọi người
Bỏ qua Pod Security Context
Mặc định, rất nhiều image container chạy với quyền root bên trong nó. Nếu ứng dụng bị khai thác, kẻ tấn công sẽ có quyền root trong container, mở đường cho các cuộc tấn công leo thang đặc quyền.
# Rủi ro bảo mật chạy với quyền root
apiVersion: v1
kind: Pod
metadata:
name: running-as-root
spec:
containers:
- name: app
image: some-image:latest
# Không có securityContext -> Rất có thể đang chạy với user 'root'
Chạy với least privilege. Ép container chạy với user không phải root.
# Pod an toàn với non-root
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext:
runAsNonRoot: true # BẮT BUỘC không chạy root
runAsUser: 1001 # Chạy với User ID 1001
runAsGroup: 1001
fsGroup: 1001
containers:
- name: app
image: my-app-nonroot:1.0 # Image này phải được build để chạy non-root
securityContext:
allowPrivilegeEscalation: false # Không cho phép leo thang đặc quyền
capabilities:
drop:
- ALL # Bỏ hết Linux capabilities không cần thiết
Cấu hình Update Strategy sai
Cách Deployment cập nhật phiên bản mới có thể gây gián đoạn dịch vụ nếu không được cấu hình đúng. Deployment mặc định dùng chiến lược RollingUpdate với maxUnavailable: 25% và maxSurge: 25%. Nếu chỉ chạy 2-3 replicas, 25% làm tròn lên là 1, nó sẽ kill 1 pod cũ trước khi tạo pod mới. Nếu pod mới không khởi động được, dịch vụ sẽ bị giảm sút năng lực xử lý. Tệ hơn, nếu dùng strategy: type: Recreate, nó sẽ kill hết pod cũ rồi mới tạo pod mới, gây downtime toàn bộ.
# Chiến lược update an toàn cho Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: safe-deployment
spec:
replicas: 5 # Ví dụ chạy 5 replicas
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # Chỉ cho phép mất TỐI ĐA 1 pod tại một thời điểm
maxSurge: 1 # Chỉ tạo thêm TỐI ĐA 1 pod mới tại một thời điểm
template:
# ...
Cách này đảm bảo quá trình update diễn ra từ từ và có kiểm soát.
Thiếu Readiness Probes
Đây là một lỗi tinh vi gây ra vô số các sự cố 500/503. Pod đã khởi động và Liveness OK, K8s ngay lập tức đưa nó vào Service để nhận traffic. Nhưng… ứng dụng bên trong chưa kết nối xong tới Redis hoặc Database. Kết quả là vài giây đầu tiên, mọi request đến pod mới này đều trả về lỗi 503. Hãy thử triển khai Readiness Probe để kiểm tra các dependency. K8s sẽ không gửi traffic đến pod cho đến khi endpoint /ready trả về mã 200.
# Thêm Readiness Probe
spec:
containers:
- name: app
image: my-service:1.0
livenessProbe:
tcpSocket: # Liveness: Rất đơn giản
port: 8080
initialDelaySeconds: 15
readinessProbe: # Readiness: Phải kiểm tra dependency
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 20 # Thường lâu hơn Liveness
periodSeconds: 10
failureThreshold: 2
Và trong code của ứng dụng (ví dụ Python/Flask):
# Code viết lại: Endpoint /ready
import psycopg2
import redis
@app.route('/ready')
def ready():
try:
# Check các dependency
db = psycopg2.connect(...) # Thử kết nối DB
db.close()
r = redis.Redis(...) # Thử ping Redis
r.ping()
return "Ready", 200
except Exception as e:
# Nếu chưa sẵn sàng, trả về 503
return "Dependencies not ready", 503





