運用ランブック
node-rotation-controller の運用ガイド。各セクションは いつ該当するか・何を見るか・何をするか に答える。
設計の理由は仕様書を参照。英語原文: docs/runbook.md。
いまインシデント対応中なら?
§7 トラブルシューティングへ直行 — 症状から対処へのインデックス。
目次
- AZ ごとの surge ヘッドルーム(ゾーン PV)
- スループットと tGP の調整
- メトリクスリファレンス
- freeze ワークフロー
- drain が詰まったときの対処
- アラート(PrometheusRule)
- トラブルシューティング
- アップグレードとロールバック
- 大規模クラスタでのサイジング
1. AZ ごとの surge ヘッドルーム(ゾーン PV)
いつ: NodePool がゾーン制約の PersistentVolume(EBS gp3/io2、または topology.kubernetes.io/zone の nodeAffinity を持つ PV)に紐づくワークロードを前段に置くとき。
制約: surge ノードは既存ボリュームを再アタッチできるよう候補の AZ に固定される。同一 AZ の容量不足は別ゾーンにフォールバックできない — readyTimeout 後にロールバックする。
何をするか:
- NodePool 全体の
spec.limitsにノード 1 台分の余力を確保し、NodePool のrequirementsが使用中の全 AZ を許可していることを確認する。spec.limitsでは AZ ごとの容量を予約できない - 各 AZ のプロバイダー実容量と、surge に必要なリージョン単位の EC2 vCPU クォータを別々に確認する
- surge インスタンスシェイプに対するキャパシティ予約を検討する
不足の検知方法:
noderotation_completed_total{outcome="failure"}の増加noderotation_retry_count >= 3(アラート:NodeRotationRetryCountHigh)
このアラートがゾーン PV の pool で発火したら、まず AZ ごとの容量不足を疑う。
2. スループットと tGP の調整
スループット(ウィンドウ容量 C)を上げる
いつ: ThroughputBelowArrival や ThroughputBurstShortfall の警告が出る、またはウィンドウ内で候補がさばけない。
スループットを決めるもの:
C = ceil(D / (provisioningEstimate + drainEstimate + cooldownAfter))| つまみ | 意味 | 設定方法 |
|---|---|---|
surge.provisioningEstimate | surge ノードが Ready になるまでの期待時間 | noderotation_duration_seconds{phase="surge_wait"} から読む |
surge.drainEstimate | 健全な drain の期待時間 | noderotation_duration_seconds{phase="drain"} から読む |
surge.cooldownAfter | 連続ローテーション間の休止 | PDB が drain を直列化しているなら 0 でも可 |
スループットを上げないもの: terminationGracePeriod。C にはもう現れない。スループット警告のために下げてはいけない。
terminationGracePeriod の選び方
いつ: Karpenter が drain 中の Pod を強制 kill するまでの猶予をどう決めるか。
基準: インシデントで許容できるダウンタイムから選ぶ。通常観測する drain 時間からではない(観測値にはこの設定が備える裾野が含まれていない)。
tGP を下げる理由(いずれもスループット目的ではない):
ageThresholdが伸びる → ノードがより遅くローテーションされ、チャーンが減る- Auto Mode の 21 日キャップが緩む(
expireAfter + tGP ≤ 21d) - stuck-drain 判定が早くなる(
noderotation_drain_stuckはtGP + bufferで発火)
導出の詳細は仕様 §3.2を参照。
3. メトリクスリファレンス
/metrics で公開。完全なセマンティクスは仕様 §4.2を参照。
NodePool 単位の系列は NodePool 削除時、または統治 RotationPolicy を失った時にクリアされる。
主要な運用メトリクス
| メトリクス | 型 | 見るべきポイント |
|---|---|---|
noderotation_candidates | Gauge | 各ウィンドウ後に 0 へ向かうべき。2 ウィンドウ跨いで > 0 → 処理が追いついていない |
noderotation_in_backoff | Gauge | 年齢的にまだ期限が来ていて、失敗した試行の retryBackoff 中でなければ候補だった claim 数。candidates + in_backoff がそのウィンドウの残作業 |
noderotation_in_progress | Gauge | 0 か 1(v1 は pool ごとに直列) |
noderotation_completed_total{outcome} | Counter | outcome ∈ {success, failure, expired}。failure/expired → 調査 |
noderotation_forceful_fallback_total | Counter | 増加中 → graceful surge がデッドラインに間に合っていない |
noderotation_window_missed_total | Counter | pool ごと — 候補が残ったまま、帰属するローテーションが何も行われずに閉じたウィンドウ発生数 |
noderotation_duration_seconds{phase} | Histogram | phase ∈ {surge_wait, drain}。見積もりの設定に使う |
noderotation_drain_stuck | Gauge | 1 → 運用者の対処が必要(§5) |
noderotation_retry_count | Gauge | ≥ 3 → systematic な失敗(preemption か AZ 不足) |
noderotation_short_lead_nodes | Gauge | 自身の expiry 前に K 回の機会を得られないノード |
スケジュールとポリシーのメトリクス
| メトリクス | 型 | 用途 |
|---|---|---|
noderotation_window_active | Gauge | 0/1(pool ごと)— いまウィンドウが開いているか |
noderotation_window_period_seconds | Gauge | 最悪ケースのウィンドウ間隔 P(pool ごと) |
noderotation_age_threshold_seconds | Gauge | 導出された ageThreshold A(pool ごと) |
noderotation_rotation_chances | Gauge | 保証されるローテーション回数 G |
noderotation_throughput_capacity | Gauge | 予測 C — ウィンドウ機会あたりの開始可能数 |
noderotation_t_rot_estimate_seconds | Gauge | t_rot_est — 健全なローテーションの期待時間 |
noderotation_t_rot_bound_seconds | Gauge | t_rot — deadline 側の上限(lead time に効く) |
noderotation_freeze_until_timestamp | Gauge | 有効な freeze のタイムスタンプ(0 = なし) |
noderotation_policy_conflict | Gauge | 1 = ポリシー競合で pool がブロックされている |
生存性の判断
コントローラーの警告ログはデデュープされる — 健全なアイドルループは 0 行 しか出さない。ログが出ないことをストールと判断しないこと。以下を使う:
controller_runtime_reconcile_total{controller="rotation"}—rate()が上昇中 = 生存workqueue_depth{name="rotation"}— 0 付近に留まるべき
4. freeze ワークフロー
目的: ビジネスクリティカルな期間中、特定の NodePool のローテーションを抑止する。
設定:
kubectl annotate nodepool <name> \
noderotation.io/freeze='2026-12-31T23:59:59Z' --overwrite解除:
kubectl annotate nodepool <name> noderotation.io/freeze-freeze 中の挙動:
- 新しいローテーションは開始しない
pending中のローテーションは保留される(drain 未開始なので安全に停止可能)draining中のローテーションは完遂する(drain の中断は安全でない)expireAfterバックストップは引き続き有効 — freeze でノード寿命は延びない
監視: noderotation_freeze_until_timestamp{nodepool}(0 = freeze なし)。
ベストプラクティス: freeze は ad-hoc な kubectl ではなく GitOps で管理する。忘れられた freeze はタイムスタンプが過ぎるまで黙って graceful ローテーションを無効にする。
5. drain が詰まったときの対処
症状: noderotation_drain_stuck == 1(アラート: NodeRotationDrainStuck)。
何が起きたか: コントローラーが旧 NodeClaim を削除し、Karpenter が voluntary 経路で drain しているが、tGP + buffer を超過した。
重要: stuck drain はその pool の全ローテーションを意図的にブロックする(maxUnavailable = 1 を尊重するため)。他の pool は影響を受けない。
判断フロー:
コマンド:
# drain 中のノードを特定
kubectl get nodeclaim -l karpenter.sh/nodepool=<pool> -o wide | grep -i terminating
# ノード上の Pod
kubectl get pods -A --field-selector spec.nodeName=<node> -o wide
# eviction をブロックしている PDB
kubectl get pdb -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,ALLOWED:.status.disruptionsAllowedコントローラーのアノテーションや placeholder を削除して「詰まりを解消」しようとしてはいけない — ステートマシンは冪等であり、再アサートする。根本の PDB / finalizer を修正すること。
6. アラート(PrometheusRule)
Helm chart にオプションの PrometheusRule が同梱(既定で無効)。有効化:
helm upgrade --install node-rotation-controller charts/node-rotation-controller \
--set prometheusRule.enabled=true| アラート | 発火条件 | 対処 |
|---|---|---|
NodeRotationCompletedFailureOrExpired | 直近 1h にローテーションが失敗/expired | §1(AZ 容量)と §5(stuck drain)を確認 |
NodeRotationCandidatesNotDraining | 2 ウィンドウ跨いで候補がさばけない | §2(スループット)を確認 |
NodeRotationStalledInWindow | ウィンドウが開いていて freeze 中でなく、candidates + in_backoff > 0、成功完了ゼロ | §1 または §5 を確認 |
NodeRotationDrainStuck | drain が tGP + buffer を超過 | §5 に従う |
NodeRotationShortLeadNodes | ノードが K 回の機会を得られない | expireAfter を引き上げるかウィンドウ日を追加 |
NodeRotationRetryCountHigh | 同じローテーションが 3 回以上失敗 | systematic な原因 — §1 を確認 |
NodeRotationForcefulFallback | ローテーションが surge-less で実行された | 設計通り。増加が過剰なら §2 で対処 |
NodeRotationWindowMissed | 候補が未ローテーションのままメンテナンスウィンドウが閉じた | 下記を参照 |
スケジュール依存のレンジを調整:
prometheusRule.candidatesNotDraining.windowRange→2·P(既定8d、{Wed, Sat}向け)prometheusRule.stalledInWindow.completionRange→ ウィンドウ長D(既定4h)prometheusRule.windowMissed.range→ ウィンドウ周期より十分長く(既定24h; 週次スケジュールでは広げる)
全チューニング項目は values.yaml を参照。
NodeRotationWindowMissed への対処
何を意味するか: メンテナンスウィンドウの発生が、年齢と状態から見て未処理の候補(eligible、または retryBackoff 中)を残したまま閉じ、その発生に帰属するローテーションが 1 件も完了しなかった。その発生に対する保証ローテーション機会は消費されたまま失われた — minRotationChances: 1(下限)の場合、それらのノードに保証された graceful な機会はもう残っておらず、置き換えのないまま expireAfter に到達することもありうる。
この文中の 2 つの語は、見た目より狭い意味を持つ: 「帰属する」であって「中で」ではない — in-window で始まり境界の後に成功した試行はその発生を settle する — と、「年齢と状態から見て未処理」であって「コントローラーがローテーションできたはず」ではない — static なプールやスケジュールが致命的に実行不能なプールは、年齢と状態から見て未処理の claim を残したまま閉じた発生のたびに報告する。どちらも spec §4.2 が定義する。
何を確認するか:
- NodePool 上の
WindowMissedEvent — そのカウント(windowOpenedAt、eligible、inBackoffTriggered)が、ウィンドウのどれだけの作業が未ローテーションだったかを示す。no rotation candidate行のinBackoffが別の値になるのは意図的である: そちらは生の census バケットで、年齢がトリガーを越えなくなり窓が何も負っていない claim も含む。census 行のinBackoffがこの Event のinBackoffTriggeredより大きいのはその差であって、不整合ではない。 - 直前の
rotation attempt failedログ行とそのreason— ウィンドウ喪失は通常、コールドスタートではなく 1 件以上の失敗した試行の末尾である。 noderotation_retry_count— エスカレートするバックオフの上限に向かって増加している場合、試行が時間切れではなく繰り返し失敗している。- 1つのメンテナンスウィンドウ内での試行のペースを決めるのは
retryBackoffではなくreadyTimeout + failurePauseである: ウィンドウを意識したクランプ(spec §3.2)が失敗した claim のリトライをそれが失敗した発生の内側に保持するのは、retryBackoffまで下げたステップがなお収まる間だけであり、retryBackoffすら収まらなくなった時点でエスカレートした待ち時間はそのまま確定し、その claim はこの発生でリトライする代わりに次の発生へ持ち越される。期間Dのウィンドウでは、タイムアウト駆動の試行数のオーダー上限は1 + D / (readyTimeout + failurePause)になる。プール全体を直接ペーシングするのはfailurePauseである——複数の claim が互いに独立して backoff とリトライを交互に行いうるため、retryBackoffはプール全体のレートの信頼できる制限にはならない。retryBackoffを引き上げても、それがこのクランプの下限であり大きな下限ほど窓の中で「何も収まらない」地点により早く到達するぶん、1 claim あたりの試行回数は減らせる。ただしこれはその claim の窓の残りを切り捨てうる鈍い、claim ごとの手段であって、プール全体のチャーンを直接制御するものではない。 - プールが static かどうか(
StaticNodePoolWarning Event、spec §3.3)— static な NodePool は surge ローテーションを一切試みないため、年齢と状態から見て未処理の claim を残したまま閉じた発生のたびにウィンドウを逃す。
何をするか: rotation attempt failed 行が示す根本原因に対処する(§1 と §5 を参照)。試行自体は健全だが、バッチがスケジュールに対して大きすぎて本当に収まらない場合は、メンテナンスウィンドウを拡張して 1 回の発生あたりの完了数を増やすか、minRotationChances(K)を引き上げて、1 回のウィンドウ喪失があっても expireAfter バックストップ前に保証された機会を残すようにする。
このシグナルの既知の限界。 発生は NodePool 上の noderotation.io/window-opened-at アノテーションの存在で識別され、reconcile が観測したものだけが存在する。設計上受け入れている帰結が 3 つあり、spec §5.3 が列挙している: 2 つの発生が 1 件として報告されうる、スケジュールの編集がウィンドウを即座に閉じうる、報告は最大 1 回で、それ以上にはならない。運用上、書き方が変わるのは最後の 1 つだけである: 正確な回数ではなく increase(...) > 0 でアラートすること。
注記: NodeRotationStalledInWindow は同じ失敗に対する in-window の早期警告だが、その予測子ではない。両者は同じ未処理作業の判定(noderotation_candidates + noderotation_in_backoff > 0、どちらも freeze 中のプールを除外)を適用し、なお異なる 3 点 — 評価のタイミング、成功がどちらをどう抑止するか、どちらも数えない in-flight のリトライ — は spec §4.2 が示す。調整が要るのは 1 点: completionRange は 1 ウィンドウ分程度に保つこと。
7. トラブルシューティング
症状から出発し、シグナルで確認し、修正先へ飛ぶ。
| 症状 | シグナル | 修正先 |
|---|---|---|
| Placeholder Pod が Pending のまま、ローテーションが失敗する | completed_total{outcome="failure"}; retry_count 増加 | §1 — 同一 AZ 容量 |
| 候補が 2 ウィンドウ跨いでもさばけない | NodeRotationCandidatesNotDraining アラート | §2 — スループット不足 |
ThroughputBurstShortfall 警告が毎ウィンドウ出る | NodePool 上の Warning Event | §2 — ウィンドウがバッチに対して短い |
| drain が止まり、新しい完了が出ない | noderotation_drain_stuck == 1 | §5 — PDB or finalizer |
in_progress が 1 で張り付き | noderotation_drain_stuck == 1。プールに他の未処理 claim があれば NodeRotationStalledInWindow も — in-flight のローテーションは candidates にも in_backoff にも入らないため、それ単独ではこのアラートは上がらない | surge 未着(→ §1)か drain 詰まり(→ §5) |
| NodePool が一切ローテーションしない、候補が溜まる | noderotation_policy_conflict == 1 | RotationPolicy のセレクタ重複を修正 |
| NodePool がローテーションせず、surge が一度も作られない | NodePool 上の InsufficientHeadroom Warning Event | プールの spec.limits に placeholder の余地がない。placeholder はドレインの前に置換キャパシティを予約するため、limit が許さない分の予算を消費する。Event はリソース名・placeholder が必要とする量・ceiling の残量を示すので、その不足分以上に spec.limits を引き上げるか、プールのプロビジョニング済みキャパシティを減らす。surge.wholeNodeReservation が有効な場合、ゲートは 1 ノード分のフットプリントを検査する(§3.3)— ドレイン分ではなく 1 インスタンス分のヘッドルームが必要 |
| NodePool が一切ローテーションせず、試行も起きない | NodePool 上の StaticNodePool Warning Event | プールが spec.replicas(static capacity)を設定しており surge ではローテーションできない。Karpenter は既存 NodePool への spec.replicas の追加・削除を禁じているため、ワークロードを dynamic な NodePool へ移すか、ポリシーのセレクタからこのプールを外す |
| NodePool がいつの間にかローテーション停止 | freeze_until_timestamp > 0 | 忘れられた freeze — §4 |
| メンテナンスウィンドウが候補未ローテーションのまま閉じた | NodeRotationWindowMissed アラート; NodePool 上の WindowMissed Warning Event | §6 — 直前の失敗した試行と retry_count を確認 |
| ノードがコントローラーにもかかわらず expireAfter に到達 | completed_total{outcome="expired"} | lead time 不足 — ウィンドウ拡張か tGP 引き下げ |
short_lead_nodes > 0 | ShortLead Warning Event | NodePool の expireAfter 引き上げかウィンドウ日追加 |
forceful_fallback_total が増加中 | ForcefulFallback Warning Event | スループットが逼迫なら §2 で対処。単発は設計通り |
| "reconcile が停止したように見える"(ログが出ない) | controller_runtime_reconcile_total が上昇中か確認 | 本当の問題ではない — ログは定常状態でデデュープされる |
8. アップグレードとロールバック
イメージのアップグレード
ローテーション中でも安全。 全状態は Kubernetes オブジェクト上にある — ローリングアップグレードで新 Pod にリーダーシップが渡り、active-rotation アノテーションから再開する。外部ステートなし、メモリ内の状態喪失なし。
事前に静止させたいなら(任意): 短い freeze を設定し noderotation_in_progress が 0 になるのを待つ。
CRD スキーマ変更
Helm は CRD をアップグレードしない。フィールドが追加されたリリースへのアップグレード時は CRD を先に適用:
kubectl apply -f charts/node-rotation-controller/crds/
helm upgrade --install node-rotation-controller charts/node-rotation-controller ...変更したスキーマがどのリリースか、そしてアップグレード時に対処が要る動作変更・values 変更は、リリースごとに changelog(英語)に記録している。アップグレード前に、導入済みバージョンから対象バージョンまでのエントリを読むこと。
chart は rotationPolicies[].spec サブツリーを封印しているため、以前のバージョンが黙って捨てていたタイポはアップグレード時にハードエラーになる。事前にドライランすること:
helm template node-rotation-controller charts/node-rotation-controller -f your-values.yaml >/dev/nullロールバック
イメージのロールバックは安全(同じオブジェクト上の状態から旧コントローラーが再開)。CRD 変更を跨ぐロールバックは、コントローラーがポリシーを不正とみなす場合がある(noderotation_policy_conflict == 1)。その pool のローテーションが停止するが expireAfter バックストップは引き続き有効。RotationPolicy オブジェクトも合わせて旧スキーマに戻すこと。
9. 大規模クラスタでのサイジング
いつ: 10k+ Pods のクラスタ。
コントローラーはクラスタ内の全 Pod をキャッシュする(placeholder のサイジングにクロスネームスペースの可視性が必要なため)。メモリは Pod 数にスケール:
| Pod 数 | キャッシュフットプリント(下限) |
|---|---|
| ~1k | ~4 MB |
| ~10k | ~37 MB |
| ~50k | ~185 MB — 実運用では 500 MB〜1 GB を見込む |
CPU は問題にならない — 50k Pod のスキャンは呼び出しあたり ~105 µs。
Deployment の memory requests/limits をこれに合わせて設定する。ベンチマーク詳細は perf note を参照。