n8nをCloud RunにDeployしてIAPで内部アクセス許可する (Terraform)

## はじめに

- [n8n](https://docs.n8n.io/)は社内のLLM活用を助ける強力なノーコードツール
- [Cloud Run](https://cloud.google.com/run?hl=en)はGCPのマネージドコンテナサービス
- [IAP](https://cloud.google.com/iap/docs/concepts-overview)は、Googleアカウントでアクセス管理簡単に実現できる

今回は「n8nをCloud Runでミニマムに動かす」というのをテーマに紹介します。

- 無料枠がたくさんあるCloud Run
- GCSをデータの永続化 (Cloud SQLのDB代も使わずに済む)
- Cloud RunのIAPがPre-GAになったので、アクセス管理が簡単に設定できます。Pre-GAなので、個人や社内利用くらいまでが良さそうです。

## Terraformの設定

### Cloud Storage

GCSのBucket作成

```hcl
resource "google_storage_bucket" "n8n_data" {
  name          = "naka-test-n8n-data"
  location      = var.region
  force_destroy = false

  uniform_bucket_level_access = true
}
```

### Service Account

Service Accountの作成と上で作成したBucketへの権限を付与

```hcl
# service account for cloud run
resource "google_service_account" "n8n" {
  account_id   = "n8n-service" # need to be between 6-30 characters long
  display_name = "n8n"
}

# access to bucket
resource "google_storage_bucket_iam_member" "n8n_gcs_access" {
  bucket = google_storage_bucket.n8n_data.name
  role   = "roles/storage.objectUser"
  member = google_service_account.n8n.member
}
```

### Cloud Run service

Cloud Run serviceとIAPの設定

```hcl
# n8n Cloud Run service
resource "google_cloud_run_v2_service" "n8n" {
  provider     = google-beta
  name         = "n8n"
  location     = var.region
  launch_stage = "BETA" // IAP is Pre-GA
  iap_enabled  = true   // enable IAP

  template {
    containers {
      image = "n8nio/n8n:1.93.0"

      ports {
        container_port = 5678
      }

      resources {
        limits = {
          cpu    = "1"
          memory = "512Mi"
        }
      }

      env {
        name  = "DB_TYPE"
        value = "sqlite"
      }

      env {
        name  = "N8N_USER_FOLDER"
        value = "/home/node/.n8n"
      }

      env {
        name = "N8N_WEBHOOK_URL"
        value = "<cloud run のendpoint>"
      }

      env {
        name = "N8N_PROTOCOL"
        value = "HTTPS"
      }

      volume_mounts {
        name       = "n8n-data"
        mount_path = "/home/node/.n8n"
      }
    }

    service_account = google_service_account.n8n.email

    # gcs volume
    volumes {
      name = "n8n-data"
      gcs {
        bucket = google_storage_bucket.n8n_data.name
      }
    }

    scaling {
      min_instance_count = 1
      max_instance_count = 1
    }
  }
}
```

:::note warn
min_instance_countを1にしてスケジュールなどのBackgroundタスクも実行できるようにします。
スケジュールなど使わない場合には、min_instance_countを0にして、アクセスがない場合にはSleepさせてコストを抑えることも可能です
:::

:::note warn
max_instance_countを1にして、複数インスタンスから同一ファイルへの書き込みロックはないので一つのインスタンスのみにしています。ここらへんが気になる場合は、DB_TYPEをpostgresdbなどのDatabaseにしたほうが良いでしょう。
> Cloud Storage FUSE では、同じファイルへの複数書き込みの同時実行制御（ファイルのロック）は行いません。複数の書き込みによってファイルの置き換えが試みられた場合は、最後の書き込みが有効となり、それより前の書き込みはすべて失われます。　([ref](https://cloud.google.com/run/docs/configuring/services/cloud-storage-volume-mounts?hl=ja#volume-mount-options))
:::

:::note info
IAPを使うには現時点では、google-beta providerの v6.30.0 以上が必要です。
:::


### IAPの権限付与

```hcl
locals {
  n8n_iap_access_members = [
    "user:your@example.com",
  ]
}
resource "google_iap_web_cloud_run_service_iam_member" "n8n_iap_access" {
  for_each               = toset(local.n8n_iap_access_members)
  project                = google_cloud_run_v2_service.n8n.project
  location               = google_cloud_run_v2_service.n8n.location
  cloud_run_service_name = google_cloud_run_v2_service.n8n.name
  role                   = "roles/iap.httpsResourceAccessor"
  member                 = each.value
}
```

:::note info
google providerの v6.31.0 以上が必要です。
:::

## Service AccountからIAPの後ろにn8nを呼び出す

:::note info
今回は n8nのWebhookのエンドポイントを叩くことにします。(ref: https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/)

GCPの公式ドキュメントは、[こちら](https://cloud.google.com/iap/docs/authentication-howto#gcloud)です。
:::


まず叩くService AccountとCloud RunのEndpointを指定します。Cloud Schedulerから叩く予定であれば、n8n-scheduler@<project>.iam.gserviceaccount.com など Cloud Runで使っているservice accountとは別のservice accountを指定する事ができます。

```
SERVICE_ACCOUNT=xxx@<project>.iam.gserviceaccount.com
URL=$(gcloud run services describe n8n --project <project> --region asia-northeast1 --format json | jq -r '.status.url')
```

次に、jwtを生成するためのclaimを作成します。

```
cat > claim.json << EOM
{
  "iss": "$SERVICE_ACCOUNT",
  "sub": "$SERVICE_ACCOUNT",
  "aud": "$URL/webhook-test/test",
  "iat": $(date +%s),
  "exp": $((`date +%s` + 3600))
}
EOM
```

:::note info
audには、`$URL/webhook-test/test` を設定していますが、n8n上でwebhookを設定したときのtest用のエンドポイントです。本番は、`$URL/webhook-test/test`を `$URL/webhook/test`になります。`/test`の部分は自分で好きなpathを決めることができます。
:::

:::note warn
audienceの設定は、`$URL` のみならず、完全なパスを指定する必要があるのでご注意ください。audienceが実際に叩くPathと異なる場合は、`Invalid IAP credentials: Audience specified does not match requested endpoint`というエラーが返ります。
:::


サインをしてjwtを発行します。

```
gcloud iam service-accounts sign-jwt --iam-account="$SERVICE_ACCOUNT" claim.json output.jwt
```

最後にendpointをたたいて見ます

```
curl -X POST -H "Authorization: Bearer $(cat output.jwt)" "$URL/webhook-test/test"
```

`{"message":"Workflow was started"}` が見えたら叩かれたことを確かめられます。

## Cloud SchedulerからIAPで設定したCloud Runのエンドポイントを叩く (できてない❌️)

Cloud SchedulerからCloud Runを叩く場合、IAPを設定していない場合には、以下のようにService Accountを指定することで実行ができます。

```hcl
# Cloud Scheduler用のサービスアカウント
resource "google_service_account" "n8n_scheduler" {
  account_id   = "n8n-scheduler"
  display_name = "Service Account for Cloud Scheduler to invoke n8n webhook"
}

# サービスアカウントにCloud Run起動権限を付与
resource "google_cloud_run_service_iam_member" "n8n_scheduler_is_run_invoker_of_n8n" {
  project  = google_cloud_run_v2_service.n8n.project
  location = google_cloud_run_v2_service.n8n.location
  service  = google_cloud_run_v2_service.n8n.name
  role     = "roles/run.invoker"
  member   = google_service_account.n8n_scheduler.member
}

# Cloud Schedulerジョブ
resource "google_cloud_scheduler_job" "n8n_webhook_test" {
  name             = "n8n-webhook-test"
  description      = "Daily job to trigger n8n webhook at 9 AM JST"
  schedule         = "0 0 * * *" # 毎日 UTC 0:00 (JST 9:00)
  time_zone        = "Asia/Tokyo"
  attempt_deadline = "120s" # タイムアウトを120秒に設定

  http_target {
    http_method = "POST"
    uri         = "${google_cloud_run_v2_service.n8n.uri}/webhook/test"

    # IAPを使用するための設定
    oidc_token {
      service_account_email = google_service_account.n8n_scheduler.email
      audience              = google_cloud_run_v2_service.n8n.uri
    }
  }

  depends_on = [
    google_cloud_run_service_iam_member.n8n_scheduler_is_run_invoker_of_n8n,
    google_iap_web_cloud_run_service_iam_member.n8n_iap_access,
  ]
}
```

IAPの場合には、User同様に `roles/iap.httpsResourceAccessor`の権限を付与しましたが、

```hcl
# サービスアカウントにIAPの権限を付与
resource "google_iap_web_cloud_run_service_iam_member" "n8n_scheduler_iap_access" {
  project                = google_cloud_run_v2_service.n8n.project
  location               = google_cloud_run_v2_service.n8n.location
  cloud_run_service_name = google_cloud_run_v2_service.n8n.name
  role                   = "roles/iap.httpsResourceAccessor"
  member                 = google_service_account.n8n_scheduler.member
}
```

実行できませんでした。❌️

Error

```json
{
  "insertId": "18etbn1fgv7hvt",
  "jsonPayload": {
    "debugInfo": "URL_ERROR-ERROR_AUTHENTICATION. Original HTTP response code number = 401",
    "status": "UNAUTHENTICATED",
    "jobName": "projects/xxxx/locations/asia-northeast1/jobs/n8n-webhook-test",
    "targetType": "HTTP",
    "@type": "type.googleapis.com/google.cloud.scheduler.logging.AttemptFinished",
    "url": "https://n8n-xxx.asia-northeast1.run.app/webhook/test"
  },
  "httpRequest": {
    "status": 401
  },
  "resource": {
    "type": "cloud_scheduler_job",
    "labels": {
      "project_id": "xxxx",
      "location": "asia-northeast1",
      "job_id": "n8n-webhook-test"
    }
  },
  "timestamp": "2025-05-22T06:36:48.073228186Z",
  "severity": "ERROR",
  "logName": "projects/xxx/logs/cloudscheduler.googleapis.com%2Fexecutions",
  "receiveTimestamp": "2025-05-22T06:36:48.073228186Z"
}
```

これはまだ解決できてないので、どなたかご存知の方いれば教えていただきたいです :bow: 

## まとめ

n8nを簡単にCloud RunでDeployしてアクセス管理をIAPを用いて行える設定を紹介しました。

これによって、Cloud Runで払い出されたエンドポイント `https://n8n-<projectnumber>.<region>.run.app`からアクセスができるようになりました。

Cloud SQLを使うケースは、[n8n on Cloud Run （ツール比較から選定まで）](https://zenn.dev/meziron/articles/bff3ac566f8b93) が参考になります。

## References

- [n8n on Cloud Run （ツール比較から選定まで）](https://zenn.dev/meziron/articles/bff3ac566f8b93)
- [Cloud Run のIAP (Pre-GA) をTerraformで設定する](https://qiita.com/nakamasato/items/cdb8dd7bc24796fc85eb)
- [IAPで保護されたAPIをcurlで叩いてみる](https://zenn.dev/xtone/articles/9b24b5e0265b05)