概要 #
GKEのPodをInternal HTTP LB(NEG)でポート別に振り分けるでは、スタンドアロンNEGを作るところまでを扱いました。しかし運用で怖いのはその先です。「Serviceを間違って消してしまった」「クラスタを作り直した」というとき、ロードバランサは自動で復旧してくれるのでしょうか。
NEGはGKEが作り、LBのバックエンドはTerraformが管理します。作る主体が違うため、片方だけが変わると両者の認識がずれます。障害が起きてから手探りで復旧手順を探すのは避けたい。そこで、実際に壊して直るまでを先に確認しておきます。
対象読者は、GKEでNEGを使ったLB構成を組んだことがある人です。NEGそのものの入門は扱いません。
Internal LBは扱いません(前掲の記事で扱っています)。Cloud Armor、マネージドSSL証明書、Shared VPCも扱いません。
検証すること #
- 正常系: 外部LB経由でPodへ到達できること
- 検証1: Serviceを削除するとどうなるか。再作成すればLBのバックエンドは自動で戻るか
- 検証2: LBを残したままGKEクラスタを作り直すとどうなるか
前提環境 #
- Google Cloud CLI、Terraform、Docker
- Billingが有効な検証用Google Cloud Project
- GKE、Compute Engineを操作できるGoogleアカウント
- 検証時のバージョン:Terraform 1.14.3、google 7.46.0
使用するTerraformコード #
今回の構成 #
リージョナル・2ゾーンのプライベートGKEクラスタと、グローバル外部Application LBです。NEGはゾーンごとに作られるため、複数ゾーンにしています。
flowchart TB
USER["インターネット"]
subgraph GCP["Google Cloud"]
subgraph Project["Project"]
LB["外部Application LB
Backend Service"]
subgraph Region["asia-northeast1"]
subgraph ZoneA["zone-a"]
NEGA["NEG(Terraform管理外)"]
PodA["Pod"]
end
subgraph ZoneB["zone-b"]
NEGB["NEG(Terraform管理外)"]
PodB["Pod"]
end
end
end
end
USER --> LB
LB --> NEGA --> PodA
LB --> NEGB --> PodB
NEGはTerraformが作らない #
ここが今回の話の起点です。NEGはGKEのNEGコントローラが、Serviceのcloud.google.com/negアノテーションから作ります。
apiVersion: v1
kind: Service
metadata:
name: neg-app-svc
annotations:
cloud.google.com/neg: '{"exposed_ports":{"80":{"name":"tf-adv-negrec-neg"}}}'
Terraformはdataソースで読むだけです。
data "google_compute_network_endpoint_group" "standalone" {
for_each = var.enable_lb ? toset(var.gke_zones) : toset([])
name = var.neg_name
zone = each.value
}
resource "google_compute_backend_service" "lb" {
# ...
dynamic "backend" {
for_each = toset(var.gke_zones)
content {
group = data.google_compute_network_endpoint_group.standalone[backend.value].id
}
}
}
前掲の記事ではNEGをURL文字列で組み立てて参照していました。今回はdataソースなので、NEGが無いときのエラーが出る場所が違います。
Error: projects/PROJECT/zones/asia-northeast1-a/networkEndpointGroups/tf-adv-negrec-neg not found
dataソースはplan時に解決されるため、applyまで到達せずplanで止まります。URL文字列の場合はplanが通り、apply時にAPIが404を返します。
正常系を確認する #
2段階applyでクラスタとLBを作ります。
terraform apply # クラスタのみ
# 踏み台からkubectl applyでPodとServiceをデプロイ、NEG作成を待つ
terraform apply -var="enable_lb=true" # LB
NEGは2ゾーンに作られました。Podが載っているゾーンだけエンドポイントを持ちます。
NAME LOCATION ENDPOINT_TYPE SIZE
tf-adv-negrec-neg asia-northeast1-a GCE_VM_IP_PORT 0
tf-adv-negrec-neg asia-northeast1-b GCE_VM_IP_PORT 2
gcloud compute backend-services get-health "$(terraform output -raw backend_service_name)" --global
curl -s -o /dev/null -w "HTTP %{http_code}\n" "http://$(terraform output -raw lb_ip)/"
healthStatus:
- healthState: HEALTHY
ipAddress: 10.41.0.5
port: 80
- healthState: HEALTHY
ipAddress: 10.41.0.6
port: 80
HTTP 200
LBの初期化に2分ほどかかりました。
検証1: Serviceを削除する #
kubectl delete svc neg-app-svc
通信は切れませんでした。
NAME LOCATION ENDPOINT_TYPE SIZE
tf-adv-negrec-neg asia-northeast1-a GCE_VM_IP_PORT 0
tf-adv-negrec-neg asia-northeast1-b GCE_VM_IP_PORT 2
HTTP 200
90秒待っても同じです。理由はGKE側のリソースを見ると分かります。
kubectl get svcneg tf-adv-negrec-neg -o yaml
deletionGracePeriodSeconds: 0
deletionTimestamp: "2026-09-03T12:09:34Z"
finalizers:
- networking.gke.io/neg-finalizer
svcnegというカスタムリソースに削除日時が入っているのに、networking.gke.io/neg-finalizerが残っています。Kubernetesのfinalizerによって削除が保留されている状態です。
NEGを直接消そうとすると理由が分かります。
gcloud compute network-endpoint-groups delete tf-adv-negrec-neg --zone=asia-northeast1-b
ERROR: (gcloud.compute.network-endpoint-groups.delete) Could not fetch resource:
- The network_endpoint_group resource '.../tf-adv-negrec-neg' is already being used by
'.../backendServices/tf-adv-gke-negrec-bes'
LBのバックエンドがNEGを掴んでいるため、GCP側が削除を拒否します。GKEもそれを検知して、finalizerを外さずに待っています。
バックエンドを外すと連鎖する #
gcloud compute backend-services remove-backend "$BES" --global \
--network-endpoint-group=tf-adv-negrec-neg --network-endpoint-group-zone=asia-northeast1-a
# zone-bも同様
両方外した瞬間、finalizerが解除されNEGが消えました。
Listed 0 items.
HTTP 502
Serviceを再作成する #
kubectl apply -f /tmp/neg-app.yaml
NEGは同じ名前で自動的に復活しました。
NAME LOCATION ENDPOINT_TYPE SIZE
tf-adv-negrec-neg asia-northeast1-a GCE_VM_IP_PORT 0
tf-adv-negrec-neg asia-northeast1-b GCE_VM_IP_PORT 2
しかしバックエンドは空のままです。
gcloud compute backend-services describe "$BES" --global --format="value(backends[].group)"
(空)
HTTP 502
NEGが同名で戻っても、Backend Serviceのbackends[]は自動では復元されません。 terraform applyで再付与します。
terraform apply -var="enable_lb=true"
# google_compute_backend_service.lb[0] will be updated in-place
Plan: 0 to add, 1 to change, 0 to destroy.
HTTP 200
Terraformが差分を検知して、バックエンドを付け直してくれました。
検証2: クラスタを作り直す #
LB・Firewall・静的IPは残したまま、クラスタとノードプールだけを消します。
terraform destroy -var="enable_lb=true" \
-target=google_container_node_pool.primary \
-target=google_container_cluster.primary
検証1とは違う壊れ方をしました。
NAME LOCATION ENDPOINT_TYPE SIZE
tf-adv-negrec-neg asia-northeast1-a GCE_VM_IP_PORT 0
tf-adv-negrec-neg asia-northeast1-b GCE_VM_IP_PORT 0
NEGはSIZE 0で残り、バックエンドの参照も残っています。エンドポイントだけが消え、get-healthは空、curlは502です。
作り直してServiceを適用すると衝突する #
terraform apply -var="enable_lb=true"
kubectl apply -f /tmp/neg-app.yaml
Podは起動しますが、NEGへの同期が失敗します。
kubectl describe svc neg-app-svc
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning SyncNetworkEndpointGroupFailed 9s (x9 over 2m4s) neg-controller
Failed to sync NEG "tf-adv-negrec-neg" (will retry): failed to get NEG for service:
found conflicting description in neg tf-adv-negrec-neg:
expected description of NEG object "asia-northeast1-a"/"tf-adv-negrec-neg" to be
{"cluster-uid":"cbb7d166-deb9-48c8-b74c-f48705be47e5", ...},
but got
{"cluster-uid":"4dac0cd8-4927-4439-81bd-d764f55312ba", ...}
NEGには作成元クラスタのUIDが記録されています。新しいクラスタ(cbb7d166...)から見ると、残っているNEGは別のクラスタ(4dac0cd8...)のものなので、自分のものとして扱えず同期を拒否します。
復旧手順 #
検証1ではremove-backendだけでNEGが自動的に消えました。今回は旧クラスタのNEGを片付ける主体がいないため、手動で消す必要があります。
# 1. バックエンドを外す(両ゾーン)
gcloud compute backend-services remove-backend "$BES" --global \
--network-endpoint-group=tf-adv-negrec-neg --network-endpoint-group-zone=asia-northeast1-a
# zone-bも同様
# 2. 旧NEGを手動で削除(両ゾーン)
gcloud compute network-endpoint-groups delete tf-adv-negrec-neg --zone=asia-northeast1-a --quiet
# zone-bも同様
# 3. svcnegとServiceを作り直す
kubectl delete svc neg-app-svc --ignore-not-found=true
kubectl delete svcneg tf-adv-negrec-neg --ignore-not-found=true
kubectl apply -f /tmp/neg-app.yaml
新しいUIDでNEGが再生成され、同期が成功しました。
kubectl get svcneg tf-adv-negrec-neg -o jsonpath='{range .status.conditions[*]}{.type}={.status} ({.reason}){"\n"}{end}'
Initialized=True (NegInitializationSuccessful)
Synced=True (NegSyncSuccessful)
最後にバックエンドを再付与します。
terraform apply -var="enable_lb=true"
HTTP 200
後片付け #
terraform destroy -var="enable_lb=true"
Destroy complete! Resources: 29 destroyed.
GKE(2ノード)・LB・踏み台VM・Cloud NATは利用中に料金が発生します。検証後は必ずdestroyします。
まとめ #
- Serviceを削除しても、バックエンドがNEGを参照している間は
svcnegのfinalizerが削除を保留する。通信も切れない - バックエンドを外すとfinalizerが解除され、NEGが消えて502になる
- Serviceを作り直すとNEGは同名で戻るが、Backend Serviceの
backends[]は自動では戻らない。terraform apply(またはadd-backend)が必要 - クラスタを作り直すと、NEGに記録された
cluster-uidが新クラスタと食い違い、SyncNetworkEndpointGroupFailedになる。 旧NEGは誰も片付けないので手動削除が要る - NEGを
dataソースで参照すると、NEG不在時はapplyではなくplanで止まる
参考資料 #
- Google Cloud: Container-native load balancing through standalone zonal NEGs
- Google Cloud: Zonal NEGs overview
- Terraform Registry: google_compute_backend_service
- Kubernetes: Finalizers