Terraformの「Saved plan is stale」エラーをCI/CDパイプラインで解決する方法

intermediate🏗️ Terraform2026-06-15| Terraform CLI、CI/CDプラットフォーム(GitHub Actions、GitLab CI)、およびAWS S3 + DynamoDBやTerraform Cloudなどのリモートバックエンド。

Error Message

Error: Saved plan is stale The given plan file can no longer be applied because the state was changed by another operation after the plan was created.
#Terraform#DevOps#IaC#CI/CD#トラブルシューティング

発生する状況terraform plan -out=tfplan の慎重なレビューを終えたところだとします。出力は完璧に見えるので、terraform apply "tfplan" を実行します。しかし、リソースが起動する代わりに、ターミナルにぶっきらぼうなエラーメッセージが表示されます。デプロイは開始される前に停止してしまいます。

実際のエラー内容```

Error: Saved plan is stale

The given plan file can no longer be applied because the state was changed by another operation after the plan was created.


変化の速いチームでは、インフラストラクチャの状態は常に変化しています。プランを適用しようとしたときには、チームメイトや自動化スクリプトによって状態ファイルが変更されている可能性があります。この不一致により、保存されたプランは瞬時に古くなってしまいます。
## 原因Terraform は、絶対的な信頼できる情報源(Source of Truth)として状態ファイル(`terraform.tfstate`)に依存しています。すべての状態ファイルには `serial` 番号が含まれています。これは 0 から始まり、状態が変更されるたびに 1 ずつ増加する整数です。
`-out` フラグを使用してプランファイルを作成すると、Terraform はその時点の状態の `serial` と `lineage` をそのファイルに刻み込みます。例えば、状態のシリアルが 45 であれば、`tfplan` は 45 を期待します。

コードのレビュー中に同僚が `terraform apply` や `terraform state rm` を実行すると、バックエンドのシリアルは 46 に上がります。最終的に apply を実行した際、Terraform は 45 が 46 より小さいことを検出し、処理を停止します。この安全確認により、古いデータに基づいた変更の適用を防ぎます。これがないと、新しく作成されたリソースを削除したり、環境を壊したりする可能性があります。
一般的な原因は以下の通りです:
- チームメイトが状態を更新するクイックホットフィックスをマージした。- 並行して実行されていた CI/CD ジョブが、別のブランチでのデプロイを完了した。- plan と apply の間に、`terraform import` を使って手動で状態を調整した。## 即座の解決策:「フレッシュスタート」メソッド古くなったプランを「リフレッシュ」したり、強制的に動作させたりすることはできません。唯一の方法は、現在の状態と同期して新しいプランを生成することです。
- **最新の状態を取得する:**Terraform は通常、プランのフェーズでこれを行いますが、念のため `terraform refresh` を実行することもできます。- **新しいプランを生成する:**```
terraform plan -out=tfplan
```- **新しいプランをすぐに適用する:**```
terraform apply "tfplan"
```すぐにまた同じエラーが発生する場合、インフラが手動のワークフローでは追いつけないほど速く変化しています。これは、チーム内の連携のボトルネックや、パイプラインの並行実行ロジックの欠陥を示唆しています。
## 恒久的な対策:ワークフローとアーキテクチャ古くなったプランのエラーが頻発するのは、デプロイプロセスが緩い兆候です。以下の 3 つの戦略を使用してワークフローを固めましょう。
### 1. 状態ロックの義務化バックエンドがロックをサポートしていない場合、競合状態(レースコンディション)を招くことになります。AWS S3 バックエンドの場合、状態ロック用の DynamoDB テーブルは必須です。これにより、一度に 1 人または 1 つのプロセスだけが状態を書き込めるようになります。

terraform { backend "s3" { bucket = "my-terraform-state" key = "prod/terraform.tfstate" region = "us-east-1" dynamodb_table = "terraform-lock-table" # チームの安全のために不可欠 } }


### 2. Plan から Apply までの時間を短縮する多くのパイプラインはプランを生成した後、手動承認のために数時間待機します。その期間が長ければ長いほど、衝突の可能性が高くなります。リスクを最小限に抑えるには:
- GitHub Actions で**並行実行制限**(`concurrency: group_name`)を使用し、環境ごとに 1 つのパイプラインのみが実行されるようにする。- `dev` や `staging` などの下位環境では変更を自動適用(Auto-apply)し、状態を迅速に動かし続ける。- 手動承認ステップに 30 分の期限を設定する。### 3. 手動の状態操作を禁止するローカルマシンから `terraform state rm` や `terraform import` などのコマンドを実行することを控えさせます。これらの操作は、即座にシリアル番号を増加させます。手動の変更が必要な場合は、パイプラインを一時停止する「メンテナンスウィンドウ」を調整してください。
## 確認手順現在の状態が同期されていることを確認するために、以下の手順を実行します:
- **現在のシリアルを確認する:**状態をプルして、手動でバージョンを確認します。```
terraform state pull | jq '.serial'
```- **ドライランを実行する:**新しく `terraform plan` を実行します。「No changes(変更なし)」と報告された場合、別のプロセスがすでに予定していた変更を適用済みです。- **ロックを検証する:**別のターミナルで 2 つの `plan` コマンドを実行してみます。2 つ目が「Lock acquired by...」エラーで失敗すれば、セーフティネットが機能しています。## 最後に「Saved plan is stale」エラーはバグではありません。Terraform がガードレールとして機能している証拠です。何時間もの作業を誤って消去してしまうような「後勝ち(last-write-wins)」シナリオから本番環境を守ってくれます。プランの再生成は一時的な解決策ですが、厳格な状態ロックとパイプラインの並行実行制御を実装することこそが、この問題を根本的に解決する唯一の方法です。

Related Error Notes