ConfigMap 與 Secret:把設定和機密從映像檔裡拆出來
· tech
📑 目錄
前面幾篇,你已經能把一個 app 部署、對外服務了。但真實的 app 還缺一塊:設定——資料庫位址、feature flag,還有密碼、API key、憑證。這些東西有一條鐵律:不該寫死在映像檔或程式碼裡。K8s 用 ConfigMap(非機密設定)和 Secret(機密)把設定外部化,在執行時才注入 Pod。這篇講為什麼要這樣、兩者差在哪、以及怎麼注入。
為什麼:同一個映像檔,要能跑遍所有環境
設定與映像檔要分開,核心理由只有一句:映像檔要「不可變、可跨環境重用」。 同一個 my-app:1.0,應該能原封不動地跑在 Development、Staging、Production——差別只在注入的設定不同:
ConfigMap vs Secret:差在「機密不機密」
兩者用法幾乎一樣,都是存 key-value(或整份設定檔),差別在放的東西機不機密:
- ConfigMap:非機密的一般設定——資料庫主機名、日誌等級、feature flag、整份
application.yaml。 - Secret:機密——資料庫密碼、API key、TLS 憑證、token。
但這裡有個最多人誤解、也最危險的坑:Secret 預設只是 base64 編碼,不是加密。 base64 是「換一種表示法」,不是「上鎖」——任何能讀到那個 Secret 的人,一行指令就能還原出明文。所以要讓 Secret 真的安全,得再做三件事:開啟 etcd 的 encryption at rest(讓它在儲存層真的被加密)、用 RBAC 嚴格限制誰能讀、以及正式環境接外部的 secret manager(Vault、雲的 KMS)。把 Secret 當成「有存取控制的設定」,而不是「加密保險箱」,才不會被那個名字給的虛假安全感騙了。
兩種注入方式:環境變數 vs 掛成檔案
設定準備好了,怎麼送進 Pod?有兩種方式,各有適合的場景:
落成 YAML:建立設定、再注入 Pod
把上面兩張圖落成實際的宣告。先建 ConfigMap(明文)與 Secret(注意 data 要放 base64 後的值,或用 stringData 直接寫明文讓 K8s 幫你編):
apiVersion: v1
kind: ConfigMap
metadata: { name: web-config }
data:
LOG_LEVEL: "info" # 非機密:直接明文
application.yaml: | # 也可以放「整份設定檔」
server:
timeout: 30s
---
apiVersion: v1
kind: Secret
metadata: { name: web-secret }
type: Opaque
stringData:
DB_PASSWORD: "s3cr3t" # stringData:寫明文,K8s 存進去時自動 base64(仍非加密)
接著在 Deployment 的 Pod 樣板裡,兩種注入方式都示範一次——env 拉成環境變數、volumeMounts 掛成檔案:
spec:
containers:
- name: web
image: myrepo/web:1.0
env:
- name: LOG_LEVEL # ① 環境變數:從 ConfigMap 拉一個 key
valueFrom: { configMapKeyRef: { name: web-config, key: LOG_LEVEL } }
- name: DB_PASSWORD # 機密也一樣,改用 secretKeyRef
valueFrom: { secretKeyRef: { name: web-secret, key: DB_PASSWORD } }
volumeMounts:
- { name: cfg, mountPath: /etc/web } # ② 掛成檔案:整份 application.yaml 出現在這個目錄
volumes:
- name: cfg
configMap: { name: web-config }
兩個對照就是前面第二張圖的重點:env/...KeyRef 是環境變數注入(啟動時定死,改了要重啟 Pod);volumeMounts + volumes.configMap 是掛成檔案(更新 ConfigMap 後檔案會自動更新,但 app 得自己重讀)。要一次把整份 ConfigMap/Secret 灌成環境變數,還有 envFrom 可用,少寫很多行。
反思
設定與映像檔分離,是「一次建置、到處執行」的地基
剛學 Docker/K8s 時,我幹過把 DB 位址、甚至帳密直接寫進映像檔的蠢事——結果就是每換一個環境就得重 build 一個映像檔,dev 一個、prod 一個,亂成一團,還差點把密碼推上 git。ConfigMap/Secret 教我的,是一個乾淨的分界:映像檔管「程式碼與相依」,設定管「這次跑在哪、用什麼參數」,兩者分開。 這個分界的價值,是讓「同一個產物跑遍所有環境」成真——你在 Staging 驗過的那個映像檔,一個位元都不用改就能上 Production,只換一份設定。這跟 hermetic build 講的「產物可重現、可跨環境」根本是同一件事的一體兩面:一個管建置產物的純淨,一個管執行設定的注入。
Secret 只是 base64——名字給了你虛假的安全感
「Secret」這個名字很危險,因為它聽起來就很安全,讓人不自覺以為「放進 Secret 就上鎖了」。但它預設只是 base64,任何能存取它的人都能一秒還原明文。這件事給我的教訓超越了 K8s 本身:別讓一個東西的「名字」替你做安全判斷。 我現在看到任何號稱「加密」「安全」「保護」的功能,都會多問一句「它到底做了什麼、防住了誰」——是真的加密,還是只是編碼?是防住外人,還是連內部有權限的人也擋?名字是行銷,實際的威脅模型才是工程。搞清楚一個安全機制具體防住什麼、沒防住什麼,遠比記住它叫什麼重要。
好的設定管理,是讓「改設定」不等於「改程式」
我越來越覺得,一個系統成不成熟,看它「改一個設定有多痛」就知道。不成熟的系統,改一個參數要動程式碼、重新 build、重新部署,一改就是一趟大工程,於是大家能不改就不改、把設定寫死。成熟的系統,設定是外部的、注入的——改一個 feature flag、調一個閾值,不必碰程式碼、甚至不必重啟(掛成檔案的話)。ConfigMap/Secret 把設定變成一等公民,讓「調整行為」跟「改寫邏輯」徹底分開,這個分離本身就是一種可維護性。把「會變的東西」(設定)和「不太變的東西」(程式)分開,讓前者可以便宜地調整——這不只是 K8s 的智慧,是我看過所有好架構共通的一條線。