ノードを空けてアップグレード計画を立てる
目標
ノード1台を安全に空にして元に戻す手順を自分の手で実行し、バージョンスキューのルールとカナリアロールアウトの計画を、ドキュメントではなく検証可能な成果物として作ります。
なぜ重要なのか
アップグレード事故の大半は、コマンドを知らないからではなく、順序と範囲を知らないから起きます。元に戻せない作業(kubeadm upgrade apply)の前には、必ずetcdスナップショットがなければなりません。コントロールプレーンのダウングレードはサポートされていないため、失敗したときの脱出口は「下げること」ではなく「スナップショットから復旧すること」だけだからです。範囲の問題は、バージョンスキューが担当します。kubeletはapiserverより低いことはできても高いことはできず、kubectlだけが唯一、上に1つの余裕が認められています。この非対称性を知らないと、「kubectlは最新を使ってもいいのに、なぜkubeletはだめなのか」といった疑問で行き詰まります。最後に、ノード作業の3段階(cordon → drain → uncordon)とPDBはセットです。PDBがなければdrainがサービスをまるごと止めてしまうことがあり、PDBが厳しすぎればdrainがいつまでも終わりません。
ステップ
/root/ops/upgrade/out/versions.jsonを作成してください。最上位のキーはserver(v1.x.y形式のサーバーバージョン文字列)とnodesです。nodesはlab-node-0、lab-node-1、lab-node-2の3つをキーに持ち、値は各ノードの実際のkubeletバージョン文字列である必要があります。lab-node-1だけをcordonし、直後のノード一覧を/root/ops/upgrade/out/cordon.txtに保存してください。ファイルにlab-node-1とSchedulingDisabledが見えていて、lab-node-0はブロックされていてはいけません。そして/root/ops/upgrade/out/cordon-note.txtに、cordonがすでに起動しているPodには手を触れないという点を1行で書いてください。- ネームスペース
ops-upgradeを作成し、その中にreplicas: 3のDeploymentweb(Podのラベルはapp=web)を作成してください。続けてPodDisruptionBudgetweb-pdbを同じネームスペースに、spec.minAvailable: 2、spec.selector.matchLabels.app: webで作成します。maxUnavailableは使わないでください。 lab-node-1をdrainして、その出力を/root/ops/upgrade/out/drain.txtに保存してください。drainが終わったあと、ops-upgradeネームスペースのPodがどのノードにあるかを、/root/ops/upgrade/out/after-drain.txtに3行以上で残してください。このファイルにlab-node-1が含まれていてはならず、lab-node-0またはlab-node-2が見えている必要があります。lab-node-1をuncordonして、その出力を/root/ops/upgrade/out/uncordon.txtに保存してください。3つのノードすべてがスケジュール可能な状態で、DeploymentwebのReadyなレプリカが再び3つになっている必要があります。/opt/lab/fixtures/k8s/skew-template.yamlを/root/ops/upgrade/skew.yamlにコピーしたあと、answersの下の疑問符を埋めてください。apiserver: "1.34"は問題の前提なので、そのままにしておきます。回答の形式は"1.31"のようなマイナーバージョンの文字列で、downgrade_supportedだけは"yes"または"no"です。埋めるべきキーはmin_kubelet、max_kubelet、max_kubectl、min_kubectl、max_controller_manager、next_upgrade_target、downgrade_supportedです。/root/ops/upgrade/runbook.mdを500バイト以上で書いてください。各項目は別々の行に置き、次の順序を守る必要があります。(1)etcdスナップショットのバックアップ、(2)kubeadm upgrade plan、(3)最初のコントロールプレーンでのkubeadm upgrade apply、(4)残りのノードでのkubeadm upgrade node、そしてノード作業の区間ではdrainがuncordonより先に出てくる必要があります。マイナーバージョンを飛ばせないという制約も、文章で書いてください(例: 「一度に1つ」)。- 3つのノードに
upgrade.labhub.io/stageラベルを付けてください。lab-node-0はcanary、lab-node-1はbatch1、lab-node-2はbatch2です。そして/root/ops/upgrade/out/rollout.jsonに計画を書いてください。stagesは長さ3の配列で、各要素はstage(canary/batch1/batch2)とnodes(ノード名の配列)を持ちます。最初のステージはcanaryで、ノードはちょうど1台である必要があり、3つのノードすべてがどこかに割り当てられている必要があります。最上位にverify_between_stagesキーを置き、ステージの間に何を確認するかを書いてください。
参考
- サーバーバージョンは
kubectl version -o jsonの.serverVersion.gitVersionに、ノードのkubeletバージョンはkubectl get nodes -o jsonの.items[].status.nodeInfo.kubeletVersionにあります。jqのfrom_entriesを使うと、名前をキーとするオブジェクトを簡単に作れます。 - drainは、DaemonSetのPodやemptyDirを使うPodがあると、そのままでは拒否します。
--ignore-daemonsets、--delete-emptydir-data、必要なら--forceを付けてください。 - Podがどのノードにあるかは、
kubectl get pods -n ops-upgrade -o wide --no-headersで、1行に1つずつ見られます。 - ラベルにドットとスラッシュが含まれる場合、jsonpathではエスケープが必要です。確認には
kubectl get nodes -L upgrade.labhub.io/stageがいちばん簡単です。 - よくある間違い1: ステップ7のランブックで、
kubeadm upgrade applyとkubeadm upgrade nodeを1行にまとめて書いてしまうことです。2つのコマンドの最初に現れた行番号で順序を判定するので、同じ行では通りません。drainとuncordonも同様です。 - よくある間違い2: ステップ6で、kubectlの上限をapiserverと同じ値にしてしまうことです。kubectlだけが、上に1つの余裕が認められています。
- ラボのPodはラボごとに新しく起動するので、前のラボのクラスターの状態は残っていません。
ops-upgradeネームスペースとDeploymentは、ステップ3で自分で作成してください。運用手順を記憶ではなくランブックとマニフェストとして残すべき理由が、まさにここにあります。
コントロールプレーンとノードのバージョンを収集する
/root/ops/upgrade/out/versions.jsonを作成してください。最上位のキーはserver(v1.x.y形式のサーバーバージョン文字列)とnodesです。nodesはlab-node-0、lab-node-1、lab-node-2の3つをキーに持ち、値は各ノードの実際のkubeletバージョン文字列である必要があります。
アップグレードは、現在のバージョンを正確に知ることから始まります。サーバーバージョンと各ノードのkubeletバージョンは、それぞれ別の場所から読み取ります。ノード情報はstatus.nodeInfoの下にあります。
ノード1台だけをスケジュール対象から外す
lab-node-1だけをcordonし、直後のノード一覧を/root/ops/upgrade/out/cordon.txtに保存してください。ファイルにlab-node-1とSchedulingDisabledが見えていて、lab-node-0はブロックされていてはいけません。そして/root/ops/upgrade/out/cordon-note.txtに、cordonがすでに起動しているPodには手を触れないという点を1行で書いてください。
一度に1台ずつが原則です。cordonはこれからのスケジュールを止めるだけで、すでに起動しているPodには手を触れないという点を、メモに残してください。
可用性を守るPDBを作成する
ネームスペースops-upgradeを作成し、その中にreplicas: 3のDeploymentweb(Podのラベルはapp=web)を作成してください。続けてPodDisruptionBudgetweb-pdbを同じネームスペースに、spec.minAvailable: 2、spec.selector.matchLabels.app: webで作成します。maxUnavailableは使わないでください。
3つのレプリカのうち、最低いくつが生きていなければならないかを宣言します。最小値と最大利用不可値は、同時には使えません。
ノードを空にしてPodの移動を確認する
lab-node-1をdrainして、その出力を/root/ops/upgrade/out/drain.txtに保存してください。drainが終わったあと、ops-upgradeネームスペースのPodがどのノードにあるかを、/root/ops/upgrade/out/after-drain.txtに3行以上で残してください。このファイルにlab-node-1が含まれていてはならず、lab-node-0またはlab-node-2が見えている必要があります。
drainはcordonに加えて、既存のPodを追い出します。DaemonSetとemptyDirに関するオプションがないと拒否されます。空にしたあと、Podがどのノードにあるかを保存してください。
作業が終わったノードを元に戻す
lab-node-1をuncordonして、その出力を/root/ops/upgrade/out/uncordon.txtに保存してください。3つのノードすべてがスケジュール可能な状態で、DeploymentwebのReadyなレプリカが再び3つになっている必要があります。
uncordonを忘れると、そのノードは静かに遊んでしまいます。3つのノードすべてがスケジュール可能な状態で、ワークロードも元の数に戻っている必要があります。
バージョンスキュー表を埋める
/opt/lab/fixtures/k8s/skew-template.yamlを/root/ops/upgrade/skew.yamlにコピーしたあと、answersの下の疑問符を埋めてください。apiserver: "1.34"は問題の前提なので、そのままにしておきます。回答の形式は"1.31"のようなマイナーバージョンの文字列で、downgrade_supportedだけは"yes"または"no"です。埋めるべきキーはmin_kubelet、max_kubelet、max_kubectl、min_kubectl、max_controller_manager、next_upgrade_target、downgrade_supportedです。
基準は常にapiserverです。kubeletは下に3マイナー、コントローラーは下に1マイナー、kubectlだけが上下に1マイナーです。マイナーは一度に1つずつ上げます。
アップグレードのランブックを書く
/root/ops/upgrade/runbook.mdを500バイト以上で書いてください。各項目は別々の行に置き、次の順序を守る必要があります。(1)etcdスナップショットのバックアップ、(2)kubeadm upgrade plan、(3)最初のコントロールプレーンでのkubeadm upgrade apply、(4)残りのノードでのkubeadm upgrade node、そしてノード作業の区間ではdrainがuncordonより先に出てくる必要があります。マイナーバージョンを飛ばせないという制約も、文章で書いてください(例: 「一度に1つ」)。
元に戻せない作業の前には、バックアップが来ます。最初のコントロールプレーンと残りのノードでは、使うkubeadmのサブコマンドが違うという点も書いてください。
カナリアロールアウトの計画を立ててラベルを付ける
3つのノードにupgrade.labhub.io/stageラベルを付けてください。lab-node-0はcanary、lab-node-1はbatch1、lab-node-2はbatch2です。そして/root/ops/upgrade/out/rollout.jsonに計画を書いてください。stagesは長さ3の配列で、各要素はstage(canary/batch1/batch2)とnodes(ノード名の配列)を持ちます。最初のステージはcanaryで、ノードはちょうど1台である必要があり、3つのノードすべてがどこかに割り当てられている必要があります。最上位にverify_between_stagesキーを置き、ステージの間に何を確認するかを書いてください。
計画をドキュメントだけに置かず、ノードのラベルとしてクラスターに刻んでください。最初のステージは1台で、ステージの間に何を確認するかがカナリアの核心です。