TT Lab
はじめる
学ぶ 学習パス コース

Kubernetes運用実務

バージョンスキューと無停止アップグレードの順序

TT Labで続きを見る

一言でいうと

アップグレードとは「新しいバージョンをインストールすること」ではなく、「複数のコンポーネントのバージョン差を許容範囲内に収めながら、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つずつ上げます。1.32から1.34へ直接は上げられません。1.33を経由する必要があります。飛ばすと、保存されたAPIオブジェクトの変換経路が保証されないからです。
  2. コントロールプレーンのダウングレードはサポートされていません。アップグレードの過程で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で作成します。