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

TerraformでGKEのスタンドアロンNEGを消したらLBのバックエンドが戻らなかった話

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

概要
#

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で止まる

参考資料
#

次回
#

GKEのPodにSecret Managerの値をファイルとしてマウントします。

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

関連記事

TerraformでGKEのPodをInternal HTTP LB(NEG)でポート別に振り分けてみた
3 分
Terraform GoogleCloud GKE LoadBalancing
Peering越しのPod→VM通信が落ちる原因を、ルートだと思い込んでいた話
4 分
Terraform GoogleCloud GKE VPC
TerraformでGKE Dataplane V2を有効化し、NetworkPolicyでPod間通信を遮断してみた
2 分
Terraform GoogleCloud GKE NetworkPolicy
TerraformでGKEのノードあたり最大Pod数を小さくしすぎるとどうなるか試してみた
2 分
Terraform GoogleCloud GKE
TerraformでGKEのコントロールプレーンをプライベート+パブリックの両方から接続してみた
2 分
Terraform GoogleCloud GKE
TerraformでGKEプライベートクラスタからMemorystore for Redis Clusterへ接続してみた
3 分
Terraform GoogleCloud GKE Redis