iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0
Kubernetes

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

Day 13|加入 PostgreSQL 與 PVC:Pod 可以死,資料不能跟著死

  • 分享至 

  • xImage
  •  

前面我們已經完成了 API 與 Redis 的串接,目前的架構大概是:

Client
↓
Service api
↓
FastAPI Pod
↓
Service redis
↓
Redis Pod

到這裡,我們已經實際碰到 Kubernetes 裡很重要的一個概念:

Service-to-Service Communication

API 不需要知道 Redis Pod 的 IP,只需要連線到:

redis:6379

Kubernetes Service 會幫我們找到真正的 Redis Pod。

今天,我們要再加入第三個元件:

PostgreSQL

最後整個架構會逐漸變成:

Client
↓
Service api
↓
FastAPI Pod
├── Service redis
│   ↓
│   Redis Pod
│
└── Service postgres
    ↓
    PostgreSQL Pod
    ↓
    Persistent Storage

不過 PostgreSQL 和前面的 API 有一個非常重要的差異。

API Pod 壞掉,其實沒有那麼可怕。

因為 Deployment 可以重新建立一顆新的 Pod:

API Pod A
被刪除
↓
Deployment 發現 replicas 不足
↓
建立 API Pod B

新的 Pod 啟動之後,Application 還是可以繼續提供服務。

PostgreSQL Pod 本身其實也一樣。

Pod 壞掉之後,也可以重新建立:

PostgreSQL Pod A
↓
刪除
↓
PostgreSQL Pod B

真正的問題不是:

Pod 能不能重建?

而是:

資料能不能留下來?

如果 PostgreSQL 裡面的 User、Order、Article、Transaction 等資料,跟著 Pod 一起消失,那就算 Kubernetes 可以在幾秒內建立新的 PostgreSQL Pod,也沒有任何意義。

因此今天真正要理解的主題,其實不是 PostgreSQL,而是:

Persistent Storage

也就是:

如何讓 Pod 可以被刪掉、重建、替換,但資料仍然存在。


為什麼不能直接把資料放在 Container 裡?

先想像 PostgreSQL Container 裡面有這個目錄:

/var/lib/postgresql/data

這是 PostgreSQL 預設用來存放 Database Data 的重要位置之一。

如果我們完全沒有配置 Volume,那 PostgreSQL 寫入的資料,就會存在 Container 自己的 Filesystem 裡面。

概念上可以想成:

PostgreSQL Pod
└── Container
    └── /var/lib/postgresql/data
        ├── users
        ├── tables
        ├── indexes
        └── database files

問題就在這裡。

Container 的 Filesystem 是跟著 Container 生命週期走的。

假設今天 PostgreSQL Pod 被刪除:

kubectl delete pod postgres-xxxxx

流程會變成:

Pod 被刪除
↓
Container 被刪除
↓
Container Filesystem 消失
↓
PostgreSQL Data 消失

Deployment 確實會重新建立一顆 PostgreSQL Pod。

但是新的 PostgreSQL Container 使用的是一份新的 Filesystem。

所以結果可能變成:

PostgreSQL Pod A
Database 有資料
↓
Pod 被刪除
↓
PostgreSQL Pod B
Database 變成全新的

這顯然不是 Database 可以接受的行為。

因此我們需要把:

Application 的生命週期

和:

Data 的生命週期

分開。

我們希望做到:

Pod 可以消失
Container 可以消失

但 Storage 不要消失

這就是 Kubernetes Volume 與 Persistent Storage 要解決的問題。


PersistentVolume 與 PersistentVolumeClaim

Kubernetes 在處理 Persistent Storage 時,會看到兩個非常重要的 Resource:

PV
PVC

完整名稱分別是:

PV  = PersistentVolume
PVC = PersistentVolumeClaim

第一次看到很容易搞混。

可以先用「租房子」理解。

假設市場上真的有一間房子:

房子

這可以理解成:

PersistentVolume

也就是:

真正可以使用的 Storage 資源。

例如底層可能來自:

Local Disk
AWS EBS
GCP Persistent Disk
Azure Disk
NFS

但 Application 通常不需要直接指定:

我要使用 PV-abc123

而是提出自己的需求。

例如:

我需要一個至少 1 GiB,而且可以 Read / Write 的 Storage。

這個「我要一個怎樣的 Storage」的要求,就是:

PersistentVolumeClaim

也就是 PVC。

所以可以理解成:

PV
=
真的存在的房子


PVC
=
我要租一間符合這些條件的房子


Pod
=
租客

最後關係會變成:

Pod
↓
PVC
↓
PV
↓
真正的 Storage

Pod 不需要直接管理底層 Disk。

Pod 只需要說:

我要使用 postgres-pvc

至於這個 PVC 最後對應哪個 Storage,交給 Kubernetes 的 Storage 機制處理。

這個抽象非常重要。

因為 Kubernetes 希望 Application 關心的是:

我要多少 Storage?
我要什麼存取模式?

而不是:

這顆 Disk 在哪?
Device ID 是多少?
AWS EBS Volume ID 是多少?

建立 PostgreSQL PVC

建立:

k8s/05-postgres-pvc.yaml

內容:

apiVersion: v1

kind: PersistentVolumeClaim

metadata:
  name: postgres-pvc
  namespace: cka-lab

spec:
  accessModes:
    - ReadWriteOnce

  resources:
    requests:
      storage: 1Gi

先看最重要的部分:

resources:
  requests:
    storage: 1Gi

意思就是:

我希望 Kubernetes 幫我準備至少 1 GiB 的 Storage。

這不是建立一個 1 GiB 的 Folder,而是在向 Kubernetes 的 Storage System 提出:

Storage Request

接著是:

accessModes:
  - ReadWriteOnce

這個地方非常容易被誤解。

ReadWriteOnce,縮寫:

RWO

比較準確的意思是:

這個 Volume 可以被一個 Node 以 Read/Write 模式掛載。

注意,是:

一個 Node

不是單純:

只能一個 Pod

如果多個 Pod 剛好位於同一個 Node,某些 Storage 情況下仍可能共同使用該 Volume。

因此目前先記成:

ReadWriteOnce
=
同一時間主要由一個 Node
以 Read / Write 方式掛載

Storage Access Mode 後面的 Storage 章節,我們會再完整拆解:

ReadWriteOnce
ReadOnlyMany
ReadWriteMany
ReadWriteOncePod

目前 PostgreSQL 單節點 Lab 使用 ReadWriteOnce 就可以了。


PVC 建立之後,PV 從哪裡來?

這時可能會有一個疑問:

我們明明只建立:

PVC

可是沒有建立:

PV

那 Storage 到底哪裡來?

如果你的 Kubernetes Cluster 有設定:

StorageClass

就可以透過:

Dynamic Provisioning

自動建立 Storage。

例如我們目前使用 kind 或其他本機 Kubernetes 環境時,通常環境本身會提供某種 Storage Provisioner。

因此可能發生:

建立 PVC
↓
StorageClass 發現新的 Storage Request
↓
自動 Provision Storage
↓
建立 / 配對 PV
↓
PVC 與 PV 綁定

這也是為什麼等等查看 PVC 時,我們希望看到:

Bound

意思就是:

這個 PVC 已經成功取得 Storage。


建立 PostgreSQL

接下來建立:

k8s/06-postgres.yaml

內容:

apiVersion: apps/v1

kind: Deployment

metadata:
  name: postgres
  namespace: cka-lab

spec:
  replicas: 1

  selector:
    matchLabels:
      app: postgres

  template:
    metadata:
      labels:
        app: postgres

    spec:
      containers:
        - name: postgres

          image: postgres:16

          ports:
            - containerPort: 5432

          envFrom:
            - secretRef:
                name: app-secret

          env:
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata

          volumeMounts:
            - name: postgres-data
              mountPath: /var/lib/postgresql/data

      volumes:
        - name: postgres-data

          persistentVolumeClaim:
            claimName: postgres-pvc

---

apiVersion: v1

kind: Service

metadata:
  name: postgres
  namespace: cka-lab

spec:
  selector:
    app: postgres

  ports:
    - port: 5432
      targetPort: 5432

這份 YAML 裡面真正的新東西主要是:

volumes
volumeMounts
persistentVolumeClaim

而這三個東西的關係一定要搞懂。


volumes 到底是在做什麼?

先看 Pod 裡面的:

volumes:
  - name: postgres-data

    persistentVolumeClaim:
      claimName: postgres-pvc

這裡是在告訴 Kubernetes:

這個 Pod 需要一個叫做 postgres-data 的 Volume。

而這個 Volume 的來源不是 Container Filesystem,而是:

persistentVolumeClaim:
  claimName: postgres-pvc

也就是我們剛才建立的:

postgres-pvc

所以可以理解成:

postgres-pvc
↓
postgres-data

postgres-data 是:

Pod 裡面對這個 Volume 使用的名稱

而真正的 Storage 來源是:

postgres-pvc

volumeMounts 又是什麼?

接著 Container 裡有:

volumeMounts:
  - name: postgres-data
    mountPath: /var/lib/postgresql/data

這是在告訴 Kubernetes:

把 Pod 裡叫做 postgres-data 的 Volume,掛載到這個 Container 的 /var/lib/postgresql/data

因此:

volumes:

負責的是:

這個 Pod 有哪些 Volume?
Volume 從哪裡來?

而:

volumeMounts:

負責的是:

這個 Container
要把 Volume 掛在哪裡?

完整資料流可以畫成:

PersistentVolume
      ↑
      │
PersistentVolumeClaim
postgres-pvc
      ↑
      │
Pod Volume
postgres-data
      ↓
      │
Container
/var/lib/postgresql/data

PostgreSQL 看起來還是在正常寫:

/var/lib/postgresql/data

但是這個 Path 現在已經不是單純存在 Container Filesystem 裡。

它背後連接到的是:

Persistent Storage

所以 PostgreSQL 寫入:

/var/lib/postgresql/data

實際上資料會進入:

PVC
↓
PV
↓
Storage

這就是 Persistence 的核心。


為什麼還要設定 PGDATA?

我們另外寫了:

env:
  - name: PGDATA
    value: /var/lib/postgresql/data/pgdata

PostgreSQL 官方 Image 會透過:

PGDATA

決定實際 Database Cluster 的資料目錄。

因此目前結構會比較像:

/var/lib/postgresql/data
└── pgdata
    ├── base
    ├── global
    ├── pg_wal
    └── ...

也就是:

PVC 掛載:
/var/lib/postgresql/data

PostgreSQL 真正 Data Directory:
/var/lib/postgresql/data/pgdata

對我們目前的 Lab 來說,先理解:

PostgreSQL 的資料目錄
位於 Persistent Volume 裡

就足夠了。


PostgreSQL Service 又在做什麼?

YAML 後半段還建立:

kind: Service

名稱:

metadata:
  name: postgres

它會透過:

selector:
  app: postgres

找到 PostgreSQL Pod。

所以之後 FastAPI 不需要知道 PostgreSQL Pod IP。

FastAPI 可以直接連:

postgres:5432

架構就變成:

FastAPI Pod
↓
postgres:5432
↓
Service postgres
↓
PostgreSQL Pod

這和前面的:

redis:6379

完全是相同概念。

所以到現在為止,我們其實已經把 Kubernetes 裡兩個非常重要的問題分開解決:

Service
→ 解決「我要怎麼找到另一個 Pod?」

PVC
→ 解決「Pod 消失後,資料怎麼留下?」

部署 PostgreSQL

建立好 YAML 之後:

kubectl apply -f k8s/

接著先確認 Pod:

kubectl get pods -n cka-lab

應該可以看到 PostgreSQL Pod。

https://ithelp.ithome.com.tw/upload/images/20260910/20168537C59qmr1OF8.png!


查看 PVC

接著:

kubectl get pvc -n cka-lab

https://ithelp.ithome.com.tw/upload/images/20260910/201685376ZlCiyliFr.png

最重要的是:

STATUS = Bound

Bound 代表:

PVC 已經成功找到並綁定一個 PersistentVolume。

也就是:

PVC
↓
PV

已經建立關係。

如果看到:

Pending

則代表:

PVC 還沒有取得 Storage

這時才需要進一步檢查 StorageClass、Provisioner 或 PV。


查看 PersistentVolume

接著執行:

kubectl get pv

可能會看到:

NAME         CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS
pvc-xxxxx    1Gi        RWO            Delete           Bound

https://ithelp.ithome.com.tw/upload/images/20260910/20168537xZ67iPVvJj.png

這就是 PVC 背後真正配對到的:

PersistentVolume

所以目前關係大概是:

PostgreSQL Pod
        ↓
postgres-data
        ↓
postgres-pvc
        ↓
PV
        ↓
Storage

到這裡雖然概念看起來合理,但光看到:

Bound

還不代表你真的理解 Persistence。

最好的方法是:

真的把 Pod 刪掉。

真正測試:刪掉 PostgreSQL Pod,資料會不會消失?

首先進入 PostgreSQL:

kubectl exec \
  -it \
  deployment/postgres \
  -n cka-lab \
  -- \
  psql \
  -U appuser \
  -d appdb

這個指令可以理解成:

kubectl exec
↓
進入 deployment/postgres 的 Pod
↓
執行 psql
↓
User:appuser
↓
Database:appdb

進去之後建立一張測試 Table:

CREATE TABLE demo (
    id SERIAL PRIMARY KEY,
    message TEXT
);

再利用

\d demo

去查看建立好的欄位

https://ithelp.ithome.com.tw/upload/images/20260910/20168537HHYJfpv0UQ.png

接著插入一筆資料:

INSERT INTO demo (message)
VALUES ('PVC survives pod replacement');

查詢:

SELECT * FROM demo;

應該會看到類似:

 id |            message
----+---------------------------------
  1 | PVC survives pod replacement

https://ithelp.ithome.com.tw/upload/images/20260910/20168537JBtB7g4XMo.png
現在這筆資料真的已經寫進 PostgreSQL。

離開 PostgreSQL:

\q

接下來做一件很暴力的事情:刪掉 PostgreSQL Pod

執行:

kubectl delete pod \
  -n cka-lab \
  -l app=postgres

這個指令會把符合:

app=postgres

Label 的 Pod 刪除。

接著查看:

kubectl get pods -n cka-lab

你可能會看到舊 Pod 消失,然後一顆新的 PostgreSQL Pod 被建立。

原因是 Deployment 裡寫了:

replicas: 1

Deployment 的期待是:

永遠維持 1 個 PostgreSQL Pod。

所以:

你手動刪掉 Pod
↓
Deployment 發現

Current = 0
Desired = 1
↓
自動建立新的 Pod

例如:

原本:

postgres-7c8d9c7b56-aaaaa


刪除之後建立了新的:

postgres-7c8d9c7b56-bbbbb

Pod Name 已經不同。

這證明:

現在是新的 Pod。

再次檢查 Database

等新的 Pod:

READY 1/1
STATUS Running

之後,再次進入 PostgreSQL:

kubectl exec \
  -it \
  deployment/postgres \
  -n cka-lab \
  -- \
  psql \
  -U appuser \
  -d appdb

再次執行:

SELECT * FROM demo;

如果還看到:

 id |            message
----+---------------------------------
  1 | PVC survives pod replacement

https://ithelp.ithome.com.tw/upload/images/20260910/20168537IjBwuwTg1f.png

這時候其實就已經實際證明了一件非常重要的事情:

舊 PostgreSQL Pod
已經不存在。

新的 PostgreSQL Pod
已經建立。

但 Database Data
仍然存在。

原因就是資料並不是綁在:

Pod

而是存在:

Persistent Storage

新的 PostgreSQL Pod 啟動之後,再重新掛載同一個 PVC:

postgres-pvc

於是它重新看到原本的 Database Data。

整個過程就是:

PostgreSQL Pod A
        ↓
      PVC
        ↓
      Data


Pod A 被刪除


PostgreSQL Pod B
        ↓
     同一個 PVC
        ↓
     同一份 Data

這就是:

Persistence

快速複習今天學的 PVC 概念

完整的流程是:

你建立 PVC
    ↓
Kubernetes 看有沒有可以用的 PV
    ↓
如果沒有,而且 Cluster 有 StorageClass + Provisioner
    ↓
Provisioner 動態建立 Storage
    ↓
Kubernetes 建立對應的 PV
    ↓
PVC ↔ PV 綁定(Bound)
    ↓
Pod 使用 PVC
    ↓
把這個 Storage 掛進 Container

Kubernetes 官方把這叫做 Dynamic Provisioning;它依賴 StorageClass 與 provisioner。沒有這套機制、也沒有事先建立好的合適 PV 時,PVC 可能就會一直停在 Pending

你可以把它想成「三件完全不同的事」

假設你寫:

kind: PersistentVolumeClaim

metadata:
  name: postgres-pvc

spec:
  resources:
    requests:
      storage: 1Gi

你其實只是在跟 Kubernetes 說:

「我要 1Gi 的硬碟空間。」

你還沒有叫 PostgreSQL 使用它。

建立 PVC 後,如果你的 Cluster 有預設 StorageClass,例如:

PVC: 我要 1Gi
      ↓
StorageClass: 好,我知道要找誰生硬碟
      ↓
Provisioner
      ↓
建立真正 Storage
      ↓
產生 PV
      ↓
PVC Bound 到 PV

StorageClass 本身就是描述「該用哪個 provisioner、什麼參數去配置 Storage」的資源。

然後第二件事才是你的 Deployment 說:

volumes:
  - name: postgres-data
    persistentVolumeClaim:
      claimName: postgres-pvc

意思不是「建立 PV」。

而只是:

我的 Pod 要使用已經叫做 postgres-pvc 的這份 Storage。

再來第三件事:

volumeMounts:
  - name: postgres-data
    mountPath: /var/lib/postgresql/data

這是在說:

把剛才那份 Storage,掛到 PostgreSQL Container 的 /var/lib/postgresql/data

所以你可以把整件事理解成:

① PVC:我要硬碟

postgres-pvc
「我要 1Gi」

② Storage System:準備硬碟

StorageClass
    ↓
Provisioner
    ↓
PV / 真正 Storage

③ Deployment:我要使用那顆硬碟

Pod
 ↓
postgres-pvc
 ↓
PV

④ volumeMounts:掛到 Container 哪裡

PostgreSQL Container
└── /var/lib/postgresql/data
         ↓
     Persistent Storage

那 PostgreSQL 實際寫資料時發生什麼?

這才是最關鍵的。

PostgreSQL 自己根本不知道什麼叫:

PVC
PV
StorageClass

它只知道:

我要把資料寫到:

/var/lib/postgresql/data

原本沒有掛 PVC 時:

PostgreSQL
    ↓
/var/lib/postgresql/data
    ↓
Container Filesystem
    ↓

Pod Delete
    ↓
💥 Data 消失

現在你加了:

volumeMounts:
  - name: postgres-data
    mountPath: /var/lib/postgresql/data

之後:

PostgreSQL
    ↓
/var/lib/postgresql/data
    ↓
PVC
    ↓
PV
    ↓
真正 Storage

所以 PostgreSQL 還是一模一樣:

寫 /var/lib/postgresql/data

但 Kubernetes 偷偷把這個資料夾「接到外面的 Storage」。

這就是 Volume Mount 最實際的意義。


到這裡真正應該理解什麼?

今天最重要的不是背:

PV = PersistentVolume
PVC = PersistentVolumeClaim

真正需要理解的是 Kubernetes 把:

Compute

和:

Storage

分開管理。

Pod 屬於 Compute。

它本來就是:

Ephemeral 短暫的

也就是可以被:

建立
刪除
重新排程
替換

因此永遠不要假設:

某一顆 Pod 會一直存在。

但 Database Data 的需求完全相反。

我們希望:

Pod 可以換
Data 不要換

所以 Kubernetes 透過:

PVC

把 Pod 和 Persistent Storage 連接起來。

最後你可以把今天整個概念濃縮成:

Service
解決:
Pod 怎麼找到 Pod?


Deployment
解決:
Pod 掛掉之後誰把它建立回來?


PVC / PV
解決:
Pod 被換掉之後資料怎麼留下?

這三個概念開始組合起來之後,你的 Kubernetes 架構才真正開始像一個完整的 Application:

                    Client
                       ↓
                 Service api
                       ↓
                  FastAPI Pod
                  ↙        ↘
                 ↓          ↓
         Service redis   Service postgres
                 ↓          ↓
             Redis Pod   PostgreSQL Pod
                              ↓
                             PVC
                              ↓
                              PV
                              ↓
                       Persistent Storage

這也是今天最重要的一句話:

Pod 是可以被替換的運算單位;真正需要長期保存的資料,不應該依賴 Pod 的生命週期。

因此當你親手執行:

kubectl delete pod

看著 PostgreSQL Pod 被換掉,再執行:

SELECT * FROM demo;

發現資料依然存在時,你理解的就不再只是:

PVC = PersistentVolumeClaim

而是真的理解了 Kubernetes 裡的:

Persistence。

另外補充一個之後會遇到的重要觀念:這一章使用 Deployment + PostgreSQL + PVC 非常適合拿來學習 Volume 與 Persistence;但正式環境中的 Stateful Database,通常還會進一步學習 StatefulSet、StorageClass、Backup、Replication 等機制。

現階段先把「Pod 可以消失,但 Data 不應該跟著消失」這個核心觀念徹底搞懂即可!


上一篇
Day 12|微服務開始成形:加入 Redis,第一次讓 Pod 透過 Service 找 Pod
下一篇
Day 14|Running 不代表健康:Liveness、Readiness、Requests 與 Limits
系列文
不是背 YAML!30 天從零打造 Kubernetes 微服務:從本機實戰一路到 CKA18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言