Terraform で Cloud Run を管理しようとした時の話

2023-06-29

はじめに

Belong では、Terraform を利用して GCP 上のリソースをコードで管理しています。 Terraform によるリソースの変更適用は CI/CD により自動化されており、任意のタイミングでリリースを行うことができます。 しかし、私たちのチームでは Cloud Run を Terraform で管理しておらず、初回デプロイ時や設定の変更時に手動での変更が必要となっている状況でした。

本記事では「Cloud Run をもうちょっと上手く使うための 5 つの Tips / 4. Terraform を活用する1」の内容を元に、実際に Cloud Run を Terraform で管理できるかを検証した結果とどのような問題があったのかを検証手順とともにお伝えしようと思います。

モチベーション

私たちのチームでは、前述の通り GCP のリソースを管理するために Terraform を利用しており、アプリケーションのリポジトリと分けてそれぞれの CI/CD で独立リリースできるような形を取っています。 Terraform 上では Data Sources で Cloud Run のリソースを参照しており、新しく Cloud Run の Service を立ち上げる際に基本的に以下のような手順でリリースを行っています。

  1. Cloud Run を適当な image を指定して手動でデプロイする。
  2. terraform apply で Cloud Run を参照するリソースを作成する。
  3. アプリケーションをビルドして CI/CD からアプリケーションの image でデプロイする。

前述の作業にあたり、以下の手間やリスクが発生していました。

  • アプリケーションのリリースとインフラのリリースに順序制約が生まれるためリリース時に注意が必要になる。
  • 初回デプロイのための gcloud コマンドの用意が必要になる。
  • IAM 管理者に gcloud コマンドを実行するための権限2申請が必要になる。
    • IAM で最小特権の原則のベストプラクティスに従い、本番環境では開発者は基本的にサポート業務のために必要となる権限以外は持たないようにしています。

加えて、その後 Cloud Run の設定(e.g. ingress)が変更になった際にも前述の同じ手間やリスクが発生していました。 頻度こそ多くないものの、より健全な形で Cloud Run を管理できないかと感じていた際に記事1を見かけ、紹介されている方法で実際に管理ができそうか試そうと思ったというのが経緯でした。

Cloud Run Admin API について

2023-06-29 現在 Cloud Run Admin API には v1 と v2 が存在しています。

同様に Terraform 側にもそれに対応する v1 と v2 のリソース存在しており、v2 が推奨であることが記載されています。

記事1では v1 のリソースが利用されていましたが、推奨である v2 のリソースを利用することにしました。

何がうまくいかなかったか

記事1にあるように image を ignore_changes に指定することでアプリケーション側の CI/CD からデプロイで変更される要素が無視され、Terraform 側で Cloud Run の設定を変更する時以外に差分が出ないことが期待値です。

検証は以下の環境で行いました。

  • terraform: v1.5.1
  • provider: hashicorp/google v4.70.0
  • gcloud: v343.0.0

まずは記事にある内容と同等の内容を google_cloud_run_v2_service で定義して terraform apply を実行します。

resource "google_cloud_run_v2_service" "default" {
  name     = "cloud-run-v2-test"
  location = "us-central1"

  template {
    containers {
      image = "us-docker.pkg.dev/cloudrun/container/hello"
    }
  }

  lifecycle {
    ignore_changes = [
      template[0].containers[0].image
    ]
  }
}

その後、アプリケーション側の CI/CD からのデプロイ再現として以下の gcloud を実行します。

% gcloud run deploy cloud-run-v2-test --region us-central1 --image xxx

gcloud では、image 以外引数の指定はしていないため、前述の通り terraform plan で差分が出ないこと期待値です。 しかし、実際に実行すると gcloud でデプロイされたことが差分として出てしまっている様子でした。

% terraform plan
...
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # google_cloud_run_v2_service.default will be updated in-place
  ~ resource "google_cloud_run_v2_service" "default" {
      - client                  = "gcloud" -> null
      - client_version          = "436.0.0" -> null
        id                      = "projects/xxx/locations/us-central1/services/cloud-run-v2-test"
        name                    = "cloud-run-v2-test"
        # (17 unchanged attributes hidden)

      ~ template {
          -revision                        = "cloud-run-v2-test-00002-pay" -> null
            # (6 unchanged attributes hidden)

            # (2 unchanged blocks hidden)
        }

        # (1 unchanged block hidden)
    }
...

gcloud デプロイによる差分は出てほしくないので、 差分が出ているものを ignore_changes に追加で指定します。

resource "google_cloud_run_v2_service" "default" {
  name     = "cloud-run-v2-test"
  location = "us-central1"

  template {
    containers {
      image = "us-docker.pkg.dev/cloudrun/container/hello"
    }
  }

  lifecycle {
    ignore_changes = [
      client,
      client_version,
      template[0].containers[0].image,
      template[0].revision,
    ]
  }
}

再度 terraform plan を実行すると差分がなくなりました。 そのまま続いて Cloud Run の設定変更を想定して ingress を all から internal へ変更して terraform apply してみます。

resource "google_cloud_run_v2_service" "default" {
  name     = "cloud-run-v2-test"
  location = "us-central1"
  ingress  = "INGRESS_TRAFFIC_INTERNAL_ONLY"

  template {
    containers {
      image = "us-docker.pkg.dev/cloudrun/container/hello"
    }
  }

  lifecycle {
    ignore_changes = [
      client,
      client_version,
      template[0].containers[0].image,
      template[0].revision,
    ]
  }
}

エラーになりました。revisionignore_changes に指定したことで、リビジョン名が衝突するようになってしまったようです。

% terraform apply
~~~
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:
  # google_cloud_run_v2_service.default will be updated in-place
  ~ resource "google_cloud_run_v2_service" "default" {
        id                      = "projects/xxx/locations/us-central1/services/cloud-run-v2-test"
      ~ ingress                 = "INGRESS_TRAFFIC_ALL" -> "INGRESS_TRAFFIC_INTERNAL_ONLY"
        name                    = "cloud-run-v2-test"
        # (18 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }
~~~
╷
│ Error: Error updating Service "projects/xxx/locations/us-central1/services/cloud-run-v2-test": googleapi: Error 409: Revision named 'cloud-run-v2-test-00002-pay' with different configuration already exists.
│
│   with google_cloud_run_v2_service.default,
│   on main.tf line 45, in resource "google_cloud_run_v2_service" "default":
│   45: resource "google_cloud_run_v2_service" "default" {
│
╵

revision で差分が出てしまうのは気になりますが、エラーになり設定変更を適用できないのは困るので ignore_changes から revision の指定を削除した上で再度 terraform apply してみます。

% terraform apply
~~~
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # google_cloud_run_v2_service.default will be updated in-place
  ~ resource "google_cloud_run_v2_service" "default" {
        id                      = "projects/xxx/locations/us-central1/services/cloud-run-v2-test"
      ~ ingress                 = "INGRESS_TRAFFIC_ALL" -> "INGRESS_TRAFFIC_INTERNAL_ONLY"
        name                    = "cloud-run-v2-test"
        # (18 unchanged attributes hidden)

      ~ template {
          -revision                        = "cloud-run-v2-test-00002-pay" -> null
            # (6 unchanged attributes hidden)

            # (2 unchanged blocks hidden)
        }

        # (1 unchanged block hidden)
    }
~~~
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

成功しました。 確認すると想定の ingress の変更は適用されていますが、template 以下の revision の差分による影響か、新しいリビジョンが意図せず作成されました。 リビジョンごとに設定する timeout や min instance などを変更した際に新しくリビジョンが作成されるのは想定通りの挙動です。 しかしサービスの設定である ingress の変更で新しいリビジョンがデプロイされるのは 期待した挙動ではありません。 実際に GCP console 画面から ingress 設定を変更しても新しくリビジョンは作成されません。

この挙動の原因について

前述の挙動について疑問に思い調べていると、それについて触れている issue3 が見つかりました。 結論から言うと、この挙動は gcloud や GCP Console の仕様上の問題であるようです。issue3 で説明されていることを自分なりに噛み砕いた内容を以下にまとめます。

  • Cloud Admin API v1 ではリビジョン名の指定が必須。
  • そのため gcloud や Cloud Console などのクライアント側でわかりやすいリビジョン名(e.g. xxx-00001-abc)を生成してリクエストしている。
  • google_cloud_run_serviceautogenerate_revision_name の設定値があるのはリビジョン名の自動生成がクライアント側に責務になったいたため。
  • Cloud Admin API v2 ではリビジョン名の指定が任意であり、空の場合には gcloud や Cloud Console が生成していたものと同等の名前を自動生成する。
  • そのため google_cloud_run_v2_service では template 以下の revision の指定を空にすることで自動的にリビジョン名の生成ができる。これが state として null になる。
  • しかし、gcloud が前述の挙動のため gcloud でデプロイすると gcloud で生成した名前でリビジョン名が指定されている状態となり Terraform としては null と差分が出る状況になる。

イメージとしてはこのような形です。

上記から、google_cloud_run_service は Cloud Admin API v1 を利用しており、autogenerate_revision_nametrue 指定する ことで同様な自動生成・リビジョン名の指定があるため、gcloud デプロイ後に Terraform と差分が出ないことになります。

v1 では実際どうだったのか

上で検証した手順と同じように terraform apply + gcloud deploy を実行しました。

resource "google_cloud_run_service" "default" {
  name     = "cloud-run-v1-test"
  location = "us-central1"

  template {
    spec {
      containers {
        image = "us-docker.pkg.dev/cloudrun/container/hello"
      }
    }
  }

  lifecycle {
    ignore_changes = [
      template[0].spec[0].containers[0].image
    ]
  }
}

その後 terraform plan を実行すると、この時点では差分なしで想定通りでした。 続いて同じように ingress を all から internal へ変更します。

resource "google_cloud_run_service" "default" {
  name     = "cloud-run-v1-test"
  location = "us-central1"

  metadata {
    annotations = {
      "run.googleapis.com/ingress" = "internal"
    }
  }

  template {
    spec {
      containers {
        image = "us-docker.pkg.dev/cloudrun/container/hello"
      }
    }
  }

  lifecycle {
    ignore_changes = [
      template[0].spec[0].containers[0].image
    ]
  }
}

するとなぜかこのタイミングの terraform plan で v2 と同じような差分が出るようになりました。 これは ignore_changes に指定して terraform apply します。

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # google_cloud_run_service.default will be updated in-place
  ~ resource "google_cloud_run_service" "default" {
        id                         = "locations/us-central1/namespaces/xxx/services/cloud-run-v1-test"
        name                       = "cloud-run-v1-test"
        # (4 unchanged attributes hidden)

      ~ metadata {
          ~ annotations      = {
              - "run.googleapis.com/client-name"    = "gcloud" -> null
              - "run.googleapis.com/client-version" = "436.0.0" -> null
              ~ "run.googleapis.com/ingress"        = "all" -> "internal"
                # (4 unchanged elements hidden)
            }
            # (6 unchanged attributes hidden)
        }

        # (2 unchanged blocks hidden)
    }
resource "google_cloud_run_service" "default" {
  name     = "cloud-run-v1-test"
  location = "us-central1"

  metadata {
    annotations = {
      "run.googleapis.com/ingress" = "internal"
    }
  }

  template {
    spec {
      containers {
        image = "us-docker.pkg.dev/cloudrun/container/hello"
      }
    }
  }

  lifecycle {
    ignore_changes = [
      metadata[0].annotations["run.googleapis.com/client-name"],
      metadata[0].annotations["run.googleapis.com/client-version"],
      template[0].spec[0].containers[0].image,
    ]
  }
}

terraform apply は成功し、ingress は internal になりつつリビジョンはデプロイされてませんでした。期待通りの挙動です。 結論、v1 API を利用している google_cloud_run_service であれば、アプリケーションの CI/CD から gcloud によるデプロイを行いつつ、 Cloud Run を Terraform で管理することができそうです。

% terraform apply
~~~
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # google_cloud_run_service.default will be updated in-place
  ~ resource "google_cloud_run_service" "default" {
        id                         = "locations/us-central1/namespaces/xxx/services/cloud-run-v1-test"
        name                       = "cloud-run-v1-test"
        # (4 unchanged attributes hidden)

      ~ metadata {
          ~ annotations      = {
              ~ "run.googleapis.com/ingress"        = "all" -> "internal"
                # (6 unchanged elements hidden)
            }
            # (6 unchanged attributes hidden)
        }

        # (2 unchanged blocks hidden)
    }
~~~
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

まとめ

v1 API を利用している google_cloud_run_service であれば、記事1にある通りアプリケーションの CI/CD から gcloud によるデプロイを行いつつ、 Cloud Run を Terraform で管理することができそうです。

しかし、v2 API を利用している google_cloud_run_v2_service では gcloud の仕様の問題により、現状は適切に管理することが難しい状況です。 私としては、絶対に Terraform で管理したいという温度感ではないことに加え、Cloud Run Admin API v2 にまだクライアントが追いついていないだけなのかもしれない、と感じたため今回は見送りすることにしました。

issue3 内でもこの問題をフォローアップをしたいと考えているように見えるので、今後 google_cloud_run_v2_service でも google_cloud_run_service と同じような管理できるようになることに期待です!

Belong ではエンジニアを募集しています。興味のある方は、以下リンクをぜひご覧ください!

Footnotes

  1. https://zenn.dev/kyo2bay/articles/ec4242286d12f2#4.-terraform-%E3%82%92%E6%B4%BB%E7%94%A8%E3%81%99%E3%82%8B 2 3 4 5

  2. https://cloud.google.com/run/docs/deploying#permissions_required_to_deploy

  3. https://github.com/hashicorp/terraform-provider-google/issues/13410 2 3