先日、既存のCloud Functionsを第1世代から第2世代へ移行する機会がありました。 その際、IAM設定が想像以上に変わっていたことに驚き、また、カスタムサービスアカウントの設定方法も第1世代とは異なっていました。

この記事では、実際の移行作業で得た技術的な気づきと、正常に動作させるまでに必要だった設定を共有します。

この記事の対象読者

  • Google CloudでCloud Functionsを使用している方
  • 第1世代から第2世代への移行を検討・実施している方
  • TerraformでCloud Functionsを管理している方

移行時に最初に直面した問題:IAMリソースタイプの変更

第2世代への移行で最初につまずいたのは、TerraformのIAMリソースが全く異なることでした。

第1世代での設定(従来の方法)

# 第1世代ではこう書いていた
resource "google_cloudfunctions_function_iam_member" "invoker" {
  project        = var.project_id
  region         = "asia-northeast1"
  cloud_function = "my-function"
  role           = "roles/cloudfunctions.invoker"
  member         = "serviceAccount:${var.service_account_email}"
}

第2世代で必要な設定(Cloud Runリソースを使用)

# 第2世代ではCloud Runのリソースを使う必要がある
resource "google_cloud_run_service_iam_member" "invoker" {
  project  = var.project_id
  location = "asia-northeast1"
  service  = "my-function"  # 関数名がそのままサービス名になる
  role     = "roles/run.invoker"  # ロールも変わっている!
  member   = "serviceAccount:${var.service_account_email}"
}

この変更の理由は、第2世代のCloud FunctionsがCloud Runベースで動作するようになったためです1。 つまり、内部的にはCloud Runサービスとして扱われているということです。

第1世代と第2世代のロールの違い(ここが重要!)

移行作業で最も苦戦したのが、必要なロールが大きく変わっていたことでした。 Google Cloudの公式ドキュメントを調査した結果、以下の違いが明確になりました:

デプロイ時に必要なロール

第1世代

resource "google_project_iam_member" "cloud_build_permissions_gen1" {
  for_each = toset([
    "roles/cloudfunctions.developer",  # Cloud Functions開発者
    "roles/iam.serviceAccountUser",    # サービスアカウント使用権限
  ])
  project = var.project_id
  role    = each.value
  member  = "serviceAccount:${google_service_account.cloud_build.email}"
}

第2世代2

resource "google_project_iam_member" "cloud_build_permissions_gen2" {
  for_each = toset([
    "roles/cloudfunctions.developer",  # Cloud Functions開発者(まだ必要)
    "roles/run.admin",                 # Cloud Run管理者権限が追加で必要!
    "roles/iam.serviceAccountUser",    # サービスアカウント使用権限
    "roles/eventarc.admin",            # Pub/Subトリガーを使う場合は必須
    "roles/artifactregistry.reader",   # コンテナイメージへのアクセス
  ])
  project = var.project_id
  role    = each.value
  member  = "serviceAccount:${google_service_account.cloud_build.email}"
}

実行時に必要なロール

第1世代

  • roles/cloudfunctions.invoker - 関数を呼び出すため

第2世代3

  • roles/run.invoker - Cloud Runサービスとして呼び出すため4
  • roles/eventarc.eventReceiver - Pub/Sub以外のEventarcイベントを受信する場合5

この違いを理解せずに移行すると、「権限があるはずなのに動かない」という状況に陥ります。 特にroles/run.adminが必要なことは、公式ドキュメントの「Cloud Functions第2世代はCloud Runインフラストラクチャ上で動作する」という記述6を理解していないと気づきにくいポイントでした。

サービスアカウントの変更点

さらに重要な変更点として、デフォルトのランタイムサービスアカウントが変更されています7

第1世代

  • ランタイム: PROJECT_ID@appspot.gserviceaccount.com(App Engineデフォルト)

第2世代

  • ランタイム: PROJECT_NUMBER-compute@developer.gserviceaccount.com(Compute Engineデフォルト)

カスタムサービスアカウントの実装で学んだこと

デフォルトサービスアカウントの課題

デフォルトではPROJECT_NUMBER-compute@developer.gserviceaccount.comが使用されますが、このアカウントはEditor権限を持っています8。セキュリティの観点から、専用のサービスアカウントを作成することにしました。

実際に動作した構成

以下は、実際のプロジェクトで正常に動作した構成を汎用化したものです:

# Cloud Build用サービスアカウント(デプロイを実行する)
resource "google_service_account" "cloud_build" {
  account_id   = "cloud-build-deployer"
  project      = var.project_id
  display_name = "Cloud Build Deployer"
  description  = "Cloud Functionsのデプロイを実行するサービスアカウント"
}

# Cloud Functions実行用サービスアカウント
resource "google_service_account" "cloud_functions" {
  account_id   = "cf-my-function"
  project      = var.project_id
  display_name = "Cloud Functions Runtime"
  description  = "Cloud Functions実行時に使用するサービスアカウント"
}

必要な権限設定

ここが重要なポイントです。Cloud BuildがCloud Functionsのサービスアカウントを使用してデプロイを実行するため、roles/iam.serviceAccountUser権限の付与が必要でした9

# Cloud BuildがFunctionsのサービスアカウントを使用するための権限
resource "google_service_account_iam_member" "cloud_build_act_as" {
  service_account_id = google_service_account.cloud_functions.name
  role               = "roles/iam.serviceAccountUser"
  member             = "serviceAccount:${google_service_account.cloud_build.email}"
}

この権限により、Cloud Buildサービスアカウントが、Cloud Functions用のサービスアカウントをCloud Functionsの実行アイデンティティとして指定できるようになります。この設定がないと、デプロイ時に権限エラーが発生します。

Pub/Subトリガーで遭遇した特殊なケース

Pub/Subサービスアカウントの形式

Pub/Subトリガーを使用する場合、以下の設定が必要でした:

resource "google_cloud_run_service_iam_member" "pubsub_invoker" {
  project  = var.project_id
  location = "asia-northeast1"
  service  = "my-function"
  role     = "roles/run.invoker"
  member   = "serviceAccount:service-${var.project_number}@gcp-sa-pubsub.iam.gserviceaccount.com"
}

ここで注意すべきは、PROJECT_IDではなくPROJECT_NUMBERを使用する点です。最初はこれに気づかず、なぜ権限エラーが出るのか悩みました。

なぜPub/SubのIAM設定が複雑になったのか

調査したところ、第2世代ではPub/SubトリガーがEventarcを介して動作するようになったためでした10。公式ドキュメントによると、Eventarcトリガーを作成する際にはroles/eventarc.admin権限が必要で11、トリガーサービスアカウントにはroles/run.invoker権限が必要です12

Cloud Build経由でデプロイする場合の追加要件

公式ドキュメントを確認したところ、Cloud Build経由でGen2関数をデプロイする場合、以下の追加権限も必要でした13

# Cloud Buildサービスアカウントに追加で必要な権限
resource "google_project_iam_member" "cloud_build_additional" {
  for_each = toset([
    "roles/storage.objectViewer",     # ソースコードへのアクセス
    "roles/logging.logWriter",        # ビルドログの書き込み
  ])
  project = var.project_id
  role    = each.value
  member  = "serviceAccount:${google_service_account.cloud_build.email}"
}

Cloud Buildでのデプロイ設定

実際に使用したCloud Build設定(汎用化済み):

steps:
  - id: deploy cloud function
    name: gcr.io/google.com/cloudsdktool/cloud-sdk
    entrypoint: 'bash'
    args:
      - '-c'
      - |
        gcloud functions deploy my-function \
        --gen2 \
        --region=asia-northeast1 \
        --trigger-topic=my-topic \
        --runtime=nodejs20 \
        --entry-point=main \
        --service-account=${_CF_SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com

--gen2フラグと--service-accountオプションが重要です。

Secret Managerとの統合(追加の発見)

環境変数ではなくSecret Managerを使う利点

移行作業中に、Secret Managerとの統合が第2世代でより使いやすくなっていることに気づきました2

# Secret Managerへのアクセス権限
resource "google_secret_manager_secret_iam_member" "function_secrets" {
  for_each = toset([
    "API_KEY",
    "DATABASE_PASSWORD",
  ])
  project   = var.project_id
  secret_id = each.value
  role      = "roles/secretmanager.secretAccessor"
  member    = "serviceAccount:${google_service_account.cloud_functions.email}"
}

デプロイ時の設定:

--set-secrets="API_KEY=API_KEY:latest" \
--set-secrets="DB_PASSWORD=DATABASE_PASSWORD:latest"

これにより、環境変数に直接機密情報を設定する必要がなくなりました。

移行作業で得た教訓

1. 関数名とCloud Runサービス名の関係

第2世代では、デプロイした関数名がそのままCloud Runサービス名になります。これを理解していないと、IAM設定時に混乱します。

2. PROJECT_NUMBERとPROJECT_IDの使い分け

特にPub/Subのサービスアカウントでは、必ずPROJECT_NUMBERを使用する必要があります:

正しい: service-123456789@gcp-sa-pubsub.iam.gserviceaccount.com
間違い: service-my-project-id@gcp-sa-pubsub.iam.gserviceaccount.com

3. 権限エラーのデバッグ方法

権限エラーが発生した場合、Cloud Loggingで以下を確認すると原因が特定しやすいです:

  • どのサービスアカウントが使用されているか
  • どのロールが不足しているか
  • リソース名(特にCloud Runサービス名)が正しいか

まとめ

Cloud Functions第2世代への移行では、以下の点で大きな変更がありました:

  1. IAMリソースタイプの変更google_cloudfunctions_function_iam_memberからgoogle_cloud_run_service_iam_member
  2. ロールの変更roles/cloudfunctions.invokerからroles/run.invoker
  3. 追加で必要なロールroles/run.adminroles/eventarc.adminroles/artifactregistry.reader
  4. Pub/Subトリガーの複雑化:Eventarc経由になったことによる追加設定
  5. デフォルトサービスアカウントの変更:App EngineデフォルトからCompute Engineデフォルトへ
  6. カスタムサービスアカウントの権限roles/iam.serviceAccountUserの必要性

これらの変更は、第2世代がCloud Runベースになったことに起因しています。 公式ドキュメントにも「Cloud Functions第2世代はCloud Runインフラストラクチャ上で動作する」と明記されており14、この根本的なアーキテクチャの変更が、IAM設定の違いにつながっています。

移行作業を行う際は、これらの違いを理解しておくと、スムーズに進められるはずです。 特に、Cloud Run関連の権限が必要になることは、事前に知っておくべき重要なポイントです。

この記事が、同じような移行作業を行う方の参考になれば幸いです。


参考資料