🔍

TerraformでGoogle Cloud APIの有効化漏れを事前検知する方法

に公開

はじめに

Google CloudでTerraformを使っていると、こんなエラーに遭遇したことはありませんか?

Error: Error creating Instance: googleapi: Error 403:
Cloud SQL Admin API has not been used in project XXXXX before or it is disabled.

これは、必要なAPIがプロジェクトで有効化されていないために発生するエラーです。特に新しいプロジェクトで作業する際や、新しいサービスを使い始めるときによく遭遇します。

terraform applyを実行して初めてエラーに気づき、APIを有効化してから再実行...という手順を踏むのは非効率です。できればterraform planの段階で気づきたいですよね。

この記事では、data.google_project_servicepreconditionを組み合わせて、plan時にAPI有効化漏れを検知する方法をご紹介します。

従来の問題点

Terraformで新しいGoogle Cloudリソースを作成する際、該当サービスのAPIが有効化されていないと、terraform apply時にエラーで失敗します。

# Cloud SQLインスタンスを作成しようとする
resource "google_sql_database_instance" "postgres" {
  name             = "my-postgres-instance"
  database_version = "POSTGRES_15"
  region           = "asia-northeast1"
  # ...
}

上記のコードでterraform applyすると、Cloud SQL Admin APIが有効化されていない場合にエラーになります。

問題点:

  • terraform planは成功するが、terraform applyで初めてエラーに気づく
  • エラーメッセージからどのAPIを有効化すればいいのか分かりにくい場合がある
  • API有効化後、再度terraform applyが必要

解決策:preconditionによる事前チェック

Terraform 1.2以降では、lifecycleブロックのpreconditionを使って、リソース作成前に条件をチェックできます。

これとdata.google_project_serviceを組み合わせることで、plan時にAPIの有効化状態を確認できます。

なぜModuleにpreconditionを実装するのか

この記事では、Module内にpreconditionを実装するパターンを紹介しています。その理由は以下の通りです。

1. 一度有効化したら同じプロジェクト内では再確認不要

同じプロジェクト内で開発を続けている限り、一度APIを有効化すれば、そのAPIが無効化されることはほとんどありません。そのため、同じプロジェクトでの開発では、このチェックは初回にしか役立ちません。

2. Moduleの再利用時に威力を発揮

しかし、Moduleを別のプロジェクトで流用する際に、このpreconditionが真価を発揮します。

例えば:

  • 新しいプロジェクトでCloud SQLモジュールを使おうとした際
  • 既存のモジュールを他のチームに共有した際
  • 異なる環境(dev/staging/production)を構築する際

このような場合、モジュールに組み込まれたpreconditionが、必要なAPIの有効化漏れに気づかせてくれます

# 新しいプロジェクトで既存のCloud SQLモジュールを使用
module "cloudsql" {
  source = "git::https://github.com/your-org/terraform-modules.git//gcp-cloudsql-postgres"

  gcp_project_id = "new-project-id"  # 新しいプロジェクト
  # ...
}

# terraform plan時にAPIが有効化されていないことに気づける!

3. Moduleの移植性と自己完結性を高める

Moduleにpreconditionを実装することで:

  • モジュール利用者が、必要なAPIを明示的に知ることができる
  • モジュール単体で前提条件をチェックできるため、ドキュメントを読まなくても使える
  • エラーメッセージで対処方法も提示されるため、初めて使う人でも迷わない

実装例:Cloud SQLモジュール

実際のCloud SQLモジュールの実装例を見てみましょう。

# 必要なAPIが有効になっているかチェック
data "google_project_service" "sqladmin" {
  project = var.gcp_project_id
  service = "sqladmin.googleapis.com"
}

data "google_project_service" "servicenetworking" {
  project = var.gcp_project_id
  service = "servicenetworking.googleapis.com"
}

resource "google_sql_database_instance" "postgres" {
  name             = "my-postgres-instance"
  database_version = "POSTGRES_15"
  region           = "asia-northeast1"
  project          = var.gcp_project_id

  settings {
    tier = "db-f1-micro"
    # ...
  }

  # 必要なAPIが有効かチェック
  lifecycle {
    precondition {
      condition     = data.google_project_service.sqladmin.id != null
      error_message = <<-EOT
        【必須API未有効】Cloud SQL Admin API (sqladmin.googleapis.com) がプロジェクト ${var.gcp_project_id} で有効化されていません。

        このモジュールでCloud SQLインスタンスを作成・管理するには、Cloud SQL Admin APIが必要です。

        【対処方法】Terraformで以下のリソースを追加してください:

          resource "google_project_service" "sqladmin" {
            project = "${var.gcp_project_id}"
            service = "sqladmin.googleapis.com"
          }

        または、gcloudコマンドで有効化:
          gcloud services enable sqladmin.googleapis.com --project=${var.gcp_project_id}
      EOT
    }

    precondition {
      condition     = data.google_project_service.servicenetworking.id != null
      error_message = <<-EOT
        【必須API未有効】Service Networking API (servicenetworking.googleapis.com) がプロジェクト ${var.gcp_project_id} で有効化されていません。

        このモジュールでプライベートVPC接続を使用したCloud SQLインスタンスを作成するには、Service Networking APIが必要です。

        【対処方法】Terraformで以下のリソースを追加してください:

          resource "google_project_service" "servicenetworking" {
            project = "${var.gcp_project_id}"
            service = "servicenetworking.googleapis.com"
          }

        または、gcloudコマンドで有効化:
          gcloud services enable servicenetworking.googleapis.com --project=${var.gcp_project_id}
      EOT
    }
  }
}

仕組みの解説

  1. data.google_project_serviceでAPIの状態を取得

    • 指定したAPIがプロジェクトで有効かどうかを確認
    • 有効な場合は情報を取得、無効な場合はidnullになる
  2. preconditionで事前条件をチェック

    • conditionでAPIが有効か(id != null)をチェック
    • 条件を満たさない場合、詳細なエラーメッセージを表示
    • terraform planの段階でエラーが表示される
  3. 分かりやすいエラーメッセージ

    • どのAPIが必要か明示
    • 対処方法を具体的に記載(Terraformでの有効化、gcloudコマンドでの有効化)

plan時のエラー表示例

APIが有効化されていない場合、terraform plan時に以下のようなエラーが表示されます:

Error: Resource precondition failed

  on main.tf line 104, in resource "google_sql_database_instance" "postgres":
 104:       condition     = data.google_project_service.sqladmin.id != null

【必須API未有効】Cloud SQL Admin API (sqladmin.googleapis.com) がプロジェクト my-project で有効化されていません。

このモジュールでCloud SQLインスタンスを作成・管理するには、Cloud SQL Admin APIが必要です。

【対処方法】Terraformで以下のリソースを追加してください:

  resource "google_project_service" "sqladmin" {
    project = "my-project"
    service = "sqladmin.googleapis.com"
  }

または、gcloudコマンドで有効化:
  gcloud services enable sqladmin.googleapis.com --project=my-project

落とし穴:API有効化リソースとの同時適用

ここまで読んで、「じゃあTerraformでAPI有効化リソースを一緒に定義すればいいじゃん!」と思った方もいるかもしれません。

# API有効化
resource "google_project_service" "sqladmin" {
  project = var.gcp_project_id
  service = "sqladmin.googleapis.com"
}

# Cloud SQLインスタンス(上記と同じファイル内)
resource "google_sql_database_instance" "postgres" {
  name             = "my-postgres-instance"
  database_version = "POSTGRES_15"
  # ...

  lifecycle {
    precondition {
      condition     = data.google_project_service.sqladmin.id != null
      error_message = "API未有効..."
    }
  }
}

しかし、これだけでは最初の適用時に失敗します

なぜなら:

  1. terraform plan時点では、まだAPIは有効化されていない
  2. data.google_project_serviceは現在の状態を確認する
  3. preconditionが失敗する

APIはリソースとして適用されるまで有効化されません。つまり、terraform applyを実行してAPI有効化リソースが適用されるまでは、data.google_project_serviceはAPIが無効だと判定します。

対策方法

この問題を回避するには、いくつかの方法があります。

対策1:モジュール呼び出しを一時的にコメントアウト

最初の適用時のみ、モジュール呼び出しをコメントアウトしてAPI有効化を先に適用します。

# API有効化を先に適用
resource "google_project_service" "sqladmin" {
  project = var.gcp_project_id
  service = "sqladmin.googleapis.com"
}

# 最初の適用時はコメントアウト
# module "cloudsql" {
#   source = "./modules/gcp-cloudsql-postgres"
#   # ...
# }

手順:

  1. モジュールをコメントアウトした状態でterraform applyしてAPIを有効化
  2. コメントを外して再度terraform apply

対策2:--targetオプションを使う

特定のリソースのみを先に適用します。

# API有効化を先に適用
terraform apply -target=google_project_service.sqladmin

# その後、全体を適用
terraform apply

対策3:gcloudコマンドやWebコンソールで事前に有効化

TerraformでAPI有効化を宣言する前に、手動で有効化しておく方法です。

gcloud services enable sqladmin.googleapis.com --project=my-project
gcloud services enable servicenetworking.googleapis.com --project=my-project

または、Google CloudのWebコンソールから有効化します。

既に有効化されているAPIをTerraformで再度有効化してもエラーにはなりません。そのため、手動で有効化した後にTerraformで管理し始めても問題ありません。

# 手動で有効化
gcloud services enable sqladmin.googleapis.com --project=my-project

# Terraformで管理開始(エラーにならない)
terraform apply

なぜモジュール内にAPI有効化リソースを含めないのか

「モジュール内にAPI有効化リソースも含めればいいのでは?」という疑問を持つ方もいるかもしれません。

# モジュール内でAPI有効化も行う例
module "cloudsql_module" {
  # モジュール内部
  resource "google_project_service" "sqladmin" {
    project = var.gcp_project_id
    service = "sqladmin.googleapis.com"
  }

  resource "google_sql_database_instance" "postgres" {
    # ...
  }
}

しかし、これは推奨されません。理由は以下の通りです。

問題点:複数モジュールでの競合

複数のモジュールで同じAPIを使う場合、各モジュールが同じAPI有効化リソースを持つことになります。

# モジュールA(Cloud SQL)
module "cloudsql" {
  # 内部で servicenetworking.googleapis.com を有効化
}

# モジュールB(別のネットワークリソース)
module "network_resource" {
  # 内部で servicenetworking.googleapis.com を有効化
}

この状態で、片方のモジュールを削除すると、API有効化も削除されてしまいます

# モジュールAを削除
# module "cloudsql" {
#   ...
# }

# モジュールBだけ残す
module "network_resource" {
  # servicenetworking.googleapis.com が無効化されてしまう可能性
}

結果:

  • モジュールAの削除時にgoogle_project_serviceも削除される
  • モジュールBが依存しているAPIが無効化され、モジュールBのリソースにも影響が出る
  • 意図しないリソース削除や障害につながる

推奨パターン

API有効化は、プロジェクトレベルで一元管理するのがベストプラクティスです。

# ルートモジュールでAPI有効化を一元管理
resource "google_project_service" "sqladmin" {
  project = var.gcp_project_id
  service = "sqladmin.googleapis.com"
}

resource "google_project_service" "servicenetworking" {
  project = var.gcp_project_id
  service = "servicenetworking.googleapis.com"
}

# モジュールはAPI有効化を前提に動作
module "cloudsql" {
  source = "./modules/gcp-cloudsql-postgres"
  # preconditionでAPI有効化をチェック
}

module "network_resource" {
  source = "./modules/gcp-network-resource"
  # preconditionでAPI有効化をチェック
}

この方法なら:

  • API有効化の状態が一目で分かる
  • 複数モジュール間での競合が起きない
  • モジュールの削除がAPI有効化に影響しない

まとめ

Google CloudでTerraformを使う際のAPI有効化漏れ対策として、以下の方法を紹介しました。

  1. data.google_project_servicepreconditionで事前チェック

    • plan時にAPI有効化状態を確認できる
    • 分かりやすいエラーメッセージで対処方法を提示
  2. 初回適用時の落とし穴に注意

    • API有効化リソースとモジュールを同時に適用すると失敗する
    • 対策:コメントアウト、--targetオプション、事前の手動有効化
  3. API有効化はプロジェクトレベルで一元管理

    • モジュール内にAPI有効化を含めない
    • 複数モジュール間での競合を避ける

この方法を使えば、API有効化漏れによるterraform applyの失敗を事前に防げます。特に、チームで開発している場合や、複数の環境を管理している場合に効果的です。

参考

Discussion