iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0
Software Development

學校沒教的後端生存指南:30 天打造非同步 S3 微服務,部署 K8s 實現 HA 架構系列 第 28 篇

[ Day 28 ] Pod 重啟資料卻失蹤 : K8s Volume 與 ConfigMap / Secret 打造狀態與機密不崩潰的高可用微服務!

  • 分享至 

  • xImage
  •  

在前面的文章中,我們完成了微服務上 K8s、滾動更新雙探針與 HPA 自動擴充。然而,當我們準備將這套系統推向真實 Production 生產環境時,我們必須確認幾件事情:

  1. 資安漏洞:敏感資料不能明文寫在程式碼中或是推上github,例如目前的程式碼中 AWS ACCESS_KEY 與 SECRET_KEY 寫在 deployment.yaml 推上 GitHub 後,可能很快就會被機器人掃走,引發幾十萬元的帳單慘案!
  2. 資料蒸發:Pod 重啟或 HPA 縮容時,寫在容器內部的 Log 日誌與 Prometheus/Grafana 歷史紀錄全數歸零!

今天,我們將帶大家了解 ConfigMap、Secret 機密控流與 PV/PVC 持久化磁碟掛載的實作


配置與機密嚴格解耦:ConfigMap & Secret

落實 Cloud-Native 的 12-Factor App 原則:配置與代碼嚴格解耦 (Decoupling Config from Code)。

1. Base64 假加密:Secret 的真正資安防線

大家都知道資安的重要性也知道有些資料除了不要明文之外,還要加密,這些觀念都沒錯,不過如果只有寫了一份 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)動態注入!

2. 撰寫 ConfigMap YAML (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"

3. 調整 deployment.yaml

我們在 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.yaml
  • env: secretKeyRef — 逐條指定,強迫你明確寫出每個 key 的名稱。如果用 envFrom 把整包 Secret 倒進去,萬一 Secret 裡有很多 key,你根本不知道 Pod 裡面被注入了什麼,安全審計完全看不透
  • 覆蓋規則 : env 優先蓋過 envFrom。若兩者有同名 Key,env 會直接覆蓋 envFrom 的數值。這個機制讓你可以用 Secret 覆蓋 ConfigMap 裡的某個值,不需要把 ConfigMap 整個改掉。

4. .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

生產環境(GitHub Actions):

AK/SK 只需要存在 GitHub Repository Secrets 這一個地方,CI pipeline 在部署時自動執行 kubectl create secret 動態注入叢集,你完全不用手動操作,也不需要 .env 檔。


持久化儲存:PV、PVC 與路徑映射對應關係

當搞定了機密與配置,接下來要解決的是 Pod 死亡後,Log 日誌不能消失,必須持久化儲存的設定,如果沒掛 volume 會怎樣呢?

  • 服務重啟後 Prometheus Log 或 Grafana 圖表紀錄不會留著 ,100% 徹底蒸發!
  • Docker 容器與 K8s Pod 預設都是無狀態。沒掛 Volume 時,所有寫入(Prometheus TSDB、Grafana 設定檔、Spring Boot 的 app.log)通通寫在容器的可寫暫存層 (Writable Container Layer)。
  • 重啟即毀滅:只要敲下 docker compose down 或 K8s Pod 發生重啟/縮容,這個暫存層會被 Linux 核心 完全銷毀歸零。這就是為什麼在 Day 17 壓測完,一重啟容器發現 Grafana 圖表歷史紀錄全部變一片空白的慘痛原因!

1. PV vs. PVC

  • PV (PersistentVolume,持久化卷):實體磁碟房東。由 K8s 管理員或雲端廠商(如 AWS EBS、EFS、GCP Persistent Disk)所提供的真實實體儲存資源。
  • PVC (PersistentVolumeClaim,持久化卷聲明):微服務的租借合約。應用程式 Pod 不需要知道底層是哪一顆 AWS EBS,只要寫一份 PVC 宣告:「我需要 5GB 空間、讀寫模式為 ReadWriteOnce」,K8s 隨即會自動媒合對應的 PV 給它!

https://ithelp.ithome.com.tw/upload/images/20260924/20183864IjMitcDyPt.png

2. Docker Volume (-v) vs. K8s PV/PVC 的差異

觀念很像,但「底層分散式架構」完全不同

  • Docker Volume (單機綁定): docker run -v /host/logs:/app/logs 資料硬性綁定在單一台宿主機硬碟上。
  • K8s PV/PVC (分散式解耦與動態掛載):k8s 是多節點叢集,Pod 會因為 HPA 擴縮容或節點故障而漂移 (Reschedule) 到其他 Node 上。其背後連接雲端儲存(如 AWS EBS/EFS),當 Pod 漂移至 Node B 時,K8s 會自動將磁碟卸載並重新掛載給 Node B 上的新 Pod,達成真正的跨節點持久化!

3. 容器內部路徑映射

https://ithelp.ithome.com.tw/upload/images/20260924/20183864RTZXnfzLfo.png

  • 映射路徑 : 設定 volumeMounts.mountPath: /app/logs,就像是在容器內部的這個資料夾上蓋一張外接硬碟,只有寫入 /app/logs 的 Log 檔會重定向儲存在 PVC 實體磁碟上。
  • 避雷 : 千萬不能把 PVC 直接掛載在 /app 根目錄!外接硬碟掛載在 /app 時,會把 Docker Image 原本放在 /app/app.jar 的程式碼遮蔽掉。Spring Boot 找不到 JAR 包,容器開起來瞬間就會直接死亡崩潰 (CrashLoopBackOff)!

4. Spring boot application.yml檔案調整

有設定Volume是一回事,能讓程式寫log又是一回事,所以我們還要設定 Spring Boot logging.file.name。因為預設 Spring Boot 只輸出 stdout,不寫檔案。要讓 log 寫到檔案,需要在 application.yml 加:

logging:
  file:
    name: /app/logs/app.log

5. 撰寫 PVC YAML (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。

6. 整合 ConfigMap、Secret 與 PVC (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

本地部署與實測

1. 重新 Build Image

每次修改 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 .

2. 建立 Secret 並套用所有 K8s 資源

# 在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/

3. 觀察滾動更新過程

kubectl get pods -w

https://ithelp.ithome.com.tw/upload/images/20260924/20183864pc1rpFVQf6.png

K8s 逐一把舊 Pod 換成新 Pod,過程中新舊 Pod 同時存在,服務不中斷。舊 Pod 最後顯示 Error 是正常的終止狀態,不是崩潰,是 graceful shutdown 完成後的短暫過渡狀態,很快就會被清掉。

重新kubectl get pods確認有三個pod在執行中就成功了

4. 實測 PVC 持久化

  • exec 進哪個 Pod 都可以,因為三個 Pod 掛的是同一個 PVC,看到的 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

https://ithelp.ithome.com.tw/upload/images/20260924/20183864gpegRaaGWG.png

  • 舊 Pod 寫入的日誌完好無損保留在 PVC 中!

5. 兩種路徑的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

https://ithelp.ithome.com.tw/upload/images/20260924/20183864JNvmPPsLlG.png


常見錯誤排查

大家在實測的時候可能也會遇到很多問題,原因有很多,硬體資源不足、檔案設定不對或是時間設定太短timeout,所以我整理一些我自己有遇到或是大家可能會遇到的錯誤

Pod 一直 0/1,describe 看到 FailedScheduling: pvc not found**

  • 原因:先 apply 了 deployment.yaml 但還沒建 PVC。
  • 解法:kubectl apply -f k8s/s3-pvc.yaml,PVC 建立後 Pod 會自動重試。

Pod 一直重啟,describe 看到 Liveness probe failed: connection refused**

  • 原因:initialDelaySeconds 設太短,Spring Boot 還沒起來就被 probe 判死。
  • 解法:把 livenessProbe.initialDelaySeconds 調高到實際啟動時間的 1.5 倍以上(Minikube 建議 60 秒),並加上 timeoutSeconds: 5。

Pod 一直重啟,describe 看到 context deadline exceeded**

  • 原因:Spring Boot 有起來但回應太慢,probe 預設 timeout 只有 1 秒。
  • 解法:加上 timeoutSeconds: 5,給 Actuator health endpoint 更多時間回應。

/app/logs/app.log 不存在**

  • 原因一:忘記在 application.yml 加 logging.file.name。
  • 原因二:有加設定但沒有重新 build image,Minikube 跑的還是舊的 jar。
  • 解法:加完設定後記得 eval $(minikube docker-env) + DOCKER_BUILDKIT=0 docker build --no-cache -t my-app:latest .,再 kubectl rollout restart deployment s3-app-deployment。

總結

今天我們完成了 K8s 機密管理與持久化儲存的關鍵升級:

  1. 破解 Base64 假加密:使用 secretKeyRef 搭配 kubectl create secret --from-literal 命令式建立,徹底消滅 Git 中的金鑰檔案。
  2. **釐清 ConfigMap vs Secret **:非敏感的批次設定用 envFrom: configMapRef,敏感金鑰用 env: secretKeyRef 逐條注入,兩者並存是標準做法,env 優先級高於 envFrom。
  3. 搞懂三個獨立概念:exec 能不能用(看 Pod 狀態)、log 檔存不存在(看 logging.file.name 設定)、重啟後資料還在不在(看有沒有掛 PVC),三個問題互相獨立。
  4. 釐清 kubectl logs vs 檔案 log 的差異:stdout 和檔案是兩條路,System.out.println 只進 stdout,Spring Boot Logger 才進 app.log。
  5. 理解 ReadWriteOnce 限制:Minikube 單節點三個 Pod 共用同一個 PVC 沒問題,多節點 EKS 需改 ReadWriteMany + EFS。
  6. PV / PVC 解決了容器隨機死亡導致資料消失的痛點,實現了真正的持久化保障。

回顧前幾天的學習,現在的微服務具備了高吞吐、雙探針自癒、HPA 擴充與 PV 持久化,不過如果每次程式跌代更新都要手動docker build和ubectl apply 就太慢了!

明天,將手把手帶大家建置 GitHub Actions CI/CD 流水線,實現當 git push 程式碼時,自動執行單元測試、打包 Docker 鏡像推送到 AWS ECR,並一鍵自動部署至 K8s 叢集,完成全自動化雲端交付!

參考資料


上一篇
[ Day 27 ] HPA 逆襲!PUT 上傳突破單機瓶頸 0% 錯誤率,高可用彈性擴充與 AWS 雲端算力成本精算!
下一篇
[ Day 29 ] DevOps 自動化交付 - GitHub Actions + AWS ECR + K8s 一鍵 CI/CD 零人工干預部署
系列文
學校沒教的後端生存指南:30 天打造非同步 S3 微服務,部署 K8s 實現 HA 架構 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言