🔐 ConfigMap và Secret: biến môi trường, volume mount và cạm bẫy

Mục tiêu bài học

  • Tạo được ConfigMap và Secret từ literal, từ file và từ cả thư mục bằng kubectl.
  • Đưa cấu hình vào Pod qua hai đường: biến môi trường (env, envFrom) và volume mount, biết khi nào dùng đường nào.
  • Nhận diện các cạm bẫy kinh điển: secret lộ trong manifest hoặc Git, base64 bị nhầm là mã hoá, sửa cấu hình mà app không reload.
  • Kiểm tra được một Pod đang ăn cấu hình từ object nào và debug lỗi thiếu key, thiếu object.

Ôn lại nhanh (2 phút)

Ở Module 1 bạn đã biết Pod là đơn vị nhỏ nhất, Deployment quản ReplicaSet và rollout theo template. Điểm mấu chốt: image là bất biến, nên mọi thứ thay đổi theo môi trường (dev, staging, prod) phải nằm ngoài image. Service và Ingress lo phần "ai gọi tới Pod", còn bài này lo phần "Pod đọc cấu hình ở đâu ra".

Lý thuyết

1. ConfigMap: cấu hình không bí mật

ConfigMap là API object chứa các cặp key-value dạng text. Nó không mã hoá gì cả, ai có quyền get object là đọc được toàn bộ.

kubectl create configmap app-config \
  --from-literal=LOG_LEVEL=info \
  --from-literal=APP_MODE=production

kubectl create configmap nginx-conf --from-file=default.conf=./nginx/default.conf

Với --from-file, tên file trở thành key; nếu muốn key khác thì viết key=đường/dẫn/file.

2. Secret: cũng là key-value, nhưng có ý nghĩa bảo mật

Secret có cấu trúc gần giống ConfigMap, thêm type (Opaque, kubernetes.io/tls, kubernetes.io/dockerconfigjson) và được các cơ chế như RBAC, encryption at rest trong etcd, audit log đối xử riêng.

[!WARNING] Secret lưu giá trị dưới dạng {{c1::base64}}, nhưng base64 chỉ là encoding chứ không phải {{c2::mã hoá}}. Bất kỳ ai đọc được object đều decode ra plaintext trong một giây. Đừng commit file Secret YAML lên Git.

3. Hai con đường đưa cấu hình vào Pod

flowchart LR
    CM["📦<br/>ConfigMap / Secret"] --> ENV["🌱<br/>env / envFrom"]
    CM --> VOL["📁<br/>volume mount"]
    ENV --> P1["Pod<br/>giá trị chốt lúc container start"]
    VOL --> P2["Pod<br/>file được đồng bộ lại khi object đổi"]

Đường 1: biến môi trường. Dùng envFrom cho toàn bộ key, hoặc env.valueFrom cho từng key cụ thể (bắt buộc khi key có dấu chấm, dấu gạch ngang, vì envFrom sẽ bỏ qua key không hợp lệ với tên biến môi trường).

Đường 2: volume mount. Mỗi key thành một file trong thư mục mount. Cách này hợp với file cấu hình (nginx.conf, application.yaml) và là cách duy nhất để app thấy thay đổi mà không cần restart.

Tiêu chí env / envFrom volume mount
Định dạng phù hợp chuỗi ngắn file cấu hình dài
Tự cập nhật khi sửa object Không Có, sau độ trễ đồng bộ của kubelet
Dùng subPath không áp dụng Mất tính năng tự cập nhật
Lưu trên đĩa node có Secret dùng tmpfs (RAM)

Biến môi trường từ ConfigMap {{c3::không tự cập nhật}} khi ConfigMap thay đổi, bạn phải kubectl rollout restart. Volume mount thì có đồng bộ lại, trừ khi dùng {{c4::subPath}}.

4. Vài thuộc tính đáng nhớ

  • optional: true: cho phép Pod vẫn chạy khi thiếu object hoặc thiếu key. Mặc định thiếu là Pod kẹt ở CreateContainerConfigError (env) hoặc FailedMount/Pending (volume).
  • immutable: true: chặn sửa/xoá, giảm tải cho API server và kubelet khi cluster lớn. Đặt immutable: {{c5::true}} nghĩa là muốn đổi thì phải tạo object mới.
  • defaultMode: với Secret nên đặt 0400 thay vì mặc định 0644.

Ví dụ có hướng dẫn

Ví dụ 1: ConfigMap làm biến môi trường

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  LOG_LEVEL: info
  APP_MODE: production
---
apiVersion: v1
kind: Pod
metadata:
  name: api
spec:
  containers:
    - name: api
      image: nginx:1.27
      envFrom:
        - configMapRef:
            name: app-config

Kiểm tra: kubectl exec api -- env | grep -E 'LOG_LEVEL|APP_MODE'. Sửa LOG_LEVEL thành debug, vào Pod xem lại: vẫn là info. Đây chính là cạm bẫy số một.

Ví dụ 2: Mount file cấu hình và quan sát reload

spec:
  containers:
    - name: web
      image: nginx:1.27
      volumeMounts:
        - name: nginx-conf
          mountPath: /etc/nginx/conf.d
          readOnly: true
  volumes:
    - name: nginx-conf
      configMap:
        name: nginx-conf

Sau khi kubectl edit configmap nginx-conf, chờ khoảng 30 tới 60 giây rồi kubectl exec web -- cat /etc/nginx/conf.d/default.conf, nội dung đã đổi. Nhưng nginx thì chưa nạp lại: app vẫn phải tự reload (nginx -s reload) hoặc bạn phải restart. Volume cập nhật file, không restart tiến trình giúp bạn.

Ví dụ 3: Secret mount an toàn hơn

  volumes:
    - name: db-secret
      secret:
        secretName: db-secret
        defaultMode: 0400

File nằm trong tmpfs, quyền chỉ đọc cho owner, không ghi xuống đĩa node.

Thực hành có hướng dẫn

Bài 1: Tạo cấu hình từ file

  1. Tạo app.properties với 2 dòng retries=3 và timeout=30s.
  2. kubectl create configmap app-props --from-file=app.properties
  3. kubectl get configmap app-props -o yaml và xác nhận key là app.properties.
Gợi ý đáp án

Toàn bộ nội dung file nằm trong một key duy nhất là tên file. Nếu muốn tách từng key, dùng --from-env-file=app.properties cho định dạng KEY=value.

Bài 2: Cố tình gây lỗi thiếu object

  1. Tạo Pod có envFrom.configMapRef.name: khong-ton-tai.
  2. kubectl get pod và kubectl describe pod để xem lỗi.
  3. Thêm optional: true và quan sát Pod chạy bình thường nhưng không có biến nào.
Gợi ý đáp án

Pod kẹt ở CreateContainerConfigError. Đây là lỗi bạn sẽ gặp rất nhiều khi deploy, nên nhớ nó nằm ở phần Events, không phải trong log container (container chưa hề start).

Bài 3: So sánh env và volume

  1. Deploy một Pod vừa dùng envFrom vừa mount cùng một ConfigMap.
  2. Sửa một value trong ConfigMap, chờ 1 phút.
  3. So sánh env và nội dung file trong thư mục mount.
Gợi ý đáp án

Env giữ giá trị cũ, file đã đổi. Đây là lý do nhiều team chọn volume mount cho cấu hình cần thay đổi nóng, và chọn env cho cấu hình ngắn, ít đổi.

Góc chuyên gia

  • Đừng commit Secret YAML. Dùng kubectl create secret --dry-run=client -o yaml để sinh nhanh, nhưng file sinh ra chỉ để dùng tại chỗ. Lên production thì cân nhắc External Secrets hoặc Sealed Secrets.
  • subPath là bẫy im lặng. Rất nhiều người dùng subPath để mount một file đơn lẻ rồi ngồi đợi file đổi. Nó không bao giờ đổi. Muốn mount một file thì mount cả thư mục.
  • Mount đè thư mục. Mount vào /app/config sẽ thay thế toàn bộ nội dung thư mục đó trong container, kể cả file có sẵn trong image. Kiểm tra lại đường dẫn trước khi mount.
  • Đổi tên object thay vì sửa. Với cấu hình quan trọng, tạo ConfigMap mới (app-config-v2) rồi trỏ Deployment sang. Rollback đơn giản hơn nhiều so với sửa tại chỗ.
  • Luôn đặt readOnly: true cho volume cấu hình, tránh app ghi ngược vào file gốc trong container.
  • Quy tắc nhanh: ngắn và tĩnh thì env, dài và cần đổi nóng thì volume.

Tóm tắt & bước tiếp theo

Bộ thẻ ôn tập

[
  { "front": "ConfigMap lưu gì?", "back": "Cấu hình dạng key-value không mã hoá, tách khỏi image." },
  { "front": "Secret khác ConfigMap ở điểm nào?", "back": "Có type, lưu base64, mount dạng tmpfs, được RBAC và encryption at rest đối xử riêng." },
  { "front": "base64 có phải mã hoá không?", "back": "Không, chỉ là encoding; ai đọc được object là decode ra plaintext." },
  { "front": "Hai cách đưa ConfigMap vào Pod?", "back": "env/envFrom hoặc mount volume." },
  { "front": "env có tự cập nhật khi ConfigMap đổi không?", "back": "Không, giá trị chốt lúc container start; phải rollout restart." },
  { "front": "Volume mount có tự cập nhật không?", "back": "Có, kubelet đồng bộ lại sau một khoảng trễ, trừ khi dùng subPath." },
  { "front": "subPath có cập nhật khi ConfigMap đổi không?", "back": "Không bao giờ, phải tạo lại Pod." },
  { "front": "Thiếu ConfigMap bắt buộc thì Pod báo gì?", "back": "CreateContainerConfigError với env, FailedMount/Pending với volume." },
  { "front": "Làm sao cho phép thiếu ConfigMap hoặc key?", "back": "Đặt optional: true." },
  { "front": "defaultMode mặc định của Secret volume là gì?", "back": "0644; nên đặt 0400 cho file secret." },
  { "front": "immutable: true có tác dụng gì?", "back": "Chặn sửa/xoá, giảm tải API server và kubelet; muốn đổi phải tạo object mới." },
  { "front": "Mount volume vào thư mục có sẵn thì sao?", "back": "Ghi đè toàn bộ nội dung thư mục đó trong container." },
  { "front": "Key có dấu chấm hoặc gạch ngang dùng envFrom được không?", "back": "Không, envFrom bỏ qua key không hợp lệ; dùng env.valueFrom riêng." }
]

Tóm tắt

ConfigMap và Secret chỉ là hai object chứa dữ liệu. Giá trị thật nằm ở cách bạn đưa chúng vào Pod: env thì đơn giản nhưng đóng băng, volume thì linh hoạt nhưng app phải tự đọc lại file. Và nhớ: Secret mount dưới dạng volume được lưu trong {{c6::tmpfs}} (RAM) trên node, không ghi xuống đĩa.

Bài tiếp theo

Cấu hình thì đã tách khỏi image, nhưng dữ liệu thật của ứng dụng (database, upload) thì chưa. Bài sau: Volume, PersistentVolume, PersistentVolumeClaim và StorageClass, nơi chúng ta giải bài toán dữ liệu sống lâu hơn Pod.