iT邦幫忙

2025 iThome 鐵人賽

DAY 16
0

gh

前情提要

昨天我們看了 Liveness Probe 與 Readiness Probe,了解了 Kubernetes 如何透過探針機制判斷一個 Pod 是否真的「健康」並能夠提供服務。這讓我們不只是在意容器有沒有啟動,還能掌握應用程式的真實狀態,避免流量被導向未準備好的 Pod 或已經失效的服務。

我們都知道微服務的核心理念是 stateless(無狀態),Pod 可以隨時被刪除、重建或擴縮容。這樣雖然讓系統更有彈性,但帶來一個現實挑戰。當 Pod 消失時,它裡面寫入的資料也會一併消失。想想看這些實際情境:

  • Web 應用上傳的檔案,Pod 重啟後就不見了
  • 資料庫寫入的內容,如果只存在容器檔案系統中,也會隨 Pod 一起消失
  • 分析任務產出的結果,如果沒有妥善存放,就會丟失

因此,我們需要確保資料能夠安全持久地保存下來。這就是 PersistentVolume (PV) 與 PersistentVolumeClaim (PVC) 的角色:讓儲存從 Pod 的生命週期中解耦,提供穩定的掛載機制,確保資料不會隨 Pod 的消失而遺失。

Kubernetes 儲存機制:從 Volumes 到 StorageClass

Kubernetes 中的 Pod 是短暫的 (ephemeral),隨時可能因為重啟、調度或縮容而被刪除重建,這就是微服務的 stateless 特性。這對 stateless 的服務(例如 API、Web server)來說完全沒問題,但對於需要保存資料的應用(例如資料庫、檔案系統、分析任務)卻是致命的。如果資料跟著 Pod 一起消失,服務根本無法穩定運作。

因此,Kubernetes 提供了不同層級的「資料掛載」與「持久化」機制。我們將從最基礎的 Volume 一路看到 PersistentVolume (PV)PersistentVolumeClaim (PVC),最後是 StorageClass 的自動化。

Volumes

在 Docker 世界裡,Container 本質上就是臨時的,容器刪掉,裡面的資料也會跟著消失。解決方法是掛載 Volume,讓資料被寫到容器外部的路徑。

Kubernetes 的 Pod 同樣是短暫的,如果沒有掛載 Volume,Pod 裡產生的資料會隨著 Pod 銷毀而消失。因此我們可以在 Pod 裡宣告 Volume,並將它掛載到容器內的某個目錄。例如把主機的 /data 目錄掛進 Pod 的 /opt,這樣即使 Pod 被刪除,檔案仍然會留在 Node 上。

不過這種 hostPath 的方式只適合單機測試環境。因為在多個 Node 的 Kubernetes 架構中,每個 Node 的 /data 並不會自動同步。要真正做到跨 Node 的一致儲存,就需要更進階的方案(例如 NFS、或雲端磁碟像 AWS EBS、GCP Persistent Disk)。

PersistentVolume (PV)

前面看到使用 Volumes 讓數據不會因為 Container 生命週期而消失,但實際應用上可能有超多個 Pod,每個 Pod 需求的儲存大小、類型都不同,開發者一個個在 Pod 配置 Volume,會相當沒有效率也容易造成混亂。

更好的方式是由 系統管理員 (Cluster Admin) 先建立一批「儲存池」,由 Kubernetes 集中管理,應用端只需透過 PVC 索取需要的資源即可。這就是 PersistentVolume (PV) 的角色。

PV 的特性

  • Cluster-scoped (叢集等級):PV 不屬於任何 Namespace,是整個 Kubernetes Cluster 共享的資源池。
  • 獨立生命週期:與 Pod 沒有直接關聯,即使 Pod 被刪除,PV 仍然存在。
  • 集中管理:由管理員統一配置,定義儲存容量、存取模式 (Access Mode)、回收策略 (Reclaim Policy)、以及底層儲存類型 (HostPath、NFS、AWS EBS、GCP Persistent Disk 等)。

存取模式 (Access Modes)

  • ReadWriteOnce (RWO):單一 Node 可讀寫。
  • ReadOnlyMany (ROX):多個 Node 可唯讀。
  • ReadWriteMany (RWX):多個 Node 可同時讀寫。

回收策略 (Reclaim Policy)

  • Retain:保留資料,需要管理員手動清理。
  • Recycle:刪除資料後回收(執行 rm -rf 類似的清除動作,已被棄用)。
  • Delete:刪除 PVC 後,連帶刪除後端儲存資源。

PV 的狀態

  • Available:可用,尚未綁定 PVC。
  • Bound:已被某個 PVC 綁定。
  • Released:PVC 已刪除,但資料仍留在 PV,等待回收。
  • Failed:回收失敗,需要人工處理。

PersistentVolumeClaim (PVC)

有了 PV 之後,開發者就不需要直接面對底層儲存配置,只需要提出「我需要多少容量、什麼存取模式」的需求即可。這個需求就是 PersistentVolumeClaim (PVC)。可以把它想像成開發者送出的「儲存申請單」,由 Kubernetes 自動幫忙配對一個合適的 PV。

PVC 的特性

  • Namespace-scoped (命名空間等級):PVC 屬於某個 Namespace,因此不同團隊或應用之間可以各自申請、互不干擾。
  • 消耗資源:就像 Pod 會消耗 CPU / Memory,PVC 會消耗 PV 的儲存資源。
  • 自動綁定:當 PVC 建立時,Kubernetes 會自動在 PV 中尋找符合需求(容量、存取模式、StorageClass 等)的資源,並綁定使用。

綁定規則

  • 一個 PVC 只能綁定到 一個 PV,不能同時綁定多個。
  • 如果有多個符合條件的 PV,Kubernetes 會選擇最合適的一個。
  • 較小的 PVC 可以綁定到較大的 PV(容量會「被浪費」),但不會反過來。
  • 綁定後,PVC 與 PV 是一對一關係,直到 PVC 被刪除或 PV 被回收。

PVC 狀態

  • Pending:PVC 還在等待合適的 PV 配對。
  • Bound:PVC 已成功綁定到某個 PV。

為什麼需要 PVC?

如果只有 PV,使用者就必須直接面對底層的儲存細節(NFS、EBS、GCP PD...),這會增加開發負擔。而 PVC 將這些抽象化,讓開發者專注於「我要什麼資源」,而不用管「資源在哪、怎麼建」。

StorageClass

前面 PV + PVC 的流程其實還是 靜態配置 (Static Provisioning),因為管理員必須先建好 PV,使用者才能綁定。如果每個新需求都要事先建立 PV,彈性就大打折扣。

為了解決這個問題,Kubernetes 提供了 StorageClass,是為了實現 「動態供應」(Dynamic Provisioning)。它的作用是定義一種「自動化供應商 (provisioner)」,例如 GCP Persistent Disk、AWS EBS 等等。當使用者建立 PVC 並指定 StorageClass 時,Kubernetes 會 自動建立 PV 與對應的雲端儲存,不再需要人工準備。

實戰 🔥

靜態配置 (Static Provisioning)

我們看到下圖顯示 Pod 裡 /log 資料夾已有檔案,但本機沒有對應的資料夾。

gh

因此我們要將當前的 Pod 的 Volumes 加入 hostPath 掛載到本機的 /var/log/webapp 資料夾。我們用 edit 來編輯 Pod 的 yaml 檔。

kubectl edit pods webapp

可以看到原本的 Pod 有掛載一個 Volume,但是不管他,我們只修改我們的部分,要在 volumeMountsvolumes 的地方寫入相對應的路徑:

apiVersion: v1
kind: Pod
metadata:
  name: webapp
  namespace: default
spec:
  containers:
  - env:
    - name: LOG_HANDLERS
      value: file
    image: kodekloud/event-simulator
    imagePullPolicy: Always
    name: event-simulator
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: kube-api-access-nfx8f
      readOnly: true
    - mountPath: /log  # 新增 Container 的 Mount Path
      name: log-volume
  volumes:
  - name: log-volume  # 新增本機的掛載路徑
    hostPath:
      path: /var/log/webapp
  - name: kube-api-access-nfx8f
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          expirationSeconds: 3607
          path: token
      - configMap:
          items:
          - key: ca.crt
            path: ca.crt
          name: kube-root-ca.crt
      - downwardAPI:
          items:
          - fieldRef:
              apiVersion: v1
              fieldPath: metadata.namespace
            path: namespace

但是我們之前有說過 edit 有一些限制,而在不能修改的情況下,可以看到 Kubernetes 會幫我們暫存到 /tmp 資料夾底下:

gh

那我們用 replace 指令來替換當前的 Pod 讓 Volume 成功掛載:

kubectl replace --force -f /tmp/kubectl-edit-893246399.yaml

👉 我們在 Pod YAML 加上 hostPath,把本機的 /var/log/webapp 掛到容器的 /log
這樣做之後,第二張圖就能看到 log 成功寫進本機,證明 Volume 已經掛載成功

gh

建立 PV:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-log
spec:
  persistentVolumeReclaimPolicy: Retain
  capacity:
      storage: 100Mi
  accessModes:
    - ReadWriteMany
  hostPath:
    path: "/pv/log"

接著我們定義了一個 PersistentVolume (PV),指定 100Mi 的容量、ReadWriteMany 模式,底層是 /pv/log
下圖可以看到 PV 已經成功建立,進入 Available 狀態

gh

建立 PVC,申請 50Mi,但指定 ReadWriteOnce。:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: claim-log-1
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 50Mi

下圖可以看到 PVC 成功被建立,但是處於 Pending 狀態

gh

下圖可以看到 Pending 的原因是沒有找到合適的 PV,因為存取模式(PVC 要 RWO,但 PV 提供 RWX)不符合。

gh

修改 PVC 的 AccessModes 後,PVC 成功綁定到 PV。下圖顯示 PVC 容量 (Capacity) 變成 100Mi,因為它被綁到一個比需求更大的 PV。
👉 Kubernetes 的邏輯是「小的 PVC 可以吃大的 PV」,反之不行

gh

我們把前面 Pod 的 Volume 從 hostPath 改成 PVC,重新替換 Pod。

apiVersion: v1
kind: Pod
metadata:
  name: webapp
  namespace: default
spec:
  containers:
  - env:
    - name: LOG_HANDLERS
      value: file
    image: kodekloud/event-simulator
    imagePullPolicy: Always
    name: event-simulator
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: kube-api-access-nfx8f
      readOnly: true
    - mountPath: /log
      name: log-volume
  volumes:
  - name: log-volume  # 換成掛載 PVC 
    persistentVolumeClaime:
      claimeName: claim-log-1
  - name: kube-api-access-nfx8f
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          expirationSeconds: 3607
          path: token
      - configMap:
          items:
          - key: ca.crt
            path: ca.crt
          name: kube-root-ca.crt
      - downwardAPI:
          items:
          - fieldRef:
              apiVersion: v1
              fieldPath: metadata.namespace
            path: namespace

那麼一樣會 edit 失敗,但是我們一樣用 replace 來替換當前的 Pod:

gh

describe 來查看可以看到 Pod 已經成功掛載到 PVC:

gh

也可以在 /pv/log 找到 log 檔案。

gh

將 PVC 刪除,下圖顯示刪除 PVC 時狀態是 Terminating,因為 Pod 還在使用它。

gh

只要把 Pod 刪除了,PVC 就會跟著被刪除了:

gh

可以看到 PVC 被刪除之後,PV 還沒辦法用,下圖顯示 PV 進入 Released 狀態,代表資料還在,因此無法被新 PVC 使用。

gh

那如果說我們要讓 PV 繼續用,同時保留 PV 指定路徑下的資料,我們可以用 edit 將 yaml 檔內的 claimRef 的部分都刪掉,就可以讓 PV 繼續用了。

apiVersion: v1
kind: PersistentVolume
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","kind":"PersistentVolume","metadata":{"annotations":{},"name":"pv-log"},"spec":{"accessModes":["ReadWriteMany"],"capacity":{"storage":"100Mi"},"hostPath":{"path":"/pv/log"},"persistentVolumeReclaimPolicy":"Retain"}}
    pv.kubernetes.io/bound-by-controller: "yes"
  creationTimestamp: "2025-09-23T14:29:41Z"
  finalizers:
  - kubernetes.io/pv-protection
  name: pv-log
  resourceVersion: "4541"
  uid: cc39646f-7ec7-4145-a7db-1b396ce22ba2
spec:
  accessModes:
  - ReadWriteMany
  capacity:
    storage: 100Mi
  # claimRef 這邊都刪除
  claimRef:
    apiVersion: v1
    kind: PersistentVolumeClaim
    name: claim-log-1
    namespace: default
    resourceVersion: "3053"
    uid: 3aad496e-4c3b-4393-8e3a-5e75461a3aa3
  hostPath:
    path: /pv/log
    type: ""
  persistentVolumeReclaimPolicy: Retain
  volumeMode: Filesystem
status:
  lastPhaseTransitionTime: "2025-09-23T14:55:25Z"
  phase: Released

gh

動態供應 (Dynamic Provisioning)

前面的實戰屬於 靜態配置 (Static Provisioning),因為管理員要事先建立 PV,才能被 PVC 綁定。當需求很多時,這顯得不夠靈活。

這時候 StorageClass 就登場了。它的角色是 當使用者建立 PVC 時,自動幫你建立 PV 與對應的儲存,不需要事先準備。這就是 動態供應 (Dynamic Provisioning)

在開始實戰之前先來看看 volumeBindingMode,他是 StorageClass 裡的一個重要參數,用來決定 PV 與 PVC 綁定 (Binding) 的時機。它有兩個主要的選項:

  • Immediate(預設值):PVC 一旦建立,Kubernetes 會立刻嘗試尋找符合需求的 PV,並綁定。但這會有問題是在多 Node 環境下,可能 PVC 綁定了一個實際無法掛載到指定 Node 的 PV,導致 Pod 調度失敗。
  • WaitForFirstConsumer:PVC 建立時不會立刻綁定,而是 等到有 Pod 使用這個 PVC 並開始調度時,才會挑選合適的 PV 並綁定。相較於 Immediate,這樣能避免「PVC 綁錯位置」的問題,確保 PV 的位置與 Pod 的 Node 相容。

建立 Storage Class,我們使用 rancher.io/local-path 作為 provisioner,代表使用到本機的路徑作為儲存基礎:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-path
provisioner: rancher.io/local-path
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer

建立與 Storage Class 連結的 PVC,指定 storageClassName: local-path,因此會透過這個 StorageClass 來自動建立對應的 PV:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: local-pvc
spec:
  storageClassName: local-path
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 500Mi

建立與 PVC 連結的 Pod,定義實際的工作負載,使用 volumeMounts 掛載 PVC 到容器內的 /var/www/html,Pod 不直接綁定 PV,而是間接透過 PVC 取得儲存資源:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - image: nginx:alpine
    name: nginx
    volumeMounts:
    - mountPath: /var/www/html
      name: local-volume
  volumes:
  - name: local-volume
    persistentVolumeClaim:
      claimName: local-pvc

這樣的結果是使用者只需要建立 PVC 與 Pod,不用操心 PV 的配置,因為 SC 會幫忙自動建立,只是看資料是掛載在哪裡而已。

總結

今天我們從 靜態配置 (Static Provisioning)動態供應 (Dynamic Provisioning),完整看過了 Kubernetes 的資料掛載機制:Volume → PV → PVC → StorageClass。

  • 靜態配置:需要管理員事先建立 PV,PVC 才能綁定。適合測試環境,但不夠靈活。
  • 動態供應:透過 StorageClass,自動化建立 PV 與對應的後端儲存,讓開發者專注於 PVC 與 Pod 的使用。

在真實的雲端場景裡,StorageClass 可以對應到雲端的 Persistent Disk(例如 GCP PD、AWS EBS、Azure Disk),完全不需要人工介入。不過這些屬於更進階的操作,並不在 CKAD 的範疇,未來如果有時間,我會再來實戰這些雲端儲存的實戰案例。

而接下來,我們就要實際驗收一下前面學到的東西:部署一個完整的應用。 明天我會使用 Kubernetes 部署 WordPress,後端使用 MariaDB。這是鐵人賽進度過半的一個小里程碑,把 Deployment、Service、PVC 這些核心概念真正串在一起!

下一篇文章:Kubernetes 實戰演練:打造 WordPress + MariaDB 部署


上一篇
【Day15】Kubernetes 健康檢查!Liveness & Readiness 的探針機制
系列文
30 天挑戰 CKAD 認證!菜鳥的 Kubernetes 學習日記16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言