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

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

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

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 でも可

スループットを上げないもの: 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_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.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 上の WindowMissed Event — そのカウント(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 かどうか(StaticNodePool Warning 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 == 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 を参照。