在前面的文章中,我們完成了微服務上 K8s、滾動更新雙探針與 HPA 自動擴充。然而,當我們準備將這套系統推向真實 Production 生產環境時,我們必須確認幾件事情:
ACCESS_KEY 與 SECRET_KEY 寫在 deployment.yaml 推上 GitHub 後,可能很快就會被機器人掃走,引發幾十萬元的帳單慘案!今天,我們將帶大家了解 ConfigMap、Secret 機密控流與 PV/PVC 持久化磁碟掛載的實作
落實 Cloud-Native 的 12-Factor App 原則:配置與代碼嚴格解耦 (Decoupling Config from Code)。
大家都知道資安的重要性也知道有些資料除了不要明文之外,還要加密,這些觀念都沒錯,不過如果只有寫了一份 s3-secret.yaml 並將金鑰轉成 Base64 字串,其實並沒有真的很安全。
Base64 只是編碼 (Encoding),絕非加密 (Encryption)!任何人拿到 Base64 字串,只要執行echo "字串" | base64 -d就能 0 秒解出明文。所以千萬不要將帶有 Base64 的 YAML 推上 GitHub,等於把金鑰光溜溜推上網!
標準實務作法:我們只在 deployment.yaml 中引用金鑰名稱(Key Name),真正的敏感數值則透過 Terminal 指令建入 K8s 叢集大腦(etcd),或是由 CI/CD 流水線(如 GitHub Actions)動態注入!
k8s/s3-app-configmap.yaml)ConfigMap 用於存放公開、非敏感的微服務環境變數(可以且應該進入 Git 版本控制):
apiVersion: v1
kind: ConfigMap
metadata:
name: s3-app-config
labels:
app: s3-service
data:
AWS_REGION: "ap-southeast-2"
S3_BUCKET: "iron-netty"
SEMAPHORE_PERMITS: "50"
我們在 deployment.yaml 要同時用 envFrom: configMapRef 和 env: secretKeyRef,前者從ConfigMap 呼叫可以公開的設定值,可以也進版本控制。後者則是不想讓任何人看到的值 (如金鑰)。
# 從 ConfigMap 一次批次注入所有通用設定
envFrom:
- configMapRef:
name: s3-app-config
# 從 Secret 逐條注入敏感金鑰
env:
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: aws-credentials
key: aws-access-key-id
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: aws-credentials
key: aws-secret-access-key
envFrom: configMapRef — 一次把 ConfigMap 裡所有 key 全部注入,適合非敏感的批次設定。ConfigMap 新增一個 key,Pod 自動就有,不用改 deployment.yamlenv: secretKeyRef — 逐條指定,強迫你明確寫出每個 key 的名稱。如果用 envFrom 把整包 Secret 倒進去,萬一 Secret 裡有很多 key,你根本不知道 Pod 裡面被注入了什麼,安全審計完全看不透env 優先蓋過 envFrom。若兩者有同名 Key,env 會直接覆蓋 envFrom 的數值。這個機制讓你可以用 Secret 覆蓋 ConfigMap 裡的某個值,不需要把 ConfigMap 整個改掉。.env 跟 K8s Secret 是兩個完全不同的世界有人可能會好奇本地已經有 .env 檔了,為什麼還要 kubectl create secret?因為它們服務的對象不一樣。
.env 檔 |
K8s Secret | |
|---|---|---|
| 誰讀它 | Docker Compose | K8s 叢集 (etcd) |
| 適用環境 | 本地開發 | 線上 K8s |
| 如何使用 | docker compose 自動讀取 | secretKeyRef 注入 Pod |
K8s 完全不認識本機的 .env 檔。Secret 必須先被建立進 K8s 叢集的 etcd 資料庫,Pod 才能去讀它。
# .env 給 Docker Compose 用(本機開發)
docker compose up -d
# kubectl create secret 給 K8s 用(本機 Minikube 測試)
kubectl create secret generic aws-credentials \
--from-literal=aws-access-key-id=YOUR_AK \
--from-literal=aws-secret-access-key=YOUR_SK
AK/SK 只需要存在 GitHub Repository Secrets 這一個地方,CI pipeline 在部署時自動執行 kubectl create secret 動態注入叢集,你完全不用手動操作,也不需要 .env 檔。
當搞定了機密與配置,接下來要解決的是 Pod 死亡後,Log 日誌不能消失,必須持久化儲存的設定,如果沒掛 volume 會怎樣呢?
app.log)通通寫在容器的可寫暫存層 (Writable Container Layer)。docker compose down 或 K8s Pod 發生重啟/縮容,這個暫存層會被 Linux 核心 完全銷毀歸零。這就是為什麼在 Day 17 壓測完,一重啟容器發現 Grafana 圖表歷史紀錄全部變一片空白的慘痛原因!
觀念很像,但「底層分散式架構」完全不同
docker run -v /host/logs:/app/logs 資料硬性綁定在單一台宿主機硬碟上。
volumeMounts.mountPath: /app/logs,就像是在容器內部的這個資料夾上蓋一張外接硬碟,只有寫入 /app/logs 的 Log 檔會重定向儲存在 PVC 實體磁碟上。/app 根目錄!外接硬碟掛載在 /app 時,會把 Docker Image 原本放在 /app/app.jar 的程式碼遮蔽掉。Spring Boot 找不到 JAR 包,容器開起來瞬間就會直接死亡崩潰 (CrashLoopBackOff)!application.yml檔案調整有設定Volume是一回事,能讓程式寫log又是一回事,所以我們還要設定 Spring Boot logging.file.name。因為預設 Spring Boot 只輸出 stdout,不寫檔案。要讓 log 寫到檔案,需要在 application.yml 加:
logging:
file:
name: /app/logs/app.log
k8s/s3-pvc.yaml)apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: s3-log-pvc
labels:
app: s3-service
spec:
accessModes:
- ReadWriteOnce # 允許單一 Node 進行讀寫
resources:
requests:
storage: 5Gi # 宣告申請 5GB 磁碟空間
ReadWriteOnce 限制:ReadWriteOnce 代表同一時間只有同一個 Node 上的 Pod 能讀寫這個 PVC。本地 Minikube 只有一個 Node,所以 3 個 Pod 都能掛到同一個 PVC,三個 Pod 的 log 會混寫進同一個 app.log 檔案裡,exec 進任何一個 Pod 看到的內容都一樣。多節點 EKS 環境需要改用 ReadWriteMany 搭配 AWS EFS。
k8s/s3-app-deployment.yaml)最後將我們剛剛有改動新增的內容再重新整合到s3-app-deployment.yaml中
apiVersion: apps/v1
kind: Deployment
metadata:
name: s3-app-deployment
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: s3-service-container
image: my-app:latest
ports:
- containerPort: 8080
# 1. 從 ConfigMap 批次注入通用設定(可公開的值)
envFrom:
- configMapRef:
name: s3-app-config
# 2. 從 Secret 逐條注入敏感金鑰(env 優先級高於 envFrom)
env:
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: aws-credentials
key: aws-access-key-id
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: aws-credentials
key: aws-secret-access-key
# 3. 掛載 PVC 磁碟至專用 Log 目錄(避開 /app 根目錄地雷!)
volumeMounts:
- name: log-persistent-storage
mountPath: /app/logs
# 4. 宣告 Volumes 來源對接 PVC
volumes:
- name: log-persistent-storage
persistentVolumeClaim:
claimName: s3-log-pvc
每次修改 application.yml 或任何 Java 程式碼後,都必須重新 build image,否則 Minikube 裡跑的還是舊的程式碼,你改的設定完全不會生效。
# 先切換 Docker context 到 Minikube 的 daemon(必須!否則 image 不在 Minikube 裡)
# 不切換也行後續再用minikube load讀進去即可
eval $(minikube docker-env)
# 重新 build(加 --no-cache 確保 application.yml 的改動有被打包進去)
DOCKER_BUILDKIT=0 docker build --no-cache -t my-app:latest .
# 在cli中建立 aws-credentials Secret(不需在 Git 留存任何金鑰檔案)
kubectl create secret generic aws-credentials \
--from-literal=aws-access-key-id=YOUR_ACTUAL_AK \
--from-literal=aws-secret-access-key=YOUR_ACTUAL_SK
# 套用 ConfigMap(通用設定)
kubectl apply -f k8s/s3-app-configmap.yaml
# 套用 PVC(Log 持久化磁碟)
kubectl apply -f k8s/s3-pvc.yaml
# 套用 Deployment、Service、HPA
kubectl apply -f k8s/s3-app-deployment.yaml
kubectl apply -f k8s/s3-app-service.yaml
kubectl apply -f k8s/s3-app-hpa.yaml
#或是直接套用全部也可以
kubectl apply -f k8s/
kubectl get pods -w

K8s 逐一把舊 Pod 換成新 Pod,過程中新舊 Pod 同時存在,服務不中斷。舊 Pod 最後顯示 Error 是正常的終止狀態,不是崩潰,是 graceful shutdown 完成後的短暫過渡狀態,很快就會被清掉。
重新kubectl get pods確認有三個pod在執行中就成功了
app.log 內容完全一樣:# 隨便選一個 Pod exec 進去
kubectl exec -it <任意pod名稱> -- cat /app/logs/app.log
接著手動刪掉這個 Pod,模擬容器崩潰:
kubectl delete pod <pod名稱>
K8s 會自動拉起一個新 Pod。等新 Pod 1/1 Running 後,再 exec 進去看:
kubectl exec -it <新pod名稱> -- cat /app/logs/app.log

kubectl logs <pod> 讀的是容器的 stdout/stderr,也就是 System.out.println 和 Spring Boot logger 打到終端機的輸出。Pod 死掉後這份 log 就消失了(K8s 預設只短暫保留)
/app/logs/app.log 是寫到檔案系統的 log,只有 Spring Boot 的 Logger(INFO/WARN/ERROR)會寫進去,System.out.println 不會。所以測試 API 後去看 app.log,不會看到業務邏輯裡用 println 印的那些 emoji 訊息,要看那些要用 kubectl logs

大家在實測的時候可能也會遇到很多問題,原因有很多,硬體資源不足、檔案設定不對或是時間設定太短timeout,所以我整理一些我自己有遇到或是大家可能會遇到的錯誤
0/1,describe 看到 FailedScheduling: pvc not found**kubectl apply -f k8s/s3-pvc.yaml,PVC 建立後 Pod 會自動重試。Liveness probe failed: connection refused**initialDelaySeconds 設太短,Spring Boot 還沒起來就被 probe 判死。livenessProbe.initialDelaySeconds 調高到實際啟動時間的 1.5 倍以上(Minikube 建議 60 秒),並加上 timeoutSeconds: 5。context deadline exceeded**timeoutSeconds: 5,給 Actuator health endpoint 更多時間回應。/app/logs/app.log 不存在**application.yml 加 logging.file.name。eval $(minikube docker-env) + DOCKER_BUILDKIT=0 docker build --no-cache -t my-app:latest .,再 kubectl rollout restart deployment s3-app-deployment。今天我們完成了 K8s 機密管理與持久化儲存的關鍵升級:
secretKeyRef 搭配 kubectl create secret --from-literal 命令式建立,徹底消滅 Git 中的金鑰檔案。envFrom: configMapRef,敏感金鑰用 env: secretKeyRef 逐條注入,兩者並存是標準做法,env 優先級高於 envFrom。kubectl logs vs 檔案 log 的差異:stdout 和檔案是兩條路,System.out.println 只進 stdout,Spring Boot Logger 才進 app.log。ReadWriteOnce 限制:Minikube 單節點三個 Pod 共用同一個 PVC 沒問題,多節點 EKS 需改 ReadWriteMany + EFS。回顧前幾天的學習,現在的微服務具備了高吞吐、雙探針自癒、HPA 擴充與 PV 持久化,不過如果每次程式跌代更新都要手動docker build和ubectl apply 就太慢了!
明天,將手把手帶大家建置 GitHub Actions CI/CD 流水線,實現當 git push 程式碼時,自動執行單元測試、打包 Docker 鏡像推送到 AWS ECR,並一鍵自動部署至 K8s 叢集,完成全自動化雲端交付!