本番環境でKubernetesストレージを6ヶ月運用して分かったこと
最初にステートフルなワークロードをKubernetesに移行したとき、ストレージが一番の難関でした。Deployment、Service、ConfigMapは直感的に理解できましたが、永続ストレージは違いました。ポッドが再起動後も消えないディスクを必要とするとき、実際に何が起きているのかを一つひとつ追っていくまで、抽象化の仕組みがまったく理解できなかったのです。
PostgreSQLのセットアップやElasticsearchスタックを含む複数のクラスターでPersistentVolumeとPersistentVolumeClaimを約6ヶ月間運用してきた今、何がうまくいくのか、何が壊れるのか、そして序盤に何時間も費やした落とし穴がどこにあるのか、かなり明確に把握できています。
2つのストレージモデル:静的プロビジョニングと動的プロビジョニング
早い段階で適切なプロビジョニングモデルを選ぶことで、後から大幅なリファクタリングをせずに済みます。2つの選択肢があり、それぞれ異なる環境に適しています。
静的プロビジョニング
静的プロビジョニングでは、既存のストレージリソース(NFSシェア、事前プロビジョニングされたクラウドディスク、ローカルパスなど)を指すPersistentVolume(PV)オブジェクトを手動で作成します。そしてPersistentVolumeClaim(PVC)がそのストレージをリクエストします。Kubernetesはキャパシティ、アクセスモード、ストレージクラスに基づいてバインドします。
# NFSシェアを指す静的PV
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv-data
spec:
capacity:
storage: 20Gi
accessModes:
- ReadWriteMany
nfs:
server: 192.168.1.50
path: /exports/k8s-data
persistentVolumeReclaimPolicy: Retain
# 上記PVにバインドするPVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data-claim
namespace: production
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 20Gi
動的プロビジョニング
動的プロビジョニングはモデルを逆転させます。オンデマンドでストレージを作成する方法を知っているStorageClassを定義します。PVCがクラスターに届くと、KubernetesはプロビジョナーにコールしてAWS EBS CSIドライバー、GCE PD、Longhornなど設定済みのものが自動的にバッキングストレージを作成します。
# AWS EBS CSIドライバーを使うStorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
type: gp3
iops: "3000"
throughput: "125"
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
# 動的プロビジョニングを使うPVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
namespace: production
spec:
storageClassName: fast-ssd
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
各アプローチのメリット・デメリット
静的プロビジョニング
- メリット:データの置き場所を完全にコントロールできます。オンプレミスのNFSや既存のSANインフラとの相性が良く、外部プロビジョナーも不要です。
- デメリット:規模が大きくなると手動管理の負担が増えます。ボリュームを事前作成して自分で追跡しなければなりません。キャパシティやアクセスモードが一つでも合わないとバインドに失敗し、PVCがPendingのまま残ります。ステートフルなアプリを何十個も立ち上げるようになると現実的ではありません。
動的プロビジョニング
- メリット:手間なくスケールできます。開発者がPVCを投入するとストレージが現れます。CSIドライバーがスナップショット、リサイズ、暗号化をそのまま処理してくれます。クラウドクラスターではストレージ運用の大部分をなくせます。
- デメリット:動作するCSIドライバーと対応ストレージプロバイダーが必要です。開発者が習慣的に500Giボリュームをリクエストするとコストが急増することがあります。また、デフォルトの
Delete回収ポリシーは、PVCが削除された瞬間にクラウドディスクを無音で破壊します。詳しくは後述します。
自宅ラボのシングルNFSサーバーのクラスターでは静的プロビジョニングで十分でした。しかしAWS EKSに移行した瞬間、EBS gp3による動的プロビジョニングが明らかに正しい選択でした。ノードの入れ替え、1.27から1.30へのクラスターアップグレード、一度の誤ったポッド削除カスケードを経た6ヶ月間で、データ損失はゼロです。
ほとんどのチームに推奨するセットアップ
クラウドベースのクラスターはCSIドライバーを使った動的プロビジョニングをデフォルトにすべきです。StorageClassを少なくとも2つ作成してください:
- standard — 汎用、gp2またはgp3、ログや重要度の低いデータ向け
- fast-ssd — 高IOPS(gp3、3000 IOPS)、データベースやメッセージキュー向け
両方にvolumeBindingMode: WaitForFirstConsumerを設定してください。これがないと、Kubernetesはポッドをスケジューリングする前にボリュームを作成します。ボリュームがus-east-1aに作られ、ポッドがus-east-1bにスケジュールされると、ポッドは永遠に起動しません。序盤にこれを見落として2時間デバッグに費やしました。
もう一つ:実データを保持するボリュームには必ずreclaimPolicy: Retainを設定してください。動的プロビジョニングされたPVのデフォルトDeleteポリシーは、PVCが削除されるとクラウドディスクを永久に破壊します。警告もなく、復元もできません。
# 本番環境向けの安全なStorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
type: gp3
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
実装ガイド
ステップ1:利用可能なStorageClassを確認する
kubectl get storageclass
# (default) マーカーを探す — storageClassNameが指定されていない場合にこれが使われる
ステップ2:PVCを作成する
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
namespace: production
spec:
storageClassName: fast-ssd
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
kubectl apply -f postgres-pvc.yaml
kubectl get pvc -n production
# ポッドから参照された後にSTATUSがBoundになる(WaitForFirstConsumer設定時)
ステップ3:PodまたはStatefulSetにPVCをマウントする
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: production
spec:
selector:
matchLabels:
app: postgres
serviceName: postgres
replicas: 1
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
storageClassName: fast-ssd
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 50Gi
StatefulSet内のvolumeClaimTemplatesはデータベースに最適なパターンです。各レプリカが自動的に独自のPVCを取得し、data-postgres-0、data-postgres-1のように命名されます。PVCを手動管理する必要はありません。
ステップ4:確認とトラブルシューティング
# PVCの状態を確認
kubectl describe pvc postgres-data -n production
# Pendingのまま止まっている場合はイベントを確認
kubectl get events -n production --sort-by=.lastTimestamp
# バインドされたPVを調べる
kubectl get pv
kubectl describe pv <pv-name>
PVCがPendingで止まっている場合、原因はほぼ2つのどちらかです。リクエストに一致するStorageClassがないか、WaitForFirstConsumerが設定されていてポッドがまだスケジュールされていないかです。kubectl describe pvcを実行してください。Eventsセクションを見れば、数秒以内に何が問題なのかほぼ正確に分かります。
ステップ5:ダウンタイムなしでボリュームを拡張する
StorageClassにallowVolumeExpansion: trueが設定されていれば、リサイズはワンライナーで完了します:
kubectl patch pvc postgres-data -n production \
-p '{"spec":{"resources":{"requests":{"storage":"100Gi"}}}}'
EBSボリュームはオンラインでリサイズされるためポッドの再起動は不要です。一部のバックエンド(特定のNFSセットアップなど)は、ベースとなるボリュームが拡張された後にファイルシステムのリサイズを反映させるためにポッドの再起動が必要な場合があります。
アクセスモードのクイックリファレンス
- ReadWriteOnce (RWO):1つのノードが読み書きでボリュームをマウントします。データベースの標準的な選択です。ほとんどのクラウドブロックストレージ(EBS、Azure Disk)はこのモードのみサポートしています。
- ReadOnlyMany (ROX):複数のノードが読み取り専用でマウントします。共有設定ファイルや静的アセットに適しています。
- ReadWriteMany (RWX):複数のノードが同時に読み書きでマウントします。NFS、CephFS、または分散ファイルシステムが必要です。EBSのような標準的なクラウドブロックストレージでは利用できません。
よく見かける最も多い間違いは、EBSバックのStorageClassでRWXをリクエストすることです。PVCはサポートされていないアクセスモードに関する不可解なエラーとともにPendingのまま永遠に残ります。AWSで本当にRWXが必要な場合は、EFS CSIドライバーと組み合わせてEFSを使用してください。
まとめ
「ディスクをアタッチする」という考え方をやめてから、Kubernetesのストレージがようやく腑に落ちました。PVとPVCは抽象化レイヤーです。アプリが何を必要とするかを宣言すれば、クラスターがそのストレージをどこから調達するかを決めてくれます。
3つの決断でほとんどは解決します。動的プロビジョニングを使うこと、重要なものには回収ポリシーをRetainに設定すること、データベースにはvolumeClaimTemplatesを使ったStatefulSetを選ぶこと。この3つを正しく押さえれば、あとは調整するだけです。

