OpenTofu で GCP ロードバランサを逆 terraform

GCP のロードバランサは Web コンソールや CLI を用いて構築することになりやすく、構成のバックアップやリストアに難があります。
一度構築したうえで OpenTofu / Terraform を併用することで Infrastructure as Code も実装可能です。

次のようなimport.tfを用意して、tofu plan -generate-config-out=export.tfを実行すると現在の構成をエクスポートできます。
tofu planは GCP にクエリするため実行の前提として、GCP オブジェクトへの適切なアクセス権限を持つユーザーでgcloud auth application-default loginで認証しておく必要があります。

# gcloud compute forwarding-rules list --global --uri 
import {
  id = "projects/<project-id>/global/forwardingRules/<object-id>"
  to = google_compute_global_forwarding_rule.<unique-name>
}

# gcloud compute target-https-proxies list --global --uri
import {
  id = "projects/<project-id>/global/targetHttpsProxies/<object-id>"
  to = google_compute_target_https_proxy.<unique-name>
}

# gcloud compute target-http-proxies list --global --uri
import {
  id = "projects/<project-id>/global/targetHttpProxies/<object-id>"
  to = google_compute_target_http_proxy.<unique-name>
}

# gcloud compute url-maps list --uri
import {
  id = "projects/<project-id>/global/urlMaps/<object-id>"
  to = google_compute_url_map.<unique-name>
}

# gcloud compute backend-buckets list --uri
import {
  id = "projects/<project-id>/global/backendBuckets/<object-id>"
  to = google_compute_global_backend_bucket.<unique-name>
}

# gcloud compute backend-services list --uri
import {
  id = "projects/<project-id>/global/backendServices/<object-id>"
  to = google_compute_backend_service.<unique-name>
}

# gcloud compute health-checks list --uri
import {
  id = "projects/<project-id>/global/healthChecks/<object-id>"
  to = google_compute_health_check.<unique-name>
}

# gcloud compute firewall-rules list --uri
import {
  id = "projects/<project-id>/global/firewalls/<object-id>"
  to = google_compute_firewall.<unique-name>
}

# gcloud compute ssl-certificates list --global --uri
# NOTE: TLS 証明書はマネージドの場合、設定変更に使える。非マネージドは宣言的に管理しづらい
import {
  id = "projects/<project-id>/global/sslCertificates/<object-id>"
  to = google_compute_ssl_certificate.<unique-name>
}

# gcloud compute addresses list --global --uri
# NOTE: IP アドレスは宣言的に管理しづらい
import {
  id = "projects/<project-id>/global/addresses/<object-id>"
  to = google_compute_global_address.<unique-name>
}

idについては、gcloud compute <gcp-object-type> listコマンドで対象の URL を特定して記載します。
toはプレフィクスに GCP オブジェクトに対応する型を指定し、<unique-name>には OpenTofu 上の識別名をつけます。

エクスポートした構成ファイルは、おそらく最適とは言えないのでリファクタリングが必要になります。
tofuコマンドがデフォルト指定する属性を削ることになるでしょう。

各フィールドの意味は簡潔であっても自明とは言えないので、リファレンスで確認する必要があります。

また、for_eachを用いて一部のパラメータのみが異なるブロックを共通化できるため、コードの簡潔さを維持できます。

tofu planで定義をチェックし、tofu applyで変更を適用するフローになります。

管理のポイント

既述例のとおり、ロードバランサは関連オブジェクトが多いため後から見た際に構成が分かりにくくなりがちです。 OpenTofu 向けの HCL で目的別にパックすることで管理しやすくなります。

とくにプロジェクト内にクラスタを複数構築する場合、クラウドコンソールなどの純正ツールでは GCP オブジェクトの種類ごとに分断されてフラットにリストされるため、依存関係の見通しは悪くなります。

ファイアーウォール

ロードバランサー関連オブジェクトの中で、もっとも挙動が読みづらいものとして プロキシ製品別のファイアーウォールルール設定 に前提条件があります。

ファイアーウォール設定がなければ、まずヘルスチェックが通過しません。
そしてドキュメントの書き方で明示していないポイントとして、実トラフィックも同じドキュメントの条件に従う挙動になります。

ヘルスチェックのターゲットポートだけを許可して別ポートの実トラフィックの設定がない場合、コンソール上は開通しているにも関わらずリクエストがタイムアウトする、という非常に分かりづらい動作をします。

そして、ファイアーウォールも素朴に管理するとグローバルオブジェクトであるため、どのクラスタが依存しているのかをほぼ把握できないでしょう。コード化しておく必然性があります。

depends_on ブロックの追記

コードの構造化のためにdepends_onブロックが役立ちます。
自動認識はせず、手動で追加する必要があります。

次のように、HCL 上の名称を用いて定義します。

resource "google_compute_target_https_proxy" "proxy_set" {
  depends_on = [
    google_compute_url_map.url_map,
    google_compute_firewall.allow_hc_v6,
    google_compute_firewall.allow_hc_v4,
    google_compute_firewall.allow_glb_v6,
    google_compute_firewall.allow_glb_v4,
  ]
}

この例のように、IPv6 と IPv4、HTTPS と HTTP のように単一オブジェクト内に混在できない制約があります。

ロードバランサの構成が成長していくとパーツが増えますが、依存関係を記述しておくとtofu plan時にチェックされ矛盾があると構成変更前にエラー検出できます。

また、GCP オブジェクト間には実際に依存関係があり、フラットに構築リクエストすると前提オブジェクト欠損によるエラーが起きます。

depends_onがあれば、定義に従って実行待ちを行うためクリーンビルド時の動作も安定します。

IP アドレスの取得

GCP の IP アドレスは HCL で管理しないという手もあります。
tofuコマンドでも作成できるはずですが、IP は欲しいアドレスを獲得できるリソースではないので、宣言の効果が弱いオブジェクトです。

IP アドレスを素朴にクラウドコンソールから取得してロックした場合、google_compute_global_forwarding_rule.ip_addressに IP を記載して固定するだけです。
エフェメラル IP を取得したい場合には、google_compute_global_forwarding_rule.ip_versionIPV4|IPV6を指定します。起動後にクラウドコンソールで静的 IP としてロックし、HCL も書き換えます。

SSL 証明書も同様で、外部プロセスで GCP に登録・更新している場合は、単にそのオブジェクトを参照するだけになるはずです。

エクスポート後のクリーンアップ

tofu plan -generate-config-out で生成されたファイルには、GCP が自動的に割り当てるフィールドや、HCL での管理が不要な読み取り専用属性が大量に含まれており、将来の更新時に不要な差分の原因になります。

具体的には以下のフィールドを削除または見直しの候補です:

  • id, self_link: リソース作成後に固定される属性です。
  • creation_timestamp: 作成日時です。
  • project: 各ブロックに頻出しますが、プロバイダレベルで設定して、簡潔のため個別のリソースでは省略した方が良いでしょう。
  • fingerprint: backend_service などに含まれる更新競合防止用の値です。初期インポート時は不要です。

また、プロジェクト ID やリージョンなどの共通する値を locals などの変数に抽出することで、複数環境への展開が容易になります。

構成の検証と Zero-diff の達成

新たに定義したHCLを用いて tofu apply が完了したら、その直後に tofu plan を実行して「No changes」の状態になるかを確認すべきでしょう。

OpenTofu は、実際の GCP 上の構成と、管理対象のリソース状態を記録した terraform.tfstate(ステートファイル)を照合します。ステートファイルは IaC における「正解」を保持する極めて重要な役割を担うため、インポート直後に差分がない状態を確立することが、その後の安定運用の大前提となります。

もし差分が出る場合は、生成された HCL の定義が実際の GCP 上の構成とわずかに食い違っています。

特に注意すべきは “Plan: 1 to add, 0 to change, 1 to destroy” のようにリソースの再作成(Replace)が発生する場合です。ロードバランサの再作成は、最悪の場合 IP アドレスの変動やサービス停止を伴うため、慎重に差分を確認し、HCL 側を現実の構成に合わせる必要があります。

標準ワークフロー準備

IaC 化が完了した後は、日常の開発フローに沿って安全に運用するためのセットアップが不可欠です。コードセットはgitなどで追跡します。

  • .tfstate: これを失うと、たとえ手元に HCL が残っていても、OpenTofu は既存リソースの存在を認識できなくなります。不用意な apply によって既存リソースの二重作成や上書きによる衝突、あるいは意図しない破壊を招くリスクがあり、復旧には全リソースの再インポートが求められます
  • .terraform.lock.hcl: 使用する google プロバイダのバージョンを固定することで、プロバイダ側の仕様変更による予期せぬ挙動を防げます

また、プルリクエスト時にCI/CDで自動的に tofu plan を実行することで、変更の影響範囲が事前に可視化され意図しないインフラ変更が本番環境に混入するのを防げます。

Google Kubernetes Engine との組み合わせ

この方式は、GKE の統合を想定するとbackendServiceから StandaloneNEG つきのServiceに接続する構成です。
基本的に k8s 標準の Service, Pod に接続できますが、Serviceにはアノテーションを追加し、GCP のヘルスチェックを受けつけるセットアップは必要です。

前提として GatewayAPI の採用をあきらめています。もちろん GatewayAPI を用いて構築できた場合にも OpenTofu で GCP オブジェクト一式をエクスポートすることも可能です。
ただ現実には GatewayAPI が要求をサポートし切れないケースが多く考えられます。たとえば、簡潔なネックとしてbackendBucketを同一のロードバランサに収容する方法を永らくサポートしていません。

GatewayAPI を採用しないとなると、このネットワークアクセス層がコードにならないため HCL が必要になります。

デュアルスタックの NEG

NetworkEndpointGroup は、アノテーション経由で追加しますが、デュアルスタック設定はServiceスペックの設定によって決まります。
既定が IPv4 のみとすると、次のような設定で NEG も IPv6 を受けつけます。

apiVersion: v1
kind: Service
metadata:
  annotations:
    cloud.google.com/neg: '{"exposed_ports": {"443": {"name": "web-neg"}}}'
spec:
  type: ClusterIP
  ipFamilyPolicy: PreferDualStack
  ipFamilies:
  - IPv6
  - IPv4

この場合、当然ながら Pod 内のアプリケーションも0.0.0.0だけでなく[::]も listen することは前提です。
また、ipFamiliesの指定順には意味があります。

⁋ 2026/03/18↻ 2026/03/18
中馬崇尋
Chuma Takahiro