TerraformのlocalバックエンドでCI/CDが詰まる構造|backend切替の設計判断軸

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOME > Linux技術 リナックスマスター.JP(Linuxマスター.JP) > Terraform > TerraformのlocalバックエンドでCI/CDが詰まる構造|backend切替の設計判断軸
「GitHub ActionsでTerraformのCIを組んだら、applyのたびに同じリソースが重複して作られる」「2人で同時にapplyしたらtfstateが壊れた」
このトラブルの原因は、ほぼ確実に backend "local" のままCI/CDを回していることにある。

この記事では、terraform backend localがCI/CDパイプラインで詰まる3つの構造的な理由を解説したうえで、S3・GCSなどのリモートバックエンドへの切替をどの判断軸で決めればよいかを実装コード付きで整理する。Terraformを使い始めたばかりのエンジニアにも、チームでの運用を始めようとしているインフラ担当者にも参考になるはずだ。

この記事のポイント

・terraform backend localはCI環境でステートが毎回リセットされリソース重複作成が起きる
・ローカルファイルロックは別マシン・別コンテナには効かず並行apply時にstate競合が発生する
・AWS環境ならS3+DynamoDB組み合わせが最小構成のリモートバックエンド定番
・localが許容できるのは個人学習・使い捨て試験環境のみ。チーム利用・CI導入時は即切替が必要


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

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" } }

localバックエンドでは 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

ローカル環境でのTerraform学習・個人プロジェクトであれば問題ない。しかしCI/CDパイプラインに持ち込んだ瞬間に、構造上回避できない3つの問題が噴出する。

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.

「既にEC2が動いているのに、planが『1 to add』と言っている」——これがlocalバックエンドをCIで使い続けたときに起きる典型的な症状だ。

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. ╵

localバックエンドでは「衝突していることすら分からずにステートが壊れる」のに対して、DynamoDBロックがあれば「後から来た側がエラーで弾かれる」設計になっている。

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

リモートバックエンドを使えばステートファイルは手元に存在しないため、このgit混入リスクが構造的に消える。

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込みで最も楽
どちらかがYesなら、localバックエンドは即座に卒業すべきだ。「いつかチームに展開するかもしれない」という見通しがある時点でリモートバックエンドを設定しておくほうが、後の移行コストを省ける。

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ブロックをlocalからs3に切り替える。

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" } }

GCSバケットは事前に以下のコマンドで作成しておく。

# バケット作成 $ 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" } ] }

GitHub ActionsからapplyするにはGitHub ActionsのIAMロールにもこの権限が必要だ。OIDCを使ったIAMロール連携(aws-actions/configure-aws-credentials)と組み合わせると、シークレットをGitHubに保存せずに済む。

本記事のまとめ

問題 localバックエンドの挙動 リモートバックエンドの解決策
CIでステートが消える ランナー起動ごとにtfstateがリセット S3・GCSに永続保存(ランナー非依存)
並行applyでstate破損 ファイルロックは同一マシン内のみ有効 DynamoDB・GCS組み込みロックで排他制御
tfstateのgit混入リスク 手元にtfstateが生成される ステートはリモートのみに存在
切替コマンド -(不要) terraform init -migrate-state で既存stateを移行
terraform backend localは学習段階には十分だが、CI/CDと組み合わせた瞬間に「ステートの不在」「ロックの不在」という2つの構造的問題が露出する。S3+DynamoDBへの切替は設定ファイル数行・CLIコマンド数本で完了する。「詰まってから切り替える」のではなく「CIを組む前に切り替える」ことが、Terraformを安定して運用するうえでの設計判断の基本だ。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、Terraformのbackend設計からモジュール化・CI/CD連携まで体系的に学べるTerraform実践入門(terraform.linuxmaster.jp)で、手を動かしながらインフラ管理の「型」を習得できます。

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。