ConfigMap và Secret
Part VI bước vào cấu hình và chính sách, và viên gạch đầu là chuyện cơ bản nhất mọi ứng dụng đều cần: cấu hình. Nướng thẳng APP_MODE=production hay mật khẩu database vào image là điều cấm kỵ: đổi cấu hình phải build lại image, và mật khẩu nằm chình ình trong layer. Kubernetes tách cấu hình ra hai object: ConfigMap cho dữ liệu không nhạy cảm, Secret cho dữ liệu nhạy cảm, rồi tiêm vào pod lúc chạy. Bài này đào cả hai song song vì chúng gần như sinh đôi, chỉ khác ở chỗ "nhạy cảm".
ConfigMap: cấu hình thường
Tài liệu: "A ConfigMap is an API object used to store non-confidential data in key-value pairs. Pods can consume ConfigMaps as environment variables, command-line arguments, or as configuration files in a volume." Hai giới hạn cần nhớ ngay: "ConfigMap does not provide secrecy or encryption. If the data ... are confidential, use a Secret" và "The data stored in a ConfigMap cannot exceed 1 MiB."
apiVersion: v1
kind: ConfigMap
metadata: {name: appcfg}
data:
APP_MODE: "production"
MAX_CONN: "100"
app.properties: | # giá trị nhiều dòng -> hợp làm file cấu hình
color=blue
retries=3
Secret: cấu hình nhạy cảm
Tài liệu: "A Secret is an object that contains a small amount of sensitive data such as a password, a token, or a key ... Secrets are similar to ConfigMaps but are specifically intended to hold confidential data." Khai bằng stringData (Kubernetes tự encode sang base64 lưu ở data):
apiVersion: v1
kind: Secret
metadata: {name: appsecret}
type: Opaque # loại mặc định cho dữ liệu tự định nghĩa
stringData:
DB_PASSWORD: "s3cr3t-pa$$w0rd"
api.key: "AKIA-FAKE-1234567890"
type: Opaque là loại chung; còn có các loại chuyên (kubernetes.io/tls cho cặp khóa TLS, kubernetes.io/dockerconfigjson cho thông tin pull image, kubernetes.io/service-account-token...). Nhìn vào data sau khi tạo:
kubectl get secret appsecret -o jsonpath='{.data}'
kubectl get secret appsecret -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
{"DB_PASSWORD":"czNjcjN0LXBhJCR3MHJk","api.key":"QUtJQS1GQUtFLTEyMzQ1Njc4OTA="}
s3cr3t-pa$$w0rd
Đây là điểm phải khắc cốt: czNjcjN0... chỉ là base64, ai cũng base64 -d ra s3cr3t-pa$$w0rd trong một giây. Tài liệu nói thẳng: "Kubernetes Secrets are, by default, stored unencrypted in the API server's underlying data store (etcd)." base64 là mã hóa ký tự, không phải mã hóa bảo mật. Secret hơn ConfigMap không phải vì giấu giỏi hơn, mà vì nó được đối xử như nhạy cảm (không in ra log/describe, có thể bật mã hóa at-rest, kiểm soát RBAC riêng).
Bốn cách tiêm vào pod
Tài liệu liệt kê bốn đường tiêu thụ: trong command/args, làm biến môi trường, làm file trong volume, hoặc đọc qua API. Một pod dùng ba cách đầu cho cả ConfigMap lẫn Secret:
spec:
containers:
- name: c
image: busybox:1.36
command: ["sh","-c","sleep 3600"]
env:
- name: MODE_FROM_CM
valueFrom: {configMapKeyRef: {name: appcfg, key: APP_MODE}} # 1 key CM
- name: PW_FROM_SECRET
valueFrom: {secretKeyRef: {name: appsecret, key: DB_PASSWORD}} # 1 key Secret
envFrom:
- configMapRef: {name: appcfg} # TẤT CẢ key CM thành env
volumeMounts:
- {name: cfgvol, mountPath: /etc/cfg, readOnly: true}
- {name: secvol, mountPath: /etc/sec, readOnly: true}
volumes:
- name: cfgvol
configMap: {name: appcfg}
- name: secvol
secret: {secretName: appsecret}
Kiểm chứng env:
kubectl exec cfg-pod -- sh -c 'echo "$MODE_FROM_CM | $PW_FROM_SECRET | $APP_MODE | $MAX_CONN"'
production | s3cr3t-pa$$w0rd | production | 100
MODE_FROM_CM/PW_FROM_SECRET lấy đúng một key (configMapKeyRef/secretKeyRef); APP_MODE và MAX_CONN đến từ envFrom — gom mọi key của ConfigMap thành biến môi trường cùng tên. Kiểm chứng volume:
kubectl exec cfg-pod -- sh -c 'ls /etc/cfg; cat /etc/cfg/app.properties; ls /etc/sec; cat /etc/sec/api.key'
APP_MODE MAX_CONN app.properties
color=blue
retries=3
DB_PASSWORD api.key
AKIA-FAKE-1234567890
Mỗi key thành một file trong thư mục mount: app.properties (giá trị nhiều dòng) trở thành file cấu hình đúng nghĩa để app đọc. Secret cũng vậy, api.key thành file (nội dung đã được giải base64 sẵn — app đọc ra giá trị thật, không phải base64).
Khác biệt then chốt: file tự cập nhật, env thì không
Đây là điều hay bị bỏ sót và gây lỗi production. Sửa ConfigMap sau khi pod đã chạy thì sao? Tài liệu phân biệt rạch ròi: volume "projected keys are eventually updated" còn "ConfigMaps consumed as environment variables are not updated automatically and require a pod restart." Thử: đổi APP_MODE và app.properties rồi chờ kubelet đồng bộ:
kubectl patch configmap appcfg --type=merge -p '{"data":{"APP_MODE":"staging","app.properties":"color=red\nretries=9\n"}}'
# ... chờ ~75 giây (kubelet sync period) ...
kubectl exec cfg-pod -- sh -c 'echo VOLUME: $(cat /etc/cfg/APP_MODE); cat /etc/cfg/app.properties'
kubectl exec cfg-pod -- sh -c 'echo ENV: $MODE_FROM_CM / $APP_MODE'
VOLUME: staging
color=red
retries=9
ENV: production / production
Tương phản nằm ngay đây: file trong volume đã tự đổi sang staging/color=red (kubelet định kỳ làm mới), nhưng biến môi trường vẫn cứng ở production, vì env được nạp một lần lúc container khởi động và đông cứng ở đó. Bài học vận hành: nếu app cần nạp lại cấu hình nóng, hãy tiêm qua volume và cho app theo dõi file; nếu tiêm qua env, đổi ConfigMap là vô nghĩa cho tới khi pod restart (và một Deployment sẽ không tự restart chỉ vì ConfigMap đổi — đó là lý do người ta hay thêm hash ConfigMap vào annotation pod template để buộc rollout). (Lưu ý độ trễ: tài liệu nói cập nhật volume mất tới "kubelet sync period + cache propagation delay" — không tức thì.)
Secret trong etcd: at-rest, và cụm của ta đã bật mã hóa
Tài liệu cảnh báo Secret "stored unencrypted in etcd" mặc định, và khuyên việc đầu tiên là "Enable Encryption at Rest". Cụm của ta đã làm đúng việc đó từ Bài 5 (EncryptionConfiguration với provider aescbc). Đọc thẳng etcd để thấy khác biệt với ConfigMap (plaintext) ở Bài 30:
sudo etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/etcd/etcd-ca.pem --cert=/etc/etcd/etcd.pem --key=/etc/etcd/etcd-key.pem \
get /registry/secrets/default/appsecret | head -c 60 | tr -c '[:print:]' '.'
/registry/secrets/default/appsecret.k8s:enc:aescbc:v1:key1:.....
Tiền tố k8s:enc:aescbc:v1:key1: cho biết giá trị đằng sau là ciphertext, đã được api-server mã hóa bằng khóa AES-CBC trước khi ghi vào etcd. So với Bài 30 nơi Deployment hiện rõ apps/v1 plaintext: ConfigMap/Deployment lưu trần, Secret thì mã hóa (ở cụm này). Đây cũng là lời cảnh báo khác của tài liệu, đáng nhớ: "anyone who is authorized to create a Pod in a namespace can use that access to read any Secret in that namespace". Mã hóa at-rest chặn người đọc đĩa/etcd, nhưng ai tạo được pod trong namespace vẫn mount được Secret ra; bảo vệ đầy đủ cần thêm RBAC (Part XI).
🧹 Dọn dẹp
kubectl delete pod cfg-pod --now
kubectl delete configmap appcfg
kubectl delete secret appsecret
Object trong cluster, xóa là sạch. Cụm về lại hai pod CoreDNS. Manifest ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 31-configmap-secret.
Tổng kết
Tách cấu hình khỏi image bằng ConfigMap (dữ liệu thường, ≤1 MiB, không bí mật) và Secret (nhạy cảm). Cả hai tiêm vào pod theo bốn cách, ta dùng ba: configMapKeyRef/secretKeyRef (một key), envFrom (mọi key thành env), và volume (mỗi key một file). Khác biệt then chốt: volume tự cập nhật khi sửa (sau ~kubelet sync period) còn env đông cứng lúc khởi động, cần restart, nên chọn volume nếu cần reload nóng. Secret không mã hóa hơn ConfigMap về bản chất: data chỉ là base64 (giải ra trong một giây), nó chỉ được đối xử nhạy cảm hơn. Mã hóa thật cần bật at-rest: cụm của ta đã bật aescbc từ Bài 5, nên Secret trong etcd hiện k8s:enc:aescbc:... (ciphertext) thay vì plaintext như ConfigMap; dù vậy ai tạo được pod vẫn đọc được Secret, nên còn cần RBAC.
Bài 32 đào sâu hơn phần resource management ở góc vận hành: cách Kubernetes tính tài nguyên node, cgroup, và các cơ chế quản lý đã chạm ở Bài 22 (requests/limits/QoS) giờ nhìn từ phía node và kubelet.