↓メインコンテンツへスキップ
  1. Blogs/

GKEノードプールのSURGEアップグレードをBlue/Greenと同じ条件で測って比べてみた

3 分
Terraform GoogleCloud GKE
0222-nnn
著者
0222-nnn
猫が好き
目次
Terraform-GoogleCloud - この記事は連載の一部です
パート 35: この記事

概要
#

ノードプールのアップグレード戦略はSURGEとBLUE_GREENの2つです。どちらを選ぶべきか決めたいのに、判断材料がありません。「BLUE_GREENは安全」「SURGEは速い」といった説明はあっても、どれくらい違うのかが書かれていないので、比較ができません。

前回はBlue/Greenを測りました。今回は同じ条件でSURGEを測って、並べます。ノード数もワークロードも計測方法も前回と同一にして、違いはupgrade_settingsだけにしてあります。

あわせてmax_surge / max_unavailableの2つの組み合わせも比べます。この2つは「速さと引き換えに何を失うか」を決める設定です。

対象読者は、GKEのノードプールを運用でアップグレードする立場の人です。Kubernetesの入門は扱いません。

Autopilotクラスタ、コントロールプレーンのアップグレード戦略、メンテナンスウィンドウは扱いません。

検証すること
#

  • SURGEでアップグレード中にどれだけ止まるか
  • max_surge = 0にすると何が変わるか
  • 一時的にノードがどれだけ増えるか
  • Blue/Greenとどちらを選ぶべきか

前提環境
#

  • Google Cloud CLI、Terraform
  • Billingが有効な検証用Google Cloud Project
  • GKE、Cloud KMS、Compute Engineを操作できるGoogleアカウント
  • 検証時のバージョン:Terraform 1.14.5、google 7.46.0
  • GKE 1.35.7-gke.1027000 → 1.35.7-gke.1150000 → 1.36.2-gke.2064000(REGULARチャンネル)

使用するTerraformコード
#

SURGEとBlue/Greenの進み方の違い
#

flowchart TB
    subgraph SURGE["SURGE"]
        S1["余剰ノードを max_surge 台足す"]
        S2["1バッチずつ入れ替える"]
        S3["古いノードを削る"]
        S1 --> S2 --> S3
    end
    subgraph BG["Blue/Green"]
        B1["新しいノード群を全部作る"]
        B2["batch_percentage ずつ移す"]
        B3["node_pool_soak_duration 待つ"]
        B4["古い群をまとめて消す"]
        B1 --> B2 --> B3 --> B4
    end

SURGE は少しずつ入れ替え、Blue/Green は新旧を並べてから移します。この違いが所要時間と切り戻しやすさに出ます。

比較を成立させる条件
#

測る条件が違えば比較になりません。 前回と揃えたものを先に示します。

項目 内容
ノード数 2(e2-medium、Spot)
ワークロード nginx 2レプリカ + podAntiAffinity + PDB(minAvailable: 1)+ preStop
計測 踏み台から内部LBへ毎秒1回リクエスト
クラスタ プライベートクラスタ、ゾーナル

違うのはupgrade_settingsだけです。

upgrade_settings {
  strategy        = "SURGE"
  max_surge       = var.max_surge        # 通常サイズを超えて足せるノード数
  max_unavailable = var.max_unavailable  # 同時に落としてよいノード数
}

blue_green_settingsはBLUE_GREEN専用で、SURGEとは併用できません。どちらか一方が0でも動きますが、両方0にはできません。進められなくなるためです。

アップグレード先を2段用意する
#

max_surgeの設定を変えて2回測るので、コントロールプレーンを2段先で作ります。

バージョン
コントロールプレーン 1.36.2-gke.2064000
ノードプール(初期) 1.35.7-gke.1027000
1回目の上げ先 1.35.7-gke.1150000
2回目の上げ先 1.36.2-gke.2064000

これで途中でコントロールプレーンを上げ直さずに済みます。前回はこれをやっておらず、2回目のために9分かけてコントロールプレーンを上げ直しました。

実行
#

terraform apply
Apply complete! Resources: 27 added, 0 changed, 0 destroyed.

設定がGKEに届いていることを確認します。

gcloud container node-pools describe tf-adv-gke-surge-np --cluster=tf-adv-gke-surge \
  --zone=asia-northeast1-a --format="yaml(upgradeSettings)"
upgradeSettings:
  maxSurge: 1
  strategy: SURGE

maxUnavailable: 0は既定値なので出力に現れません。

1回目: max_surge = 1 / max_unavailable = 0
#

gcloud container clusters upgrade tf-adv-gke-surge --node-pool=tf-adv-gke-surge-np \
  --cluster-version=1.35.7-gke.1150000 --zone=asia-northeast1-a --async

ノード数の推移にmax_surge = 1の動きがそのまま出ます。

=== 21:16:02 nodes=2 ===
=== 21:16:18 nodes=3 ===   ← 先に1台足す
   ...
=== 21:18:13 nodes=2 ===   ← 旧ノードを1台落とす
=== 21:18:45 nodes=3 ===   ← また1台足す
   ...
=== 21:21:12 nodes=2 ===   ← 完了
MAX_NODES=3

先に足してから落とすので、処理中も稼働ノードが2台を下回りません。

tr ' ' '\n' < /tmp/runA-probe.txt | grep -v '^$' | sort | uniq -c
    359 200
gcloud container operations list --zone=asia-northeast1-a \
  --filter="operationType=UPGRADE_NODES AND targetLink~tf-adv-gke-surge" \
  --format="table(status,startTime,endTime)"
STATUS  START_TIME                      END_TIME
DONE    2026-09-04T21:15:06.494755341Z  2026-09-04T21:21:02.860952638Z

失敗0回、5分56秒。

設定変更はノードを作り直さない
#

max_surgeを0にして、2回目を測ります。

terraform apply
  # google_container_node_pool.primary will be updated in-place
          ~ max_surge       = 1 -> 0
          ~ max_unavailable = 0 -> 1

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

upgrade_settingsはin-placeで変わります。次のアップグレードから新しい設定が使われます。

2回目: max_surge = 0 / max_unavailable = 1
#

追加ノードなしで同じことをします。

=== 21:22:41 nodes=2 ===
   ...
=== 21:27:01 nodes=1 ===   ← 増えないまま、1台に減る
=== 21:27:33 nodes=2 ===
MAX_NODES=2
     19 000
    236 200

19秒の完全断。 生の記録では000が連続して並びます。

200 200 ... 200 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 000 200 200 ...

所要時間は3分20秒(21:22:35 → 21:25:55)で、1回目より速くなりました。

なぜ止まったのか
#

原因はPod側とLB側の両方にありました。

kubectl get events --sort-by=.lastTimestamp | grep FailedScheduling
Warning  FailedScheduling  pod/nginx-cmek-668999dbd8-f4kls
  0/2 nodes are available: 1 node(s) didn't match pod anti-affinity rules,
  1 node(s) were unschedulable.

追加ノードがないので、1台をcordonすると退避したPodの行き先がありません。もう1台には既にnginxがいて、podAntiAffinityが同居を拒みます。レプリカが1個に減ったまま次のノードの番が来て、受け先がゼロになりました。

内部LB側でも起きていました。

Warning  EnterDegradedMode  service/nginx-cmek-ilb
  Entering degraded mode for NEG default/nginx-cmek-ilb-... due to sync err:
  endpoint information for attach operation is incorrect
Warning  AttachFailed       service/nginx-cmek-ilb
  Failed to Attach 1 network endpoint(s): googleapi: Error 400:
  Invalid value for field 'resource.instance': 'gke-tf-adv-gke-surge-...-ywfi'.
  Couldn't find instance with a specified name ...

ノードが作り直される間、NEGが存在しないインスタンスを参照して縮退しました。

max_surge = 0は「追加ノードが要らない」設定ではありません。「余力を持たずに入れ替える」設定です。 ワークロードがノード数に対して余裕を持っていないと、そのまま断になります。

3方式を並べる
#

同じクラスタ構成・同じワークロード・同じ計測方法で測った結果です。

観点 Blue/Green SURGE max_surge=1 SURGE max_surge=0
断 0回 0回 19秒の完全断
所要時間 17分3秒 5分56秒 3分20秒
最大ノード数(2ノード時) 4(倍) 3(+1) 2(増えない)
追加ノードのコスト 倍のノード × 17分 +1ノード × 6分 なし
Pod IPの一時消費 倍 +1ノード分 なし
vCPUクォータの一時増 倍 +1ノード分 なし
ロールバック soak期間中は可能 不可 不可
切り戻しの猶予 node_pool_soak_duration なし なし
途中で気付く機会 バッチ間に待機あり 逐次 逐次
手順の複雑さ cancel → rollback が要る 単純 単純
向く場面 戻せることが最優先 既定として妥当 検証環境・コスト最優先

この表から読めること
#

断と時間だけ見るとSURGEが有利です。 Blue/Greenは3倍近く遅く、ノードも倍要ります。それでいて断はmax_surge = 1と同じ0回でした。

Blue/Greenの価値は「上げたあとに問題が分かったとき」にあります。 SURGEは置き換えたノードが残らないので、戻すにはもう一度アップグレードするしかありません。ダウングレードはできません。Blue/Greenはnode_pool_soak_durationの間なら旧ノードが残っていて、前回の実測では3分10秒で戻せました。

この差は所要時間の表に出てきません。「アップグレードが速いか」ではなく「間違えたときに戻せるか」で選ぶ、という整理になります。

max_surge = 0は安易に選べません。 速くて無料ですが、ワークロードにノードの余裕がないと断が出ます。今回は2ノードに2レプリカ+ハードなアンチアフィニティという、余裕がまったくない構成でした。

裏を返すと、断が出るかどうかはノード数とワークロードの配置に余裕があるかで決まる部分が大きいとも言えます。戦略の選択だけで無停止になるわけではありません。

アップグレード後の terraform plan で確認する
#

terraform plan
No changes. Your infrastructure matches the configuration.

lifecycle.ignore_changes = [version]が効いています。前回はコントロールプレーンもgcloudで上げたため、読み取り専用のmaster_versionに差分が出ました。今回は最初から上げ先のバージョンで作ってあるので差分が出ません。

後片付け
#

内部LBはKubernetes側で作ったものなので、先に消します。残しておくと転送ルールがVPCに残ります。

kubectl delete -f k8s/03-nginx-ilb.yaml
terraform destroy
Destroy complete! Resources: 27 destroyed.

Cloud KMSのキーリングと鍵は削除できず、鍵バージョンは破棄がスケジュールされます。再検証するときは別の名前を指定します。

まとめ
#

  • SURGEは速い。 max_surge = 1で5分56秒。Blue/Greenの17分3秒に対して3分の1以下
  • max_surge = 1でも断は0回だった。 ノードを先に足してから落とすので、稼働ノードが初期値を下回らない
  • max_surge = 0は19秒止まった。 追加ノードがないと退避したPodの行き先がなく、レプリカが減った状態で次のノードの番が来る
  • 止まる原因はPod側だけではない。 内部LBのNEGも、消えたインスタンスを参照して縮退していた
  • 選択の軸は速さではなく「戻せるか」。 SURGEは置き換えたノードが残らない。Blue/Greenだけがsoak期間中に戻せる
  • upgrade_settingsの変更はノードを作り直さず、in-placeで反映される

参考資料
#

Terraform-GoogleCloud - この記事は連載の一部です
パート 35: この記事

関連記事

GKEノードプールをBlue/Greenでアップグレードして、soak期間中にロールバックしてみた
5 分
Terraform GoogleCloud GKE CloudKMS
GKEノードのブートディスクをCMEKで暗号化して鍵をローテーションしてみた
6 分
Terraform GoogleCloud GKE CloudKMS CMEK
TerraformでGKEのPodにSecret Managerの値をファイルとしてマウントしてみた
3 分
Terraform GoogleCloud GKE SecretManager
TerraformでGKEのスタンドアロンNEGを消したらLBのバックエンドが戻らなかった話
3 分
Terraform GoogleCloud GKE LoadBalancing
Peering越しのPod→VM通信が落ちる原因を、ルートだと思い込んでいた話
4 分
Terraform GoogleCloud GKE VPC
TerraformでGKE Dataplane V2を有効化し、NetworkPolicyでPod間通信を遮断してみた
2 分
Terraform GoogleCloud GKE NetworkPolicy