Các cấu hình sai lầm trên Kubernetes mà tôi biết

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 😀

9972f2df-7bb0-44d4-a469-1ec74a6487d2

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 requestslimits 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.

  1. 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!
  2. 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%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

Thông tin nổi bật

Báo cáo quan trọng

Chia sẻ bài viết:
Theo dõi
Thông báo của
0 Góp ý
Được bỏ phiếu nhiều nhất
Mới nhất Cũ nhất
Phản hồi nội tuyến
Xem tất cả bình luận