ConfigMap 與 Secret:把設定和機密從映像檔裡拆出來

· tech

#kubernetes#concept

📑 目錄

前面幾篇,你已經能把一個 app 部署對外服務了。但真實的 app 還缺一塊:設定——資料庫位址、feature flag,還有密碼、API key、憑證。這些東西有一條鐵律:不該寫死在映像檔或程式碼裡。K8s 用 ConfigMap(非機密設定)和 Secret(機密)把設定外部化,在執行時才注入 Pod。這篇講為什麼要這樣、兩者差在哪、以及怎麼注入。

為什麼:同一個映像檔,要能跑遍所有環境

設定與映像檔要分開,核心理由只有一句:映像檔要「不可變、可跨環境重用」。 同一個 my-app:1.0,應該能原封不動地跑在 Development、Staging、Production——差別只在注入的設定不同:

同一個映像檔,跑遍所有環境 映像檔my-app:1.0(不可變) DevelopmentConfigMap + Secret StagingConfigMap + Secret ProductionConfigMap + Secret Pod(dev) Pod(staging) Pod(production) 12-factor:設定放環境/外部,不烤進映像檔——同一映像檔才能跨環境重用
同一個不可變映像檔(藍),配上三個環境各自的 ConfigMap + Secret(橘),就跑出三個環境的 Pod。這正是 12-factor 的核心原則——設定放在環境裡、不烤進映像檔。把設定抽出來,你才能「一次 build、到處跑」,而不是每個環境各 build 一個塞死設定的映像檔;這跟 hermetic build 追求的「產物可重現、可跨環境」是同一種偏執

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?有兩種方式,各有適合的場景:

兩種注入方式:環境變數 vs 掛成檔案 ① 環境變數(env) 把 key 當環境變數注入 ✓ 簡單直覺、適合少量設定 ✗ 改了設定要「重啟 pod」才生效 (env 是啟動時定的,不會熱更新) ② 掛成檔案(volume) 掛成目錄裡的檔案 ✓ 適合整份設定檔 / TLS 憑證 ✓ 更新 ConfigMap → 檔案自動更新 (不必重啟,app 需自己重讀) ⚠ Secret 預設只是 base64,不是加密! 真安全要:encryption at rest + RBAC 限制存取 + 正式環境接外部 KMS / Vault
環境變數最簡單,但它在 pod 啟動時就定好了,改了設定得重啟 pod 才生效。掛成檔案(volume)則適合整份設定檔或憑證,而且有個好處:更新 ConfigMap 之後,掛進去的檔案會自動更新(雖然 app 通常還是得自己重讀檔案)。實務上小量設定用 env、整份設定檔或憑證用 volume。無論哪種,都別忘了那個紅字——Secret 的名字給了你安全感,但預設它只是 base64

落成 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 的智慧,是我看過所有好架構共通的一條線。