iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Kubernetes

不是背 YAML!30 天從零打造 Kubernetes 微服務:從本機實戰一路到 CKA系列 第 11

Day 11|ConfigMap 與 Secret:為什麼設定不能全部寫死在程式?

  • 分享至 

  • xImage
  •  

來到 Day 11 啦!

昨天我們已經把自己的 FastAPI Application 放進 Kubernetes 裡面,現在架構大概是:

Client
↓
Service
↓
FastAPI Pod

目前這個 API 還很單純,幾乎沒有什麼外部設定。

但真正在公司開發服務時,一個 Application 通常不會只有一套環境,而是可能同時存在:

development
staging
production

也就是開發環境、測試環境與正式環境。

而不同環境使用的設定通常也不一樣。

例如 development 的 PostgreSQL 可能叫:

postgres-dev

production 的 PostgreSQL 則可能是:

postgres-prod

甚至它們可能位於完全不同的 Cluster、Network 或 Cloud Service。

這時候就會遇到一個很重要的問題:

這些環境設定到底應該寫在哪裡?


不要把環境設定寫死在程式裡

假設我們直接在 Python 裡面寫:

DATABASE_HOST = "10.0.0.17"

乍看之下好像沒什麼問題,程式也確實可以正常執行。

但之後如果 staging 的 Database IP 是:

10.0.1.23

production 又變成:

10.0.5.31

那我們是不是每換一個環境,就要修改一次程式碼?

甚至還要重新:

Build Image
↓
Push Image
↓
Deploy

這樣其實很不合理。

因為:

Application Logic

跟:

Environment Configuration

本來就是兩件不同的事情。

Application Image 應該只負責描述:

我的程式怎麼執行。

至於 Database 在哪裡、Redis Host 是什麼、目前是哪個環境,則應該由部署環境決定。

所以我們希望做到:

Application Image
+
Environment Configuration

彼此分離。

這樣同一個 Docker Image 就可以部署到 development、staging、production,只需要提供不同的 Configuration。

這也是 Container 化非常重要的一個觀念。


先修改 FastAPI,讓程式從環境變數讀設定

首先修改:

app/main.py

內容改成:

import os

from fastapi import FastAPI


app = FastAPI()


APP_ENV = os.getenv(
    "APP_ENV",
    "unknown"
)


@app.get("/")
def root():

    return {
        "message": "Hello from Kubernetes",
        "environment": APP_ENV
    }


@app.get("/health/live")
def live():

    return {
        "status": "alive"
    }

這裡最大的變化是:

os.getenv("APP_ENV", "unknown")

它的意思是:

從目前 Process 的 Environment Variable 中,尋找一個叫做 APP_ENV 的變數。

如果有找到,例如:

APP_ENV=production

那:

APP_ENV

就會得到:

production

如果完全沒有設定 APP_ENV,則使用第二個參數:

"unknown"

作為預設值。

所以現在 Application 不再需要知道:

我現在是不是 production?

它只需要知道:

我要讀取 APP_ENV。

至於:

APP_ENV 到底是多少?

交給 Kubernetes 決定。

這就是我們要的:

Application
≠
Configuration

Kubernetes 怎麼管理 Configuration?

Kubernetes 提供了一個很常使用的 Resource:

ConfigMap

ConfigMap 的主要用途,就是保存:

非敏感的 Application Configuration。

例如:

APP_ENV
REDIS_HOST
DATABASE_HOST
LOG_LEVEL
FEATURE_FLAG

這些資料通常不是秘密,但不同環境可能會有不同的值。


建立 ConfigMap

新增:

k8s/02-configmap.yaml

內容:

apiVersion: v1

kind: ConfigMap

metadata:
  name: app-config
  namespace: cka-lab

data:
  APP_ENV: local-k8s

  REDIS_HOST: redis

  DATABASE_HOST: postgres

這裡我們建立了一個:

ConfigMap

名稱叫:

app-config

並且放在:

cka-lab

這個 Namespace 裡。

真正的設定內容放在:

data:

下面。

例如:

APP_ENV: local-k8s

代表:

APP_ENV=local-k8s

而:

REDIS_HOST: redis

代表未來 Application 要連線 Redis 時,可以使用:

redis

這個 Hostname。

同樣地:

DATABASE_HOST: postgres

則表示未來 Database Host 預計使用:

postgres

在 Kubernetes 裡,我們之後通常會讓 Service Name 本身就成為其他 Pod 可以解析的 DNS Name,因此像:

redis
postgres

這種名稱在 Kubernetes 架構裡非常常見。

後面接 Redis 與 PostgreSQL 時,我們會真正把這件事情串起來。


ConfigMap 適合放什麼?

你可以把 ConfigMap 想成:

Application 的一般設定檔。

例如不同環境可能設定:

APP_ENV=staging

或者:

LOG_LEVEL=debug

甚至:

FEATURE_NEW_CHECKOUT=true

這些資料即使被看到,通常也不會直接造成安全問題。

所以很適合放進 ConfigMap。

但如果今天資料變成:

DATABASE_PASSWORD
API_TOKEN
PRIVATE_KEY

就不應該再放進 ConfigMap。

這時候要使用另外一種 Kubernetes Resource:

Secret

Secret 是做什麼的?

Secret 專門用來管理比較敏感的資料。

例如:

Password
Token
API Key
Certificate
Private Key

建立:

k8s/03-secret.yaml

內容:

apiVersion: v1

kind: Secret

metadata:
  name: app-secret
  namespace: cka-lab

type: Opaque

stringData:
  POSTGRES_USER: appuser
  POSTGRES_PASSWORD: devpassword
  POSTGRES_DB: appdb

這裡建立了一個 Secret:

app-secret

我們使用:

type: Opaque

代表這是一個一般用途的 Secret。

https://ithelp.ithome.com.tw/upload/images/20260909/20168537WOxh7cwkYE.png

資料則放在:

stringData:

下面。

例如:

POSTGRES_PASSWORD: devpassword

之後 Kubernetes 就可以把這些值注入 Container。


Secret 和 ConfigMap 最大差別是什麼?

從 Application 的角度來看,它們其實很像。

最後都可以變成:

Environment Variable

讓程式讀取。

但 Kubernetes 希望我們從語意上把它們分開。

ConfigMap 表示:

普通設定

Secret 表示:

敏感設定

這不只是看起來比較漂亮。

因為之後在正式環境裡,我們可以針對 Secret 做更嚴格的:

RBAC Permission
Encryption
Audit
Secret Rotation

管理。

因此從一開始就把它們分開,是很好的習慣。


但是要注意:Secret 不代表絕對安全

很多剛開始學 Kubernetes 的人會有一個誤解:

只要放進 Kubernetes Secret,Password 就安全了。

其實不是。

Secret 只是 Kubernetes 專門管理敏感資料的一種 Resource。

真正到了 Production,通常還需要考慮:

RBAC
Encryption at Rest
External Secret Manager
Git Secret Management

例如限制:

哪些 ServiceAccount 可以讀取 Secret?

或者啟用:

Encryption at Rest

讓 etcd 裡面的敏感資料被加密。

大型系統甚至可能使用:

AWS Secrets Manager
Google Secret Manager
HashiCorp Vault

再透過 External Secrets 等機制同步進 Kubernetes。

另外也非常重要:

Production 的 Secret 不應該直接把真正 Password 寫進 Git Repository。

不過我們現在只是本機 Kind Lab,所以暫時使用:

devpassword

來練習 Secret 的概念即可。

重點是先理解:

ConfigMap
→ 一般 Configuration

Secret
→ Sensitive Configuration

接下來,把 ConfigMap 和 Secret 注入 API Container

現在 ConfigMap 與 Secret 都建立好了,但 Application 還不知道它們存在。

因此我們要修改 API Deployment。

在 Container 裡加入:

envFrom:

  - configMapRef:
      name: app-config

  - secretRef:
      name: app-secret

那要加在哪裡呢?
要加在你 k8s/01-api.yaml 裡面。

我們現在的結構是:

k8s-30days/
├── app/
│   ├── Dockerfile
│   ├── main.py
│   └── requirements.txt
│
├── k8s/
│   ├── 00-namespace.yaml
│   ├── 01-api.yaml        ← 加在這裡
│   ├── 02-configmap.yaml
│   └── 03-secret.yaml
│
└── kind-config.yaml

02-configmap.yaml 是「建立 ConfigMap」,03-secret.yaml 是「建立 Secret」,但真正要告訴 API Pod「請把這兩個設定讀進來」的地方,是 Deployment,也就是你的 01-api.yaml

假設你原本的 01-api.yaml 類似這樣:

apiVersion: apps/v1
kind: Deployment

metadata:
  name: api
  namespace: cka-lab

spec:
  replicas: 2

  selector:
    matchLabels:
      app: api

  template:
    metadata:
      labels:
        app: api

    spec:
      containers:
        - name: api
          image: cka-api:v2
          imagePullPolicy: IfNotPresent

          ports:
            - containerPort: 8000

你要把:

envFrom:
  - configMapRef:
      name: app-config

  - secretRef:
      name: app-secret

加在 同一個 Container 裡面

也就是變成:

apiVersion: apps/v1
kind: Deployment

metadata:
  name: api
  namespace: cka-lab

spec:
  replicas: 2

  selector:
    matchLabels:
      app: api

  template:
    metadata:
      labels:
        app: api

    spec:
      containers:
        - name: api
          image: cka-api:v2
          imagePullPolicy: IfNotPresent

          ports:
            - containerPort: 8000

          envFrom:
            - configMapRef:
                name: app-config

            - secretRef:
                name: app-secret

這邊要特別注意一下 YAML 的層級:

Deployment
└── spec
    └── template
        └── spec
            └── containers
                └── - name: api
                    ├── image
                    ├── ports
                    └── envFrom   ← 加這裡

所以 envFrom 是跟:

完整概念可以想成:

ConfigMap
    ↓
Environment Variables
    ↓
Container

以及:

Secret
    ↓
Environment Variables
    ↓
Container

envFrom 的意思就是:

把這整個 ConfigMap 或 Secret 裡的 Key,全部轉成 Environment Variable 注入 Container。

例如 ConfigMap 裡面有:

APP_ENV: local-k8s

Container 裡最後就會看到:

APP_ENV=local-k8s

Secret 裡有:

POSTGRES_USER: appuser
POSTGRES_PASSWORD: devpassword

Container 裡則會出現:

POSTGRES_USER=appuser
POSTGRES_PASSWORD=devpassword

這時候 Python 就可以透過:

os.getenv(...)

取得這些資料。

整個流程其實就是:

02-configmap.yaml
        ↓
建立 app-config

03-secret.yaml
        ↓
建立 app-secret

01-api.yaml
        ↓
Deployment 指定 envFrom
        ↓
Pod 啟動
        ↓
ConfigMap / Secret
被轉成 Environment Variables
        ↓
FastAPI main.py
透過 os.getenv() 讀取

Application 完全不需要知道 ConfigMap 或 Secret 的存在。

它只需要知道:

我要讀 Environment Variable。

這種設計非常重要,因為未來即使離開 Kubernetes,你把 Application 放到 Docker Compose、VM 或 GCP 的 Cloud Run,程式都不一定需要修改。


因為程式修改了,所以重新 Build Image

剛剛我們修改了:

app/main.py

我們在 main.py 裡面加上了

APP_ENV = os.getenv( "APP_ENV", "unknown" )

所以舊的:

cka-api:v1

Image 裡面還是舊程式。

因此我們要重新 Build 一個新版本:

先回到 k8s-30days 的位置,執行

docker build -t cka-api:v2 ./app

現在本機 Docker 裡會多出:

cka-api:v2

https://ithelp.ithome.com.tw/upload/images/20260909/20168537dgYxsQjnCo.png

但要記得,我們使用的是:

Kind Kubernetes Cluster

Kind 的 Node 本身其實是一個 Docker Container。

所以你在 Mac 本機 Build 出來的 Image,不代表 Kind 裡面自動就看得到。

因此還要執行:

kind load docker-image \
  cka-api:v2 \
  --name cka-lab

https://ithelp.ithome.com.tw/upload/images/20260909/20168537sLURiqbBTo.png

把 Image 載入 Kind Cluster。

接著把 Deployment 裡原本的:

image: cka-api:v1

改成:

image: cka-api:v2

那是要改哪個檔案?
要改的是:

k8s/01-api.yaml

因為 image: cka-api:v1 是寫在 Deployment 裡,用來指定 API Pod 要啟動哪個 Docker Image。

你現在的結構是:

k8s-30days/
├── app/
├── k8s/
│   ├── 00-namespace.yaml
│   ├── 01-api.yaml   ← 改這個
│   ├── 02-configmap.yaml
│   └── 03-secret.yaml
└── kind-config.yaml

打開 k8s/01-api.yaml,找到:

containers:
  - name: api
    image: cka-api:v1

改成:

containers:
  - name: api
    image: cka-api:v2

改完後,在專案根目錄執行:

最後重新套用 Kubernetes YAML:

kubectl apply -f k8s/

https://ithelp.ithome.com.tw/upload/images/20260909/20168537MPb1g4en7i.png

你可以從 Deployment 的 YAML 或 describe 結果看。

最直接的是:

kubectlget deployment api \-n cka-lab \-o yaml
v1

變成:

v2

https://ithelp.ithome.com.tw/upload/images/20260909/20168537QDZz1in5A8.png

就會進行 Rolling Update,建立新的 Pod。


驗證 Environment Variable 是否真的有進去

接下來我們不要只相信 YAML。

直接進 Container 看。

執行:

kubectl exec -it deployment/api -n cka-lab -- sh

這個指令代表:

kubectl exec

在 Container 裡執行指令。

-it

建立互動式 Terminal。

deployment/api

選擇 api Deployment 所管理的其中一個 Pod。

最後:

-- sh

代表進入 Container 裡面的 Shell。

進去之後執行:

env | grep APP_ENV

如果設定成功,應該可以看到:

APP_ENV=local-k8s

你也可以順便看看:

env | grep REDIS_HOST

應該會看到:

REDIS_HOST=redis

甚至:

env | grep POSTGRES

應該會看到:

POSTGRES_USER=appuser
POSTGRES_PASSWORD=devpassword
POSTGRES_DB=appdb

https://ithelp.ithome.com.tw/upload/images/20260909/20168537k0XlJBuqAm.png
這就代表:

ConfigMap
+
Secret

已經成功變成 Container 裡面的 Environment Variable。


再直接測試 API

因為 FastAPI 現在 / Endpoint 會回傳:

{
    "message": "Hello from Kubernetes",
    "environment": APP_ENV
}

所以你重新存取 API 時,應該可以看到:

{
  "message": "Hello from Kubernetes",
  "environment": "local-k8s"
}

這代表 Application 成功從 Kubernetes 取得了:

APP_ENV

而不是把:

local-k8s

寫死在 Python 裡。

這看起來只是很小的一個改變,但其實已經開始接近真實 Production Application 的設計方式。


一個非常重要的問題:ConfigMap 改了,Pod 會自動更新嗎?

接下來做一個很重要的實驗。

把 ConfigMap 裡:

APP_ENV=local-k8s

修改成:

APP_ENV=ironman

我們可以直接執行:

kubectl patch configmap app-config \
  -n cka-lab \
  -p '{"data":{"APP_ENV":"ironman"}}'

現在 Kubernetes 裡面的 ConfigMap 已經變成:

APP_ENV=ironman

你可以確認:

kubectl get configmap app-config \
  -n cka-lab \
  -o yaml

應該會看到:

data:
  APP_ENV: ironman

https://ithelp.ithome.com.tw/upload/images/20260909/20168537vtEsZYN4sP.png

但是這時候有一個很容易搞混的地方。

如果你重新打 API,很可能還是看到:

{
  "environment": "local-k8s"
}

為什麼?

因為:

Environment Variable

是在 Container 啟動的時候注入進去的。

原本那個 Container 啟動時讀到的是:

APP_ENV=local-k8s

之後你修改 ConfigMap,不會突然跑進正在執行中的 Process,把:

local-k8s

改成:

ironman

也不會因為 ConfigMap 更新,Kubernetes 就自動幫你重新啟動 Deployment。

所以目前狀況其實是:

ConfigMap
APP_ENV=ironman

但是

Existing Pod
APP_ENV=local-k8s

兩邊暫時不一致。


重新啟動 Deployment

如果我們希望新的設定生效,可以執行:

kubectl rollout restart \
  deployment/api \
  -n cka-lab

https://ithelp.ithome.com.tw/upload/images/20260909/20168537upnykWHM81.png

這個指令會要求 Deployment:

把目前的 Pod 重新建立一次。

你可以接著觀察:

kubectl get pods \
  -n cka-lab \
  -w

會看到舊 Pod 被移除,新的 Pod 被建立。

新的 Container 啟動時,就會重新從 ConfigMap 讀取:

APP_ENV=ironman

這時候再次呼叫 API,就應該看到:

{
  "message": "Hello from Kubernetes",
  "environment": "ironman"
}

這個實驗非常值得自己實際做一次。

因為它會讓你真正理解:

修改 ConfigMap
≠
已經修改正在執行中的 Environment Variable

而是:

修改 ConfigMap
↓
重新建立 Pod
↓
Container 啟動
↓
重新注入 Environment Variable
↓
Application 讀到新值

為什麼這件事情很重要?

未來你在公司很可能真的會遇到這種狀況:

Redis Host 改了
Database Endpoint 改了
Feature Flag 改了
API Endpoint 改了

你明明:

kubectl apply

了 ConfigMap,卻發現 Application 完全沒有變化。

如果不知道 Environment Variable 是:

Container Start Time

注入的,就很容易懷疑:

是不是 Kubernetes 壞了?
是不是 ConfigMap 沒有更新?
是不是 Deployment 有問題?

其實可能只是 Pod 還沒有重新建立。

因此這個觀念,不只是 CKA 會遇到,也是實際維運 Kubernetes 很常碰到的問題。

其實觀念都不難很簡單,只是順序怕有時會搞錯,忘記指令
所以最後幫大家做個總整理的圖!
https://ithelp.ithome.com.tw/upload/images/20260909/20168537KqREVh8N5C.png


今天真正要理解的事情

今天表面上我們學的是:

ConfigMap
Secret
envFrom

但真正重要的其實是:

Application Image

與:

Environment Configuration

要分離。

同一份 Application Image:

cka-api:v2

未來可以部署到不同環境:

Development
Staging
Production

差別只在它們各自提供不同的:

ConfigMap
Secret

例如:

Development
APP_ENV=development
DATABASE_HOST=postgres-dev
Staging
APP_ENV=staging
DATABASE_HOST=postgres-staging
Production
APP_ENV=production
DATABASE_HOST=postgres-prod

但 Application Image 可以完全一樣。

這就是:

Build Once
Deploy Anywhere

背後非常重要的一部分。

今天我們已經開始把昨天那個單純的 FastAPI Pod,慢慢改造成真正可以支援不同環境的 Application。

下一步,就可以開始讓這個 API 不只是自己回:

Hello from Kubernetes

而是真正和其他 Service 溝通。


上一篇
Day 10|不再部署 nginx:完整實戰 自己寫 FastAPI、Build Image,再丟進 k8s
系列文
不是背 YAML!30 天從零打造 Kubernetes 微服務:從本機實戰一路到 CKA11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言