K3sのsystem-upgrade-controllerで自動アップグレードを組んだはずなのに、気づけばPlanのステータスが何日も「進行中」のまま動かなくなっていた——。複数ノードで構成するK3sクラスタを自動運用していると、こうした「止まっているのに誰も気づかない」トラブルに出会うことがあります。
今回取り上げるのは、K3s特有の自動アップグレード機構であるsystem-upgrade-controllerが、特定のノードだけで詰まり続けてフリート全体のアップグレードがブロックされる問題です。原因を整理すると、PodDisruptionBudget(PDB)とtaint、そしてdrainタイムアウトという3つの要因が絡み合っていました。
system-upgrade-controllerは「ただのCronJob」ではない

まず前提として、system-upgrade-controllerがどういう仕組みかを整理します。これはRancher/SUSEが提供するOSSツールで、Planというカスタムリソースでアップグレードポリシーを宣言的に定義し、コントローラがノードを監視してアップグレード用のJobをスケジュールする仕組みです(K3s公式ドキュメント)。
標準のKubernetesをkubeadmで構築した場合、この仕組みは存在しません。kubeadmにはクラスタ自体を自動でローリングアップグレードする標準機構がなく、アップグレードは基本的に手動またはCI/CDパイプラインに自前で組み込む必要があります。一方K3sは、system-upgrade-controllerをインストールしてPlanを適用するだけで、コントロールプレーンノード→エージェントノードの順にローリングアップグレードを自動実行できます(SUSE公式ドキュメント)。
Planの基本的な動き
- ノードセレクタでアップグレード対象のノード群を指定する
- concurrencyフィールドで同時にアップグレードするノード数を制御する
- channel(リリースチャンネル)またはversion(特定バージョン)でアップグレード先を指定する
- サーバノード用Planとエージェントノード用Planを分け、サーバが先に完了してからエージェントが追従する設計が推奨されている
便利な反面、この「Plan→Job→ノード選択」という3層構造を理解せずに運用していると、どこで詰まっているのかを特定するのに時間がかかります。実際、GitHub上の公式リポジトリにも、Planの内部でノードごとにJobがどう生成されるかの詳細な仕様がまとめられています(rancher/system-upgrade-controller)。
Planが詰まる3つの原因

system-upgrade-controllerのPlanがノードごとに進まなくなる原因は、突き詰めるとアップグレード対象ノードの「安全な退避(drain)」がどこかで止まっていることに尽きます。代表的な3つの要因を整理します。
1. PodDisruptionBudgetが退避を許可しない
KubernetesのPodDisruptionBudget(PDB)は、意図的な中断(voluntary disruption)の際にアプリケーションの可用性を守るための仕組みです。minAvailableまたはmaxUnavailableを指定すると、クラスタは「現在許可されている中断数(disruptionsAllowed)」を計算します(Kubernetes公式ドキュメント)。
この値が0になっていると、ノードのdrain処理はPodのeviction要求に対して429 Too Many Requestsを返され続け、いつまで経ってもPodが退避できません。これはK3s固有の問題ではなくKubernetesのEviction API全体の仕様で、PDBチェックはAPIサーバーのアドミッション制御段階で行われます(Kubernetes公式ドキュメント: API-initiated Eviction)。Microsoft LearnのAKSトラブルシューティング記事でも、全く同じ「ALLOWED DISRUPTIONSが0のまま詰まる」パターンが報告されています(Microsoft Learn)。つまりこれはマネージドサービスかセルフホストかを問わない、Kubernetes運用に共通する落とし穴です。
2. taintのあるノードはデフォルトで対象外になる
system-upgrade-controllerが生成するアップグレードJobのPodは、デフォルトではnode.kubernetes.io/unschedulable:NoScheduleという最小限のtolerationしか持ちません。つまり、それ以外のカスタムtaint(例えば専用ワークロード用に付けたtaint)がノードに設定されていると、明示的にtolerationを追加しない限り、そのノードにはアップグレードJob自体がスケジュールされず、Planが「待機中」のまま止まって見えることになります(rancher/system-upgrade-controller)。
taintとtolerationの基本的な仕組み自体は標準Kubernetesの機能で、NoScheduleは新規Podのスケジューリングのみを拒否し、NoExecuteは既存Podまで退避させるという違いがあります(Kubernetes公式ドキュメント: Taints and Tolerations)。system-upgrade-controllerの文脈では、この挙動を知らないまま「なぜかこのノードだけアップグレードされない」と見過ごされがちです。
3. drainタイムアウトでPlanが待ち続ける
PDBやtaintの問題がなくても、特定のPodが何らかの理由で退避に時間がかかっている場合、drain処理はタイムアウトまで待ち続けます。Planのdrain仕様にはforceやignoreDaemonSets、skipWaitForDeleteTimeoutといったオプションがあり、これらを事前に設定していないと、本番環境で想定より長く待機してしまうケースがあります(rancher/system-upgrade-controller)。
なお、本記事で扱う3要因のうち、PodDisruptionBudget単体の挙動については、K3sでPodDisruptionBudgetが引き起こす大量退避の問題でも別の角度から取り上げている。今回はあくまで「system-upgrade-controllerのPlanを止める一要因」として扱う。
復旧と予防策

Planが実際に詰まってしまった場合の緊急対応と、再発防止のための事前設定を分けて整理します。
緊急対応(詰まった場合)
kubectl get plan -n system-upgradeとkubectl get jobs -n system-upgradeで、どのノードのJobが止まっているかを確認する- 原因がPDBであれば、
kubectl get pdb -AでALLOWED DISRUPTIONS列を確認し、0になっているPDBを一時的に調整する - それでも進まない場合は、対象のPlanを一度削除し、cordon状態のままになっているノードを
kubectl uncordonで手動復旧してから、設定を直したPlanを再適用する
予防策(事前にやっておくこと)
- Planの
drain設定にforceやignoreDaemonSetsを明示的に指定し、デフォルト挙動に任せない - アップグレード対象ノードに付与しているカスタムtaintを棚卸しし、必要であればPlanのJob側にtolerationを追加する
- PDBの
minAvailable/maxUnavailableがレプリカ数に対して厳しすぎないか、アップグレード前にレビューする - Planの進行状況を監視できるよう、
Planリソースのステータスをモニタリングに組み込む
K3sのsystem-upgrade-controllerそのものは、インストールしてPlanを書くだけで動き出すお手軽さが魅力です(K3s公式ドキュメント)。しかしこの「お手軽さ」の裏には、PDBやtaint、drainタイムアウトという3つの落とし穴が隠れていることを理解しておく必要があります。
こうした自動化の仕組みを運用し続ける負担自体を減らしたい場合、マネージドK3sサービスを使えば、アップグレード運用の設計・監視を含めて任せることができます。特にオフラインや断続的な接続しかないエッジ環境では、Planが詰まっていることに気づくまでのタイムラグが大きなリスクになります。Kubo On-PremiseはAir-Gapped環境にも対応しているため、閉域ネットワークのエッジノード群でもこうした自動化の設計・監視を外部委託しやすくなります。
クラスタ構成そのものを見直すタイミングかもしれない
アップグレード運用でPDBやtaintの設定ミスが頻発するようであれば、それはクラスタ全体の設計を見直すサインでもあります。Kubo CloudのAI-Driven DeploymentとCaptain UIを使えば、Planの進行状況やノードの状態を可視化しながら、ブラックボックスにならない形でK3sクラスタを運用できます。標準Kubernetes互換であるためベンダーロックインもなく、既存のAWS/Azureの知識や資産をそのまま活かせる点も、アップグレード自動化の設計を見直す際のハードルを下げてくれます。
まとめ
- K3sの
system-upgrade-controllerは、kubeadmベースの標準Kubernetesには存在しない、K3s固有のローリングアップグレード機構である - Planが特定ノードで止まる原因は、主に①PodDisruptionBudgetによる退避拒否、②taintによるJobスケジュール対象外、③drainタイムアウトの3つに集約される
- 緊急時はPlanの削除とノードのuncordon、恒常対策としてはdrain設定の明示化とPDB/taintの事前レビューが有効
- エッジ環境など「止まっていることに気づきにくい」構成では、Kuboのようなマネージドサービスでアップグレード運用自体を委譲する選択肢も検討する価値がある
自動化は仕組みを理解してこそ安心して任せられます。次にPlanを適用する前に、今回紹介した3つのチェックポイントを確認してみてください。