Kubernetesディストリビューション — 自分で立てる
マイナーバージョンは一段ずつ上げる
一言でいうと
Kubernetesのアップグレードは「最新バージョンをインストールすること」ではなく、マイナーバージョンを1つずつ上げながら、コンポーネント間のバージョン差(スキュー)を許容範囲内に収める作業です。k3sもこのルールの例外ではありません。
なぜ必要なのか
k3sはインストールが1行で終わるので、アップグレードも1行だと考えがちです。インストールスクリプトをもう一度実行すれば、新しいバイナリを取得してサービスを再び起動してくれるからです。そのため、よくこうします。「今のstableチャネルは何だろう。それで上げよう。」
ここに落とし穴があります。チャネルはその瞬間の最新の推奨バージョンを指すだけで、みなさんのクラスターが今どのバージョンかは知りません。1.34で止まっていたクラスターをstableチャネルで上げると、stableが1.36を指している日には、マイナーバージョンを2つ一度に飛ばします。インストールスクリプトは、これを防ぎません。
ところが、Kubernetesプロジェクトのポリシーははっきりしています。バージョンスキューポリシーは、kube-apiserverをアップグレードするときはマイナーバージョンを飛ばしてはならず、インスタンスが1つだけのクラスターも同様だと書いています。k3sの手動アップグレードのドキュメントも、同じポリシーが適用されるとして、途中のマイナーバージョンを飛ばさないよう求めています。理由は、データストアに入っているオブジェクトの変換、非推奨APIの削除、コントローラーの動作の変化が、1バージョン単位でしかテストされないからです。2つ飛ばすと、誰もテストしていない経路を、みなさんが最初に歩くことになります。
どう動くのか
バージョン差の許容範囲
スキューポリシーは、コンポーネントごとに「APIサーバーよりいくつ先のバージョンまで異なってよいか」を決めています。核心は、どのコンポーネントもAPIサーバーより新しくてはならないということです。
구성요소 API 서버가 1.35 일 때 허용되는 판
kube-apiserver (HA) 서로 한 마이너 이내
controller-manager·scheduler 1.35, 1.34 (한 판 낮게까지)
kubelet·kube-proxy 1.35, 1.34, 1.33, 1.32 (세 판 낮게까지)
kubectl 1.36, 1.35, 1.34 (위아래 한 판)
このコードブロックの韓国語の部分は、表の見出しがコンポーネントとAPIサーバーが1.35のときに許容されるバージョンであること、kube-apiserverは互いにマイナー1つ以内であること、controller-managerとschedulerは1つ低いバージョンまでであること、kubeletとkube-proxyは3つ低いバージョンまでであること、kubectlは上下1つまでであることを述べています。
そのため、上げる順序も決まります。コントロールプレーン(APIサーバー)が先で、kubeletは後です。逆にすると、kubeletがAPIサーバーより新しくなる瞬間ができます。k3sのドキュメントがサーバーノードを1台ずつ先に、そのあとエージェントノードという順序を求める理由が、これです。また、kubeletが3バージョンまで遅れてもよいおかげで、コントロールプレーンを1つずつ何度も上げる間、ワーカーを毎回追いかけて上げなくても済みます。
k3sで実際に起きること
k3sは、APIサーバー・kubelet・コントローラーが1つのバイナリです。インストールスクリプトをINSTALL_K3S_VERSIONでバージョンを固定してもう一度実行すると、そのバージョンのバイナリを取得してサービスを再起動します。1つのノードではコントロールプレーンとkubeletが一緒に上がるので、スキューはノード間でしか生じません。
注意する点が2つあります。
1つ目、インストール時に環境変数や引数で渡した設定は、再実行するときにもう一度渡さないと消えます。k3sのドキュメントが明記している動作です。そのため、設定は/etc/rancher/k3s/config.yamlに置くほうが安全です。インストールスクリプトとは無関係に残るからです。
2つ目、k3sを止めても、Podのコンテナは動き続けます。そのため、APIが一時的に切れてもサービスはたいてい生きていますが、APIが切れている間に問題になるワークロードなら、先にdrainするようドキュメントが勧めています。
実測で見た姿も書いておきます。このラボと同じVMで、v1.34.11+k3s1をv1.35.8+k3s1にインストールスクリプトで上げるのに11秒かかり、あらかじめ起動しておいたnginxのPod 2つは、PodのUIDとコンテナIDがそのままで、再起動回数も0でした。一方、kubeadmアップグレードのドキュメントは、コンテナのspecのハッシュが変わるため、アップグレード後にすべてのコンテナが再起動されると書き、マイナーバージョンのkubeletのアップグレード前には必ずdrainするよう求めています。同じ「Kubernetesのアップグレード」でも、ディストリビューションとバージョンの組み合わせによってワークロードが経験することが異なるので、ドキュメントの1行で決めつけず、測る必要があります。
drainができることとできないこと
kubectl drainは、ノードをcordon(新しいPodの配置を禁止)したあと、Podをeviction APIで追い出します。evictionはPodDisruptionBudgetを守ります。バジェットを破るevictionは拒否され、drainは拒否されたPodを何度も再試行します。
これは、行き先がなければdrainは終わらないという意味です。ノードが1台だけだと、追い出されたPodの代わりのPodが、cordonされたそのノードに載れずにPendingになり、準備ができたPodの数がバジェットを下回って、次のevictionが止められます。実測では、drainがCannot evict pod as it would violate the pod's disruption budgetを5秒間隔で8回繰り返したあと、--timeout=40sに達して終わりました。このラボで、その場面を自分で見ます。
バックアップと非推奨APIの点検
k3sのデフォルトのデータストアはSQLiteです。バックアップと復旧のドキュメントは、/var/lib/rancher/k3s/server/db/と一緒に、トークンファイル(/var/lib/rancher/k3s/server/token)を必ず確保するよう求めています。トークンがデータストア内の機密データの暗号化に使われるため、DBだけがあってトークンがないと復旧できません。
非推奨APIは、APIの移行ガイドでバージョンごとに何がなくなるかを確認し、APIサーバーのapiserver_requested_deprecated_apisメトリクスで、実際に誰がそのAPIを呼んでいるのかを確認します。ドキュメントだけを見ると「うちは使っていない」と信じてしまいますが、メトリクスを見ると、古いHelmチャートやスクリプトがまだ古いバージョンを呼んでいることが明らかになります。
現場での姿
エッジに散らばったk3s数十台をしばらく放置して、セキュリティの通知のために急いで上げるケースがよくあります。このとき「どうせ最新に行くのだから」とチャネルをstableにすると、ノードごとに出発バージョンが異なるので、あるノードは1つ、あるノードは3つを飛ばします。上げることには成功しても、古いAPIで作ったオブジェクトや、削除された機能に頼っていたワークロードが、その後に静かに壊れます。
自動化するときは、Rancherのsystem-upgrade-controllerを使います。Planオブジェクトに目標バージョン(version)またはチャネル(channel)を書き、concurrency・cordon・nodeSelectorで、一度に何台をどう上げるかを決めます。ここでもチャネルを指定すると、同じ落とし穴が生まれます。しかも実測してみると、Plan CRD(v0.20.1)はversionもchannelもないPlanを、サーバーの検証でそのまま受け入れました。適用が成功したことは、バージョンが正しいという意味ではありません。バージョンを固定して、1つずつPlanを書き換えていくほうが、スキューポリシーと合います。
実務で本当に大切なこと
- 出発バージョンを先に書きます。アップグレードの後は、以前のバージョンをどこからも見直せません。バージョン・ノードのUID・ワークロードのUIDを記録しておくことで、初めて「同じクラスターが上がった」ことを証明できます。
- 目標バージョンは、チャネルではなく正確なバージョンで固定します。マイナーバージョンの差が1つだけかどうかを、数字で確認します。
- バックアップは、DBとトークンを一緒に取得します。トークンが抜けたバックアップは、復旧できません。
- drainは、行き先があるときにだけ終わります。ノードが1台なら、drainがPDBに止められるのが正常で、その場合はk3sがコンテナを生かしておくという性質と、API中断の影響を考えて判断します。
- アップグレードの後は、記録と照合します。ノードのUIDが同じか、ワークロードが同じオブジェクトとして生きているか、設定(例: 無効にしたtraefik)が維持されているかを確認して、初めて終わりです。
次のラボですること
VM内のk3s 1.34を1台で出発します。出発バージョンを記録し、PDBが設定されたワークロードを起動してから、非推奨APIのメトリクスとSQLiteのバックアップで準備をします。drainが1台構成のノードでなぜ止まるかを見て、インストールスクリプトで1.35へ1つだけ上げたあと、ワークロードとノードが同じオブジェクトとして生きているかを照合します。最後に、次の1つのためのsystem-upgrade-controllerのPlanを書き、stableチャネルに従っていたら、いくつ飛ばしていたかを計算します。