バージョンスキューと無停止アップグレードの順序
一言でいうと
アップグレードとは「新しいバージョンをインストールすること」ではなく、「複数のコンポーネントのバージョン差を許容範囲内に収めながら、1段階ずつ進めること」です。
なぜ必要なのか
Kubernetesは1つのプログラムではなく、apiserver、controller-manager、scheduler、kubelet、kube-proxy、kubectlがそれぞれ別のプロセスとして動く分散システムです。これらを同時に入れ替えることはできません。ノードが7台ならkubeletも7つあり、そのうち1つが再起動に失敗すれば、そのノードだけが古いバージョンのまま残ります。そこでKubernetesは、バージョンが異なっていてもよい範囲を公式に定義しています。これがバージョンスキューポリシーです。
筆者のホームラボ拡張の記録にも、この問題が登場します。新しく追加したミニPC2台に、以前のクラスターの残骸であるv1.32のkubeletが残っていて、その状態でクラスターに参加させていたら、コントロールプレーンとノードのバージョンがずれたまま動いていたはずです。結局、teardownからやり直すスクリプトにまとめて整理しました。バージョンは「だいたい近ければよい値」ではなく、サポート範囲がドキュメントで明記された契約です。
どう動くのか
基準点は常にkube-apiserverです。ほかのコンポーネントは、apiserverを中心に許容範囲が決まります。
| コンポーネント | apiserverに対する許容範囲 |
|---|---|
| kubelet | 最大3マイナーまで低くてもかまいません。絶対に高くてはいけません |
| kube-proxy | kubeletと同じルール |
| controller-manager / scheduler | 最大1マイナーまで低くてもかまいません。高くてはいけません |
| kubectl | 上下に1マイナーまで(±1) |
apiserverが1.34だとします。この表をそのまま適用すると、kubeletは1.31から1.34まで、controller-managerは最大1.34、kubectlは1.33から1.35までになります。kubectlだけが唯一apiserverより高くてもよいという点が、よく混同される部分です。
これに加えて、ルールがあと2つあります。
- マイナーは一度に1つずつ上げます。1.32から1.34へ直接は上げられません。1.33を経由する必要があります。飛ばすと、保存されたAPIオブジェクトの変換経路が保証されないからです。
- コントロールプレーンのダウングレードはサポートされていません。アップグレードの過程でetcd内のデータが新しいスキーマで書き込まれるため、元に戻す道は「以前のバージョンに下げること」ではなく、アップグレード直前に取っておいたetcdスナップショットから復旧することです。この一文が、前のモジュールとこのモジュールをつなぎます。
kubeadmクラスターでの実際の順序は次のとおりです。
etcd 스냅샷 백업
→ kubeadm upgrade plan (무엇이 가능한지 확인)
→ 첫 컨트롤 플레인에서 kubeadm upgrade apply v1.x.y
→ 나머지 컨트롤 플레인에서 kubeadm upgrade node
→ 노드마다: drain → kubelet/kubeadm 패키지 교체 → uncordon
drainとcordonの違いもここで分かれます。cordonはこれからスケジュールされないようにするだけで、すでに起動しているPodはそのままにします。drainはそれに加えて、既存のPodを追い出します(evict)。そのため、ノード作業の標準的な順序は、cordonで流入を止め、drainで空にし、作業をして、uncordonで元に戻すことです。uncordonを忘れると、そのノードは静かに遊んでしまいます。
Podを追い出すときにサービスが落ちないように守ってくれる仕組みが、PodDisruptionBudgetです。minAvailable: 2を設定すると、evictのリクエストはその線を超えない範囲でのみ承認されます。ただしminAvailableとmaxUnavailableは、どちらか一方だけしか使えません。
現場での姿
1つ目は、カナリアなしの一括アップグレードです。ノードを一度にすべて上げると、問題が起きたときに比較対象となる対照群がありません。まずノード1台を上げてワークロードが正常か確認し、そのあと残りをバッチに分けるのが基本です。ノードにステージのラベルを付けておけば、計画がドキュメントではなくクラスターの状態になります。
2つ目は、PDBがdrainを永遠にブロックするケースです。レプリカが1つしかないのにminAvailable: 1を設定すると、そのPodは絶対にevictされません。drainが数分間止まったままなら、まずPDBを見てください。
3つ目は、「状態がReady」と「実際に動作している」は別の命題だということです。筆者がKubeVirtを導入したとき、すべてのコンポーネントがAllComponentsReadyなのにVMが起動しなかったことを記録しています。アップグレード後も同じです。ノードがReadyであることと、ワークロードが正常であることは、別々に確認する必要があります。
次のラボですること
実際のノードをcordonし、PDBを設定したワークロードをdrainで移動してから、uncordonで元に戻します。apiserver 1.34を基準にスキュー表の空欄を自分で埋め、バックアップからuncordonまでのアップグレードのランブックを書き、ノードにステージのラベルを付けて、カナリアロールアウトの計画をJSONで作成します。