インフラエンジニアの羅針盤

インフラエンジニア1〜3年目のための技術ガイド

K8s更新計画|戻せない地点から書く

公開

第13回で、既存システムから新しい基盤へ切り替えました。ここでクラスタは「作るもの」から「上げ続けるもの」に変わります。

そして最初のマイナーバージョンアップが近づくと、多くの現場で同じ質問が出ます。「上げて問題が起きたら、戻せますか」。

この質問に対するGoogle Kubernetes Engine(GKE)の答えは明確です。公式ドキュメントは「You can’t downgrade a cluster control plane to a previous minor version, including after a one-step control plane minor upgrade」と書いています。同じページには「You cannot upgrade your cluster’s control plane more than one minor version at a time」ともあります。バージョン1.32から1.34へ直接は上げられず、1.33を経由する。そして一度上げたマイナーバージョンは、戻りません。

第13回で、移行計画で最初に決めるのは手順ではなく切り戻せなくなる時点だと確認しました。本記事も同じ形から始めます。In-Place方式でクラスタを更新する場合、戻せなくなる時点は、コントロールプレーンの更新が始まった瞬間です。

目次
  1. 更新計画は、コマンドではなく「戻せない地点」から書く
    1. この記事で作る成果物
  2. 年3回は選べない。日付は先に決まっている
    1. 4つの日付を先に埋める
    2. 自動更新の最低間隔が、計画の刻みを決めている
    3. 維持除外は3スコープ。「パッチだけ受ける」を既定にする
    4. 60日前告知は、クラウドからの通知では間に合わない
    5. 環境をまたぐ順序は、設定として持てる
  3. 更新方式は2つ。In-Place と Blue/Greenクラスタ移行
    1. 選択軸は3つ
    2. In-Placeで戻せる範囲を、正確に把握する
    3. Blue/Greenクラスタ移行は、第13回の再利用
    4. どちらを選んでも、コントロールプレーンは1マイナーずつ
  4. 調査その1:削除されるAPIは、30日の窓でしか見えない
    1. 3層と静的検査で調べる
    2. 30日に1度も動かないものは、検出されない
    3. 検出されると自動更新は止まる。止まったことに気づく必要がある
    4. ベータAPIは、調査ではなく台帳で管理する
  5. 調査その2:壊れるのはAPIだけではない
    1. 互換性マトリクスの行は「ツール」ではなく「壊れ方」で立てる
    2. 実例:containerd 2.0 でイメージのプルが失敗する
    3. マネージドを選んだ数だけ、この表の行は減る
    4. Admission Webhook は、更新中に効いてくる
  6. ノードの更新は、計画された大規模なノード障害
    1. kubeletのマイナー更新は、必ずドレインを伴う
    2. サージとblue-greenの違いは「戻せるか」
    3. 所要時間と必要なリソースを、押す前に計算する
    4. Autopilotでは猶予に上限がある
    5. 更新を中止する条件を、先に決める
  7. 証明書と資格情報は、別のカレンダーで動く
    1. GKEでローテーションされる4つ
    2. 期限は5年か30年。まず自分のクラスタがどちらかを確認する
    3. 自前で構築していたら、これは毎年の作業だった
  8. 実務テンプレート:アップグレードランブックと互換性チェックリスト
    1. アップグレードランブック(記入式・7項目)
    2. 互換性チェックリスト(毎回使い回す・7行)
  9. 次回予告

更新計画は、コマンドではなく「戻せない地点」から書く

本記事の位置づけを、前の2回との関係で示します。

第12回第13回本記事(第14回)
主題何を確認するかどう切り替えるかどう更新するか
実施回数繰り返す検証一度きり年に数回、これから何年も
失敗したとき設定を直して再実験中止して切り戻すコントロールプレーンは戻らない

右端の列が本記事の制約です。第13回のカットオーバーは、事前に用意した経路をたどれば旧環境へ戻せました。マイナーバージョンアップには、その経路が既定では存在しません。

そして「上げない」という選択肢も、期限つきです。GKEは「Regardless of your cluster’s settings, GKE performs automatic upgrades at the end of support.」と明記しています。放置は「更新しない」ではなく、更新日を他人に決めさせるという選択になります。

この記事で作る成果物

2つです。

  1. アップグレードランブック。日付、方式、順序、ノード戦略、中止条件、完了条件、後片付けまでを記入式で持つもの
  2. 互換性チェックリスト。更新のたびに同じものを使い回す、壊れ方ごとの確認表

本記事ではコマンドをほとんど書きません。gcloud container clusters upgrade の使い方は公式ドキュメントにあり、読者は既に見つけています。足りないのは、そのコマンドを押す前に決めておくべき数値と、押した後に戻れる範囲の把握です。

年3回は選べない。日付は先に決まっている

更新計画は、自分の都合から始められません。カレンダーは2つの外部要因で先に埋まっています。Kubernetes本体のリリース頻度と、GKEのサポート期間です。

4つの日付を先に埋める

Kubernetes公式は「Kubernetes releases currently happen approximately three times per year.」と書いています。GKE側も「Kubernetes releases a new minor version three times a year.」と同じ頻度を示します。約4ヶ月に1度、新しいマイナーバージョンが出ます。

サポート期間は次のとおりです。「GKE provides up to a total of 24 months of support for a minor version after the version has been made available for new cluster creation in the Regular channel. This support includes around 14 months of standard support」。差し引き約10ヶ月が拡張サポートで、これはExtendedチャネルに登録したクラスタだけが受けられます。

この2つの数字を掛け合わせると、計画の骨格が出ます。標準サポートの14ヶ月のあいだに、新しいマイナーバージョンが約3回出ます。そしてコントロールプレーンは1回の更新で1マイナーしか進めません。つまり毎回見送っていると、期限の直前に3回分の更新を連続して実施することになります。これは公式の記述ではなく、2つの数字からの計算です。しかし計画に効くのはこちらです。

計画書の最初のページには、手順ではなく4つの日付を置いてください。

  • 現行バージョンの標準サポート終了日。この日を過ぎると新機能もセキュリティパッチも来ません
  • 現行バージョンの拡張サポート終了日。費用を払って時間を買える期限
  • 次のバージョンの提供開始日。チャネルごとに違います
  • 自組織の更新実施予定日。上の3つから逆算して自分で決める唯一の日付

拡張サポートの費用は第3回で確認済みです。標準サポート終了後は1クラスタあたり1時間$0.50の追加で、月額に直すと約$438。これは標準サポート下のクラスタ管理費の約6倍にあたります。費用を払えば時間は買えますが、買えるのは約10ヶ月だけです。

標準サポート14か月と拡張サポート約10か月の帯に、約4か月ごとのマイナーリリースを重ね、期限までに何回分の更新が積み上がるかを時間軸で示した図
標準サポートの14か月のあいだに新しいバージョンが約3回出ます。見送るたびに、期限までに実施すべき更新の回数が積み上がります。

自動更新の最低間隔が、計画の刻みを決めている

GKEにはクラスタ中断予算という仕組みがあり、自動更新どうしの最低間隔を決めています。既定値はパッチ更新が24時間、マイナー更新が30日です。この間隔は自動更新どうしだけでなく、クラスタ作成から初回の自動更新までの間、および手動更新をした後の次回自動更新までにも適用されます。

設定範囲は0日から90日で、--maintenance-minor-version-disruption-interval--maintenance-patch-version-disruption-interval で変更します。これは自動更新を抑える仕組みであり、手動更新を止めるものではありません。遅れを取り戻すための連続した手動更新は実行できます。ただし手動更新の後、次の自動更新までは再びこの間隔が効きます。

ここに重要な例外があります。公式は「GKE adheres to a separate cluster disruption budget for minor upgrades of 7 days when a minor version reaches the end of support, and doesn’t adhere to any cluster disruption budget that you configured.」と書いています。サポート終了に達すると、設定した間隔は無視され7日に切り詰められます。「90日に設定しておけば当面は動かない」という理解は、期限を過ぎた瞬間に成り立たなくなります。

維持除外は3スコープ。「パッチだけ受ける」を既定にする

更新の時期を制御する手段が維持除外です。ここは「止める・止めない」の二択ではありません。スコープが3つあり、設定できる長さの上限がスコープごとに違います。

スコープ止まるもの設定できる長さの上限
No upgrades(既定)コントロールプレーンとノードの、マイナーもパッチも90日を超えられない(30日未満を推奨)
No minor upgradesコントロールプレーンとノードのマイナーのみ。パッチは受けるサポート終了日まで(終了日に追従させる設定もある)
No minor or node upgradesマイナーに加えてノードのパッチも。コントロールプレーンのパッチのみ通すサポート終了日まで(同上)

この差が設計の分かれ目です。「すべて止める」を選ぶと90日で切れます。一方「マイナーだけ止める」なら、サポート終了日まで持ちます。つまりパッチは自動で受け続け、マイナーだけは自分の日程で上げるという運びは、GKEが正式に用意している選択肢です。セキュリティパッチを受け取りながら、破壊的変更を伴うマイナーの時期だけ握る。これを既定にしてください。

維持窓のほうにも制約があります。公式は「You must allow at least 48 hours of maintenance availability in a 92-day rolling window. Only contiguous availability windows of at least four hours are considered.」と書いています。4時間未満の細切れの窓は、窓として数えられません。「深夜2時から5時まで許可」は3時間なので、計算上は0時間です。計画書の維持窓の欄には、時間帯だけでなく連続何時間かを書いてください。

60日前告知は、クラウドからの通知では間に合わない

第2回で、運用チームのSLAとして「K8s更新は60日前に告知し、検証期間を2週間確保する」と決めました。ここに落とし穴があります。

GKEの予定アップグレード通知は「between 72 and 96 hours in advance of the start time」です。3日から4日前。通知を待ってから告知すると、自分で決めたSLAを毎回破ることになります。

したがって60日前告知の起点は、通知ではなくリリーススケジュールです。公開されているサポート終了日と提供開始日を自分でカレンダーに転記する。この転記が、更新計画の最初のタスクになります。Pub/Subに配信される UpgradeAvailableEventUpgradeInfoEventSecurityBulletinEvent は、告知の起点ではなく自分の計画とのずれを検出する監視として位置づけてください。

告知の宛先と中身も決めておきます。開発チームが知りたいのは更新日そのものではなく、「自分のアプリケーションに何が起きるか」と「自分が何をいつまでにやるか」です。この2つを毎回ゼロから書くと告知が遅れます。定型の説明は第18回で作る開発者向けプレイブックに置き、告知では日付と依頼事項だけを差し替える形にしてください。

環境をまたぐ順序は、設定として持てる

第3回で、Prod(リージョン)、Stg(リージョン・ミラー)、Dev(ゾーン)の3クラスタ構成を決めました。更新はこの順序の逆、Devから流します。

GKEにはロールアウトシーケンスという機能があり、この順序を設定として持てます。ステージはフリート単位で、1シーケンスあたり最大15ステージ、フリートあたり250クラスタ、シーケンス全体のsoak時間は最大90日。既定ではパッチもマイナーも対象になります。手順書に「Devから流すこと」と書いて人が守るのではなく、順序そのものを設定に持たせる。第8回でインフラを手順ではなくコードで持つと決めたのと同じ形です

ただしステージ間の日数は機能が決めてくれません。決め方はこうです。

  • 下限は「その環境で動く定期処理の最長周期 + 1日」。週次バッチがあるなら8日、月次の締め処理まで含めるなら32日
  • 上限は2つの制約の小さいほう。現行バージョンのサポート終了日と、シーケンス全体のsoak上限90日

下限を守る理由は単純です。これを下回る間隔で先へ進めると、「Stgで問題が出なかった」の意味が「Stgでまだ月次処理を1度も回していない」に変わります。累積型の不具合を見たい場合は、第12回で定常状態を測ったときの観測期間をそのまま使ってください。

更新方式は2つ。In-Place と Blue/Greenクラスタ移行

方式は2つあります。既存クラスタをその場で上げるIn-Place。新しいバージョンでクラスタを作り、トラフィックを移してから古いクラスタを消すBlue/Greenクラスタ移行。結論から書くと、大半のクラスタではIn-Placeが正解です。Blue/Greenは「戻せないことが受け入れられない」ときにだけ払う保険であり、新しいから優れているわけではありません。

選択軸は3つ

  • 戻せる時間が要るか。In-Placeではコントロールプレーンを戻せません。Blue/Greenは旧クラスタを消すまで戻せます。自分の環境で、更新後に問題が判明したとき何分で戻す必要があるかを数値で答えられますか
  • 二重の費用と作業を払えるか。クラスタ、ノード、Pod IP、そしてGitOpsの同期先が2つになります
  • 状態を持つものを移せるか。PersistentVolume、外部システムに登録した送信元IP、固定ホスト名、ホスト単位のライセンス

2番目について補足します。第1回で試算したK8s税は、リージョンクラスタ1環境あたり月約$160でした。Blue/Greenクラスタ移行の期間中は、そこにもう1クラスタ分が乗ります。加えて第4回でコスト按分ラベルを設計しましたが、そのラベルに「移行期間」を表す値を用意しておかないと、二重に発生した費用が既存のチームへ配賦され、原因不明の増額として現れます。並行稼働の費用は誰の予算から出るのかを、方式を選ぶ前に決めてください。

In-Placeで戻せる範囲を、正確に把握する

「戻せない」と一括りにすると設計を誤ります。層ごとに違います。

対象戻せるか条件
コントロールプレーンのマイナー戻せない1マイナーだけ上げた直後であっても戻せない
コントロールプレーンのパッチ戻せる同一マイナー内の以前のパッチであること
ノードプールの更新(成功後)戻せない成功した更新はロールバックの対象外
ノードプールの更新(中止・失敗・未完了)可能な範囲で戻せるrollback は最善努力での復帰

ノードプールの rollback は、更新を中止したとき、失敗したとき、維持窓が切れて未完了になったときに使う操作です。成功した更新を取り消す手段ではありません。

ここから設計上の結論が1つ出ます。In-Placeで唯一の保険になるのは、古いノードを消さずに残しておくことです。これを機能として提供しているのが、後述するblue-greenノードプール戦略のsoak時間です。戻すための機構を後から作ることはできません。更新を実行する前に、戦略として選んでおく必要があります。

Blue/Greenクラスタ移行は、第13回の再利用

クラスタごと入れ替える方式は、第13回で作った切り替え機構をそのまま使います。違いは移行元がオンプレミスではなくGKEであることだけです。

クラスタをまたぐ場合はマルチクラスタGatewayを使います。config clusterと呼ぶ1つのクラスタにGatewayとHTTPRouteを配置し、gke-l7-global-external-managed-mc のようにマルチクラスタ対応のGatewayClassを指定する。すべてのクラスタをフリートに登録することが前提で、サービス検出にはマルチクラスタServiceを使います。重みによるトラフィック分割に対応しているため、第13回と同じ刻み方で移せます。

第13回で確認した性質も、そのまま引き継ぎます。

  • 重みを0にしても、旧クラスタへの通信は即座には止まりません。コネクションドレイニングの設定時間に加え、接続プールを使うクライアントはドレイン中のバックエンドへ新しいリクエストを送り続けます
  • 完了条件は設定値ではなく観測値で書きます。「重みを0にした」ではなく「旧クラスタのアクセスログが0件になった」
  • 刻み幅は率ではなく、観測できる母数で決めます。毎分のリクエスト数が少ない環境では、1%という段に意味がありません

そして状態を持つものは自動では移りません。PersistentVolumeの扱いは第7回の設計に依存します。移せない部分がある場合、そこだけIn-Placeで上げて他をBlue/Greenにする混成も成立します。方式はクラスタ単位で1つに決める必要はありません。

どちらを選んでも、コントロールプレーンは1マイナーずつ

ここは方式に関係なく効く制約です。2マイナー遅れているクラスタを最新に追いつかせるには、更新を2回実施する必要があります。そしてその間には、クラスタ中断予算のマイナー30日が入りうる。

つまり「今週末にまとめて最新まで上げる」という計画は成立しません。遅れを取り戻すには、遅れた分だけの回数と期間が必要です。これが「毎回見送っていると期限直前に3回分の更新が来る」という先ほどの計算の、実務上の意味です。

ノード側の制約も押さえてください。上流のバージョンスキューポリシーでは kubelet はAPIサーバより最大3マイナー古くて構いません。しかしGKEは「A cluster’s node pools can be no more than two minor versions behind the control plane version」と定めており、上流より1つ厳しいのです。上流の3を前提に計画すると、GKEでは1マイナー分の余裕を失います。

In-Place更新ではコントロールプレーンの更新開始を境に戻せる範囲が失われるのに対し、Blue/Greenクラスタ移行では旧クラスタを削除するまで戻せる範囲が続くことを、工程を並べて対比した図
戻せる範囲は工程ごとに切れる位置が違います。In-Placeではコントロールプレーンの帯だけが最初に切れ、それを延ばす手段はありません。

調査その1:削除されるAPIは、30日の窓でしか見えない

方式が決まったら、次は影響調査です。マイナーバージョンアップで最も多い事故は、削除されたAPIバージョンを使い続けていたことによるものです。

3層と静的検査で調べる

手段見えるもの見えないもの
GKEの非推奨インサイト過去30日に呼ばれた非推奨API。呼び出し元のuserAgentと回数まで30日間呼ばれなかったもの
メトリクス apiserver_requested_deprecated_apisgroup / version / resource / subresource / removed_release のラベル付きで、どのバージョンで消えるかを絞り込める呼び出し元。保持期間より前の記録
監査ログ個々の要求。非推奨API要求には "k8s.io/deprecated":"true" のアノテーションが付くログを取得していない期間
マニフェストの静的検査まだ適用していないものを含む、Git上にあるものすべて手やスクリプトから直接叩かれるAPI

この表の要点は最後の2行の対比にあります。上の3つは「実際に呼ばれたもの」しか見えず、静的検査は「これから呼ばれる予定のもの」しか見えません。どちらか一方では足りません。

そして静的検査には、他の3つにない性質があります。CIに組み込めば毎回自動で動きます。第9回で設計したパイプラインに検査を1本足すだけで、次回以降の調査コストが下がります。1度きりの調査ではなく、継続的な検査に変える。ここが更新作業を年3回こなせるかどうかの分かれ目になります。

なお、非推奨APIを呼ぶとAPIサーバは応答に Warning ヘッダを付けます。kubectl v1.19以降はこれを標準エラー出力に表示し、client-go v0.19以降はログに出します。更新のたびにログを読み返すのではなく、警告が出た時点で気づける仕組みにしてください。

30日に1度も動かないものは、検出されない

GKEの非推奨インサイトは、観測期間を observationPeriod: 2592000s、つまり30日として計算し、「Your API clients have used deprecated APIs in the last 30 days」と報告します。deprecatedClientStats には userAgentnumberOfRequestsLast30DayslastRequestTime が含まれ、どのクライアントが何回呼んだかまで分かります。これは強力な情報です。

裏返すと、30日に1度も動かなかったものは検出されません。四半期末のバッチ、年次の棚卸ジョブ、災害時にだけ実行するDRの手順、普段は停止している管理用CronJob。これらはすべて「問題なし」と表示されます。

したがって検出結果は「使っていない証拠」ではなく、「過去30日に呼ばれなかったという事実」として読んでください。そのうえで、30日より長い周期で動くものの一覧を別に作ります。一覧の母集団は次の4つで、いずれも人が思い出すのではなく機械で列挙できます。

  1. 全NamespaceのCronJobの schedule を抽出し、実行間隔が30日を超えるものを抜く。月末実行は30日以内なので対象外になり、四半期・半期・年次が残ります
  2. CI/CDから起動されるが、定常では動かないジョブ。第9回のパイプライン定義が母集団です
  3. 手順書にしか存在しない操作。第17回で扱うDR手順、年次の鍵の入れ替え、監査対応のデータ抽出
  4. 停止中またはレプリカ0のワークロード。

この一覧は、一度作れば更新のたびに使い回せます。互換性チェックリストの添付として、毎回同じものを見ます。第2回のRACIと同じ性質の資産です。

検出されると自動更新は止まる。止まったことに気づく必要がある

GKEは非推奨機能の使用を検出すると「Automatic upgrade to the upcoming minor version is paused.」という挙動をとります。次のマイナーへの自動更新が一時停止されます。

親切な仕組みですが、副作用があります。放置すると「上がらないまま期限だけが近づく」という状態になります。自動更新に任せていたつもりが止まっており、サポート終了日の直前に、非推奨APIの修正と3回分の更新を同時に迫られる。

したがって自動更新が停止している理由を、監視の対象に入れてください。更新の状態と停止理由はAPIから取得できます。何を見るかまでが本記事の範囲で、監視基盤の作り方とアラートの設計は第15回で扱います。

ベータAPIは、調査ではなく台帳で管理する

Kubernetesの非推奨ポリシーは、猶予期間を明文化しています。ベータAPIは「deprecated no more than 9 months or 3 minor releases after introduction (whichever is longer)」で非推奨になり、「no longer served 9 months or 3 minor releases after deprecation (whichever is longer)」で提供が止まります。GA APIは「must not be removed within a major version of Kubernetes」で、メジャーバージョンが変わらない限り消えません。アルファは予告なく削除されえます。

ここから出る運用ルールは、調査手順ではありません。いま使っているAPIがベータなら、遅くとも9ヶ月程度で消える可能性がある。つまり更新のたびに探すのではなく、ベータAPIを使っている箇所の一覧を常設で持つのが正しい形です。

読者に渡すべきルールは1行です。apiVersionにベータを含むものは、棚卸しの対象台帳に載せて四半期に一度見る。第10回で共通Helm Chartによる抽象化を設計していれば修正は1箇所で済みます。各チームがマニフェストを個別に持っていると、同じ修正を開発チームの数だけ依頼することになります。これが第10回で作った抽象化の、更新の場面での配当です。

調査その2:壊れるのはAPIだけではない

非推奨APIの調査を終えると安心しがちですが、実際の障害はAPI以外からも来ます。ここで作るのが互換性チェックリストです。

互換性マトリクスの行は「ツール」ではなく「壊れ方」で立てる

ツール名で表を作ると、読者は「Argo CD、対応済み」と書いて終わります。それでは確認したことになりません。行を壊れ方で立てると、検出方法と「壊れたと分かる時刻」が行ごとに違うことが見えます。

壊れ方検出方法壊れたと分かる時刻
削除されたAPIを使うマニフェストインサイト/メトリクス/監査ログ/静的検査次にそのマニフェストを適用したとき(更新直後とは限らない)
Admission Webhook(第5回)対象APIグループとバージョン、失敗時ポリシー更新中
CRDとそれを読むコントローラCRDのapiVersion、コントローラの対応表更新後、そのカスタムリソースを次に変更したとき
CSIドライバ(第7回)ドライバの対応Kubernetesバージョンノード更新中(ボリュームの着脱時)
ノードランタイムとイメージ形式ノードのログ、イメージの棚卸しノード更新中(イメージのプル失敗)
ノードのOSやカーネルに依存する常駐物提供元の対応ノードイメージ表ノード更新中、または更新後の起動時
GitOpsコントローラと同期(第6回・第9回)コントローラ自身の対応バージョン、同期対象に含まれる削除済みAPI更新直後

列には所有者を必ず追加してください。第2回のRACIで決めた責任分界が、そのまま各行の担当になります。所有者が空欄の行は、更新当日に誰も見ません。

最終行のGitOpsは、他の行と性質が違うので個別に説明します。更新中もコントローラは動き続け、Gitに置いてあるマニフェストを繰り返し適用しようとします。削除されたAPIバージョンのマニフェストがGitに残っていると、更新が終わった瞬間から同期が失敗し続けます。しかも第6回で確認したとおりGitOpsコントローラはクラスタで最も強い権限を持つ主体であるため、ここが止まると「マニフェストを直したのに反映されない」という二次的な障害になります。対処は、更新前にGit側を直しておくことです。クラスタ側だけを直しても、次の同期で戻されます。

実例:containerd 2.0 でイメージのプルが失敗する

API以外が壊れる実例を1つ挙げます。Linuxノードは GKE 1.33、Windows Serverノードは GKE 1.35 から containerd 2.0 を使います。containerd 2.0 はDocker Schema 1 形式のイメージCRI v1alpha2 APIのサポートを削除しました。

該当するイメージを参照していると、プルが失敗します。エラーは「Pulling Schema 1 images have been deprecated and disabled by default since containerd v2.0」です。

この事例の性質を押さえてください。この障害は、マニフェストのapiVersionをいくら調べても検出できません。検出手段は別にあります。Cloud Loggingでは jsonPayload.SYSLOG_IDENTIFIER="containerd" に対して「conversion from schema 1 images is deprecated」という文字列で絞り込みます。ノード上で直接確認するなら ctr --namespace k8s.io images list で該当ラベルを持つイメージを探します。

そしてこの事例の形を見てください。古い外部イメージを1つ参照しているだけで、ノードの更新が進まなくなります。自分たちがビルドしているイメージではなく、数年前に導入したサイドカーやツールのイメージが原因になりがちです。互換性マトリクスに「ノードランタイムとイメージ形式」の行を立てる根拠は、ここにあります。

GKE側の挙動も押さえてください。Linuxでは非推奨機能の使用を検出したクラスタについてのみ1.33への自動更新が一時停止されます。一方Windows Serverノードを持つクラスタは、使用の有無にかかわらず1.35への自動更新が一時停止されます。Windowsノードを含む構成では、更新は自分で判断して実行することになります。

マネージドを選んだ数だけ、この表の行は減る

第6回のADRで、CNI、ポリシーエンジン、GitOps、監視、シークレット、サービスメッシュの6カテゴリについてツールを選びました。そこでマネージドを選んだものは、互換性マトリクスから行ごと消えます。提供元がクラスタのバージョンに追随させるためです。

逆に言えば、互換性マトリクスの行数は、第6回でセルフホストを選んだ数だけ増えます。そして更新は年3回来ます。行が3つ増えれば、年9回分の確認作業が増える。

第6回では「マネージドを選ぶ側の代償」として、変更管理を握られること、機能追加のペースを決められないこと、クラウドから出にくくなることの3点を挙げました。ここで対になる配当が出ます。マネージドを選んだ分だけ、年3回の確認作業が減る。ADRを書いた時点では見えなかったこの数字が、1年目の終わりに実感として現れます。

Admission Webhook は、更新中に効いてくる

表の中で1行だけ、壊れる時刻が「更新中」になっているものがあります。Admission Webhookです。

第5回で、ポリシーエンジンによる検証と変更の仕組みを設計しました。Webhookはクラスタへの書き込み要求の経路に割り込みます。失敗時ポリシーの設定によっては、Webhookが応答しない間、対象となる書き込みがすべて拒否されます。更新中はコンポーネントが順に入れ替わるため、Webhookを提供するPod自体が退避と再作成の対象になります。

更新前に確認する項目は3つです。Webhookが対象としているAPIグループとバージョン、失敗時ポリシーの設定、そしてWebhookを提供するPodが更新中も動き続ける配置になっているか。ポリシーそのものの設計は第5回の管轄なので、本記事では確認項目として扱います。

ノードの更新は、計画された大規模なノード障害

第12回で「更新は計画された大規模なノード障害である」と定義しました。ここではその中身を、設定値の水準まで下ろします。

kubeletのマイナー更新は、必ずドレインを伴う

Kubernetes公式は「In-place minor version kubelet upgrades are not supported.」と明記しています。その直前には「Before performing a minor version kubelet upgrade, drain pods from that node」とあります。ノードのマイナーバージョンを上げるとは、そのノードを空にして作り直すことです。GKEのドキュメントも「nodes are drained and re-created to match the current control plane version」と書いています。

順序も決まっています。第12回で確認したとおり、更新は kube-apiserver から始まり、kube-controller-managerkube-scheduler、次に kubelet、最後に kube-proxy と進みます。常に先へ進むのはコントロールプレーンで、ノードが後を追うという形は変えられません。GKEでもコントロールプレーンの更新が完了してからノードプールの更新が始まるまでには「typically a few days」の間隔が入ります。つまりコントロールプレーンだけが新しい状態は、事故ではなく数日続く正規の構成です。

そして「無停止でアップグレードします」という説明は、正確ではありません。止まらないのはリージョナルクラスタのコントロールプレーンであり、ノード上のPodは全数が一度退避されます。ゾーンクラスタの場合はコントロールプレーンも止まり、公式は「the control plane is unavailable while it is being upgraded. For the most part, workloads run normally but cannot be modified during the upgrade」と書いています。動いているPodは動き続けますが、その間デプロイも設定変更もできません。

この工程で最も危ないのは、状態を持つPodです。第7回で設計したStatefulSetとPersistentVolumeは、ドレインのたびに退避と再スケジュール、そしてボリュームの再アタッチを経験します。互換性マトリクスのCSIドライバの行と、この節は同じ事象を指しています。ノード障害時の異常な挙動については第12回で扱ったので、ここでは繰り返しません。計画された更新では、正常な着脱が全ノード分繰り返されるという点だけ押さえてください。

サージとblue-greenの違いは「戻せるか」

Standardクラスタのノードプールには、2つの更新戦略があります。

サージアップグレードblue-greenアップグレード
既定値maxSurge=1 / maxUnavailable=0soak時間 3,600秒(1時間)
同時に処理される数maxSurge と maxUnavailable の和(Standardは最大100)バッチ単位(台数または割合で指定)
使うリソースサージ分だけ一時的に増える更新中は2倍
PDBと猶予最大1時間まで尊重。超過分は強制退避ドレイン中に退避できないPodは削除フェーズへ回る
戻せるか成功後は戻せない旧ノードプールを削除するまで戻せる

サージの動作は、公式の記述が正確です。「Drain the existing node, respecting PodDisruptionBudget and GracefulTerminationPeriod settings for up to one hour. After one hour, any remaining Pods are forcefully evicted so that the upgrade can proceed.」1時間は待ちますが、その後は強制されます。

blue-greenは5つのフェーズで進みます。greenプールの作成、blueプールのcordon、blueプールのドレイン、soak、blueプールの削除。soakのあいだ旧ノードが残っているため、この期間だけは戻せます。

ただし見落とされやすい点があります。公式は「For Pods that have PodDisruptionBudget violations or long terminationGracePeriodSeconds during the draining, they will be deleted in the Delete blue pool phase when the node is deleted.」と書いています。blue-greenでも、退避できなかったPodは最後のフェーズで消えます。しかも削除フェーズでは「The deletion caps a Pod’s terminationGracePeriodSeconds to no more than 60 minutes.」と、猶予が60分で頭打ちになります。

第11回で「PDBに期待してよいのは遅らせることだけ」と確認しました。更新の場面でも同じ形です。2つの戦略の違いは「Podが守られるかどうか」ではありません。「soakのあいだ旧ノードを残して戻せるかどうか」です。

なお自動スケーリングを有効にしたノードプールのblue-greenでは数値が変わり、「GKE respects PodDisruptionBudget settings for up to 1 hour, and terminationGracePeriodSeconds settings for up to 24 hours.」となります。自動スケーリングの有無で猶予の上限が変わる点は、設計値を決める前に確認してください。

公式が示す選択の目安も、そのまま使えます。コストを重視し、ワークロードが60分未満で停止できるならサージ。高可用な本番ワークロードで、一時的なコスト増を許容できるならblue-green。

所要時間と必要なリソースを、押す前に計算する

サージで同時に処理されるのは maxSurgemaxUnavailable の和です。ここから所要時間が出ます。

所要時間 = 切り上げ(ノード数 ÷(maxSurge + maxUnavailable))×(1ノードあたりのドレイン時間 + 新ノードの起動時間 + イメージのプル時間)

1ノードあたりの時間は自分の環境で実測する値です。例として置いてみます。30ノードを既定値(1と0)で更新し、1ノードあたり5分なら150分で、深夜4時間の維持窓に収まります。ところが同じ30ノードでも、PDBで退避が滞るPodが各ノードに1つでも乗っていると1ノードあたり60分に張り付き、合計30時間になります。同じクラスタで12倍の差が出ます。

この計算をせずに「深夜の窓で終わる」と見積もると、窓の中で終わらず、翌週まで中途半端なバージョン混在が続きます。第12回で確認したとおり、コントロールプレーンだけが新しい状態は毎回必ず発生する正規の構成です。ここではノードプールの内部でも混在が起きます。

時間と同じくらい問題になるのが、押した瞬間に必要になるリソースです。サージは追加ノードを作ってから古いノードを消し、blue-greenは公式の表現で「the node pool uses double the number of resources during the upgrade」。つまり更新は、第4回で設計したノード自動拡張の上限とCPUのクォータ、第3回で設計したPod IPの範囲を、平常時より高い水準で一時的に消費します。

とくに注意が要るのがSpotノードプールの更新です。第4回でSpot比率を高く設定したクラスタほどサージノードをSpotで確保できるとは限らないという制約に当たります。更新の進み方が在庫に左右されます。

計画書には「見積もり所要時間」「維持窓の連続時間」「更新中の最大ノード数」の3つを並べて書いてください。収まらないときの選択肢は3つあり、どれにも代償があります。

  1. maxSurgeを上げる。代償はIPとクォータの追加消費
  2. 窓を分割して複数週に分ける。代償はバージョン混在期間が延びること
  3. blue-greenで一度に作る。代償は更新中の費用が2倍になること

無料の選択肢はありません。どの代償を払うかを、更新日の前に決めておくのが計画です。

Autopilotでは猶予に上限がある

Autopilotクラスタでは、ノードの更新方法を選べません。「GKE uses surge upgrades for Autopilot nodes, upgrading up to 20 nodes in a group at the same time.」サージのみで、同時に最大20ノードです。自動更新を無効化することもできず、制御できるのは維持窓と維持除外による時期の指定だけです。

そして猶予に上限があります。PodDisruptionBudgetは1時間尊重され、terminationGracePeriodSeconds は大半のPodで10分(600秒)が上限、Spot Podでは25秒が上限です。

第11回で、猶予の上限は3方向から来ると整理しました。自発的な中断は設計どおり(GKEのノード更新では最大1時間)、Spot VMのプリエンプションではアプリケーションPodに15秒、node-pressure evictionでは0秒。ここに4方向目が加わります。Autopilotでの更新時の600秒です。

混同しやすいので明示します。Autopilotの25秒は更新時のドレインにおける猶予の上限であり、第1回で確認したSpot VMのプリエンプション猶予15秒とは別の数値です前者は計画された更新、後者は予告のない回収。同じ「Spot」という語でも、効いてくる場面が違います。

更新を中止する条件を、先に決める

第12回で「受け入れ基準とは別に、中止条件と復旧手順を先に書く」と決めました。更新にも同じものが要ります。実行中は、更新が起こした異常と、たまたま同時に起きた別の障害を区別できないためです。

ただし更新の中止には、第13回のカットオーバーとは違う非対称があります。中止しても、コントロールプレーンは元のマイナーバージョンに戻りません。中止という操作が意味を持つのは、ノードの更新に対してだけです。

したがって中止条件は、次の形で書きます。

  • 何を見るか。第12回で定義した定常状態の指標をそのまま使います。更新のために新しい指標を作る必要はありません
  • どの値を超えたら中止するか。数値で書きます。「様子がおかしければ」は判断できません
  • 中止したときに何が残るか。コントロールプレーンは新バージョンのまま、ノードは新旧が混在します。この状態はスキューの範囲内であり、正規の構成です
  • 誰が中止を宣言できるか。第2回のRACIで決めた責任者。恒常的なインシデント対応体制は第16回で扱います

3番目が実務上いちばん効きます。中止した状態は異常ではなく、待機してよい状態です。これを事前に共有しておかないと、中止した直後に「早く元に戻せ」という圧力がかかり、戻せないものを戻そうとして事態が悪化します。

証明書と資格情報は、別のカレンダーで動く

ライフサイクル計画には証明書の更新も含まれます。ただしこれはバージョンアップとは別の周期で動くため、同じカレンダーに載せると混乱します。

GKEでローテーションされる4つ

GKEの資格情報ローテーションが回すのは、次の4つです。APIサーバが使うIPアドレス、クラスタのルートCA、ServiceAccountの署名鍵、集約レイヤのCA。

操作は2段階です。開始すると、コントロールプレーンは元のIPアドレスに加えて新しいIPアドレスでも応答するようになり、新しい資格情報が発行されます。完了すると、元のIPアドレスでの応答が止まり、既存の静的なServiceAccount資格情報を含む古い資格情報が失効します。

この2段階のあいだに、やるべき作業が2つあります。クラスタの外にあるAPIクライアント(開発者端末のkubectl、CIのサービスアカウント、外部の監視ツール)を、新しいIPアドレスと資格情報へ切り替えること。そしてノードは再作成されるため、ノード更新と同じ影響を受けることを計画に織り込むことです。開始と完了のあいだに作業期間を置ける設計になっているのは、この切り替えのためです。

期限は5年か30年。まず自分のクラスタがどちらかを確認する

CAの有効期間は、クラスタを作成した時期で変わります。2021年10月頃より前に作成したクラスタは5年、それ以降に作成したクラスタは30年です。

期限に関する挙動は厳格です。「Your cluster enters the DEGRADED state seven days before the current credentials expire.」失効の7日前にクラスタはDEGRADED状態になります。そして失効してしまうと復旧できません。最後の手段として、GKEはCAの失効日から30日以内に自動でローテーションを開始します。

ここから出る結論は、多くの読者の予想と違うはずです。近年作成したクラスタでは、証明書の更新は年次作業ではありません。30年先の期限に対して毎年何かをする必要はない。この節でやるべきことは、更新作業そのものではなく「自分のクラスタがどちらの期限を持っているかを確認し、ランブックに書いておくこと」です。5年側に該当する古いクラスタが1つでも残っていれば、それは他のどの作業よりも優先度が高い項目になります。

自前で構築していたら、これは毎年の作業だった

比較のために、kubeadmで自前構築した場合を見ます。クライアント証明書の既定の有効期間は1年、CAは10年です。そしてコントロールプレーンの証明書は、アップグレードでは自動更新されません。kubeadm certs renew を明示的に実行する必要があります。確認は kubeadm certs check-expiration です。自動でローテーションされるのはkubeletのクライアント証明書だけです。

つまり自前構築では、年1回、全コントロールプレーンノードで証明書を更新する作業が恒久的に発生します。忘れると、ある朝クラスタが応答しなくなる。

第3回と第6回で、マネージドを選ぶという判断をしました。その判断の配当が、ここで初めて金額ではなく作業量として現れます。ADRに書いた「変更管理を握られる」という代償と、この「年1回の作業が消える」という配当は、同じ選択の裏表です。選定の妥当性は、選定した時点ではなく、5年後の運用カレンダーで確かめられます。

実務テンプレート:アップグレードランブックと互換性チェックリスト

ここまでの判断を、2つの成果物に落とします。どちらも1回きりの文書ではなく、年3回、同じものを使い回します。

アップグレードランブック(記入式・7項目)

  1. 日付。標準サポート終了日/拡張サポート終了日/次バージョンの提供開始日/実施予定日の4つ。これに現行バージョン、目標バージョン、告知日(実施の60日前)を添えます
  2. 方式と選択理由。In-Place/Blue/Greenクラスタ/混成。3つの選択軸(戻せる時間/二重の費用/状態の移し替え)のどれで決めたかを1行で書きます
  3. 順序。Dev、Stg、Prodの各実施日と、ステージ間の日数。その日数の根拠(その環境で動く定期処理の最長周期)を併記します
  4. ノード戦略。サージかblue-greenか/maxSurgemaxUnavailable/soak時間/見積もり所要時間維持窓の連続時間更新中の最大ノード数
  5. 中止条件。見る指標(第12回から)/中止する数値/中止時に残る状態/中止を宣言できる人(第2回から)
  6. 更新中に回す確認と完了条件。第12回のシナリオから選んだもの。完了条件は設定値ではなく観測値で書きます
  7. 後片付け。維持除外の解除/旧ノードプールと旧クラスタの削除日/暫定で緩めたPDBの復旧/固定したバージョン指定の解除

6番目を補足します。「全ノードが新バージョンになった」は設定値です。観測値とは、更新前に取得した定常状態の指標が、更新後も範囲内に収まっていることを指します。その指標は第12回で定義済みなので、新しく作る必要はありません。

7番目も落とせません。第13回で「移行の最後の作業は、旧環境の削除ではなく旧環境に紐づいた設定の削除」と書きました。更新でも同じです。維持除外を解除し忘れると次の更新が止まり、暫定で緩めたPDBを戻し忘れると次の障害時に効きません。後片付けを工程に含めていない計画は、次回の計画を壊します。

互換性チェックリスト(毎回使い回す・7行)

先に示した壊れ方の7行に、次の列を持たせます。

  • 検出方法。行ごとに違います。コマンドかログクエリを書いておきます
  • 所有者。第2回のRACIから引きます
  • 確認日と結果。更新ごとに上書きします
  • 対応期限。修正が必要になったとき、開発チームへ依頼する期限です。マニフェストやアプリケーションコードの修正そのものは開発チームの責任であり、運用チームの作業ではありません

そして添付として「30日より長い周期で動くものの一覧」を必ず付けます。非推奨インサイトの30日窓では見えない部分を埋めるのは、この一覧だけです。

この2つの成果物を、更新のたびに複製して埋めます。2回目以降に新しく埋める欄は、日付とバージョン、そして前回から構成が変わった箇所だけになります。年3回という頻度に耐えられるかどうかは、手順の巧拙ではなく、この使い回しができる形になっているかで決まります。

次回予告

本記事では「更新中に何を見るか」「完了条件は観測値で書く」「中止する数値を先に決める」と繰り返しました。

しかし、その観測値がどこから来るのかは、まだ設計していません。第12回で定常状態を測ると決め、本記事でそれを判定に使うと決めましたが、ログをどう出し、メトリクスをどう集め、どの値でアラートを出すかは未着手です。

次回の第15回「オブザーバビリティ・監視設計書」では、ログのJSON化標準、stdoutへの送信の義務化、Trace IDの伝播ルール、メトリクス収集アーキテクチャ、そしてアラート閾値の定義を扱います。本記事で「監視項目に入れてください」と書いた自動更新の停止理由も、そこで具体的な検知ルールになります。

前の記事
次の記事