Mở đầu — vì sao bài này quan trọng
Ở các bài trước, bạn đã biết cách đóng gói ứng dụng thành Pod, phơi bày nó ra ngoài bằng Service. Nhưng có một câu hỏi cực kỳ thực tế mà mọi DevOps Engineer đều gặp trong tuần làm việc đầu tiên: "Ứng dụng của tôi cần biết địa chỉ database, cần mật khẩu Redis, cần API key của cổng thanh toán — tôi nhét mấy thứ này vào đâu?"
Câu trả lời sai (mà rất nhiều team Việt Nam vẫn đang làm) là: hardcode thẳng vào Docker image, hoặc nhét vào file .env rồi copy vào image lúc build. Cách này biến config thành một phần của artifact — nghĩa là mỗi lần đổi mật khẩu database, bạn phải build lại image, push lại registry, deploy lại. Tệ hơn, mật khẩu nằm chình ình trong lịch sử image, ai docker history cũng thấy.
Kubernetes tách bạch hai khái niệm này bằng hai object: ConfigMap (cho config không nhạy cảm) và Secret (cho dữ liệu nhạy cảm như mật khẩu, token, chứng chỉ TLS). Đây chính là ứng dụng thực tế của nguyên tắc "config tách khỏi code" trong triết lý Twelve-Factor App — cùng một image chạy được ở dev, staging, production, chỉ khác nhau ở lớp config được "tiêm" vào lúc runtime.
Nắm vững ConfigMap và Secret là ranh giới giữa một người "biết chạy kubectl apply" và một DevOps Engineer thực thụ. Đây cũng là chủ đề gần như chắc chắn xuất hiện trong mọi buổi phỏng vấn Kubernetes tại Việt Nam.
Khái niệm cốt lõi
ConfigMap — nơi chứa config không nhạy cảm
ConfigMap là một object lưu dữ liệu dạng key-value dưới dạng plain text. Dùng nó cho những thứ như: URL của service khác, log level, feature flag, tên môi trường, hay cả một file cấu hình hoàn chỉnh (ví dụ nginx.conf).
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
namespace: production
data:
APP_ENV: "production"
LOG_LEVEL: "info"
DB_HOST: "postgres.production.svc.cluster.local"
DB_PORT: "5432"
MAX_CONNECTIONS: "100"
Lưu ý: mọi giá trị trong data đều là chuỗi. Con số 5432 phải để trong ngoặc kép, nếu không YAML sẽ hiểu là số nguyên và Kubernetes sẽ báo lỗi. Đây là lỗi kinh điển của người mới.
ConfigMap còn có trường binaryData (cho dữ liệu nhị phân, base64) nhưng 99% trường hợp bạn chỉ dùng data.
Secret — nơi chứa dữ liệu nhạy cảm
Secret trông rất giống ConfigMap về cấu trúc, nhưng giá trị được lưu dưới dạng base64. Điều tối quan trọng cần khắc cốt ghi tâm: base64 KHÔNG phải là mã hóa (encryption), nó chỉ là encoding. Bất kỳ ai có echo <chuỗi> | base64 -d đều đọc ngược ra được. Base64 chỉ giúp lưu dữ liệu nhị phân (như file chứng chỉ) một cách an toàn về mặt định dạng, chứ không bảo vệ về mặt bảo mật.
apiVersion: v1
kind: Secret
metadata:
name: api-secret
namespace: production
type: Opaque
stringData:
DB_PASSWORD: "Sup3rS3cr3t!2026"
JWT_SECRET: "aVeryLongRandomStringHere"
Mẹo quan trọng: dùng stringData thay vì data, Kubernetes sẽ tự động base64-encode giúp bạn khi lưu. Nếu dùng data, bạn phải tự encode trước — dễ sai và khó đọc.
Trường type phổ biến:
Opaque: kiểu mặc định, dữ liệu tùy ý (mật khẩu, token).kubernetes.io/dockerconfigjson: credential để pull image từ private registry.kubernetes.io/tls: chứng chỉ TLS (tls.crtvàtls.key).
Hai cách "tiêm" config vào Pod
Cả ConfigMap lẫn Secret đều có thể được đưa vào container theo hai cách, và việc chọn đúng cách rất quan trọng:
Cách 1 — Environment variables: Biến từng key thành biến môi trường. Đơn giản, phù hợp với vài giá trị lẻ. Nhược điểm: env var không tự cập nhật khi ConfigMap thay đổi (phải restart Pod), và biến môi trường dễ bị lộ qua log hay kubectl describe pod.
Cách 2 — Volume mount: Mount ConfigMap/Secret thành các file trong một thư mục. Phù hợp khi bạn có cả một file config (như application.yml, nginx.conf) hoặc chứng chỉ. Ưu điểm lớn: khi ConfigMap thay đổi, file trong volume tự động cập nhật (sau khoảng 1 phút) mà không cần restart Pod — dù ứng dụng của bạn phải tự đọc lại file.
Bức tranh bảo mật thật của Secret
Đây là phần nhiều người hiểu sai. Mặc định, Secret được lưu trong etcd (cơ sở dữ liệu của cluster) ở dạng base64, không mã hóa. Muốn thực sự an toàn, bạn cần:
- Bật Encryption at Rest cho etcd (mã hóa dữ liệu Secret trong etcd).
- Siết RBAC để giới hạn ai được
get/listSecret. - Cân nhắc giải pháp bên ngoài như HashiCorp Vault hoặc cloud KMS (sẽ học ở Bài 40).
Tình huống thực tế
Ví dụ 1 — Tiki tách config theo môi trường
Giả sử đội backend của một sàn thương mại điện tử lớn như Tiki vận hành dịch vụ order-service. Trước đây, mỗi môi trường (dev, staging, production) có một Docker image riêng vì config được build thẳng vào image. Kết quả: một image "chạy tốt ở staging" nhưng khi lên production lại lỗi vì ai đó quên đổi biến, và tệ hơn — image staging vô tình chứa credential production.
Sau khi chuẩn hóa, họ giữ một image duy nhất order-service:1.4.2. Mỗi namespace (staging, production) có một ConfigMap order-config và một Secret order-secret riêng. Cùng một image, deploy lên staging thì lấy config staging, lên production thì lấy config production. Bài toán "chạy được ở máy tôi mà không chạy trên production" gần như biến mất, vì artifact được kiểm thử ở staging chính xác là artifact chạy production.
Bài học: Tách config khỏi image giúp đảm bảo immutability của artifact — thứ bạn test chính là thứ bạn ship. Config khác biệt nằm ở lớp Kubernetes, không nằm trong image.
Ví dụ 2 — Startup fintech ở TP.HCM và cú lộ API key
Một startup fintech khoảng 20 người ở Quận 1 tích hợp cổng thanh toán VNPay. Bạn dev đưa VNPAY_SECRET_KEY vào một ConfigMap (chứ không phải Secret) để "cho nhanh". ConfigMap đó được commit lên Git repo cấu hình dùng cho GitOps. Vài tuần sau, khi họ chuyển repo config từ private sang public để chia sẻ mẫu với đối tác, secret key thanh toán bị lộ ra internet. Hậu quả là phải quay (rotate) toàn bộ credential khẩn cấp lúc 2 giờ sáng.
Sau sự cố, họ áp ba quy tắc cứng: (1) mọi thứ nhạy cảm bắt buộc là Secret, không bao giờ là ConfigMap; (2) không commit Secret thô lên Git — dùng Sealed Secrets (mã hóa Secret trước khi đưa vào Git, chỉ controller trong cluster mới giải mã được); (3) bật Encryption at Rest cho etcd.
Bài học: Ranh giới ConfigMap/Secret không phải chuyện kỹ thuật thuần túy — nó là ranh giới bảo mật. Đặt sai chỗ một lần có thể trả giá bằng cả đêm rotate credential và niềm tin của đối tác.
Ví dụ 3 — Team platform cập nhật feature flag không cần rebuild
Một công ty SaaS ở Đông Nam Á có dịch vụ frontend đọc feature flag từ file features.json được mount từ ConfigMap dưới dạng volume. Khi họ muốn bật tính năng "thanh toán trả góp" cho 10% người dùng, họ chỉ cần kubectl edit configmap để sửa giá trị flag. Sau khoảng một phút, file trong Pod tự cập nhật, ứng dụng (có cơ chế watch file) đọc lại và bật tính năng — không rebuild image, không restart Pod, không downtime.
Nhưng họ cũng học được một điều đau: nếu config được tiêm qua env var thay vì volume, thay đổi sẽ không có hiệu lực cho tới khi Pod restart. Có lần một bạn sửa ConfigMap dạng env var rồi ngồi đợi mãi không thấy đổi, tưởng bug, mất nửa buổi debug.
Bài học: Volume mount cập nhật động, env var thì không. Chọn cách tiêm phù hợp với nhu cầu "cần đổi nóng" của bạn.
Hướng dẫn từng bước
Ta sẽ deploy một ứng dụng web đọc config từ ConfigMap và mật khẩu từ Secret.
Bước 1 — Tạo ConfigMap. Cách khai báo (imperative), nhanh cho lúc thử:
kubectl create configmap api-config \
--from-literal=APP_ENV=production \
--from-literal=LOG_LEVEL=info \
-n production
Hoặc tạo từ file YAML (declarative, khuyến nghị cho production để lưu vào Git):
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
namespace: production
data:
APP_ENV: "production"
LOG_LEVEL: "info"
Bước 2 — Tạo Secret bằng stringData để khỏi tự encode:
apiVersion: v1
kind: Secret
metadata:
name: api-secret
namespace: production
type: Opaque
stringData:
DB_PASSWORD: "Sup3rS3cr3t!2026"
Áp dụng: kubectl apply -f secret.yaml
Bước 3 — Tiêm vào Pod qua env var. Dùng envFrom để nạp toàn bộ, và valueFrom cho từng giá trị lẻ:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: production
spec:
replicas: 2
selector:
matchLabels: { app: api }
template:
metadata:
labels: { app: api }
spec:
containers:
- name: api
image: mycompany/api:1.4.2
envFrom:
- configMapRef:
name: api-config
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: api-secret
key: DB_PASSWORD
Sau đó bên trong container, ứng dụng đọc APP_ENV, LOG_LEVEL, DB_PASSWORD như biến môi trường bình thường.
Bước 4 — (Tùy chọn) Tiêm qua volume mount nếu cần cả file config:
volumeMounts:
- name: config-volume
mountPath: /etc/app/config
readOnly: true
volumes:
- name: config-volume
configMap:
name: api-config
Mỗi key trong ConfigMap sẽ thành một file (ví dụ /etc/app/config/APP_ENV). Với Secret, thay configMap bằng secret và đặt thêm defaultMode: 0400 để giới hạn quyền đọc file.
Bước 5 — Kiểm tra. Xem Pod đã nhận config chưa:
kubectl exec -it deploy/api -n production -- env | grep APP_ENV
kubectl exec -it deploy/api -n production -- cat /etc/app/config/APP_ENV
Lỗi thường gặp & mẹo
1. Nhét dữ liệu nhạy cảm vào ConfigMap. Sai lầm nghiêm trọng nhất. Quy tắc: cứ là mật khẩu, token, key, chứng chỉ thì phải Secret. Không có ngoại lệ.
2. Tưởng base64 là bảo mật. kubectl get secret api-secret -o yaml cho ra base64, decode một dòng là ra plaintext. Muốn an toàn thật phải bật Encryption at Rest và siết RBAC.
3. Sửa ConfigMap nhưng Pod không cập nhật. Nếu tiêm qua env var, thay đổi chỉ có hiệu lực sau khi Pod restart. Mẹo: thêm annotation checksum của ConfigMap vào Pod template (kubectl rollout restart deploy/api để buộc rollout), hoặc dùng công cụ như Reloader để tự restart Pod khi ConfigMap/Secret đổi.
4. Quên -n namespace. ConfigMap và Secret là namespaced — Pod chỉ thấy được ConfigMap/Secret trong cùng namespace. Tạo ConfigMap ở default rồi mong Pod ở production dùng được là không xảy ra.
5. Giá trị số không để trong ngoặc kép. DB_PORT: 5432 sẽ lỗi vì ConfigMap chỉ nhận string. Phải viết DB_PORT: "5432".
6. Pod không khởi động vì thiếu key. Nếu dùng secretKeyRef trỏ tới key không tồn tại, Pod sẽ kẹt ở trạng thái CreateContainerConfigError. Kiểm tra bằng kubectl describe pod.
Mẹo vàng: Với Secret dùng cho GitOps, đừng bao giờ commit Secret thô. Dùng Sealed Secrets (Bitnami) hoặc External Secrets Operator để giữ Git sạch mà vẫn khai báo được.
Bài tập thực hành
- Tạo một ConfigMap tên
web-configchứaWELCOME_MSG="Xin chào"vàTHEME="dark", một Secretweb-secretchứaAPI_KEY. Deploy một Pod nginx và tiêm cả hai vào (ConfigMap quaenvFrom, Secret quavalueFrom). Dùngkubectl exec ... -- envđể xác nhận.
- Mount
web-configdưới dạng volume vào/etc/config. Vào trong Pod kiểm tra: mỗi key có phải một file riêng không? Nội dung file là gì?
- Sửa giá trị
THEMEtrong ConfigMap. Quan sát: với cách env var, giá trị trong Pod có đổi không? Với cách volume, sau khoảng 1 phút file có tự cập nhật không? Ghi lại khác biệt.
- (Nâng cao) Chạy
kubectl get secret web-secret -o yaml, lấy giá trị base64 củaAPI_KEYvà decode ngược lại bằngbase64 -d. Tự rút ra vì sao Secret mặc định chưa thực sự an toàn.
Tóm tắt
- ConfigMap chứa config không nhạy cảm (plain text); Secret chứa dữ liệu nhạy cảm (base64-encoded, không phải mã hóa).
- Base64 không phải bảo mật. Muốn an toàn thật: bật Encryption at Rest cho etcd, siết RBAC, dùng Sealed Secrets/Vault khi làm GitOps.
- Hai cách tiêm: env var (đơn giản, nhưng không tự cập nhật, phải restart Pod) và volume mount (hợp cho file, tự cập nhật động).
- Dùng
stringDatacho Secret để khỏi tự encode; nhớ để giá trị số trong ngoặc kép ở ConfigMap. - ConfigMap/Secret là namespaced — Pod chỉ dùng được trong cùng namespace.
- Tách config khỏi image giúp một image chạy được mọi môi trường, đúng tinh thần Twelve-Factor App — nền tảng của mọi quy trình deploy chuyên nghiệp.