インフラ

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 mvterraform state rm ではなく、設定ファイルに書いてplanで差分を確認できる movedブロック・removedブロックへ寄せる。この3点が決まっていれば、state起因の事故はほぼ防げます。

読み取り系の terraform state listterraform state show は、いつ実行してもstateを変えません。現状を安全に確認できる入口として、先に慣れておく操作です。

tfstateに記録される情報と、コードだけでは足りない理由

stateに何が入っているかが曖昧なまま操作すると、rmとdestroyの区別すら付かなくなります。

tfstateが保持するリソースID・属性・依存関係という3種類の記録

tfstateはJSON形式のファイルです。先頭にstate形式のバージョン、書き込んだTerraformのバージョン、後述する seriallineage が並び、その後ろに管理下のリソースが配列で続きます。各リソースには、コード上のアドレス(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バックエンドの必須引数は bucketkeyregion の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.tfstateterraform.tfstate.backup:除外する
  • .terraform ディレクトリ:除外する(プロバイダの実体が入る)
  • crash.log:除外する
  • 機密値を含む terraform.tfvars:除外する
  • .terraform.lock.hcl:除外せずコミットする

最後のロックファイルだけは、ほかと扱いが逆です。プロバイダのバージョンとハッシュを固定するファイルで、これを外すと環境ごとに異なるプロバイダが入り、planの差分が人によって変わります。ファイル構成や命名の統一まで含めた取り決めはTerraformコーディング規約(スタイルガイド)にまとめてあります。

バージョニングを有効にしたS3から壊れたstateを戻す手順

公式のS3バックエンドのドキュメントは、誤削除や人為的なミスからの復旧のためにバケットのバージョニングを有効にするよう警告文で明示しています。有効にしていれば、壊れる直前のオブジェクトバージョンを取り出せます。

復旧の流れは、対象キーの直前バージョンをダウンロードし、seriallineage が想定どおりかを確認したうえで 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の seriallineage を確認してから terraform state push で書き戻します。ローカル実行なら terraform.tfstate.backup に直前の1世代が残りますが、共有もされないため復旧手段として当てにしないでください。

関連記事

資料請求

RELATED POSTS 関連記事