Skip to content

運用ランブック

node-rotation-controller の運用ガイド。各セクションは いつ該当するか何を見るか何をするか に答える。

設計の理由は仕様書を参照。英語原文: docs/runbook.md

いまインシデント対応中なら?

§7 トラブルシューティングへ直行 — 症状から対処へのインデックス。


目次

  1. AZ ごとの surge ヘッドルーム(ゾーン PV)
  2. スループットと tGP の調整
  3. メトリクスリファレンス
  4. freeze ワークフロー
  5. drain が詰まったときの対処
  6. アラート(PrometheusRule)
  7. トラブルシューティング
  8. アップグレードとロールバック
  9. 大規模クラスタでのサイジング

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)を上げる

いつ: ThroughputBelowArrivalThroughputBurstShortfall の警告が出る、またはウィンドウ内で候補がさばけない。

スループットを決めるもの:

C = ceil(D / (provisioningEstimate + drainEstimate + cooldownAfter))
つまみ意味設定方法
surge.provisioningEstimatesurge ノードが Ready になるまでの期待時間noderotation_duration_seconds{phase="surge_wait"} から読む
surge.drainEstimate健全な drain の期待時間noderotation_duration_seconds{phase="drain"} から読む
surge.cooldownAfter連続ローテーション間の休止PDB が drain を直列化しているなら 0 でも可

スループットを上げないもの: terminationGracePeriodC にはもう現れない。スループット警告のために下げてはいけない。

terminationGracePeriod の選び方

いつ: Karpenter が drain 中の Pod を強制 kill するまでの猶予をどう決めるか。

基準: インシデントで許容できるダウンタイムから選ぶ。通常観測する drain 時間からではない(観測値にはこの設定が備える裾野が含まれていない)。

tGP を下げる理由(いずれもスループット目的ではない):

  • ageThreshold が伸びる → ノードがより遅くローテーションされ、チャーンが減る
  • Auto Mode の 21 日キャップが緩む(expireAfter + tGP ≤ 21d
  • stuck-drain 判定が早くなる(noderotation_drain_stucktGP + buffer で発火)

導出の詳細は仕様 §3.2を参照。


3. メトリクスリファレンス

/metrics で公開。完全なセマンティクスは仕様 §4.2を参照。

NodePool 単位の系列は NodePool 削除時、または統治 RotationPolicy を失った時にクリアされる。

主要な運用メトリクス

メトリクス見るべきポイント
noderotation_candidatesGauge各ウィンドウ後に 0 へ向かうべき。2 ウィンドウ跨いで > 0 → 処理が追いついていない
noderotation_in_backoffGauge年齢的にまだ期限が来ていて、失敗した試行の retryBackoff 中でなければ候補だった claim 数。candidates + in_backoff がそのウィンドウの残作業
noderotation_in_progressGauge0 か 1(v1 は pool ごとに直列)
noderotation_completed_total{outcome}Counteroutcome ∈ {success, failure, expired}。failure/expired → 調査
noderotation_forceful_fallback_totalCounter増加中 → graceful surge がデッドラインに間に合っていない
noderotation_window_missed_totalCounterpool ごと — 候補が残ったまま、帰属するローテーションが何も行われずに閉じたウィンドウ発生数
noderotation_duration_seconds{phase}Histogramphase ∈ {surge_wait, drain}。見積もりの設定に使う
noderotation_drain_stuckGauge1 → 運用者の対処が必要(§5
noderotation_retry_countGauge≥ 3 → systematic な失敗(preemption か AZ 不足)
noderotation_short_lead_nodesGauge自身の expiry 前に K 回の機会を得られないノード

スケジュールとポリシーのメトリクス

メトリクス用途
noderotation_window_activeGauge0/1(pool ごと)— いまウィンドウが開いているか
noderotation_window_period_secondsGauge最悪ケースのウィンドウ間隔 P(pool ごと)
noderotation_age_threshold_secondsGauge導出された ageThreshold A(pool ごと)
noderotation_rotation_chancesGauge保証されるローテーション回数 G
noderotation_throughput_capacityGauge予測 C — ウィンドウ機会あたりの開始可能数
noderotation_t_rot_estimate_secondsGauget_rot_est — 健全なローテーションの期待時間
noderotation_t_rot_bound_secondsGauget_rot — deadline 側の上限(lead time に効く)
noderotation_freeze_until_timestampGauge有効な freeze のタイムスタンプ(0 = なし)
noderotation_policy_conflictGauge1 = ポリシー競合で pool がブロックされている

生存性の判断

コントローラーの警告ログはデデュープされる — 健全なアイドルループは 0 行 しか出さない。ログが出ないことをストールと判断しないこと。以下を使う:

  • controller_runtime_reconcile_total{controller="rotation"}rate() が上昇中 = 生存
  • workqueue_depth{name="rotation"} — 0 付近に留まるべき

4. freeze ワークフロー

目的: ビジネスクリティカルな期間中、特定の NodePool のローテーションを抑止する。

設定:

sh
kubectl annotate nodepool <name> \
  noderotation.io/freeze='2026-12-31T23:59:59Z' --overwrite

解除:

sh
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 は影響を受けない。

判断フロー:

コマンド:

sh
# 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 が同梱(既定で無効)。有効化:

sh
helm upgrade --install node-rotation-controller charts/node-rotation-controller \
  --set prometheusRule.enabled=true
アラート発火条件対処
NodeRotationCompletedFailureOrExpired直近 1h にローテーションが失敗/expired§1(AZ 容量)と §5(stuck drain)を確認
NodeRotationCandidatesNotDraining2 ウィンドウ跨いで候補がさばけない§2(スループット)を確認
NodeRotationStalledInWindowウィンドウが開いていて freeze 中でなく、candidates + in_backoff > 0成功完了ゼロ§1 または §5 を確認
NodeRotationDrainStuckdrain が tGP + buffer を超過§5 に従う
NodeRotationShortLeadNodesノードが K 回の機会を得られないexpireAfter を引き上げるかウィンドウ日を追加
NodeRotationRetryCountHigh同じローテーションが 3 回以上失敗systematic な原因 — §1 を確認
NodeRotationForcefulFallbackローテーションが surge-less で実行された設計通り。増加が過剰なら §2 で対処
NodeRotationWindowMissed候補が未ローテーションのままメンテナンスウィンドウが閉じた下記を参照

スケジュール依存のレンジを調整:

  • prometheusRule.candidatesNotDraining.windowRange2·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 上の WindowMissed Event — そのカウント(windowOpenedAteligibleinBackoffTriggered)が、ウィンドウのどれだけの作業が未ローテーションだったかを示す。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 かどうか(StaticNodePool Warning Event、spec §3.3)— static な NodePool は surge ローテーションを一切試みないため、年齢と状態から見て未処理の claim を残したまま閉じた発生のたびにウィンドウを逃す。

何をするか: rotation attempt failed 行が示す根本原因に対処する(§1§5 を参照)。試行自体は健全だが、バッチがスケジュールに対して大きすぎて本当に収まらない場合は、メンテナンスウィンドウを拡張して 1 回の発生あたりの完了数を増やすか、minRotationChancesK)を引き上げて、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 == 1RotationPolicy のセレクタ重複を修正
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 > 0ShortLead Warning EventNodePool の 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 を先に適用:

sh
kubectl apply -f charts/node-rotation-controller/crds/
helm upgrade --install node-rotation-controller charts/node-rotation-controller ...

変更したスキーマがどのリリースか、そしてアップグレード時に対処が要る動作変更・values 変更は、リリースごとに changelog(英語)に記録している。アップグレード前に、導入済みバージョンから対象バージョンまでのエントリを読むこと。

chart は rotationPolicies[].spec サブツリーを封印しているため、以前のバージョンが黙って捨てていたタイポはアップグレード時にハードエラーになる。事前にドライランすること:

sh
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 を参照。