トラス構造が幾何学的に組まれた建築物のファサード
現場の実践

Terraform運用でstateロック競合が起きる原因と対策

目次を見る

複数人で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 version

Terraform 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は最後の手段であり、日常運用の一部にしてはいけません。運用ルールとして明文化し、チーム全体で共有しておくことが、次に同じエラーに出会ったときの被害を小さくします。

参考

Terraform vs OpenTofu vs Pulumi: Which One Should a Small Team Actually Commit To?

この記事について: 本記事は AI を活用して作成し、forva AI 編集部が内容を確認・監修しています。

AI 駆動開発のご相談は forva AI へ。まずはお気軽にどうぞ。