Rebounder Tech Blog

運用している当事者が書く、本番システムの記録。

Cloud Run Job を初めて有効化すると SecretsAccessCheckFailed で落ちる

公開 読了時間 約5分執筆: Rebounder 開発チーム(当該システムの運用当事者)

※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。

結論

Terraform で Cloud Run Job と、その job が参照する secret accessor の IAM バインディングを同じ apply で新規作成すると、IAM 権限が伝播しきる前に Cloud Run 側の作成時アクセス検証が走り、SecretsAccessCheckFailed で job が tainted される。

結論

Terraform で Cloud Run Job を新規に有効化するとき、job が参照する Secret Manager の accessor IAM バインディングを同じ apply の中で一緒に作成すると、job が SecretsAccessCheckFailed で tainted になることがあります。 原因は権限の設定漏れではなく、IAM 権限が伝播しきる前に Cloud Run 側の作成時検証が走ってしまう競合です。depends_on で「IAM を先に確定させてから job を作る」という順序を明示すれば止まります。

症状

count = var.enabled ? 1 : 0 のように、フラグで新規に有効化する形の google_cloud_run_v2_job リソースがあります。この job は DB 接続文字列・Sentry DSN・Slack Webhook URL の3つを google_secret_manager_secret_iam_member で許可された Secret Manager のシークレットから読みます。

enabled = true にして**本番で初めて apply した 2026-06-09 に、実際にこれを踏みました。**job の作成自体が失敗し、terraform apply はリソースを tainted のまま終了します。

このとき IAM の設定自体は正しく入っています。後述するとおり原因が「設定漏れ」ではなく「伝播の遅れ」なので、IAM ポリシーを読み出しても必要なバインディングは揃って見えるはずです。設定は合っているのに、作成だけが失敗するという形で表に出ます。

原因

google_cloud_run_v2_job と、それが依存する google_secret_manager_secret_iam_member を同じ apply の中で新規に宣言した場合、両者の間に明示的な依存関係が無ければ Terraform はこの2種類のリソースを並行に作成しようとします。

一方、IAM のバインディングは API 呼び出しが成功を返した時点で、Cloud Run のデータプレーン側まで即座に反映されるわけではありません。反映には短い遅延があります。ここに、Cloud Run Job の作成処理自体が持つ挙動が重なります。job を新規作成する際、Cloud Run は指定された secret に対してアクセス権があるかを作成時に検証します。IAM のバインディングが API 上は成功していても、その伝播がまだ終わっていないタイミングでこの検証が走ると、権限が無いのと同じ扱いになり SecretsAccessCheckFailed で job が error 状態に入り、Terraform 側ではリソースが tainted になります。

Terraform の依存関係グラフは、リソース間の参照(var.xxxgoogle_xxx.yyy.zzz のような式での参照)が無い限り、実行順序を保証しません。この構成では job 側が IAM バインディングのリソース属性を直接参照していなかったため、Terraform には「IAM を先に作ってよい」根拠が無く、並行実行を選んでいました。

直し方

google_cloud_run_v2_job に、参照する3つの google_secret_manager_secret_iam_member への depends_on を明示します。

resource "google_cloud_run_v2_job" "tv_liveness" {
  count = var.enabled ? 1 : 0

  depends_on = [
    google_secret_manager_secret_iam_member.runtime_database_url,
    google_secret_manager_secret_iam_member.runtime_sentry_dsn,
    google_secret_manager_secret_iam_member.runtime_slack_webhook_url,
  ]

  project  = var.project_id
  location = var.region
  name     = var.job_name
  # ...
}

これで Terraform は IAM バインディングの apply が完了してから job の作成に進むようになります。IAM バインディング自体の伝播遅延そのものは無くなりませんが、job の作成時検証が走るタイミングを、伝播がほぼ終わったあとまで後ろに押し出すことになるため、実務上は競合が再現しなくなります。

すでに tainted になっている job は、terraform untaint で taint 状態を外してから再度 apply すれば復旧します。IAM バインディング自体はすでに存在し伝播も済んでいるため、2回目の作成では同じ競合は起きません。

再発防止

この depends_on は本番だけでなく staging 側の同じ module にも適用しました。同じ apply の中で secret accessor の IAM バインディングと、それを参照する Cloud Run リソースを新規作成する構成では、参照式による暗黙の依存が無い限り depends_on を明示する、という方針に固定しています。var.xxx を介した参照であれば Terraform が自動で順序を解決しますが、この構成のように IAM 側のリソース名を直接式に埋め込んでいない場合は、依存関係が Terraform から見えないため明示するしかありません。

Cloud Run Job まわりでは、実行はできるのに中身が古いままという別種の落とし穴もあります。Cloud Run Job はイメージが凍る を参照してください。

よくある質問

Q1なぜ最初の apply でしか起きないのですか?

count = var.enabled ? 1 : 0 のように job をトグルで新規作成する構成では、job リソースと secret accessor の IAM バインディングが同じ apply の中で並行に作られます。IAM の伝播はコマンドが返ってから実際に効くまでに遅延があるため、Cloud Run 側の作成時検証がその遅延の中に割り込みます。一度 job が作られてしまえば以降の apply では作成処理自体が走らないため、同じ競合は再現しません。

Q2IAM ポリシーを確認しても正しく見えるのはなぜですか?

IAM ポリシーの読み出しが返すのはコントロールプレーン側の設定値で、その場で正しく見えます。失敗の原因は設定の誤りではなく、その設定が Cloud Run のデータプレーン側まで反映されるまでの遅延です。権限が「無い」のではなく「まだ効いていない」状態なので、ポリシーを何度読み直しても手がかりは出ません。

Q3tainted になった job はどう復旧しますか?

terraform untaint で対象リソースの taint 状態を外し、再度 apply します。IAM バインディング自体はすでに作成されて伝播も済んでいるため、2回目の apply では同じ競合は起きず job が Ready になります。

Q4この depends_on は他の secret を使う Cloud Run Job にも要りますか?

同じ apply で secret accessor の IAM バインディングと job を新規作成する構成であれば、参照する secret の数によらず必要です。IAM バインディングをリソースとして先に確定させる依存関係を明示しない限り、Terraform 側には両者を並行に作ってよい根拠しか無いためです。

この記事の根拠

  • Terraformファイル 63〜105行目コミット 61d59dd

本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。