Pod は使い捨てなのに、DB のデータはなぜ消えないのか?
あなたはこれまで、Deployment が管理する Pod はいつ壊れて作り直されてもいい使い捨ての実行単位だと学んできたはずだ。では同じ Pod の中で PostgreSQL や Redis を動かしていたら、Pod が作り直されるたびに中のデータも消えてしまうのだろうか。実際には多くの本番環境で、DB を積んだ Pod が何度再作成されてもデータを失わずに起動し直している。
- Pod のファイルシステムは既定で Pod のライフサイクルに縛られた一時領域で、
PersistentVolume(PV)/PersistentVolumeClaim(PVC)を使って初めて Pod から切り離した永続化ができる。 - PVC は「このくらいの容量とアクセスモードが欲しい」というリクエストにすぎず、
StorageClassが設定されていれば実ディスクの用意まで自動でやってくれる(動的プロビジョニング)。 - PVC を消したとき PV がどうなるかは
reclaimPolicyが決め、動的プロビジョニングの既定はDelete(ディスクごと消える)、手動で作った PV の既定はRetain(残る)という非対称な設計になっている。
Pod のディスクは、消える前提で設計されている
コンテナが書き込むファイルは、Pod のライフサイクルに紐づく一時領域に保存される。これはemptyDirボリュームを明示的に宣言した場合も変わらない。同じ Pod 内でコンテナだけが再起動すればデータは残るが、Pod 自体が削除されたりノード障害で別のノードに再スケジュールされたりすると、その中身はまるごと失われる。
この挙動は欠陥ではなく、Pod を「いつ壊れて作り直されてもいい使い捨ての実行単位」として扱う Kubernetes の前提と表裏一体の設計だ。ステートレスな Web アプリならこれで困らない。しかし PostgreSQL や Redis のようにプロセスの外に状態を持つアプリケーションを同じ発想で動かそうとすると、Pod が1回作り直されるだけでデータベースの中身が消えてしまう。
PersistentVolume と PersistentVolumeClaim — ライフサイクルを Pod から切り離す
PersistentVolume(PV)はクラスタの中にある一塊のストレージをオブジェクトとして表現したもので、特定の Pod とは結びつかない独立したライフサイクルを持つ。一方PersistentVolumeClaim(PVC)はアプリ開発者が「これくらいの容量とアクセスモードが欲しい」と書く要求書で、Pod が CPU やメモリのrequestsを書くのと同じ感覚で、ストレージについての要求を書くイメージに近い。
コントロールプレーンの制御ループが新しく作られた PVC を監視していて、容量・アクセスモード・StorageClassの条件に合う PV を見つけると 1 対 1 でバインドする。一度結ばれた関係はClaimRefという参照で固定され、他の PVC に横取りされない。条件に合う PV がなければ、PVC はバインドされないまま待ち続ける。
┌─────┐ 参照 ┌─────┐ バインド ┌────┐ │ Pod │─────▶│ PVC │────────▶│ PV │ └─────┘ └─────┘ └────┘ 消える 残り続ける Retain/Delete (作り直し) で運命決定
PVC のaccessModesは、PV が同時にどこからどう使われてよいかを宣言する項目だ。ReadWriteOnce(RWO)を指定すると、そのボリュームは同時に1つのノードからしかマウントできず、別ノードにスケジュールされた Pod が同じ PVC を使おうとすると Pod はバインド待ちのまま止まる。複数ノードから同時に読み書きしたい場合はReadWriteMany(RWX)が必要になるが、対応するストレージ基盤がないクラスタでは使えない。
PVC と PV の関係は、賃貸マンションの入居申込書と実際の部屋に似ている。入居希望者(Pod)は申込書(PVC)に「2DK 以上・駅徒歩10分以内」のような条件だけを書いて管理会社に出し、どの部屋がどのオーナーの持ち物かは気にしない。管理会社の担当者(バインドの制御ループ)が条件に合う空き部屋(PV)を見つけて鍵を渡すとそこで契約成立だ。入居者が退去しても部屋そのものは管理会社の管理下に残り、すぐ次の入居者向けに解体するか、しばらく当時のまま確保しておくかは、契約時に決めた規約(reclaimPolicy)で決まる。
StorageClass と reclaimPolicy — 動的プロビジョニングとデータの運命
PV は管理者が事前に手作業で用意しておかなくてもよい。PVC がstorageClassNameを指定すると(何も書かなければクラスタのデフォルトStorageClassが使われる)、StorageClassのprovisionerフィールドが指すドライバーが実ディスクをオンデマンドで作成し、それを指す PV を自動生成する。これを動的プロビジョニングと呼ぶ。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
storageClassName: standard
PV の「P」は Persistent(永続的)の頭文字だから、PV さえ使えばデータは絶対に安全だと思うかもしれない。しかし PVC を削除したときに残された PV がどうなるかはreclaimPolicyという設定ひとつで決まる。動的プロビジョニングで作られた PV の既定はDeleteで、PVC を消すと PV と裏の実ディスクそのものも一緒に削除される。一方、管理者が手作業で作った PV の既定はRetainで、PVC を消しても PV はバインド解除されたまま残り続け、別の PVC に再利用するには管理者の手作業が必要になる。既定のDeleteのまま運用していると、うっかり PVC を消しただけで本番データが復元不能になる事故につながるため、重要なデータを持つStorageClassにはreclaimPolicy: Retainを明示することがある。
StorageClassのvolumeBindingModeにも、既定の挙動と変えたときの違いがある。既定のImmediateは PVC が作られた瞬間にディスクを確保してしまうため、クラウド環境でそのディスクが特定のアベイラビリティーゾーンに縛られるタイプだと、後から Pod がそのゾーンにないノードにスケジュールされてバインド待ちのまま動かなくなることがある。WaitForFirstConsumerに設定すると、Pod がスケジュールされて配置ノードが決まるまでプロビジョニングを遅らせるため、Pod が実際に立つゾーンに合わせてディスクを作れる。
StatefulSet — 複数の Pod がそれぞれ専用の PVC を持つ
PV と PVC だけでは、同じデータベースを複数レプリカで動かそうとした途端に壁にぶつかる。Deployment が複数の Pod に同じ 1 つの PVC を渡すと、ReadWriteOnceの制約でバインド待ちのまま止まったり、全レプリカが同じディスクを取り合う構成になってしまう。StatefulSetはこの問題を、各レプリカ専用の PVC をvolumeClaimTemplatesで自動生成することで解決する。Pod 名はweb-0、web-1のように連番で固定され、各 Pod は作り直されるたびに自分専用の PVC(例: www-web-0)へ結びつき直す。
StatefulSetは Pod の作成・終了も連番どおりに 1 つずつ進める点で Deployment と異なる。加えて、StatefulSetを削除したりレプリカ数を減らしたりしても、volumeClaimTemplatesが作った PVC は自動では削除されないという仕様だ。これはデータ保護を目的とした意図的な設計であり、PVC を消すかどうかの判断は運用者に委ねられている。
自分で PV の YAML を書く機会は少なくても、Helm chart で PostgreSQL や Redis をデプロイするとvolumeClaimTemplatesが裏で自動生成されていることが多い。EKS や GKE では CSI ドライバ経由で EBS や永続ディスクが自動プロビジョニングされ、クラウドの請求に乗ってくるストレージコストの正体でもある。「Pod が Pending のまま動かない」というトラブルは PVC がバインドできていないケースが多く、kubectl get pvcのSTATUS列とkubectl describe pvcのイベントを見るのが最初の一手になる。
kind クラスタで PVC を作り、ある Pod が書き込んだファイルを、その Pod を消したあとに別の Pod から読めるか確認する。最後に PVC を消して PV が本当に一緒に消えるか(既定の reclaimPolicy: Delete)も見る。
kind create cluster --name pv-demo
cat <<'YAML' | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: demo-pvc
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: writer
spec:
restartPolicy: Never
containers:
- name: writer
image: busybox
command: ["sh", "-c", "echo hello from pod1 > /data/msg.txt"]
volumeMounts:
- {name: vol, mountPath: /data}
volumes:
- name: vol
persistentVolumeClaim: {claimName: demo-pvc}
YAML
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/writer --timeout=60s
kubectl delete pod writer
cat <<'YAML' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: reader
spec:
restartPolicy: Never
containers:
- name: reader
image: busybox
command: ["cat", "/data/msg.txt"]
volumeMounts:
- {name: vol, mountPath: /data}
volumes:
- name: vol
persistentVolumeClaim: {claimName: demo-pvc}
YAML
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/reader --timeout=60s
kubectl logs reader
kubectl get pv
kubectl delete pvc demo-pvc
kubectl get pv
kind delete cluster --name pv-demo
kubectl logs readerでwriterが書き込んだ文字列がそのまま出力されれば、Pod を作り直しても PVC を介してデータが引き継がれたことが確認できる。最後のkubectl get pvでは、kubectl delete pvcの直後に PV がもうリストに出てこなくなっているはずだ。kind の既定StorageClassであるstandard(rancher.io/local-path)はreclaimPolicyがDeleteなので、PVC を消すと裏の PV と実ディスクの中身も一緒に消える。
- PVC を消せばいつでもデータも消える — 実際にはどうなるかは
reclaimPolicyが決める。動的プロビジョニングの既定はDeleteで消えるが、管理者が手作業で作った PV の既定はRetainで残り続ける。 - StatefulSet はスケールダウン時に不要な PVC も片付けてくれる —
volumeClaimTemplatesが作った PVC は、レプリカを減らしてもStatefulSet自体を削除しても自動では消えない。データ保護のための意図的な仕様で、消すかどうかは運用者の手作業に委ねられている。 - PersistentVolume という名前の通り、使えば永続性は保証される — PV は Pod のライフサイクルから独立しているだけで、
reclaimPolicyの設定ミスや PVC の誤削除、裏にあるディスク自体の障害までは守ってくれない。
- PersistentVolume(PV)
- クラスタ内の実ストレージを表す、Pod とは独立したライフサイクルを持つオブジェクト。
- PersistentVolumeClaim(PVC)
- 容量やアクセスモードを指定してストレージを要求する、ユーザー側のリクエストオブジェクト。
- StorageClass
- どの
provisionerでディスクを動的に作るか、reclaimPolicyやvolumeBindingModeをまとめて定義するプロファイル。 - reclaimPolicy
- PVC 削除後に PV と実ディスクを残す(
Retain)か消す(Delete)かを決める設定。 - volumeBindingMode
- ディスクの確保を PVC 作成直後(
Immediate)にするか、Pod のスケジュール後(WaitForFirstConsumer)まで遅らせるかの設定。 - StatefulSet / volumeClaimTemplates
- 連番の安定した Pod 名を持つワークロード管理オブジェクトと、各レプリカに専用 PVC を自動生成する仕組み。
- Persistent Volumes(kubernetes.io) — PV/PVC のバインドの仕組みと、reclaimPolicy・accessModes の一次資料。
- Storage Classes(kubernetes.io) — provisioner・reclaimPolicy・volumeBindingMode の公式定義。
- StatefulSets(kubernetes.io) — volumeClaimTemplates と、PVC が自動削除されない仕様の公式説明。