Bài 05
Bài 05 — ConfigMap & Secret: tách cấu hình khỏi code
Bốn bài đầu bạn đã chạy được app: đóng vào Pod, nhân bản bằng Deployment, phơi ra bằng Service, ngăn nắp bằng Namespace + RBAC. Nhưng có một thứ vẫn dính chặt sai chỗ: cấu hình.
Nhìn lại các YAML ta viết — biến môi trường, tên host DB, thậm chí mật khẩu — đang nằm cứng trong manifest hoặc nướng vào image. Bài này giải nỗi đau đó bằng một nguyên tắc kinh điển (12-factor app): cấu hình thuộc về môi trường, không thuộc về code. K8s hiện thực nó bằng hai đối tượng: ConfigMap (config thường) và Secret (dữ liệu nhạy cảm). Đây cũng là tiền đề để một image chạy được ở cả dev, staging, prod — chỉ đổi config, không đổi code.
Vấn đề: config dính vào code
❌ Nướng config vào image:
Dockerfile có ENV DB_HOST=10.0.0.5
→ đổi 1 setting = build lại image, push, redeploy. Kinh khủng.
→ dev/staging/prod khác config → phải build 3 image khác nhau?!
❌ Hardcode trong YAML rồi commit git:
env:
- name: DB_PASSWORD
value: "secretpass123" ← mật khẩu nằm chình ình trong git
→ ai clone repo cũng thấy. Lộ là toang.
❌ Mỗi môi trường 1 bộ YAML copy-paste:
deploy-dev.yaml, deploy-prod.yaml... lệch nhau từng dòng
→ sửa 1 chỗ quên sửa chỗ kia = bug môi trường.Cùng một mẫu đau ở chặng Compose (.env, biến môi trường) — giờ K8s làm bài bản hơn: tách config thành đối tượng riêng, gắn vào Pod lúc chạy, thay vì trộn vào image/manifest.
ConfigMap — kho cấu hình thường
ConfigMap lưu cấu hình không nhạy cảm dưới dạng key-value, tách rời khỏi Pod. Pod "nạp" ConfigMap lúc chạy. Đổi config = sửa ConfigMap, không đụng image.
Tạo ConfigMap — 4 cách
# 1. Từ literal (gõ tay từng cặp)
kubectl create configmap app-config \
--from-literal=APP_ENV=production \
--from-literal=LOG_LEVEL=info
# 2. Từ một file (cả file thành 1 value)
kubectl create configmap nginx-config --from-file=nginx.conf
# 3. Từ file .env (mỗi dòng KEY=VALUE thành 1 key)
kubectl create configmap app-config --from-env-file=app.env
# 4. Khai báo YAML (cách nên dùng — commit git được)apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: myapp
data:
APP_ENV: "production" # key-value đơn giản
LOG_LEVEL: "info"
DB_HOST: "mysql-service" # tên Service (Bài 04), không phải IP
app.properties: | # cả một FILE config (giá trị nhiều dòng)
timeout=30
retries=3
cache.enabled=trueNạp ConfigMap vào Pod — 3 kiểu
Kiểu 1 — từng biến môi trường (valueFrom): chọn lọc key.
env:
- name: ENVIRONMENT # tên biến trong container
valueFrom:
configMapKeyRef:
name: app-config
key: APP_ENV # lấy đúng key nàyKiểu 2 — nạp TẤT CẢ key thành biến (envFrom): gọn khi muốn cả bộ.
envFrom:
- configMapRef:
name: app-config # mọi key thành biến môi trườngKiểu 3 — mount thành FILE (volume): cần cho file cấu hình (vd nginx.conf).
volumeMounts:
- name: config-vol
mountPath: /etc/app/config # mỗi key thành 1 file trong thư mục này
volumes:
- name: config-vol
configMap:
name: app-config env / envFrom → config thành BIẾN MÔI TRƯỜNG (cho app đọc env)
volume mount → config thành FILE (cho app đọc file conf)💡 Mount
subPathnếu chỉ muốn chèn một file vào thư mục có sẵn mà không "đè" cả thư mục — ví dụ thay riêng/etc/nginx/nginx.confmà giữ nguyên các file khác.
Secret — kho dữ liệu nhạy cảm
Secret giống ConfigMap nhưng dành cho dữ liệu nhạy cảm: mật khẩu, API key, token, chứng chỉ TLS. Cách nạp vào Pod gần như y hệt ConfigMap.
⚠️ Sự thật quan trọng nhất: Secret KHÔNG phải mã hóa
Secret chỉ được encode base64 — KHÔNG phải encryption.
base64 ai cũng giải ngược được trong 1 giây:
echo "c2VjcmV0cGFzcw==" | base64 -d → secretpassĐừng để chữ "Secret" ru ngủ. Mặc định nó chỉ tránh "lộ thiên trong YAML", không chống được người đọc được etcd hay người có quyền get secret. Phần "làm cho Secret an toàn thật" ở cuối bài.
Tạo Secret
# Từ literal — kubectl tự encode base64 giúp
kubectl create secret generic db-secret \
--from-literal=DB_PASSWORD=secretpass123 \
--from-literal=DB_USER=adminapiVersion: v1
kind: Secret
metadata:
name: db-secret
namespace: myapp
type: Opaque
stringData: # stringData: bạn ghi PLAIN, K8s tự encode
DB_PASSWORD: "secretpass123"
DB_USER: "admin"
# data: # data: bạn phải tự encode base64 (phiền hơn)
# DB_PASSWORD: c2VjcmV0cGFzczEyMw==💡 Dùng
stringDatađể ghi plain cho dễ; K8s tự chuyển sang base64 khi lưu.datalà khi bạn đã có sẵn chuỗi base64.
Các loại Secret (không chỉ Opaque)
Opaque dữ liệu tùy ý (mặc định)
kubernetes.io/dockerconfigjson đăng nhập registry private
kubernetes.io/tls cặp cert + key cho HTTPS/Ingress
kubernetes.io/basic-auth user/password
kubernetes.io/ssh-auth khóa SSHVí dụ thực dụng — kéo image từ registry private (rất hay gặp khi không dùng Docker Hub công khai):
kubectl create secret docker-registry regcred \
--docker-server=registry.cong-ty.vn \
--docker-username=ci-bot \
--docker-password='****' \
-n myappspec:
imagePullSecrets: # khai trong Pod để được phép kéo image
- name: regcred
containers:
- name: app
image: registry.cong-ty.vn/myapp:1.0Nạp Secret vào Pod
# Kiểu env (1 biến)
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef: { name: db-secret, key: DB_PASSWORD }
# Kiểu envFrom (cả bộ)
envFrom:
- secretRef: { name: db-secret }
# Kiểu volume (mount thành file — hay dùng cho cert TLS)
volumeMounts:
- name: tls
mountPath: /etc/tls
readOnly: true
volumes:
- name: tls
secret: { secretName: tls-secret }Đổi config xong, Pod có tự cập nhật không?
Đây là chỗ nhiều người vấp đau:
Nạp kiểu ENV (env/envFrom):
Đổi ConfigMap/Secret → Pod đang chạy KHÔNG thấy thay đổi.
Biến môi trường chỉ đọc 1 lần lúc container start.
→ PHẢI restart Pod: kubectl rollout restart deployment/<tên>
Nạp kiểu VOLUME (mount file):
File trong container TỰ cập nhật sau ít phút (kubelet sync).
Nhưng app phải biết đọc lại file — nhiều app vẫn cần reload/restart.⚠️ Đây là cái bẫy kinh điển: sửa ConfigMap, thấy kubectl get configmap đã đổi, nhưng app vẫn chạy giá trị cũ — vì nó nạp qua env và chưa restart. Nhớ: đổi config kiểu env thì rollout restart.
Immutable — khóa cứng để an toàn & nhanh
apiVersion: v1
kind: ConfigMap
metadata: { name: app-config }
immutable: true # khóa: không sửa được nữa, muốn đổi phải tạo cái mới
data: { APP_ENV: "production" }ConfigMap/Secret immutable: true không sửa được (tránh lỡ tay), và cluster đỡ tốn công theo dõi thay đổi → tốt cho cluster lớn. Muốn đổi thì tạo ConfigMap tên mới rồi trỏ Deployment sang — cũng tự nhiên kích hoạt rolling update.
Làm cho Secret an toàn THẬT
base64 chưa đủ. Sản xuất thật nên kết hợp:
- Encryption at rest cho etcd — bật mã hóa Secret khi lưu vào etcd (cấu hình
EncryptionConfigurationở API Server). Khi đó người đọc trộm etcd cũng chỉ thấy ciphertext. - RBAC siết quyền
get secret(Bài 04) — chỉ SA/người thật sự cần mới được đọc.kubectl auth can-i get secrets -n prod. - External secret managers — không lưu secret trong git/cluster trực tiếp:
- Sealed Secrets (Bitnami) — mã hóa secret thành dạng an toàn để commit git, chỉ controller trong cluster giải được.
- External Secrets Operator / Vault — secret nằm ở Vault/AWS Secrets Manager, cluster đồng bộ về khi cần.
- Không bao giờ commit Secret plain lên git. Đây là rò rỉ phổ biến nhất đời thực.
💡 Quy tắc vàng: ConfigMap có thể vào git thoải mái; Secret thì không bao giờ ở dạng plain trong git. Cần GitOps cho secret thì dùng Sealed Secrets / External Secrets.
🚀 Lab — ConfigMap & Secret từ A tới Z
Dùng cluster kind từ các bài trước.
1. Tạo & nạp ConfigMap kiểu env
kubectl create configmap demo-config \
--from-literal=APP_ENV=production --from-literal=LOG_LEVEL=debug
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata: { name: cfg-demo }
spec:
restartPolicy: Never
containers:
- name: app
image: busybox
command: ["sh","-c","echo ENV=\$APP_ENV LOG=\$LOG_LEVEL; sleep 3600"]
envFrom:
- configMapRef: { name: demo-config }
EOF
kubectl logs cfg-demo # ENV=production LOG=debug ← nạp từ ConfigMap!2. Mount ConfigMap thành file
kubectl create configmap site --from-literal=index.html="<h1>Chao tu ConfigMap</h1>"
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata: { name: web-cfg }
spec:
containers:
- name: nginx
image: nginx:1.25
volumeMounts:
- { name: html, mountPath: /usr/share/nginx/html }
volumes:
- name: html
configMap: { name: site }
EOF
kubectl exec web-cfg -- cat /usr/share/nginx/html/index.html # thấy file từ ConfigMap3. Secret & sự thật base64
kubectl create secret generic demo-secret --from-literal=PASSWORD=sieubaomat
kubectl get secret demo-secret -o jsonpath='{.data.PASSWORD}' # chuỗi base64
kubectl get secret demo-secret -o jsonpath='{.data.PASSWORD}' | base64 -d # → sieubaomat
# → tận mắt: "Secret" chỉ là base64, giải ngược trong 1 giây.4. Bẫy "đổi config không ăn"
kubectl create deployment envtest --image=busybox \
-- sh -c 'echo MSG=$MSG; sleep 3600'
kubectl set env deployment/envtest MSG=v1
kubectl logs deploy/envtest # MSG=v1
kubectl set env deployment/envtest MSG=v2 # set env tự trigger rollout — quan sát:
kubectl rollout status deployment/envtest
kubectl logs deploy/envtest # MSG=v2
# (nếu sửa qua ConfigMap thay vì set env, phải: kubectl rollout restart deployment/envtest)5. Dọn
kubectl delete pod cfg-demo web-cfg 2>/dev/null
kubectl delete deployment envtest 2>/dev/null
kubectl delete configmap demo-config site 2>/dev/null
kubectl delete secret demo-secret 2>/dev/nullCâu chuyện thực tế
Hai sự cố config dạy tôi nhớ đời.
Sự cố một — rò secret lên git. Thời mới chuyển K8s, một bạn trong team commit thẳng file secret.yaml chứa mật khẩu DB production (dạng stringData, plain) lên repo nội bộ. Không ai để ý, repo đó vài chục người truy cập. Hai tuần sau audit bảo mật phát hiện. May là repo nội bộ, không phải public, nhưng chúng tôi vẫn phải đổi toàn bộ mật khẩu DB và rà lại lịch sử git. Từ đó tôi đặt luật cứng: Secret plain không bao giờ vào git — cần thì Sealed Secrets. Một dòng value: "secretpass" tưởng vô hại có thể thành nửa ngày dọn dẹp.
Sự cố hai — "tôi sửa config rồi mà sao không ăn?". Một chiều, dev đổi LOG_LEVEL trong ConfigMap từ info sang debug để soi một bug khó. Đợi mãi log vẫn không chi tiết hơn. Bạn ấy kubectl get configmap — rõ ràng đã là debug. Loay hoay gần một tiếng, nghĩ ConfigMap hỏng. Tôi hỏi đúng một câu: "Pod nạp qua env hay volume?" — env. "Em restart Pod chưa?" — chưa. kubectl rollout restart deployment/... — log debug hiện ra ngay. Biến môi trường chỉ đọc một lần lúc container khởi động; sửa ConfigMap không "thấm ngược" vào Pod đang chạy.
Bài học:
Config thuộc về môi trường, không phải image. Build image một lần, chạy mọi nơi bằng cách đổi ConfigMap/Secret. Build lại image chỉ để đổi một setting là dấu hiệu làm sai.
"Secret" chỉ là base64, không phải mã hóa. Đừng để cái tên đánh lừa. An toàn thật cần encryption-at-rest + RBAC + (Sealed Secrets/Vault).
Secret plain KHÔNG BAO GIỜ vào git. Đây là rò rỉ phổ biến nhất. Một dòng vô ý = nửa ngày đổi mật khẩu và rà log.
Đổi config kiểu env → phải
rollout restart. Pod không tự thấy thay đổi env. Đây là cái bẫy "sửa rồi mà không ăn" kinh điển. Mount kiểu volume thì tự cập nhật (nhưng app vẫn cần biết reload).ConfigMap vào git được; Secret thì không. Phân biệt rạch ròi hai loại ngay từ đầu, đừng nhét mật khẩu vào ConfigMap cho tiện.
Pitfalls
Nhét dữ liệu nhạy cảm vào ConfigMap. ConfigMap không hề được bảo vệ — mật khẩu phải vào Secret (và lý tưởng là Secret được mã hóa thật).
Tưởng Secret là mã hóa. Chỉ base64.
base64 -dra plain ngay. Bật encryption-at-rest nếu cần thật.Commit Secret plain lên git. Rò rỉ kinh điển. Dùng Sealed Secrets/External Secrets cho GitOps.
Sửa ConfigMap/Secret rồi tưởng Pod tự ăn. Kiểu env không tự cập nhật →
kubectl rollout restart.datavsstringDatalẫn lộn.datacần base64 sẵn;stringDataghi plain. Nhét plain vàodata→ giá trị bị giải sai.Quên
imagePullSecretskhi dùng registry private → Pod kẹtImagePullBackOffvới lỗi auth. Tạo secretdocker-registryvà khai vào Pod.ConfigMap/Secret khác namespace với Pod. Chúng là tài nguyên namespaced (Bài 04) — Pod chỉ nạp được ConfigMap/Secret cùng namespace. Khác ns là không thấy.
Mount volume đè cả thư mục ngoài ý muốn. Mount ConfigMap vào
/etc/nginxsẽ che hết file gốc trong đó. DùngsubPathđể chỉ chèn 1 file.Để key sai tên trong
configMapKeyRef/secretKeyRef→ Pod kẹt tạo (CreateContainerConfigError).describe podchỉ rõ key nào thiếu.Dồn mọi config vào 1 ConfigMap khổng lồ. Tách theo concern (app-config, db-config...) để dễ tái dùng và phân quyền RBAC riêng.
Tóm tắt
- Nguyên tắc: cấu hình thuộc môi trường, không thuộc code (12-factor). Build image 1 lần, chạy mọi nơi bằng cách đổi ConfigMap/Secret.
- ConfigMap = config thường, key-value. Tạo từ literal/file/env-file/YAML.
- Secret = dữ liệu nhạy cảm. ⚠️ chỉ base64, KHÔNG mã hóa. Loại: Opaque, dockerconfigjson (registry private), tls, basic-auth, ssh-auth.
- 3 cách nạp (cả hai):
env(1 biến quavalueFrom),envFrom(cả bộ), volume (thành file — chonginx.conf, cert TLS).subPathđể chèn 1 file. - Cập nhật: kiểu env KHÔNG tự ăn →
kubectl rollout restart; kiểu volume tự sync (app cần reload).immutable: trueđể khóa. stringDataghi plain (K8s tự encode);datacần base64 sẵn.- Registry private: Secret
docker-registry+imagePullSecrets. - An toàn thật: encryption-at-rest cho etcd + RBAC siết
get secret+ Sealed Secrets/Vault. Secret plain không bao giờ vào git. - Cùng namespace: Pod chỉ nạp ConfigMap/Secret cùng ns.
Bài sau (Bài 06 — Lưu trữ & StatefulSet) trả nốt món nợ ta nhắc đi nhắc lại: "database phải dùng StatefulSet, không phải Deployment". Ta sẽ đào sâu storage (PV/PVC/StorageClass, access mode, reclaim policy) rồi gặp StatefulSet — thứ cho mỗi Pod một danh tính và một ổ đĩa riêng ổn định, để chạy DB và app có trạng thái trên K8s mà không làm hỏng dữ liệu.
Nếu hôm nay bạn không còn nhét mật khẩu vào YAML và biết vì sao sửa ConfigMap mà app "không ăn" — bạn đã quản cấu hình như dân chuyên.
— Minh Hưng