はじめに
Deployment では、レプリカは相互に置き換え可能なものとして扱われます。一方、ステートフルなアプリケーションでは、安定した Pod 名、予測可能な起動順序、Pod が置き換えられても保持されるストレージが必要になることがあります。StatefulSet は、Pod ごとのクレームとこれらのアイデンティティを組み合わせて管理します。
この実験では、動的ストレージの状態を確認し、2 レプリカの StatefulSet をデプロイしてデータを書き込みます。その後、Pod を置き換え、アイデンティティとデータの両方が保持されることを確認します。
3 種類のアイデンティティを分けて追跡してください。StatefulSet は必要なレプリカ数を管理し、各 Pod には -0 のような安定した序数が割り当てられ、各序数には専用の PersistentVolumeClaim が割り当てられます。クレームと保存データが残るため、Pod 自体は置き換えることができます。
動的ストレージを確認する
このステップでは、ストレージを要求する前に、クラスターの StorageClass を確認します。
PersistentVolumeClaim はワークロードが必要とするストレージを定義し、StorageClass はクラスターがそのストレージをどのようにプロビジョニングできるかを定義します。利用可能なクラスを先に確認しておくと、バインドできないクレームを作成する事態を防げます。
cd /home/labex/project/stateful-storage
kubectl get storageclasses
kubectl get storageclass -o custom-columns='NAME:.metadata.name,PROVISIONER:.provisioner,DEFAULT:.metadata.annotations.storageclass\.kubernetes\.io/is-default-class'
デフォルトの StorageClass は、クレームで storageClassName を省略した場合に PersistentVolume を動的に作成します。これにより、学習用マニフェストでノード固有の hostPath 設定を記述する必要がなくなります。
通常の一覧で (default) と表示されるか、カスタム出力の DEFAULT 列に true と表示されるものを探してください。プロビジョナー名はこの環境の実装上の詳細であり、重要なのはデフォルトクラスが存在することです。
この環境では、次の結果になります。
NAME PROVISIONER ...
standard (default) k8s.io/minikube-hostpath ...
NAME PROVISIONER DEFAULT
standard k8s.io/minikube-hostpath true
したがって、standard がデフォルトクラスです。StatefulSet では storageClassName を省略するため、Kubernetes がこのクラスを自動的に選択します。
StatefulSet とヘッドレス Service を作成する
このステップでは、安定した DNS 名前解決を提供するヘッドレス Service と、レプリカごとに 1 つのクレームを作成する StatefulSet を定義します。
このマニフェストには、相互に関連する複数のフィールドが含まれています。適用する前に確認しましょう。
clusterIP: Noneにより Service がヘッドレスになり、DNS で個々の Pod を表現できるようになります。serviceName: stateful-webにより、StatefulSet がその Service に関連付けられます。volumeClaimTemplatesにより、テンプレート名と Pod の序数から名前を生成したクレームが 1 つずつ作成されます。volumeMountsにより、各 Pod 固有のクレームが NGINX のドキュメントディレクトリにマウントされます。
ラベルによる Service から Pod への選択は、最初のコースで学んだ通常の仕組みと同じです。
cd /home/labex/project/stateful-storage
cat <<'EOF' > stateful-web.yaml
apiVersion: v1
kind: Namespace
metadata: {name: practice-stateful}
---
apiVersion: v1
kind: Service
metadata: {name: stateful-web, namespace: practice-stateful}
spec:
clusterIP: None
selector: {app: stateful-web}
ports: [{name: http, port: 80, targetPort: http}]
---
apiVersion: apps/v1
kind: StatefulSet
metadata: {name: stateful-web, namespace: practice-stateful}
spec:
serviceName: stateful-web
replicas: 2
selector: {matchLabels: {app: stateful-web}}
template:
metadata: {labels: {app: stateful-web}}
spec:
containers:
- name: web
image: nginx:1.27-alpine
imagePullPolicy: IfNotPresent
ports: [{name: http, containerPort: 80}]
volumeMounts: [{name: web-data, mountPath: /usr/share/nginx/html}]
volumeClaimTemplates:
- metadata: {name: web-data}
spec:
accessModes: [ReadWriteOnce]
resources: {requests: {storage: 100Mi}}
EOF
マニフェストを適用し、順序制御されたロールアウトが完了するまで待ちます。StatefulSet では通常、序数 0 の Pod が先に作成され、その後に序数 1 の Pod が作成されるため、2 つ目の Pod が表示されるまで少し時間がかかる場合があります。
kubectl apply -f stateful-web.yaml
kubectl rollout status statefulset/stateful-web -n practice-stateful --timeout=90s
kubectl get pods,pvc -n practice-stateful
Pod 名には安定した序数が使用されます。今回の名前は stateful-web-0 と stateful-web-1 です。各クレーム名には、テンプレート名、Pod 名、序数が含まれます。
web-data-stateful-web-0 と web-data-stateful-web-1 というクレームが作成され、どちらも Bound になっていることを確認してください。Bound は、各要求を満たすストレージを Kubernetes が検出したか、動的にプロビジョニングしたことを示します。
ロールアウトのメッセージから Pod が順序どおりに作成される様子を確認でき、その後に 2 つのクレームが表示されます。
Waiting for 2 pods to be ready...
Waiting for 1 pods to be ready...
partitioned roll out complete: 2 new pods have been updated...
pod/stateful-web-0 1/1 Running ...
pod/stateful-web-1 1/1 Running ...
persistentvolumeclaim/web-data-stateful-web-0 Bound ... 100Mi RWO standard
persistentvolumeclaim/web-data-stateful-web-1 Bound ... 100Mi RWO standard
生成されるボリューム ID は実行するたびに異なります。安定したクレーム名、Bound ステータス、要求容量、ストレージクラスに注目してください。
Pod 固有の永続データを書き込む
このステップでは、各 Pod にマウントされたクレームへ異なる内容を書き込みます。
出力リダイレクト(>)をコンテナ内で実行する必要があるため、kubectl exec ... -- sh -c を使用します。sh -c を使わない場合、ローカルシェルが Pod 内ではなく VM 上で出力をリダイレクトしようとします。
cd /home/labex/project/stateful-storage
kubectl exec stateful-web-0 -n practice-stateful -- sh -c 'echo persistent-zero > /usr/share/nginx/html/index.html'
kubectl exec stateful-web-1 -n practice-stateful -- sh -c 'echo persistent-one > /usr/share/nginx/html/index.html'
kubectl exec stateful-web-0 -n practice-stateful -- cat /usr/share/nginx/html/index.html
kubectl exec stateful-web-1 -n practice-stateful -- cat /usr/share/nginx/html/index.html
kubectl get pods -n practice-stateful -o custom-columns='POD:.metadata.name,HOSTNAME:.spec.hostname,CLAIM:.spec.volumes[0].persistentVolumeClaim.claimName'
Pod は同じイメージを実行していますが、異なるクレームをマウントしているため、データは共有されず、それぞれ独立しています。
2 つの cat コマンドからは異なる文字列が返されます。その後のカスタムテーブルでは、安定した Pod 名とクレームの対応関係が表示され、ストレージの関連付けを確認できます。
persistent-zero
persistent-one
POD HOSTNAME CLAIM
stateful-web-0 stateful-web-0 web-data-stateful-web-0
stateful-web-1 stateful-web-1 web-data-stateful-web-1
3 つの列すべてに序数が現れています。この繰り返される 0 または 1 が、Pod と専用ストレージを結び付けるアイデンティティです。
Pod を置き換えてデータを復元する
このステップでは、序数 0 の Pod を削除し、StatefulSet に同じアイデンティティで再作成させます。
管理対象の Pod を削除することは、必要なレプリカ数を減らすこととは異なります。StatefulSet は引き続き 2 つの Pod を必要とするため、stateful-web-0 という名前の Pod を再作成し、その序数用に保持されているクレームを再アタッチします。
cd /home/labex/project/stateful-storage
kubectl delete pod stateful-web-0 -n practice-stateful --wait=true
kubectl wait --for=condition=Ready pod/stateful-web-0 -n practice-stateful --timeout=90s
kubectl exec stateful-web-0 -n practice-stateful -- cat /usr/share/nginx/html/index.html
kubectl get pod stateful-web-0 -n practice-stateful -o jsonpath='pod={.metadata.name} claim={.spec.volumes[0].persistentVolumeClaim.claimName}{"\n"}'
Pod オブジェクト自体は新しくなりますが、名前とクレームの関連付けは維持されます。復元されたファイルによって、データが削除されたコンテナのファイルシステムではなく、PersistentVolume に保存されていたことが確認できます。
pod "stateful-web-0" deleted from practice-stateful namespace
pod/stateful-web-0 condition met
persistent-zero
pod=stateful-web-0 claim=web-data-stateful-web-0
重要なのは persistent-zero という行です。これは削除前に書き込まれ、置き換え後の Pod から読み取られています。
マウントされたディレクトリの外側にファイルを書き込んでいた場合、そのファイルは古いコンテナのファイルシステムとともに失われます。データの永続性を決めるのは StatefulSet の名前だけではなく、マウントパスです。
まとめ
デフォルトの StorageClass、動的な PVC プロビジョニング、ヘッドレス Service、StatefulSet の安定した序数、Pod ごとのストレージを組み合わせて確認しました。Pod を置き換えても、そのアイデンティティとデータの両方が保持されることを確認できました。


