Terraform stateとは?tfstateの構造とS3バックエンド・移動削除の安全手順
Terraformを2人以上で回し始めた途端に問題になるのが、state(tfstate)の置き場所と書き換え方です。stateはコードとクラウド上の実体をつなぐ対応表で、これが壊れるとplanが「全部を新規作成する」と言い出したり、applyが既存リソースを二重に作ったりします。この記事では、tfstateに何が記録されているかを押さえたうえで、S3バックエンドとロックの設定、state list・showでの読み取り、movedブロックとremovedブロックによる移動・削除、平文で残る機密値の扱いまでを実装の順序どおりに整理し、最後にstateを分割すべき条件を言い切ります。バージョン依存の記述は2026年8月13日に公式ドキュメントとCHANGELOGで確認した内容です。
まとめ:tfstateで先に決める保存先・ロック・書き換え手段の3点
結論から並べます。stateはローカルに置かず、S3などのリモートバックエンドへ移す。ロックは use_lockfile = true によるS3ネイティブロックを既定にし、DynamoDBテーブルは新規に作らない。そしてstateの書き換えは、CLIの terraform state mv や terraform state rm ではなく、設定ファイルに書いてplanで差分を確認できる movedブロック・removedブロックへ寄せる。この3点が決まっていれば、state起因の事故はほぼ防げます。
読み取り系の terraform state list と terraform state show は、いつ実行してもstateを変えません。現状を安全に確認できる入口として、先に慣れておく操作です。
tfstateに記録される情報と、コードだけでは足りない理由
stateに何が入っているかが曖昧なまま操作すると、rmとdestroyの区別すら付かなくなります。
tfstateが保持するリソースID・属性・依存関係という3種類の記録
tfstateはJSON形式のファイルです。先頭にstate形式のバージョン、書き込んだTerraformのバージョン、後述する serial と lineage が並び、その後ろに管理下のリソースが配列で続きます。各リソースには、コード上のアドレス(aws_instance.web のような type と name の組)、プロバイダ名、そして実体の属性が入ります。
属性のなかで決定的なのがIDです。EC2インスタンスなら i- で始まるインスタンスID、S3バケットならバケット名が、実体を指す鍵として保存されます。もう一つが依存関係の記録で、どのリソースがどれを参照していたかが残っているため、destroyのときに逆順で消せます。
対応表を失うとplanが全リソースの新規作成として出る仕組み
Terraformの設定ファイルには、クラウド側のIDがどこにも書かれていません。aws_s3_bucket.assets というアドレスと実在するバケットを結び付けているのはstateだけです。stateを失えば、Terraformは「まだ何も作っていない」と判断し、planの出力はすべて create になります。そのままapplyすると、同名で作れないリソースはエラー、作れるリソースは二重に増えます。
この対応表を持つことがIaCの前提条件で、ツールを問わず共通する構造です。宣言的にインフラを記述する考え方そのものはIaC(Infrastructure as Code)の仕組みと導入判断の側で整理しています。
serialとlineageが担うstateの世代管理と取り違えの検知
serial は、stateが書き換えられるたびに増える整数です。バックエンド側のserialが手元のものより進んでいれば、古いstateで上書きしようとしていることが分かります。lineage はstateを新規作成したときに一度だけ振られる識別子で、別系統のstateを取り違えて書き戻そうとすると不一致で弾かれます。
この2つがあるおかげで、後述する terraform state push は無条件の上書きになりません。強制フラグで検査を無効化する操作は、事故の検知機構を自分で外す行為にあたります。
ローカルstateの共有をやめてS3バックエンドへ移すときの設定
1人で検証している間はローカルの terraform.tfstate で足ります。共同作業に入った時点で決め直します。
ローカルのtfstateを共有した場合に起きる上書き事故の型
ローカルstateをファイル共有やGitで配って回すと、2人が同時にapplyしたときに後から書き戻した側が勝ちます。先にapplyした人が作ったリソースの記録は消え、実体だけがクラウドに残る。次のplanでは、その実体は管理外の見えないリソースとして扱われ、誰も消せない状態になります。
Gitで共有した場合はさらに厄介で、JSONの構造上、tfstateのコンフリクトを人手で正しくマージできません。共有が必要になった時点でリモートバックエンドへ移す、という判断で構いません。
use_lockfileによるS3ネイティブロックと1.11系での位置づけ
S3バックエンドの必須引数は bucket・key・region の3つです。ロックについては、v1.10.0(2024-11-27公開)でS3ネイティブロックが追加され、v1.11.0のCHANGELOGで「S3 native state locking is now generally available」と正式版になりました。同じ版でDynamoDB関連の引数は非推奨となり、将来のマイナーバージョンで削除される予定です。新規に組むならDynamoDBテーブルは作りません。
terraform {
backend "s3" {
bucket = "issoh-tfstate-prod"
key = "network/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
use_lockfile = true
}
}
バケット本体の作成、バージョニング、パブリックアクセスブロック、そしてGitHub Actionsからの接続までを含む実装はGitHub ActionsとTerraformでAWSのCI/CDを構築する手順で扱っているため、パイプラインへ組み込む段階でそちらを参照してください。
backend設定を変えたあとにstateを移すinitの操作と選択肢
backendブロックを書き換えた直後は、planもapplyも通りません。initのやり直しが要求されます。ここで選択肢が2つに分かれます。
terraform init -migrate-state は、現在のstateの中身を新しいバックエンドへコピーしたうえで設定を差し替えます。terraform init -reconfigure は中身を移さず、バックエンド設定だけを初期化し直す指定です。ローカルからS3へ移す、あるいはバケットを引っ越すなら前者。作業ディレクトリの向き先を変えるだけなら後者を使います。移行後は必ずplanを流し、差分ゼロを確認してから次へ進んでください。
stateを壊さずに現状を読み取るサブコマンドの使い分けと実行順序
公式のコマンドリファレンスが列挙する terraform state のサブコマンドは、list・mv・pull・replace-provider・rm・show の6つです。このうち書き換えを伴わないものから順に押さえます。
terraform state listでアドレスを絞り込んでから操作に入る手順
terraform state list は、管理下のリソースアドレスを1行ずつ出力します。引数なしで全件、アドレスの一部を渡せば前方一致で絞り込めます。モジュール配下のリソースは module.network.aws_subnet.private[0] のようにインデックス付きで出るため、この文字列をそのまま次の操作へコピーするのが基本です。
アドレスを目視で組み立てるのは事故のもとです。移動でも削除でも、まずlistで実在を確認してから手を動かしてください。
terraform state showで属性を確認しdriftの当たりを付ける
terraform state show aws_instance.web のようにアドレスを1つ渡すと、そのリソースの属性がplanと同じ書式で表示されます。planの差分だけを見ていると「コードと実体のどちらが変わったのか」が判別できませんが、showでstate側の値を確認すれば切り分けられます。
毎回同じ属性に差分が出るときは、既定値やcomputed属性をコードに書いてしまっているケースを疑います。showで実際に入っている値を見て、コード側の記述を削るか値をそろえるかを決めてください。
pullとpushを直接使ってよい場面と避けるべき場面の線引き
terraform state pull は、リモートのstateをJSONとして標準出力へ出します。ファイルへ落として保管するバックアップ用途や、機械的な棚卸しには安全に使えます。書き換えは一切起きません。
一方 terraform state push は、serialとlineageの検査を通ったうえでstateを丸ごと置き換えます。使ってよいのは、バージョニングから取り出した過去のstateへ戻す復旧作業のときだけ。JSONを手で編集して戻す運用は、依存関係の記録を壊しても気付けないため採用しません。
移動と削除をmovedブロック・removedブロックで安全に行う手順
名前を変えたい、モジュールへ畳みたい、管理から外したい。いずれもstateの書き換えを伴い、CLIで即座に書き換える方法と、設定に書いてplanで確認する方法があります。
state mvとmovedブロックの違いを差分確認の有無で使い分ける
両者の到達点は同じでも、途中の安全性が違います。CLIは実行した瞬間に書き換わり、事前確認の機会がありません。
| 観点 | terraform state mv | movedブロック |
|---|---|---|
| 実行の形 | CLIで即時に書き換え | 設定に記述しplanで確認 |
| 事前レビュー | 不可 | planの出力をPRで確認 |
| 他環境への波及 | 環境ごとに手で実行 | 同じコードで自動適用 |
| 導入版 | 初期から提供 | v1.1.0で追加 |
v1.1.0のCHANGELOGには「moved blocks for refactoring within modules」として、アドレスの変更をソースコードに記録する機能だと書かれています。開発と本番で同じコードを流しているなら、CLIで環境の数だけ手を動かす理由はありません。
movedブロックで参照を保ったままアドレスを付け替える書き方
リソースブロックの名前を新しいものへ書き換えたうえで、旧アドレスから新アドレスへの対応を1つ足します。planには「moved」として表示され、作成でも削除でもないことが読み取れます。
moved {
from = aws_instance.web
to = aws_instance.app
}
適用後もブロックはコードに残します。消すと、まだ古いstateを持つ別環境で次のapplyが走ったときに削除と再作成として扱われるためです。全環境の適用が済んだことを確認してから外してください。
removedブロックで実体を残したままTerraform管理から外す
v1.7.0のCHANGELOGは、設定からリソースやモジュール呼び出しが消えたことをソースコードに記録し、対象を削除するのか単にstateから外すのかをTerraformへ伝えられるようになった、と説明しています。公式ドキュメントはこれを「a safer way to remove resources」と位置づけ、操作結果をpreviewできる点を理由に挙げています。
removed {
from = aws_s3_bucket.legacy_assets
lifecycle {
destroy = false
}
}
対象のresourceブロックを消してremovedブロックへ置き換えるだけです。destroy = false を書き忘れると実体ごと消えるため、ここは必ず明記します。from にインスタンスキー付きのアドレスは書けないので、count や for_each で展開したリソースは全体が対象になります。その属性を参照している箇所も同時に消してください。
state rmを使わざるを得ない場面と実行前に取るバックアップ
removedブロックが使えないのは、設定を書ける状態にないときです。モジュールのソースごと失われている、あるいはv1.7系より前で固定されている環境が該当します。この場合だけ terraform state rm を使います。
実行前に terraform state pull でJSONを保存し、S3のバージョニングが有効であることも確認しておきます。誤って外したリソースを管理下へ戻す作業は、削除の取り消しではなくterraform importによる既存リソースの取り込みの工程になります。戻せる前提を作ってから実行する、という順序を守ってください。
ロックが残ってplanが通らないときのforce-unlockの判断基準
CIのジョブが強制終了すると、ロックが解放されないまま残ります。以降のplanもapplyも止まるため、terraform force-unlock にロックIDを渡して解除します。
解除する前に確認するのは2点です。実際に他のapplyが動いていないこと、そして手元のエラーメッセージに出ているロックIDと解除しようとしているIDが一致していること。動いているapplyのロックを外すと、2つのプロセスが同時に書き込んでstateが壊れます。
stateに平文で残る機密値と、リポジトリへ混入させない運用
stateは機密情報の保管庫にもなります。見落とすと、バケット設定の不備がそのまま資格情報の漏えいにつながります。
sensitive指定でもstateには平文で入るという前提での防御
変数や出力に sensitive = true を付けても、伏せられるのはCLIの表示だけです。stateファイルの中身は平文のまま保存されます。RDSの管理者パスワードやIAMアクセスキーをTerraformで作れば、その値はstateに残ると考えてください。
防御は保存側で行います。encrypt = true とKMSによるサーバーサイド暗号化、パブリックアクセスブロックの全面有効化、stateバケットを読めるIAMプリンシパルを実行ロールに絞る。この3層を先に用意してから、機密を含むリソースをコード化します。
gitignoreに入れるファイルと除外してはいけないファイルの区別
迷いやすいのは、除外するものと必ずコミットするものの線引きです。実務では次の分け方で足ります。
terraform.tfstateとterraform.tfstate.backup:除外する.terraformディレクトリ:除外する(プロバイダの実体が入る)crash.log:除外する- 機密値を含む
terraform.tfvars:除外する .terraform.lock.hcl:除外せずコミットする
最後のロックファイルだけは、ほかと扱いが逆です。プロバイダのバージョンとハッシュを固定するファイルで、これを外すと環境ごとに異なるプロバイダが入り、planの差分が人によって変わります。ファイル構成や命名の統一まで含めた取り決めはTerraformコーディング規約(スタイルガイド)にまとめてあります。
バージョニングを有効にしたS3から壊れたstateを戻す手順
公式のS3バックエンドのドキュメントは、誤削除や人為的なミスからの復旧のためにバケットのバージョニングを有効にするよう警告文で明示しています。有効にしていれば、壊れる直前のオブジェクトバージョンを取り出せます。
復旧の流れは、対象キーの直前バージョンをダウンロードし、serial と lineage が想定どおりかを確認したうえで terraform state push で書き戻す、という順序です。作業前にロックが残っていないかを確かめ、書き戻した後はplanで差分ゼロを確認します。差分が出るなら、その間の変更が失われている証拠なので、applyせずに調べ直してください。
stateを分割すべき条件と、1本のまま運用してよい条件の線引き
state分割は、やれば整理される類の作業ではありません。境界を切った瞬間に、参照とCIとレビューの手数が増えます。
適用時間・権限境界・変更頻度の3観点から分割の可否を判断する基準
判断材料は3つで、2つ以上に当てはまるなら分割します。1つだけなら見送ります。state側ではなくコード側の分割粒度から決めたい場合は、Terraform moduleの分割粒度とoutput設計|自作とRegistryの判断を先に読んでください。
1つ目は適用時間。planとapplyの往復が10分を超え、1行の変更でも全体の適用を待たされているなら、待ち時間が開発速度を削っています。2つ目は権限境界で、ネットワークとアプリケーションで担当チームもIAMロールも別なら、stateを分けたほうが権限を絞れます。3つ目は変更頻度の差。VPCは年に数回、アプリのタスク定義は日次という具合に10倍以上の開きがあれば、そこが境界の候補になります。
分割を見送るべき小規模構成の目安と、分割で増える運用コストの内訳
ここは言い切ります。リソースが50本前後まで、担当が1〜2人、applyが数分で終わる構成なら、stateは分割しないでください。得られる利点より、増える手数のほうが確実に大きいためです。
分割すると、バックエンドのキー設計、環境ごとのCIジョブ、境界をまたぐ変更のたびに2回applyを回す段取りが新たに必要です。最後のものは、順序を間違えると片方だけ適用された中途半端な状態を作ります。分割前提でAWS上の構成を設計し直す場面や、既存環境の棚卸しから伴走が必要な場面では、AWSインフラの設計・移行・運用のように外部の実装体制と組む選択肢もあります。分割は、痛みが実測できてから行う作業です。
terraform_remote_stateでの参照を増やさないための設計判断
分割すれば、片方のstateからもう片方の値を取りたくなるものです。terraform_remote_state データソースは相手のstate全体への読み取り権限を要求するため、機密を含むstateを広く読ませることになります。参照を増やすほど境界が固まり、後から動かせなくなる副作用もあります。
代替は2つ。出力すべき値をSSMパラメータストアへ書き、読み手はそこから取る。あるいはタグやNameでの検索が効くデータソース(VPCやサブネットの参照など)で解決する。参照は必ず片方向に限り、相互参照は作らないでください。双方向の依存ができた時点で、その2つは分ける意味を失っています。
よくある質問
terraform stateの操作でつまずきやすい点を、検索されている質問の形で整理しました。
terraform state rmを実行するとクラウド上のリソースも削除されますか?
削除されません。stateから記録を外すだけで、実体はクラウドに残ります。ただし対応するリソースブロックも消してあれば、以後Terraformはその実体を認識しないため、変更も削除もできない管理外の状態になります。管理から外すこと自体が目的なら、planで結果を確認できるremovedブロック(v1.7系で追加)に destroy = false を書く方法のほうが安全です。
tfstateをGitリポジトリに置いてもよいですか?
置かないでください。stateには機密値が平文で保存されるため、リポジトリへのアクセス権がそのままDBパスワードへのアクセス権になります。JSONのコンフリクトを人手で正しくマージできない問題もあります。terraform.tfstate とバックアップファイルはgitignoreへ入れ、S3などのリモートバックエンドで共有してください。
stateロックにDynamoDBは今も必要ですか?
新規構築では不要です。S3ネイティブロックはv1.10.0で追加され、v1.11.0で正式版になりました。DynamoDB関連の引数は同じv1.11系で非推奨となり、将来のマイナーバージョンで削除される予定です。既存環境は、チーム全員とCIランナーがv1.11系以降になるまで両方を併記して段階移行してください。
terraform state mvとmovedブロックはどちらを使うべきですか?
原則はmovedブロックです。v1.1.0で追加され、設定ファイルに書くためplanで移動の内容を事前に確認でき、レビューにも残ります。複数環境へ同じコードを流している場合、CLIのように環境ごとに手で実行する必要もありません。CLIを使うのは、設定ファイルを書ける状態にない場合の例外的な対応に限ります。
tfstateが壊れたときはどこから復旧しますか?
S3バックエンドでバージョニングを有効にしてあれば、そのバケットの直前バージョンが第一の復旧元です。取り出したJSONの serial と lineage を確認してから terraform state push で書き戻します。ローカル実行なら terraform.tfstate.backup に直前の1世代が残りますが、共有もされないため復旧手段として当てにしないでください。
関連記事
- IaCとは?Infrastructure as Codeの仕組み・メリットと導入判断を解説:stateが必要になる前提を導入判断の側から整理しています。
- GitHub ActionsとTerraformでAWSのCI/CDを構築する手順:stateバケットの作成とOIDC認証をパイプライン側から扱います。
- terraform importで既存リソースをコード化する手順:state外のリソースを管理下へ取り込む工程の解説です。
- Terraformコーディング規約(スタイルガイド):分割の境界を決める前段になるファイル構成と命名の規約です。
- HCP Terraformとは?旧Terraform Cloudの料金・機能:stateの保管とロックをSaaSへ預ける選択肢の費用をまとめています。