概要 #
ノードプールのアップグレードには3つの戦略があります。既定のSURGEは既存ノードを順番に置き換えていく方式。BLUE_GREENは新しいノード群(green)を丸ごと作ってからワークロードを移し、しばらく旧ノード群(blue)を残します。
3つ目のAutoscaled blue-greenはPreviewで、greenを0台から必要に応じて増やします。ノードが倍にならない代わりに、キャンセル後のロールバックには対応していません。この記事で測るのは標準のBLUE_GREENだけです。
この「しばらく残す」時間がnode_pool_soak_durationで、ロールバックできる猶予にあたります。満了するとblueは削除され、後戻りできません。
ところが、その窓の中で実際にどうロールバックするのかは、あまり書かれていません。試したところgcloud container node-pools rollbackを打っても拒否されました。正しい手順は別にあります。
前回はkubectl drainで手動移行し、断をゼロにするまで構成を作り込みました。今回は同じワークロードをGKEに移させて、同じものさしで測ります。
対象読者は、GKEのクラスタをTerraformで作ったことがある人です。ノードプールのアップグレードを運用でやる立場を想定しています。Kubernetesの入門は扱いません。
SURGE戦略との実測比較は次回に回します。Autopilotクラスタとコントロールプレーンのアップグレード戦略は扱いません。
検証すること #
- アップグレード中にサービスがどれだけ止まるか
- soak期間中に本当にロールバックできるか。どうやるか
- いまアップグレードのどの段階にいるかを、どう知るか
- ブートディスクのCMEK鍵バージョンはどうなるか
- アップグレード後の
terraform planに差分が出ないか
前提環境 #
- 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(REGULARチャンネル)。この記事でノードが到達したのはここまでです。 後半に出る 1.36.2-gke.2064000 はコントロールプレーンの
master_versionで、2回目の移行先は記録していません
Blue/Greenアップグレード中はノードが一時的に倍になります。PodのセカンダリレンジとvCPUの割り当てに余裕が要ります。
使用するTerraformコード #
今回の構成 #
flowchart TB
subgraph GCP["Google Cloud"]
subgraph VPC["VPC"]
subgraph NP["ノードプール(1つ)"]
Blue["blue
旧K8sバージョン"]
Green["green
新K8sバージョン"]
end
ILB["内部LB"]
Bastion["踏み台"]
end
end
Bastion -->|"毎秒1回 HTTP"| ILB
ILB --> Blue
ILB --> Green
ノードプールは1つのままです。blueとgreenはその中で入れ替わります。前回のようにプールを2つ用意する必要はありません。
Blue/Greenの設定 #
upgrade_settings {
strategy = "BLUE_GREEN"
blue_green_settings {
node_pool_soak_duration = "600s" # ロールバックできる猶予
standard_rollout_policy {
batch_percentage = 0.5 # 0〜1の割合。パーセントではない
batch_soak_duration = "60s"
}
}
}
batch_percentageは割合です。50と書くとエラーになります。
max_surge / max_unavailableはSURGE用で、BLUE_GREENとは併用できません。
アップグレード先を用意する #
ノードプールはコントロールプレーンより新しくできません。そのため、コントロールプレーンを先に新しくして作ります。
min_master_version = var.master_version # 1.35.7-gke.1150000
version = var.node_pool_version # 1.35.7-gke.1027000
lifecycle {
ignore_changes = [version]
}
アップグレードはgcloudで打つので、ignore_changesがないと次のapplyでバージョンが巻き戻ります。
バージョンはリリースチャンネルから外れていきます。この記事の開始バージョン(1.35.7-gke.1027000)は、すでに提供が終わっている可能性が高いです。 applyの前に確認してください。
gcloud container get-server-config --zone=asia-northeast1-a --format="json(channels)"
測り方を先に決める #
Blue/Greenはblueノードを全て入れ替えます。 クラスタの中にプローブ用のPodを置くと、計測対象のワークロードと一緒に退避されてしまいます。
そこで内部LBを立て、踏み台から毎秒1回リクエストします。アップグレード全体を通して観測でき、外から見た可用性という点でも実態に近くなります。
apiVersion: v1
kind: Service
metadata:
name: nginx-cmek-ilb
annotations:
networking.gke.io/load-balancer-type: "Internal"
spec:
type: LoadBalancer
selector:
app: nginx-cmek
ports:
- port: 80
targetPort: 80
何を測るかの前に、どこから測るかを決める必要があります。
実行 #
terraform apply
Apply complete! Resources: 27 added, 0 changed, 0 destroyed.
設定がGKEに届いていることを確認します。
gcloud container node-pools describe tf-adv-gke-bg-np --cluster=tf-adv-gke-bg \
--zone=asia-northeast1-a --format="yaml(upgradeSettings)"
upgradeSettings:
blueGreenSettings:
nodePoolSoakDuration: 600s
standardRolloutPolicy:
batchPercentage: 0.5
batchSoakDuration: 60s
strategy: BLUE_GREEN
Blue/Greenの共存を見る #
アップグレードを開始します。
gcloud container clusters upgrade tf-adv-gke-bg --node-pool=tf-adv-gke-bg-np \
--cluster-version=1.35.7-gke.1150000 --zone=asia-northeast1-a --async
2分ほどでgreenが立ち上がり、同じノードプールの中に新旧が並びます。
NAME VERSION SCHED
gke-tf-adv-gke-bg-tf-adv-gke-bg-np-41052e12-0zr5 v1.35.7-gke.1150000 <none>
gke-tf-adv-gke-bg-tf-adv-gke-bg-np-eabbc370-m475 v1.35.7-gke.1027000 <none>
gke-tf-adv-gke-bg-tf-adv-gke-bg-np-eabbc370-vhjl v1.35.7-gke.1027000 <none>
名前の中ほどにあるハッシュ(41052e12 / eabbc370)がインスタンステンプレートの世代です。これがblue/greenの見分けになります。
その後blueがcordonされ、Podが移り、blueが消えます。
=== 11:57:42 ===
gke-...-41052e12-0zr5 v1.35.7-gke.1150000 <none>
gke-...-41052e12-z46k v1.35.7-gke.1150000 <none>
gke-...-eabbc370-m475 v1.35.7-gke.1027000 true ← cordon済み
gke-...-eabbc370-vhjl v1.35.7-gke.1027000 true
=== 11:58:44 ===
gke-...-41052e12-0zr5 v1.35.7-gke.1150000 <none>
gke-...-41052e12-z46k v1.35.7-gke.1150000 <none>
進行段階は UPGRADE_STAGE で読む #
kubectl get nodesだけでは、いまblueをdrainしているのか、soak期間に入ったのかが分かりません。operationのメトリクスに出ます。
ノードプール側にもupdateInfo.blueGreenInfo.phaseという公開フィールドがあります。今回はこちらを使っていません。
実行中の operation に rollback を単独で打つと、試した2段階では拒否されました。
sequenceDiagram
participant OP as 運用者
participant GKE as GKE
participant B as blue(旧)
participant G as green(新)
OP->>GKE: clusters upgrade --async
GKE->>G: green を作る(約2分)
Note over B,G: 同じノードプールに新旧が並ぶ
GKE->>B: CORDONING_BLUE_POOL
GKE->>B: DRAINING_BLUE_POOL
B->>G: Pod が移る
GKE->>GKE: NODE_POOL_SOAKING
alt そのまま完了させる
GKE--xB: soak 満了後に blue を削除
else 戻す
OP--xGKE: rollback 単独 → 400 incompatible operation
OP->>GKE: operations cancel(先にこれ)
OP->>GKE: node-pools rollback(その後始末)
GKE->>B: blue を uncordon
GKE--xG: green を drain して削除
end
戻すときに消えるのは green です。 blue と green のどちらが残るかが、完了と巻き戻しで入れ替わります。
各段階は次のコマンドで読めます。
gcloud container operations describe OPERATION_ID --zone=asia-northeast1-a \
--format="value(progress.metrics[0].stringValue,status)"
[0]は今回の並び順に依存しています。 名前で取るなら metrics を絞り込みます。
12:14:29 DRAINING_BLUE_POOL RUNNING
12:15:13 DRAINING_BLUE_POOL RUNNING
...
12:18:07 DRAINING_BLUE_POOL RUNNING
12:18:28 NODE_POOL_SOAKING RUNNING
CORDONING_BLUE_POOL → DRAINING_BLUE_POOL → NODE_POOL_SOAKING と進みます。ロールバックを試すならこれを見ます。
移行中: 900回のサンプルで失敗0回 #
アップグレードの全期間にわたって、毎秒1回リクエストしました。
tr ' ' '\n' < /tmp/bg-probe.txt | grep -v '^$' | sort | uniq -c
900 200
gcloud container operations list --zone=asia-northeast1-a \
--filter="operationType=UPGRADE_NODES" --format="table(status,startTime,endTime)"
STATUS START_TIME END_TIME
DONE 2026-09-04T11:41:41.337259145Z 2026-09-04T11:58:44.228254785Z
900回すべて200。17分3秒。
計測はこれです。
while true; do
curl -s -o /dev/null -m 2 -w '%{http_code} ' "http://${ILB_IP}/" >> /tmp/bg-probe.txt
sleep 1
done
「毎秒1回」は正確ではありません。 17分3秒は1023秒ですが、記録は900回です。sleep 1にリクエストの処理時間が乗るぶん、サンプルは秒数より少なくなります。
リクエストの間に落ちていた区間は、この集計には現れません。 また%{http_code}は最後に受け取った応答コードなので、200を受けたあとに転送が失敗しても200のままです。時刻付きで記録していないので、そこまでは切り分けられません。
ワークロード側の設定は前回と同じです(2レプリカ + podAntiAffinity + PDB + preStop)。Blue/Greenのために足したものはありませんが、可用性のための設定は入っています。 Blue/Greenだけで失敗が出なくなったわけではありません。
前回との比較です。
| 移行方法 | 計測先 | 失敗 | 所要時間 |
|---|---|---|---|
| 手動drain(レプリカ1個) | Service名 | 21回連続 | 約2分 |
| 手動drain(2レプリカ + PDB) | ClusterIP | 30回中5回 | 約2分 |
手動drain(+ preStop) |
ClusterIP | 33回中0回 | 約2分 |
| Blue/Green | 踏み台→内部LB | 900回中0回 | 17分3秒 |
この表から比を出すのは無理があります。 計測先が3種類あり、計測区間も揃っていません。前回はクラスタの中から、今回は踏み台から内部LB越しに測っています。
言えるのはこの設定では時間がかかったことです。17分3秒のうち、node_pool_soak_durationの600秒とバッチごとの待機が占めます。complete-upgradeでsoakを途中で終わらせられるので、所要時間は固定ではありません。
ロールバックは「キャンセルしてから」 #
ここからは2回目のアップグレードです。1回目で green だった 41052e12 が、この回では blue になります。
12:12:26 アップグレード開始
12:13:43 blue が cordon され、green が Ready
12:14:03 rollback → 拒否(DRAINING_BLUE_POOL)
12:18:28 NODE_POOL_SOAKING に到達
12:18:41 rollback → 拒否(incompatible operation)
12:19:13 operations cancel → ABORTING
12:19:45 DONE / "Operation was aborted"
12:20:01 rollback → 成功
12:23:11 完了
移行先のバージョンは記録に残していません。 ロールバック後に 1.35.7-gke.1150000 へ戻っていることから、開始前がそのバージョンだったことだけが分かります。
実行中のoperationがあるあいだにrollbackを打つと拒否されます。
gcloud container node-pools rollback tf-adv-gke-bg-np --cluster=tf-adv-gke-bg \
--zone=asia-northeast1-a
ERROR: (gcloud.container.node-pools.rollback) ResponseError: code=400,
message=Cluster is running incompatible operation operation-1788523948727-...
試したのはDRAINING_BLUE_POOLとNODE_POOL_SOAKINGの2段階で、どちらも同じでした。エラーメッセージからは理由が読み取れませんが、ヘルプに書いてあります。
gcloud container node-pools rollback --help
DESCRIPTION
Rollback a node-pool upgrade.
Rollback is a method used after a canceled or failed node-pool upgrade. It
makes a best-effort attempt to revert the pool back to its original state.
キャンセルが先で、rollbackはその後始末をするコマンドでした。
gcloud container operations cancel OPERATION_ID --zone=asia-northeast1-a
status: ABORTING
今回は30秒ほどで完了しました。固定の待ち時間ではないので、status が DONE になったことを確かめてから次に進みます。
progress:
metrics:
- name: UPGRADE_STAGE
stringValue: NODE_POOL_SOAKING
status: DONE
statusMessage: 'Operation was aborted: operation was aborted.'
キャンセルしてもblueは残っています。ここでrollbackを打ちます。
--respect-pdb=true を付けています。PDBを置いただけでは、ロールバックのdrainがそれを尊重するとは限りません。 このフラグで明示します。
gcloud container node-pools rollback tf-adv-gke-bg-np --cluster=tf-adv-gke-bg \
--zone=asia-northeast1-a --respect-pdb=true
Updated [https://container.googleapis.com/v1/projects/YOUR_PROJECT_ID/zones/asia-northeast1-a/clusters/tf-adv-gke-bg/nodePools/tf-adv-gke-bg-np].
3分10秒で完了しました。
$ gcloud container node-pools describe tf-adv-gke-bg-np --cluster=tf-adv-gke-bg \
--zone=asia-northeast1-a --format="value(version)"
1.35.7-gke.1150000
NAME VERSION READY SCHED
gke-tf-adv-gke-bg-tf-adv-gke-bg-np-41052e12-0zr5 v1.35.7-gke.1150000 True <none>
gke-tf-adv-gke-bg-tf-adv-gke-bg-np-41052e12-z46k v1.35.7-gke.1150000 True <none>
greenは削除され、blueはuncordonされてPodも戻りました。節の冒頭に出したタイムラインの 12:20:01 から 12:23:11 にあたります。
この一連(アップグレード → キャンセル → ロールバック)の全期間でも、サンプルに失敗はありませんでした。
631 200
アップグレード後の terraform plan で確認する #
terraform plan
Note: Objects have changed outside of Terraform
~ master_version = "1.35.7-gke.1150000" -> "1.36.2-gke.2064000"
Changes to Outputs:
~ master_version = "1.35.7-gke.1150000" -> "1.36.2-gke.2064000"
ノードプールには差分が出ません。lifecycle.ignore_changes = [version]が効いています。master_versionは読み取り専用の属性なので、出力値が変わるだけでインフラは変更されません。
min_master_versionは「これ以上」を指す下限で、作成時にしか効かない属性ではありません。 provider は現在のバージョンと比べ、指定値のほうが新しいときだけ更新します。
今回差分が出なかったのは、gcloudで上げた結果が変数の値より新しいためです。
ローテーション直後のアップグレードでは、旧鍵バージョンで作られた #
アップグレードの直前に鍵をローテーションしました。ところがgreenノードのブートディスクは旧バージョンでした。
原因を特定したわけではありません。 primary の切り替えは結果整合で、反映中は旧 primary が使われることがあるという仕様と、今回の観測は矛盾しません。ただし見たのはディスク2時点の情報だけで、反映が終わった時刻も個々の暗号化要求の状態も確かめていません。
NAME CREATION_TIMESTAMP KMS_KEY_NAME
gke-tf-adv-gke-bg-tf-adv-gke-bg-np-41052e12-0zr5 2026-09-04T04:41:54.435-07:00 1
gke-tf-adv-gke-bg-tf-adv-gke-bg-np-41052e12-z46k 2026-09-04T04:41:54.434-07:00 1
前回は「新しく作られたノードは新しい鍵バージョンになる」と確認していたので、逆の結果です。
Blue/Green の仕組みが原因ではなさそうです。インスタンステンプレートが持っているのは鍵のパスだけで、バージョンを含みません。
projects/YOUR_PROJECT_ID/locations/asia-northeast1/keyRings/tf-adv-gke-bg-ring/cryptoKeys/tf-adv-gke-bg-boot-disk
同じノードプールに、時間を空けてノードを1台足すと新しいバージョンになりました。
NAME CREATION_TIMESTAMP KMS_KEY_NAME
gke-tf-adv-gke-bg-tf-adv-gke-bg-np-41052e12-0zr5 2026-09-04T04:41:54.435-07:00 1
gke-tf-adv-gke-bg-tf-adv-gke-bg-np-41052e12-rv2c 2026-09-04T04:59:37.504-07:00 2
gke-tf-adv-gke-bg-tf-adv-gke-bg-np-41052e12-z46k 2026-09-04T04:41:54.434-07:00 1
| ノード作成 | ローテーションからの経過 | 鍵バージョン |
|---|---|---|
| 11:41:54 | +17秒 | 1 |
| 11:59:37 | +18分 | 2 |
同じプール、同じテンプレートで結果が割れました。primaryバージョンの切替が反映されるまでに時間がかかります。
When you change the primary key version, the change typically becomes consistent within 1 minute. However, this change can take up to 3 hours to propagate in exceptional cases. (Rotate a key)
鍵をローテーションしてすぐ移行作業を始めると、新しいノードが旧バージョンのまま作られることがあります。 今回の2時点の観測は伝播の仕様と矛盾しませんが、原因を特定したわけではありません。
前回の「移行完了はterraform planではなくgcloud compute disks listで確認する」がここでも効きます。
後片付け #
内部LBはKubernetes側で作ったものなので、先に消します。残しておくと転送ルールがVPCに残ります。
kubectl delete -f k8s/03-nginx-ilb.yaml
terraform destroy
Destroy complete! Resources: 27 destroyed.
terraform destroyはCloud KMSのキーリングと鍵を消さず、鍵バージョンの破棄だけがスケジュールされます(削除機能そのものはGA済み)。再検証するときは別の名前を指定してください。
まとめ #
- Blue/Greenのために足した設定は無く、断も出なかった。 ワークロード側は前回と同じ2レプリカ +
podAntiAffinity+ PDB +preStopで、これらは可用性のために入れてある。17分3秒のあいだ900回すべて200 - 実行中のoperationがあると
rollbackは拒否される。 今回試した2段階ではどちらも400。正しくはoperations cancel→rollback。ヘルプの “after a canceled or failed node-pool upgrade” が答えで、失敗済みのアップグレードならcancelは要りません - アップグレード → キャンセル → ロールバックの全期間でも、サンプルに失敗は出なかった。 ただし観測できるのはサンプルの時点だけで、無停止の保証ではない
- 進行段階は
UPGRADE_STAGEで読める。kubectl get nodesだけではsoak窓に入ったか判断できない。ノードプールのupdateInfo.blueGreenInfo.phaseでも読めるが、今回は使っていない - ローテーション直後に移行を始めると、旧鍵バージョンで作られることがある。 primary の切り替えは結果整合で、通常1分以内・例外的に最大3時間。今回の観測はこの仕様と矛盾しないが、原因を特定したわけではない
batch_percentageは0〜1の割合。パーセントではない- 計測点はクラスタの外に置く。 Blue/Greenはblueノードを全て入れ替えるので、中に置いたプローブは計測対象と一緒に消える