このトラブルの原因は、ほぼ確実に
backend "local" のままCI/CDを回していることにある。この記事では、terraform backend localがCI/CDパイプラインで詰まる3つの構造的な理由を解説したうえで、S3・GCSなどのリモートバックエンドへの切替をどの判断軸で決めればよいかを実装コード付きで整理する。Terraformを使い始めたばかりのエンジニアにも、チームでの運用を始めようとしているインフラ担当者にも参考になるはずだ。
この記事のポイント
・terraform backend localはCI環境でステートが毎回リセットされリソース重複作成が起きる
・ローカルファイルロックは別マシン・別コンテナには効かず並行apply時にstate競合が発生する
・AWS環境ならS3+DynamoDB組み合わせが最小構成のリモートバックエンド定番
・localが許容できるのは個人学習・使い捨て試験環境のみ。チーム利用・CI導入時は即切替が必要
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
terraform backend localとはどういう設定か
Terraformはインフラの現状をステートファイル(terraform.tfstate)に記録する。このファイルをどこに保管し、誰がアクセスできるかを決めるのが「バックエンド」の役割だ。backend "local" は、terraform init を実行したディレクトリにステートをそのまま保存するデフォルト設定。設定ファイルにbackendブロックを書かなければ、Terraformは自動的にlocalバックエンドを使う。# backend未指定 = 暗黙的にlocalバックエンドが選ばれる terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } # 明示的に書く場合(デフォルトと同じ動作) terraform { backend "local" { path = "terraform.tfstate" } }
terraform init 後にカレントディレクトリに以下のファイルが生成される。$ ls -la total 32 drwxr-xr-x 3 ec2-user ec2-user 4096 Sep 28 09:00 . drwxr-xr-x 10 ec2-user ec2-user 4096 Sep 28 08:55 .. drwxr-xr-x 3 ec2-user ec2-user 4096 Sep 28 09:00 .terraform -rw-r--r-- 1 ec2-user ec2-user 197 Sep 28 09:00 .terraform.lock.hcl -rw-r--r-- 1 ec2-user ec2-user 234 Sep 28 08:55 main.tf -rw-r--r-- 1 ec2-user ec2-user 4521 Sep 28 09:02 terraform.tfstate
CI/CDでlocalバックエンドが詰まる3つの構造的問題
1. ステートファイルが実行環境に閉じてしまう
GitHub ActionsやCircleCIのランナーは、ジョブのたびに使い捨ての新しいコンテナ(またはVM)を起動する。前回ジョブで生成したterraform.tfstate は、次回ジョブでは存在しない。この状態で
terraform plan を実行すると、Terraformは「現在のインフラ状態が空(リソースは何もない)」と認識する。既にAWSに構築済みのリソースも「初めて作るもの」として扱われ、差分として表示される。applyすれば同名のリソースが重複作成されるか、既存リソースとの名前衝突でエラーになる。以下は、localバックエンドのままGitHub Actionsでplanを流した際の実際の出力例だ。
$ terraform plan Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # aws_instance.web will be created + resource "aws_instance" "web" { + ami = "ami-0c02fb55956c7d316" + instance_type = "t3.micro" + id = (known after apply) + private_ip = (known after apply) + public_ip = (known after apply) } Plan: 1 to add, 0 to change, 0 to destroy.
2. ロック機構が別マシンには効かない
Terraformのlocalバックエンドには、OSレベルのファイルロック機能がある。同一マシン上で複数のterraformプロセスが同時にapplyしようとすると、2つ目のプロセスはロック取得に失敗してエラーになる。ただし、このロックはあくまで「同一マシン上」の話だ。CI環境では、開発者Aの手元のPCとGitHub Actionsのランナーは物理的に別のマシン。2人が同時にapplyを実行すると、両方がそれぞれ「ロックを取得できた」と認識したまま処理を進める。その結果、ステートファイルへの書き込みが衝突してステートが破損する。
S3+DynamoDBのリモートバックエンドであれば、DynamoDBの条件付き書き込みでロックを共有管理できる。以下はロック衝突時のエラー例だ。
$ terraform apply Acquiring state lock. This may take a few moments... ╷ │ Error: Error acquiring the state lock │ │ Error message: ConditionalCheckFailedException: │ The conditional request failed │ Lock Info: │ ID: d2c7a9b1-3f4e-41a2-85c8-1b2d3e4f5a01 │ Path: s3://my-tf-state/prod/terraform.tfstate │ Operation: OperationTypeApply │ Who: alice@alice-workstation │ Version: 1.9.5 │ Created: 2026-09-28 09:12:34.123456789 +0000 UTC │ │ Terraform acquires a state lock to protect the state from being written │ by multiple users at the same time. ╵
3. ステートファイルのgit混入リスク
localバックエンドを使う開発者が.gitignore に *.tfstate と *.tfstate.backup を追加し忘れると、ステートファイルがgitリポジトリにpushされる。terraform.tfstateには、作成したリソースの詳細情報が平文で記録される。RDSのマスターパスワード、IAMアクセスキー、プライベートIPアドレスなど、外部に漏れるとセキュリティリスクになる情報が含まれる場合がある。
# .gitignoreに必ず追加すること(localバックエンド使用時) # リモートバックエンドに切り替えれば手元にtfstateは生成されない .terraform/ terraform.tfstate terraform.tfstate.backup *.tfvars *.tfvars.json override.tf override.tf.json
backend切替の設計判断軸
1. チーム規模とCI利用の有無で分岐する
判断の分岐点は「CI/CDで自動applyするか」と「2人以上でapplyするか」の2つだけだ。| 状況 | 推奨バックエンド | 理由 |
|---|---|---|
| 個人学習・使い捨て試験環境(CI不使用) | local(デフォルト) | 切替コスト > メリット |
| 個人開発でCI/CDを使う | S3またはGCS | ランナー間でステート共有が必須 |
| 2人以上のチームでterraform applyを実行する | S3+DynamoDBまたはGCS | ロック機構で並行apply防止が必須 |
| マネージドサービスで運用コストを下げたい | Terraform Cloud(cloud) | ステート管理・ロック・RBAC込みで最も楽 |
Terraformのbackend設計に迷ったら、Terraform実践入門(terraform.linuxmaster.jp)でbackend設計の判断基準を体系的に学べる。
2. AWSならS3+DynamoDBが定番の最小構成
AWSを使っているなら、S3でステートを保管してDynamoDBでロックを管理するのが定番だ。追加の外部サービス登録不要で、既存のAWSアカウント内で完結する。まずS3バケットとDynamoDBテーブルを用意する(この部分は先にコンソールかAWS CLIで作成する)。
# S3バケット作成(バケット名はグローバルで一意) $ aws s3api create-bucket --bucket my-terraform-state-20260928 --region ap-northeast-1 --create-bucket-configuration LocationConstraint=ap-northeast-1 { "Location": "http://my-terraform-state-20260928.s3.amazonaws.com/" } # バージョニング有効化(applyミス後のstate復旧に必須) $ aws s3api put-bucket-versioning --bucket my-terraform-state-20260928 --versioning-configuration Status=Enabled # DynamoDBテーブル作成(パーティションキーはLockIDが必須) $ aws dynamodb create-table --table-name terraform-lock --attribute-definitions AttributeName=LockID,AttributeType=S --key-schema AttributeName=LockID,KeyType=HASH --billing-mode PAY_PER_REQUEST --region ap-northeast-1 { "TableDescription": { "TableName": "terraform-lock", "TableStatus": "CREATING" } }
terraform { backend "s3" { bucket = "my-terraform-state-20260928" key = "prod/terraform.tfstate" region = "ap-northeast-1" dynamodb_table = "terraform-lock" encrypt = true } }
encrypt = true を必ず指定すること。S3のサーバーサイド暗号化が有効になり、ステートファイルに含まれるシークレット情報が平文で保存されなくなる。backend設定を変更したら
terraform init -migrate-state を実行する。既存のlocalステートをS3に移行するか確認されるので「yes」と答える。$ terraform init -migrate-state Initializing the backend... Do you want to copy existing state to the new backend? Pre-existing state was found while migrating the previous "local" backend to the newly configured "s3" backend. No existing state was found in the newly configured "s3" backend. Do you want to copy this state to the new "s3" backend? Enter "yes" to copy and "no" to start with an empty state. Enter a value: yes Successfully configured the backend "s3"! Terraform has been successfully initialized!
3. GCPならGCSバックエンドを選ぶ
GCPを使っている場合はGoogle Cloud Storageバックエンドが対応する。GCSはオブジェクトのメタデータを利用したロック機構が内蔵されているため、DynamoDBのような別サービスが不要だ。terraform { backend "gcs" { bucket = "my-terraform-state-bucket" prefix = "prod/terraform/state" } }
# バケット作成 $ gsutil mb -l asia-northeast1 gs://my-terraform-state-bucket Creating gs://my-terraform-state-bucket/... # バージョニング有効化 $ gsutil versioning set on gs://my-terraform-state-bucket Enabling versioning for gs://my-terraform-state-bucket/...
localバックエンドが有効な場面
localバックエンドがまったく使えないわけではない。以下の条件をすべて満たす場合は、切り替えコストに見合わないのでlocalのままで問題ない。・自分1人しかterraformを実行しない
・CI/CDパイプラインからterraform applyを実行しない
・使い捨て環境(学習・PoC・社内デモ用)で恒久運用しない
・ステートファイルをgitにpushしないよう.gitignoreを確実に設定済み
この4条件をすべて満たすプロジェクトは、localバックエンドで十分だ。localからリモートバックエンドへの移行は
terraform init -migrate-state 1コマンドで完了するため、後から切り替える際のコストは低い。ただし「後で移行する」と先延ばしにするうちに、ステートが壊れてから慌てて対処する羽目になるケースも多い。backend切替時のトラブルシュート
「Backend configuration changed」エラーが出る
terraform init 後にbackendブロックを変更すると、次回の terraform init 時に以下のエラーが出る。$ terraform init ╷ │ Error: Backend configuration changed │ │ A change in the backend configuration has been detected, which may require │ migrating existing state. │ │ If you wish to attempt automatic migration of the state, use "terraform │ init -migrate-state". │ If you wish to store the current configuration with no changes to the │ state, use "terraform init -reconfigure". ╵
・-migrate-state:既存のステートを新しいバックエンドにコピーしたい場合。localからs3への切替など、既存リソースの管理を継続する場面で使う
・-reconfigure:ステートを移行せずに新しいバックエンド設定でinitを実行したい場合。新規プロジェクトの設定変更や、ステートを意図的に捨てる場面で使う
通常のbackend切替では
-migrate-state を選ぶこと。-reconfigure はステートが空になるため、既存リソースとのドリフトが発生するリスクがある。S3バケットへの書き込みでAccessDeniedが出る
S3バックエンド使用時にAccessDeniedが出る場合、Terraformを実行するIAMロール・ユーザーに必要な権限が付与されているか確認する。# 最小限必要なIAMポリシー(S3+DynamoDB用) { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::my-terraform-state-20260928", "arn:aws:s3:::my-terraform-state-20260928/*" ] }, { "Effect": "Allow", "Action": [ "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem" ], "Resource": "arn:aws:dynamodb:ap-northeast-1:123456789012:table/terraform-lock" } ] }
aws-actions/configure-aws-credentials)と組み合わせると、シークレットをGitHubに保存せずに済む。本記事のまとめ
| 問題 | localバックエンドの挙動 | リモートバックエンドの解決策 |
|---|---|---|
| CIでステートが消える | ランナー起動ごとにtfstateがリセット | S3・GCSに永続保存(ランナー非依存) |
| 並行applyでstate破損 | ファイルロックは同一マシン内のみ有効 | DynamoDB・GCS組み込みロックで排他制御 |
| tfstateのgit混入リスク | 手元にtfstateが生成される | ステートはリモートのみに存在 |
| 切替コマンド | -(不要) | terraform init -migrate-state で既存stateを移行 |
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:TerraformでALBとターゲットグループを設計する方法|aws_lb・aws_lb_listener・ヘルスチェックとHTTPSリダイレクトの実践
- この記事の属するカテゴリ:Terraformへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます