StatefulSet / DaemonSet / Job / CronJob
Deployment の stateless 前提では表現できない4系統のワークロードを扱うコントローラを整理します。StatefulSet のアイデンティティと PVC 1:1、DaemonSet のノード単位1個、Job の終了モデル、CronJob の cron スケジューリングと concurrencyPolicy · startingDeadlineSeconds の安全装置までを一連の流れで扱います。
2部 (ワークロードと運用) の最初の章です。第4章 Deployment と ReplicaSet の Deployment は stateless ワークロードの上に立つコントローラです。同じ Pod が複数あってもそれらは互いに同じだと仮定し、消えてもまた起動すれば済むという単純なモデルです。しかしアイデンティティとディスクを必要とする DB、ノードごとに正確に1つずつ起動しなければならないエージェント、一度実行して終わらなければならないマイグレーション、毎日回るバックアップ — この4つは Deployment では表現できません。本章ではその空白を埋める4つのコントローラ StatefulSet、DaemonSet、Job、CronJob をまとめて整理します。
本章の終わりには クラスタのマニフェストディレクトリで kind: が何であれ、その意図を一行で読み取れる決定木 が手に入ります。各コントローラを「なぜ Deployment では駄目なのか」の問題から始め、マニフェスト1枚と運用時の注意点までを一連の流れで追いかけます。
Deployment では表現できないワークロードたち #
第4章 でつかんだ Deployment の頭の中のモデルを一行に縮めるとこうなります — 同じ Pod テンプレートで N 個を常に維持し、新バージョンが来たら段階的に置き換えます。 このモデルがよく合うワークロードは、stateless な Web サーバ、API サーバ、ワーカーキューのコンシューマのように Pod 同士が区別されなくてもよい場合 です。web-abc123-aa11 であれ web-abc123-bb22 であれ同じコードが回り、どの Pod が死んでも別の Pod がその役割を埋めれば終わりです。
このモデルではうまく解けない4つのパターンがあります。
- Pod 同士が互いに異なると仮定しなければならないワークロード — データベースクラスタの primary と replica、Kafka の broker-0 / broker-1 / broker-2 のように、各 Pod が自分だけのアイデンティティと自分だけのディスクを持たなければならない場合です。Deployment が作る Pod は名前が任意値で、ディスクも共有されません。
- ノードごとに正確に1つずつ起動しなければならないワークロード — ログ収集器、ノードモニタリングエージェント、CNI (コンテナネットワークインターフェース) エージェントです。「replicas の個数」ではなく「ノード数に自動で合わせる」が必要なのですが、Deployment の
replicasフィールドはその意図を表現できません。 - 一度実行して終わらなければならないワークロード — DB マイグレーション、一度きりのデータレポート、クラスタセットアップスクリプトです。Deployment は Pod が終了するとまた起動しようとしますが、こうした仕事は終わるのが正常です。
- 周期的に実行されなければならないワークロード — 毎日未明のバックアップ、毎時0分の整理作業、毎週のレポート生成です。cron のようなスケジューリングがコントローラの次元になければなりません。
この4つを K8s がそれぞれ別のコントローラに分けてあるのが StatefulSet、DaemonSet、Job、CronJob です。1つずつ見ていきます。
StatefulSet — アイデンティティとディスクが必要なワークロード #
データベースを K8s に起動しようとすると、Deployment が最初にぶつかる壁が明確です。PostgreSQL primary が死んで新しい Pod が立ち上がったとき、その新しい Pod は 以前の Pod のデータディレクトリをそのまま引き継がなければなりません。 名前が任意値に変わっても困りますし、他の replica たちが primary をどう呼ぶかも安定していなければなりません。Deployment はこの3つのどれも保証しません。
StatefulSet が解いてくれるのは次の3つです。
- 安定的な Pod 名 — Pod が
<name>-0、<name>-1、<name>-2のようにインデックスが付いた名前を受け取ります。Pod が再起動されても同じインデックスを維持します。web-0が死んでまた立ち上がってもまたweb-0です。 - Pod ごとに 1:1 の永続ボリューム —
volumeClaimTemplatesで書いた PVC が各 Pod ごとに自動で作られます。web-0はdata-web-0PVC を、web-1はdata-web-1PVC を持ち、そのマッピングが Pod のライフサイクルを越えて維持されます。PV / PVC モデルそのものは 第9章 PV / PVC / StorageClass で深く扱います。 - 順次的なライフサイクル — 基本的に Pod は0番から順番に作られ、終了は逆順 (N-1 番から) で進みます。ローリングアップデートも同じ順序に従います。primary が先に立ち上がってこそ replica が付けるトポロジーに合わせたモデルです。
Headless Service と対になる #
StatefulSet は普通 headless Service と対で作ります。Pod ごとに安定的な DNS 名が必要だからです。headless Service の概念そのものは 第5章 Service の §「Service タイプ一表」ですでに一行で押さえてあります。
apiVersion: v1
kind: Service
metadata:
name: web
spec:
clusterIP: None
selector:
app: web
ports:
- port: 80
targetPort: 80核心は clusterIP: None の一行です。この Service は自分の仮想 IP を持たず、代わりに Pod ごとに個別の DNS レコードを作ってくれます。 クラスタ内部から次の名前で各 Pod を直接呼べます。
web-0.web.default.svc.cluster.local
web-1.web.default.svc.cluster.local
web-2.web.default.svc.cluster.local<pod>.<headless-service>.<namespace>.svc.cluster.local の形です。通常の ClusterIP Service が「複数の Pod の前段の仮想 IP」だとすれば、headless Service は「各 Pod の安定的な名札の発行機」だと見ればよいです。
StatefulSet マニフェスト #
上の headless Service と一緒に適用される StatefulSet マニフェストです。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: web
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1GiDeployment と違う部分が3つあります。
spec.serviceName: web— 上で作った headless Service の名前を指します。StatefulSet が Pod の DNS レコードをどこに登録するかを教えてくれるフィールドです。spec.volumeClaimTemplates— Pod ごとに PVC を自動で作り出すテンプレートです。上のマニフェストはdata-web-0、data-web-1、data-web-2の3つの PVC を作り、各 Pod の/usr/share/nginx/htmlにマウントします。この PVC が実際にどのディスクに接続されるかはStorageClassの動的プロビジョニングが決め、この一連の流れは 第9章 の本テーマです。replicasと Pod 名 — Deployment と同じreplicas: 3ですが、作られる Pod 名はweb-0、web-1、web-2に固定されます。ReplicaSet という中間オブジェクトもありません。
kubectl get pods,pvc -l app=webNAME READY STATUS RESTARTS AGE
pod/web-0 1/1 Running 0 1m
pod/web-1 1/1 Running 0 50s
pod/web-2 1/1 Running 0 40s
NAME STATUS VOLUME CAPACITY AGE
persistentvolumeclaim/data-web-0 Bound pvc-... 1Gi 1m
persistentvolumeclaim/data-web-1 Bound pvc-... 1Gi 50s
persistentvolumeclaim/data-web-2 Bound pvc-... 1Gi 40sPod が 0、1、2 の順に時間差を置いて立ち上がっていて、PVC も Pod ごとにそれぞれ作られているのが見えます。
運用時の一つの注意 — スケールダウン時に PVC は残る #
StatefulSet を replicas: 3 から replicas: 1 に減らすと、Pod web-1、web-2 は終了しますが PVC data-web-1、data-web-2 はそのまま残ります。 意図した動作です — データを誤って消さないようにするための安全装置です。再び replicas: 3 に増やすと、新しく立ち上がった web-1、web-2 はその PVC を再びマウントし、以前のデータをそのまま見ます。
PVC まで片付けるには明示的に消さなければなりません。
kubectl delete pvc data-web-1 data-web-2この安全装置のおかげで、運用事故で StatefulSet の replicas を誤って減らしてもデータは生きています。K8s 1.27 からは spec.persistentVolumeClaimRetentionPolicy でこの動作を変えられますが、データ保存の面では基本値のままにしておく方が安全です。
運用環境で DB のような状態性ワークロードを K8s の上に直接起動するパターンは 第18章 CRD と Operator パターン で Operator モデル (例: CloudNativePG、Zalando Postgres Operator) でもう一度扱います。StatefulSet 1枚だけではバックアップ · フェイルオーバー · 復旧まで運用するのは難しいので、普通はその上にドメインコントローラをもう一層載せる形です。
DaemonSet — ノードごとに正確に1つずつ #
運用クラスタには「各ノードの状態をそのノードの中から覗き込まなければならない」ワークロードがあります。ノードのコンテナログを集めて中央へ送る Fluent Bit、ノードの CPU · メモリ · ディスクを測定して Prometheus に公開する Node Exporter、Pod 間のネットワークを構成してくれる CNI エージェント (Calico、Cilium など) です。こうしたワークロードの共通点は ノード数の分だけ立ち上がっていなければならない という点です。
Deployment の replicas: N ではこの意図を表現できません。ノード数が増えたり減ったりするたびに人が N を手で合わせなければならず、1つのノードに同じ Pod が2つ立ち上がったり、あるノードにはまったく立ち上がらない状況も防げません。
DaemonSet が解いてくれることは単純です — クラスタの各ノードに自分の Pod を正確に1つずつ立ち上げます。 新しいノードがクラスタに加わるとそのノードにも自動で1つ立ち上げ、ノードが抜けるとそのノードの Pod も一緒に消えます。
DaemonSet マニフェスト #
replicas フィールドがないのが最大の違いです。
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter
namespace: monitoring
spec:
selector:
matchLabels:
app: node-exporter
template:
metadata:
labels:
app: node-exporter
spec:
hostNetwork: true
containers:
- name: node-exporter
image: prom/node-exporter:v1.8.2
args:
- --path.rootfs=/host
ports:
- containerPort: 9100
hostPort: 9100
volumeMounts:
- name: rootfs
mountPath: /host
readOnly: true
volumes:
- name: rootfs
hostPath:
path: /Deployment と同じ selector + template の構造ですが replicas はありません。個数はノード数が決めます。hostNetwork: true と hostPath ボリュームは DaemonSet ワークロードでよく見るパターンです — ノードのネットワークインターフェースで直接 Pod を公開したり、ノードのファイルシステムを直接覗き込まなければならないワークロードが多いからです。
kubectl get ds -n monitoring
kubectl get pods -n monitoring -o wideNAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
node-exporter 3 3 3 3 3 <none> 2m
NAME READY STATUS RESTARTS AGE IP NODE
node-exporter-7xk2p 1/1 Running 0 2m 10.0.0.11 node-1
node-exporter-9mn4v 1/1 Running 0 2m 10.0.0.12 node-2
node-exporter-bc8qr 1/1 Running 0 2m 10.0.0.13 node-3DESIRED 3 がノード数に応じて自動で決まった値だという点が核心です。ノードを1台追加すると DESIRED 4 に変わり、新しい Pod がそのノードに自動で立ち上がります。
一部のノードにだけ立ち上げる — nodeSelector / tolerations #
基本の DaemonSet は すべてのワーカーノード に Pod を立ち上げます。ただ運用では一部のノードにだけ立ち上げたい場合がよくあります — GPU が付いたノードにだけ GPU モニターを立ち上げたり、コントロールプレーンノードにはワークロードを載せなかったりする場合です。
nodeSelector でノードラベルにマッチするノードにだけ限定できます。
spec:
template:
spec:
nodeSelector:
hardware: gpu逆に、taint が付いたノード (例: コントロールプレーン) にも立ち上げるには tolerations を書きます。
spec:
template:
spec:
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule実際にクラスタの kube-system ネームスペースに立ち上がっている kube-proxy が DaemonSet です。コントロールプレーンノードを含むすべてのノードに立ち上がらなければならないので、上のような toleration を持っています。kubectl get ds -n kube-system で一度確認してみるとよいです。
ノードが cordon / drain されると #
運用中にノードを点検するとき、よく使う命令が kubectl cordon と kubectl drain です。cordon は新しい Pod のスケジューリングだけを止め、drain はノード上の Pod を別のノードへ移します。DaemonSet Pod は drain の基本動作では移されません — ノードごとに1つずつ立ち上がっているのが本分なので、別のノードへ移す意味がないからです。drain 命令が DaemonSet Pod のために止まったら --ignore-daemonsets フラグを一緒に与えるのが標準パターンです。
kubectl drain node-1 --ignore-daemonsets --delete-emptydir-dataノードアップグレードの流れの安全な使用パターンは 第30章 アップグレード戦略 で PodDisruptionBudget · terminationGracePeriodSeconds と一緒に本格的に扱います。
Job — 一度実行して終わる仕事 #
DB スキーママイグレーション、一度きりのデータ整合性チェック、新しいクラスタの初期セットアップスクリプトです。こうした仕事は 終わったら終わり です。ところが Deployment マニフェストでマイグレーションコンテナを立ち上げるとどうなるでしょうか。コンテナが正常終了 (exit 0) する瞬間、Deployment は「なぜ死んだ?」と言ってまた立ち上げます。マイグレーションが無限反復する事故になります。
Job はこのシナリオのためのコントローラです。Pod が成功して終了することを正常とみなす という点で Deployment と正反対のモデルです。
Job マニフェスト #
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
spec:
completions: 1
parallelism: 1
backoffLimit: 4
activeDeadlineSeconds: 600
template:
spec:
restartPolicy: OnFailure
containers:
- name: migrator
image: myapp/migrator:1.4.0
command: ["./migrate.sh"]
env:
- name: DB_HOST
value: postgres.default.svc.cluster.localapiVersion が batch/v1 である点が新しいです。Deployment 系列は apps/v1 でしたが、Job / CronJob は別のグループです。核心フィールドを一行ずつ押さえておきます。
completions: 1— Pod が成功で終了しなければならない回数です。上の例は1回で終わりです。大きなデータを N 個に分けて処理するときは N にします。parallelism: 1— 同時に立ち上がっている Pod の個数です。completions: 10、parallelism: 3にすると10個を処理しつつ一度に3個ずつ並列で回します。backoffLimit: 4— Pod が失敗したときの再試行回数の上限です。基本値は6です。この回数を超えると Job 自体がFailedで締まります。activeDeadlineSeconds: 600— Job 全体の時間上限です。600秒以内に終わらなければ Pod を強制終了します。無限ループに陥ったマイグレーションを切る安全装置です。
restartPolicy の制約 #
Pod の restartPolicy は普通 Always、OnFailure、Never の3つがありますが、Job の Pod テンプレートでは Always が許可されません。 マニフェストに Always を書くと apiserver が拒否します。
理由は単純です。Always は Pod がどのように終わろうと (成功であれ失敗であれ) また立ち上げろという意味ですが、Job は 終了を期待するワークロード です。Always を許可すると成功してもまた立ち上げることになり、Job の意味が消えます。だから OnFailure (失敗するときだけ再試行) か Never (絶対に再試行しない、新しい Pod でまた作る) のどちらか一つだけが使えます。
両者の違いは微妙です — OnFailure は同じ Pod の中でコンテナだけを再起動し、Never はその Pod 自体を失敗としてマークし新しい Pod をまた作ります。ログを保存してデバッグしたいなら Never が、速い再試行を望むなら OnFailure が普通の選択です。
Job 動作の確認 #
kubectl apply -f db-migration-job.yaml
kubectl get jobs
kubectl get pods --selector=job-name=db-migrationNAME COMPLETIONS DURATION AGE
db-migration 0/1 20s 20s
NAME READY STATUS RESTARTS AGE
db-migration-xkz2p 1/1 Running 0 20sNAME COMPLETIONS DURATION AGE
db-migration 1/1 45s 2m
NAME READY STATUS RESTARTS AGE
db-migration-xkz2p 0/1 Completed 0 2mCOMPLETIONS 1/1 が出て Pod が Completed で締まったのが正常終了の形です。ログは kubectl logs db-migration-xkz2p でマイグレーション出力をそのまま受け取れます。Job は kubectl delete job db-migration で明示的に片付けないとクラスタに残ります — 履歴として置いて見ておきたいならそのまま、片付けたいなら ttlSecondsAfterFinished を追加して自動で片付けさせることもできます。
CronJob — 周期実行 #
毎日未明3時に DB バックアップ、毎時0分に一時ファイル整理、毎週月曜の朝に統計レポート生成です。このパターンが CronJob です。モデルは単純です — cron 式に従って決まった時刻ごとに Job オブジェクトを作り出します。 Job の上に cron スケジューラを一層さらに載せた形です。
CronJob マニフェスト #
apiVersion: batch/v1
kind: CronJob
metadata:
name: db-backup
spec:
schedule: "0 3 * * *"
timeZone: "Asia/Seoul"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
startingDeadlineSeconds: 300
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 1800
template:
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: myapp/backup:2.1.0
command: ["/usr/local/bin/backup.sh"]
env:
- name: S3_BUCKET
value: my-backupsCronJob マニフェストの核心は2層です — 外側の spec のスケジューリングフィールドと、内側の jobTemplate の Job 定義です。内側の jobTemplate は上で見た Job マニフェストの spec とまったく同じ形をしています。
外側の核心フィールドを押さえておきます。
schedule: "0 3 * * *"— 標準 cron 式の5フィールドです。順に分 時 日 月 曜日です。この例は毎日午前3時0分です。*/15 * * * *(15分ごと)、0 9 * * 1-5(平日午前9時) のような一般的な cron 文法をそのまま使います。timeZone: "Asia/Seoul"— 1.27 から安定化されたフィールドです。以前は CronJob の時刻がコントロールプレーンコンポーネントのタイムゾーンに従って UTC と解釈されるのが普通で、「なぜ未明3時のバックアップが12時に回るのか」のような事故が頻発しました。このフィールドを明示しておくとその曖昧さが消えます。concurrencyPolicy— 前の回の Job がまだ終わっていないのに新しい回の時刻が来たときのポリシーです。基本値はAllowです。successfulJobsHistoryLimit/failedJobsHistoryLimit— 成功 · 失敗した Job オブジェクトを何個までクラスタに残しておくかを決めます。基本はそれぞれ3と1です。大きくしすぎると etcd に Job が累積します。startingDeadlineSeconds: 300— 予定時刻からこの秒数以内に開始できなければその回はスキップします。コントロールプレーンが一時的に止まってから回復したとき、溜まった回を一気に全部立ち上げる事故を防ぐ安全装置です。
concurrencyPolicy の3つ #
基本値 Allow をそのままにしておくと運用事故が起きやすいです。3つのオプションの動作がはっきり異なります。
| ポリシー | 動作 |
|---|---|
Allow (基本) | 前の回の Job が終わっていなくても新しい回の Job を追加で作ります。同時に複数立ち上がっていることがあります |
Forbid | 前の回が終わっていなければ今回はスキップします |
Replace | 前の回の Job を殺して新しい回で置き換えます |
DB バックアップのように同じデータに同時に2つが手を付けてはいけないワークロードは Forbid が正解 です。前のバックアップが30分かかってスケジュールが毎時0分なら、Allow にしておくと毎時新しいバックアップが追加で立ち上がって累積する事故になります。「最新の回だけ生きていればよい」ようなワークロード (例: キャッシュウォーミング) は Replace が合います。
startingDeadlineSeconds がないときの危険 #
CronJob の微妙な落とし穴の一つが startingDeadlineSeconds です。このフィールドがないか大きすぎる値に設定されていて、コントロールプレーンがしばらく止まってから回復すると、溜まった回を一気に全部立ち上げようとする試み が起こり得ます。毎分回る CronJob が1時間止まってから目覚めると Job を60個同時に作る、という具合です。
運用クラスタの CronJob には startingDeadlineSeconds を合理的な値 (例: 300秒) にほぼ常に書いておくのが安全です。回がその中で開始できなかったならその回はただスキップする方が、目覚めたときに一気に60個を回すよりほぼすべての場合に良いです。
CronJob 動作の確認 #
kubectl get cronjob,jobs,podsNAME SCHEDULE TIMEZONE LAST SCHEDULE AGE
cronjob.batch/db-backup 0 3 * * * Asia/Seoul 8h 2d
NAME COMPLETIONS DURATION AGE
job.batch/db-backup-29345400 1/1 14m 8h
job.batch/db-backup-29346840 1/1 13m 20m
NAME READY STATUS RESTARTS AGE
pod/db-backup-29346840-7kxqr 0/1 Completed 0 20m3段の形が見えます — CronJob が1つあり、その下に回ごとに Job オブジェクトが作られ、各 Job の下に Pod が一度ずつ立ち上がっては Completed で締まります。Job が終わっても successfulJobsHistoryLimit の個数分はオブジェクトが残っていて、事後デバッグに使えます。
いつどのコントローラを使うか #
1部の Deployment まで加えて5つのコントローラを一表に整理します。
| コントローラ | 適したワークロード | Pod 識別子 | 終了モデル |
|---|---|---|---|
| Deployment | stateless な Web · API サーバ、ワーカーコンシューマ | 任意値 (web-abc-aa11) | 死んだらまた立ち上げる |
| StatefulSet | DB、メッセージキューブローカー、分散キャッシュ | web-0、web-1 (固定) | 死んだら同じインデックスでまた立ち上げる |
| DaemonSet | ノードエージェント、ログ収集器、CNI | ノードごとに1個 | 死んだらまた立ち上げる |
| Job | DB マイグレーション、一度きりのバッチ | 任意値 | 成功で終わったら終わり |
| CronJob | 周期バックアップ、整理、レポート | 回ごとに Job | 各回が Job の終了モデル |
頭の中の決定木は単純です。
- Pod 同士が互いに同じでよいか? — 違うなら StatefulSet で、合っているなら次の質問へ進みます。
- ノードごとに正確に1個立ち上がらなければならないか? — そうなら DaemonSet で、違うなら次の質問へ進みます。
- 一度実行して終わらなければならないか? — そうなら周期実行なら CronJob、一度きりなら Job、違うなら Deployment です。
この4つのコントローラを知ると、クラスタのマニフェストディレクトリで kind: が何であれその意図を一行で読み取れます。
練習問題 #
- 上の本文どおり
web-headless.yaml(headless Service) とweb-statefulset.yamlをreplicas: 3で適用したあと、kubectl get pods,pvc -l app=webで Pod と PVC がどう対になるかを確認します。その次にkubectl delete pod web-1で真ん中の Pod を強制削除し、新しく立ち上がった Pod の名前とマウントされた PVC が同一かを記録します。第4章 Deployment の self-healing とどの点が違うかを一段落でメモします。 - DB マイグレーション Job をわざと失敗させてみましょう —
command: ["false"]のような形で exit 1 を出させてbackoffLimit: 2に設定したあとkubectl applyします。kubectl get pods --selector=job-name=db-migrationの出力を時間順に記録して再試行がどう起こるか、最終的に Job がFailedで締まるのに何回の失敗が必要かを §「Job マニフェスト」のbackoffLimitの説明と合わせてメモします。 - CronJob マニフェストの
scheduleを*/1 * * * *(毎分) に、concurrencyPolicyをAllow/Forbid/Replaceの3つに順に変えながらactiveDeadlineSeconds: 90のように1分より長い時間に設定して適用してみてください。各場合のkubectl get jobsの出力で、同時に立ち上がっている Job の個数がどう違うかを表で整理し、DB バックアップのようなワークロードでどのポリシーが安全かを §「concurrencyPolicy の3つ」と合わせて自分の表現で一段落にまとめます。
一行まとめ: Deployment の stateless 前提では表現できない4系統のワークロードのため、K8s は StatefulSet (アイデンティティと PVC 1:1)、DaemonSet (ノード単位1個)、Job (成功終了を正常とみなすモデル)、CronJob (Job の上の cron スケジューラ) の4コントローラを分けてある。決定木は「Pod 同士が同じか / ノード単位か / 終了を期待するか」の3つの質問で分かれる。
次の章 #
本章では StatefulSet の volumeClaimTemplates が PVC を自動で作ると一行で押さえて通り過ぎましたが、その PVC が本当にどのディスクにどう接続されるかは扱いませんでした。運用クラスタではその一行の裏に PV (PersistentVolume)、PVC (PersistentVolumeClaim)、StorageClass の三角関係があります — Pod のライフサイクルとディスクのライフサイクルがどう分離されるのか、ディスクが動的にどう作られるのか、accessModes (ReadWriteOnce、ReadOnlyMany、ReadWriteMany) の違いは何か、reclaimPolicy が PVC が消えたときにディスクをどう処理するのかです。
第9章 PV / PVC / StorageClass では、この3つのオブジェクトの関係を整理し、StatefulSet の volumeClaimTemplates がその上で本当に何を作り出すのかを一連の流れで追いかけます。