Deployment の manifest、`replicas` の下に並ぶ設定は何をしているのか?
検索で見つけた Deployment の YAML をコピペして kubectl apply したら、たまたま動いた——そんな経験はないだろうか。replicas: 3 の下には strategy や readinessProbe といった項目がさらに並んでいたはずだが、それぞれが実際の挙動の何を左右しているかまでは説明できないかもしれない。
strategy.rollingUpdateのmaxSurge(新規に追加できる Pod 数の上限、端数は切り上げ)とmaxUnavailable(同時に落とせる Pod 数の上限、端数は切り捨て)はどちらもデフォルト 25% で、この 2 値がロールアウト中の可用性とキャパシティのトレードオフを決めているreadinessProbeの失敗は Service のトラフィックから外れるだけで Pod は生き続けるが、livenessProbeの失敗はコンテナの強制終了と再起動を招く——同じ「ヘルスチェック」でも結果はまったく違うprogressDeadlineSeconds(デフォルト 600 秒)はロールアウトが詰まったことを知らせるだけで自動停止はさせず、revisionHistoryLimit(デフォルト 10)はロールバックに使える過去世代の保持数を決める——どちらも日々のデプロイ速度そのものには影響しない
manifest のどこから読めばいいのか ―― フィールドの地図
Deployment の spec には、役割が異なる設定がいくつかの層になって並んでいる。まず replicas と strategy が「何台を、どう入れ替えるか」というロールアウト全体の方針を決める。次に spec.template.spec.containers[] の中にある readinessProbe や livenessProbe は、個々の Pod が「トラフィックを受けられる状態か」「生きているか」を継続的に判定する。そして progressDeadlineSeconds や revisionHistoryLimit は、そのロールアウト自体が詰まっていないか、後から戻せるかを保証する安全網にあたる。
同じ containers[] の下にある resources(requests/limits) も重要なフィールドだが、CPU スロットリングと OOM Killer の挙動込みで別記事で扱った。ここでは「Pod ごとの資源枠を決める設定がここにもある」という位置づけの確認にとどめ、この記事では replicas・strategy・probe・rollout の安全網の 4 つを順に見ていく。
replicas と strategy ―― 何台で、どう入れ替えるか
replicas を省略すると 1 台起動になる。デフォルトが 1 なので、複数台での可用性が要る場合は明示的な指定が要る。入れ替え方を決めるのが strategy.type で、既定値は RollingUpdate(新旧 Pod を少しずつ入れ替える)だ。もう一つの選択肢である Recreate は、古い Pod を全て削除してから新しい Pod を作るためダウンタイムが生じる代わりに、新旧のバージョンが同時に稼働する瞬間が存在しない。DB スキーマの互換性が新旧で崩れる更新など、新旧混在そのものが困る場面ではこちらを選ぶ。
RollingUpdate の中身を決めるのが maxSurge と maxUnavailable で、どちらもデフォルト 25% だ。パーセンテージから実数に変換するときの丸め方向が逆で、maxSurge は切り上げ、maxUnavailable は切り捨てになる。replicas: 3 のデフォルト設定で計算すると、25% は 0.75 なので maxSurge は 1 台(切り上げ)、maxUnavailable は 0 台(切り捨て)になる。つまり既定値のままでも、常に 3 台以上が稼働し続けたまま最大 4 台まで一時的に増える形でロールアウトが進む。
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 既定 25% → 3台なら切り上げで1
maxUnavailable: 0 # 既定 25% → 3台なら切り捨てで0
この 2 値は仕様上どちらも 0 にはできない(両方 0 だと新旧を入れ替える余地が無くなるため)。maxUnavailable: 0 にすると常に希望台数を維持できるが、その分だけ maxSurge の Pod をスケジュールする余剰リソースがクラスタに要る。ノードに空きが無いとその surge 分の Pod が Pending のまま止まり、ロールアウトが進まなくなることがある。
片側交互通行の道路工事にたとえるとわかりやすい。道路を全面通行止めにはできないので、区間を区切って少しずつ舗装をやり直す。一度に規制していい作業区間数の上限(maxSurge に相当する「一時的に増やせる作業スペース」)と、常に確保しておかねばならない通行可能な車線数の下限(maxUnavailable が許す欠落の裏返し)を両方守りながら、区間ごとに終わらせては次へ移る。Deployment のローリングアップデートも同じ構造で、新旧 Pod をどこまで同時に許すかという上限と、常に何台は稼働させておくかという下限を、2 つの数値で両側から縛っている。
probe ―― どのヘルスチェックが何を引き起こすか
readinessProbe(アプリがトラフィックを受けられる状態かを判定する probe)が失敗すると、Pod は Service の転送先リストから外れる。既存の接続は維持されることがあるが、新しいリクエストはもう回ってこない。ここで Pod 自体は終了も再起動もされない。一方 livenessProbe(アプリが生きているか、デッドロックなどで応答不能になっていないかを判定する probe)が失敗すると、kubelet がコンテナを強制終了し restartPolicy に従って再起動する。同じ「失敗」でも、トラフィックから外れるだけなのか、コンテナごと作り直されるのかがまったく違う。
両者とも既定値は periodSeconds: 10・timeoutSeconds: 1・failureThreshold: 3・initialDelaySeconds: 0 で、起動直後からチェックが始まり、10 秒間隔で 3 回連続失敗するまで約 30 秒かかる計算になる。起動に数十秒〜数分かかるアプリでこの既定値のまま livenessProbe を設定すると、起動が終わる前に 3 回失敗して再起動され、いつまで経っても起動し切れない再起動ループに陥りかねない。これを避けるための項目が startupProbe(起動が完了したかだけを判定する probe)で、これが成功するまで livenessProbe と readinessProbe の実行自体が始まらない。起動の遅いアプリでは startupProbe の failureThreshold を大きく取り、起動完了後の 2 つの probe には通常の間隔を使う、という役割分担になる。
readinessProbe 失敗
└─ Service の転送先から除外
(Pod は生存, 再起動なし)
livenessProbe 失敗
└─ kubelet がコンテナを kill
└─ restartPolicy に従い再起動
ロールアウトの停止検知とロールバック ―― progressDeadlineSeconds と revisionHistoryLimit
progressDeadlineSeconds(デフォルト 600 秒)は、その秒数だけ進捗が無いロールアウトを「詰まっている」と判定する項目だ。例えば新しい Pod が CrashLoopBackOff を繰り返して readinessProbe を一向に通過できない場合に働く。ただしこの値が切れても Deployment がロールアウトを自動で止めたり以前のバージョンへ自動ロールバックしたりはしない。Deployment の condition に ProgressDeadlineExceeded が立ち、kubectl rollout status がタイムアウトで抜けるという形で異常を知らせるだけで、そこから戻すかどうかは運用者の判断に委ねられている。
revisionHistoryLimit(デフォルト 10)は、ロールバック用に残しておく過去の ReplicaSet の世代数を決める。この値を減らしても日々のデプロイやロールアウトの速度は変わらず、変わるのは kubectl rollout undo で戻れる世代の深さだけだ。0 にすると過去の ReplicaSet が残らなくなり、直前のバージョンへのロールバックができなくなる。
デプロイした直後に kubectl get pods を見ると新しい Pod が 0/1 のまま増えず止まっている、という場面の多くは readinessProbe が通っていないケースだ。kubectl describe pod の Events に Readiness probe failed が並んでいないか、逆に Pod が何度も再作成されているなら Liveness probe failed と Back-off restarting failed container が出ていないかを見比べると、どちらの probe が原因かの見当がつく。CI/CD のデプロイジョブが kubectl rollout status --timeout=... でタイムアウトして失敗する場合は、progressDeadlineSeconds より前に CI 側のタイムアウトが先に切れていないかも合わせて確認する価値がある。
readinessProbe をわざと失敗させ、Pod が再起動されずに READY だけが 0/1 になり続けることを確かめる。
kind create cluster --name probe-demo 2>/dev/null || true
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: probe-demo
spec:
replicas: 1
selector:
matchLabels: { app: probe-demo }
template:
metadata:
labels: { app: probe-demo }
spec:
containers:
- name: nginx
image: nginx:1.27
readinessProbe:
httpGet: { path: /not-found, port: 80 }
periodSeconds: 5
failureThreshold: 2
EOF
kubectl get pods -l app=probe-demo -w
10〜15 秒ほどで READY 列が 0/1 のまま固定されるはずだ。それでも RESTARTS 列は 0 のまま増えない——コンテナ自体は生きていて、Service につながっていれば単にトラフィックが回ってこないだけの状態だと分かる。同じ YAML の readinessProbe: を livenessProbe: に書き換えて再度 apply すると、今度は RESTARTS が増え始める違いも合わせて確認できる。
- readinessProbe が失敗すると Pod ごと再起動される — readiness の失敗は Pod を Service の転送先から外すだけで、Pod は終了も再起動もされない。コンテナを強制終了して再起動を引き起こすのは livenessProbe の失敗だ。
- maxUnavailable を 0 にすればロールアウト中も万全 — maxUnavailable を 0 にすると、仕様上 maxSurge を 0 にはできないため必ず一時的に余分な Pod がスケジュールされる。クラスタに空きリソースが無いとその Pod が Pending のまま詰まり、ロールアウト自体が進まなくなることがある。
- revisionHistoryLimit を小さくすればデプロイが速くなる — この値は過去の ReplicaSet を何世代残すかを決めるだけで、ロールアウトの速度そのものには関わらない。小さくすると変わるのは、ロールバックで戻れる世代の深さだけだ。
- RollingUpdate
- 新旧の Pod を少しずつ入れ替える Deployment の既定の更新方式。
- maxSurge
- 希望 Pod 数を超えて一時的にスケジュールできる Pod 数の上限。パーセント指定時は切り上げ。
- maxUnavailable
- ロールアウト中に同時に利用不可にできる Pod 数の上限。パーセント指定時は切り捨て。
- readinessProbe
- Pod がトラフィックを受けられる状態かを判定する probe。失敗すると Service の転送先から外れる。
- livenessProbe
- アプリが生きているかを判定する probe。失敗するとコンテナが強制終了・再起動される。
- progressDeadlineSeconds
- ロールアウトが指定秒数進捗しない場合に停滞と判定する項目。自動停止・自動ロールバックはしない。
- Deployments | Kubernetes — replicas・strategy・progressDeadlineSeconds・revisionHistoryLimit の挙動を定義した公式ドキュメント。
- Configure Liveness, Readiness and Startup Probes | Kubernetes — 3 種類の probe の役割分担と設定例を解説する公式ドキュメント。