並行デプロイでTerraformのmigrate_image_tagがliveとズレた
※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。
結論
Terraformのdeployは『apply前にlocalsへタグを書く→applyする』の順でしか記録できないため、2つのセッションが同時にdeployすると、片方の記録がコミットされる前にもう片方のコミットがmainへ先に乗り、live自体は正しいのに台帳だけが古いタグを指す drift が生まれる。
結論
Terraformでは「localsにタグを書く→commitする→applyする」という順でしか本番操作を記録できない。2つのセッションが同時にCloud Runへデプロイすると、片方が実行した操作(migration Jobの実行やstagingへのdeploy)の記録コミットが、もう片方の別コミットより後にmainへ乗ることがある。live自体はどちらも正しく動いているのに、台帳(Terraformのlocals)だけが一時的に古いタグを指す「未記録のdrift」が生まれた。
症状
2026-06-16、ヘッダ崩れを修正した #968 を本番へ反映するため、staging→prodの順でdeployとmigrationを実行した。処理自体は失敗しておらず、staging・prodともに疎通は200。
ところがその裏で、別の並行セッションが #969・#972 という2つの prod web bump を先にコミットしていた。自分が実行した操作の記録をまとめてcommitしようとした時点で、mainは既にそちらのコミットを含んでいる。
# 自分のセッションが記録しようとした時点でmainに残っていた古い値
# (prod/main.tf:169, staging/main.tf:79)
migrate_image_tag = "6618708" # migration 20260615120000(academic_year列 削除+新設)適用のため bump(#954)
# (staging/main.tf:228)
web_image_tag = "6618708" # エディタ刷新 + academic_year撤去。疎通200
自分が実際に本番で実行したのは、#965(監査ハッシュチェーンの誤検知是正)に伴う migration 20260615164918(audit_log.occurred_at のDEFAULTをclock_timestamp()へ変更)と、stagingへの web/migrate 両イメージのdeploy。これらは実行済みだったが、main.tf のlocalsにはまだ反映されていなかった。
原因
このコピー作業自体に欠陥はない。問題は、「本番で何かを実行する」と「それをTerraformのlocalsへ書いてcommitする」が同一トランザクションではないことだった。
- 自分のセッション: staging deploy実行 → prod migration Job実行 → (記録をまとめてcommitする前に)別作業へ
- 並行セッション:
#969(prod webを0a01b5cへbump)→#972(prod webを8aefde1へbump、こちらが#968を内包)
infrastructure/** は元々「直列(tf state)・infra-token」というレーン規律の対象だと文書化されていた(docs/parallel-lanes.md 54行目)。
| **infra レーン** | `infrastructure/**`(Terraform) | 直列(tf state) | infra-token |
だが、この規律が防げるのは「同じ行を同時に書き込んで衝突する」競合であり、今回はstagingの2行とprodの1行で、並行セッションが触ったprodのweb_image_tag行とは重ならなかった。行としては衝突していないのに、記録のコミット順という時間軸でdriftが起きた。
結果として、terraform plan を実行すれば「現在commitされている6618708から実際にliveで動いている227a512相当への差分」が出る状態――つまりcommitted値がliveより古いまま取り残される状態が一時的に生まれていた。
直す
実行済みの操作をすべてlocalsへ反映し、terraform planで両環境とも No changes になることを確認して「committed == live」を取り戻した。
# infrastructure/terraform/envs/prod/main.tf:169
migrate_image_tag = "227a512" # migration 20260615164918: audit_log.occurred_at の DEFAULT を
# clock_timestamp() に変更(#965 是正・列DEFAULT変更のみ=非破壊)。
# staging先行検証後 prod Job 実行済
# infrastructure/terraform/envs/staging/main.tf:79, 228
migrate_image_tag = "227a512" # 同上。staging Job 実行済
web_image_tag = "227a512" # #968 ヘッダ崩れ解消 + #967 WYSIWYG直接編集 + #965 是正。
# ※prod は並行 deploy で 8aefde1(#970 含む)先行・staging は本 227a512
prod側のweb_image_tagはこの修正では触っていない。#972が8aefde1として既にcommit済みで、その8aefde1は#968を祖先として含んでいたため、committedとliveはその時点で一致していた。直す対象は「自分のセッションが実行したのに、まだ誰もcommitしていなかった操作」だけに絞った。
再発防止
このdriftが怖いのは、apply自体は常に成功し、エラーも出ないことだった。terraform applyは「今commitされている値」を正しくliveへ適用するだけで、「committされている値が、実際に実行した作業を正しく反映しているか」までは検証しない。気づく手段は、自分でterraform planを取って現在のliveと見比べるという、能動的な確認しかなかった。
docs/parallel-lanes.mdのinfraレーンの規律は「同じ行を同時に編集しない」ことを目的に設計されている。今回のように行は重ならないが記録のタイミングがずれるパターンまでは、トークンによる直列化だけでは防げない。実行した操作は、他の作業へ移る前に、その場でlocalsへの反映とcommitまで終わらせる――という運用上の徹底が、結局のところ唯一の防御線になっている。
よくある質問
Q1terraform planでNo changesと出ればdriftは無いのでは?
逆に、No changesが出ているからこそdriftが見えにくい。今回のケースは、台帳を修正したあとにplanがNo changesを返したことで『直った』と確認している。修正前は、台帳の値(古いタグ)とlive(新しいタグ)が一致していないにもかかわらず、誰も気づかずに次のデプロイが積まれていく状態だった。plan自体は『今committedな値と比べて差分が無い』ことしか言わず、『committedな値が実際のliveと一致しているか』は別の問題。
Q2git mergeのコンフリクトで検知できなかったのですか?
コンフリクトは起きていない。2つのセッションは同じファイルの別々の行(stagingのweb_image_tagとmigrate_image_tag、prodのmigrate_image_tag)を編集していたため、gitの行単位マージは機械的に両方を正しく統合できた。問題はマージの正しさではなく、実行した本番操作(staging deploy・migration Job実行)を記録するコミット自体が、他セッションの別コミットより後にmainへ乗ったため、その間の状態がリポジトリ上『まだ記録されていない』まま残っていたこと。
Q3このリポジトリにはこの種の衝突を防ぐ仕組みが元から無かったのですか?
`infrastructure/**`を触るレーンはトークンで直列化する、という規律自体は本件より前の2026-05-31付けで文書化されていた(parallel-lanesのinfraレーン)。ただしその規律が防ぐのは『同じ行を同時に書き込む』競合で、今回のように『実行はすでに終えたのに、記録のコミットが他セッションのコミットより遅れて乗る』という時間差には、記録のタイミング自体を強制する仕組みまでは無かった。
Q4なぜ本番環境のmigrate_image_tagだけが古いままで、web_image_tagは問題にならなかったのですか?
prodのweb_image_tagは別セッションの#972が8aefde1へのbumpを先にコミットして記録していたため、その時点で台帳とliveが一致していた。問題が残ったのは、自分のセッションが実行したmigration Job(20260615164918)の記録だけが、staging分と合わせてコミットされる前に取り残されていたため。
確認した環境
- Terraform >=1.9.0 / Cloud Run (Google Cloud)
- 2026-06-16 に発覚・同日中に台帳を補正(#973)
この記事の根拠
- Terraformファイル 169〜169行目コミット b6e5c47
- Terraformファイル 169〜169行目コミット 3a8f39f
- Terraformファイル 79〜79行目コミット b6e5c47
- Terraformファイル 228〜228行目コミット 3a8f39f
- Markdownファイル 54〜54行目コミット 580d9ec
本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。