來到 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 化非常重要的一個觀念。
首先修改:
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 提供了一個很常使用的 Resource:
ConfigMap
ConfigMap 的主要用途,就是保存:
非敏感的 Application Configuration。
例如:
APP_ENV
REDIS_HOST
DATABASE_HOST
LOG_LEVEL
FEATURE_FLAG
這些資料通常不是秘密,但不同環境可能會有不同的值。
新增:
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 想成:
Application 的一般設定檔。
例如不同環境可能設定:
APP_ENV=staging
或者:
LOG_LEVEL=debug
甚至:
FEATURE_NEW_CHECKOUT=true
這些資料即使被看到,通常也不會直接造成安全問題。
所以很適合放進 ConfigMap。
但如果今天資料變成:
DATABASE_PASSWORD
API_TOKEN
PRIVATE_KEY
就不應該再放進 ConfigMap。
這時候要使用另外一種 Kubernetes Resource:
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。

資料則放在:
stringData:
下面。
例如:
POSTGRES_PASSWORD: devpassword
之後 Kubernetes 就可以把這些值注入 Container。
從 Application 的角度來看,它們其實很像。
最後都可以變成:
Environment Variable
讓程式讀取。
但 Kubernetes 希望我們從語意上把它們分開。
ConfigMap 表示:
普通設定
Secret 表示:
敏感設定
這不只是看起來比較漂亮。
因為之後在正式環境裡,我們可以針對 Secret 做更嚴格的:
RBAC Permission
Encryption
Audit
Secret Rotation
管理。
因此從一開始就把它們分開,是很好的習慣。
很多剛開始學 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 都建立好了,但 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,程式都不一定需要修改。
剛剛我們修改了:
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

但要記得,我們使用的是:
Kind Kubernetes Cluster
Kind 的 Node 本身其實是一個 Docker Container。
所以你在 Mac 本機 Build 出來的 Image,不代表 Kind 裡面自動就看得到。
因此還要執行:
kind load docker-image \
cka-api:v2 \
--name cka-lab

把 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/

你可以從 Deployment 的 YAML 或 describe 結果看。
最直接的是:
kubectlget deployment api \-n cka-lab \-o yaml
v1
變成:
v2

就會進行 Rolling Update,建立新的 Pod。
接下來我們不要只相信 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

這就代表:
ConfigMap
+
Secret
已經成功變成 Container 裡面的 Environment Variable。
因為 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 裡:
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

但是這時候有一個很容易搞混的地方。
如果你重新打 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
兩邊暫時不一致。
如果我們希望新的設定生效,可以執行:
kubectl rollout restart \
deployment/api \
-n cka-lab

這個指令會要求 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 很常碰到的問題。
其實觀念都不難很簡單,只是順序怕有時會搞錯,忘記指令
所以最後幫大家做個總整理的圖!
今天表面上我們學的是:
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 溝通。