複数人でTerraform(インフラをコードで定義・管理するツール)を使った構成管理をしているチームで、CIのapply(変更をクラウドに適用する実行)が稀に失敗することはないでしょうか。
エラーメッセージにError acquiring the state lockやConditionalCheckFailedExceptionが出ている場合、原因はHCL(Terraformの設定記述言語)の書き方ではありません。ほぼ確実にstate(現在のインフラ状態を記録したファイル)の競合です。業務システムのインフラをTerraformで管理している担当者、特にCIパイプラインを複数チームで共用している現場に向けて、原因と対策を整理します。
何が起きるか
GitHub Actionsなどで複数のパイプラインが同時に走ると、次のようなエラーが出ます。
Error: Error acquiring the state lock
Error message: operation error DynamoDB: PutItem, ConditionalCheckFailedException
Lock Info:
ID: 0b3c9f41-...
Operation: OperationTypePlan
Who: runner@fv-az1234このエラーが出ると、再実行しても同じ結果になります。ロックがまだ解放されていないからです。
対処に慣れていないとforce-unlockでロックIDを強制解除する操作に流れがちです。これは一時しのぎにはなりますが、本番のapply中に誤って使うと、state破損のリスクを背負うことになります。
さらに踏み込んで「ロック機構そのものが邪魔」と判断し、ロック用のDynamoDBテーブルを削除してしまうケースもあります。これは根本原因を見誤った対応です。
なぜ起きるか
原因を分解すると、構造はシンプルです。
1つのstateファイルに対して、2つ以上のパイプラインが同時にplanやapplyを実行しようとしている。これがすべての始まりです。
Terraformのstateは「このリソースは今どういう設定で存在しているか」を記録した唯一の台帳です。複数のプロセスが同時に書き込めば、台帳の整合性が崩れます。
それを防ぐためのロック機構があるのに、CI側の設計がそれを前提にしていないことが根本原因です。具体的には、同じstateを触るワークフローに「同時実行の制御」が入っていないケースがほとんどです。
たとえば、PRのマージごとにapplyジョブが起動する設定だと、短時間に複数のマージが続いた場合、ジョブが並列に走ってしまいます。stateは1つしかないのに、それを触る手が2つ以上あるという状態です。
また、ロック用のバックエンド(AWSならS3+DynamoDBの組み合わせが定番)を別建てで運用していることも、トラブルシューティングを複雑にしている一因です。DynamoDBのテーブル設定やIAM権限の不備でロック取得自体が不安定になり、「競合」なのか「ロック機構の故障」なのか切り分けづらくなります。
自分のプロジェクトが該当するか確認する方法
まず、CIのワークフロー定義を開いて、同じstateを参照するジョブに同時実行制御があるか確認してください。
GitHub Actionsなら.github/workflows/*.ymlの中にconcurrencyキーがあるかを探します。ない場合は、複数ジョブが同時にapplyへ到達できる状態です。
次に、バックエンド設定を確認します。backend.tfやmain.tfのterraform { backend "s3" { ... } }ブロックを見てください。dynamodb_tableの指定があれば、ロック管理にDynamoDBを使う旧来型の構成です。
バージョンも併せて確認します。
terraform versionTerraform 1.6以降、またはOpenTofu(Terraformからフォークしたオープンソース版)の比較的新しいバージョンでは、S3バケット自体にロック情報を保持する仕組みがサポートされています。これが使えるかどうかで、別途DynamoDBテーブルを維持する必要性が変わります。
なお、OpenTofuはLinux Foundation配下のプロジェクトで、HashiCorpが2023年にTerraformのライセンスをMPL 2.0からBUSL 1.1(本番利用に一部制限がある商用ソースライセンス)に変更したことを機にフォークされた経緯があります。ライセンス面の判断基準は社内の調達・法務プロセスに関わる話なので、技術選定とは別軸で検討する必要があります。
対策の手順
stateロックの競合は、2段構えで対処します。
1. パイプライン側で直列化する
CI側で、同じstateを触るジョブが同時に走らないよう制御します。GitHub Actionsであれば、concurrencyグループをstateの識別子で指定します。
concurrency:
group: infra-apply-${{ github.ref }}
cancel-in-progress: falseこれにより、2つ目のジョブはキャンセルされるのではなく、1つ目が終わるまで待機します。applyの途中でキャンセルされる事故を避けられます。
2. ロック基盤を見直す
ロック専用のDynamoDBテーブルを運用する必要が本当にあるか、見直します。Terraform 1.6以降・最近のOpenTofuでは、S3バケット内にロックファイルを置く方式がサポートされています。これが使えれば、別立てのテーブル管理・IAM権限管理が1つ減ります。
ただし、既存のstateが旧来のDynamoDBロック方式に依存している場合、切り替えには移行手順を踏む必要があります。バックエンド定義を書き換えたあとにterraform init -migrate-stateを実行し、ロック方式の移行が正しく完了したかをterraform planで差分が出ないことを確認してください。
3. 緊急時の運用ルールを明文化する
force-unlockを使う場面を、Runbook(障害対応手順書)としてチームで共有しておきます。誰が・どの条件で・誰の承認を得て実行するかを決めておくと、焦った担当者が独断で実行する事故を防げます。
まとめ
stateロックの競合は、HCLの書き方や経験年数の問題ではありません。CIの同時実行制御が不足していることがほとんどです。
まず.github/workflowsなどのCI定義を開き、同じstateを触るジョブにconcurrency制御があるかを確認してください。次にbackend.tfを見て、DynamoDBロックからS3ネイティブロックへの移行余地があるかを調べます。
force-unlockは最後の手段であり、日常運用の一部にしてはいけません。運用ルールとして明文化し、チーム全体で共有しておくことが、次に同じエラーに出会ったときの被害を小さくします。