Terraform 基礎 #5 state の正体 — tfstate ファイルの構造と plan が変更を計算する仕組み

読了 5分

#1 で destroy が作ったものを正確にたどって消すのを見て、「Terraform は自分が作ったものをどうやって覚えているのか」という質問を残しておきました。答えは、あのときディレクトリにできた terraform.tfstate ファイルです。state は Terraform の動作原理の中心であると同時に、実務で起きる Terraform の事故の大部分がこのファイルをめぐって発生します。今回はファイルを実際に開いて構造を見て、plan が何と何を比較しているのか、そして state を操作すべき場面と絶対に触ってはいけないものを区別していきます。

tfstate ファイルを開いてみる #

バケットを 1 つ apply したディレクトリで terraform.tfstate を開くと、JSON が出てきます。重要な部分だけ残すとこうなります。

terraform.tfstate
{
  "version": 4,
  "terraform_version": "1.15.8",
  "serial": 3,
  "resources": [
    {
      "mode": "managed",
      "type": "aws_s3_bucket",
      "name": "hello",
      "instances": [
        {
          "attributes": {
            "bucket": "my-terraform-hello-20260822",
            "arn": "arn:aws:s3:::my-terraform-hello-20260822",
            "region": "ap-northeast-2"
          }
        }
      ]
    }
  ]
}

構造は単純です。Terraform が管理中のリソースの一覧と、最後に確認した各リソースの属性値です。typename#2 で学んだリソースアドレスで、attributes が参照の文法で取得していた値の出どころです。aws_s3_bucket.hello.arn を参照すると、Terraform は AWS に問い合わせるのではなく、まずこのファイルを見ます。

ここでセキュリティの問題が 1 つ付いてきます。attributes にはすべての属性が平文で保存されます。RDS を作るときに入れたマスターパスワードも、#3 で sensitive で隠した変数の値も、state の中にはそのまま残っています。sensitive は画面出力を隠すだけです。だから state ファイルは絶対に Git にコミットせず、.gitignore に次を入れるのが標準です。

.gitignore
.terraform/
*.tfstate
*.tfstate.*

注意すべき例外が 1 つあります。init が作る .terraform.lock.hcl はプロバイダーのバージョンを固定するファイルで、名前に lock が入っていますが state とは無関係で、コミットすべきファイルです。

plan は三者比較です #

plan はコードと state の 2 つだけを比較すると思いがちですが、実際には 3 つを比較します。

plan 比較構造
コード(.tf)  vs  state(.tfstate)  vs  実際のインフラ(AWS API)

plan を実行すると、Terraform はまず state にあるリソースの現在の姿を AWS に照会して state を更新し(refresh)、そのあとコードと比較してやることを計算します。実験で確認できます。Terraform で作ったバケットをコンソールから手で消したあとに plan を実行すると、Terraform は refresh の段階でバケットが消えたことに気づき、「1 to add」を提案します。コードには存在すべきだと書いてあるので、作り直すというわけです。

逆に、コンソールでタグを手で変えると、plan はコードの値に戻す変更を提案します。このようにコードの外で起きた変更とコードのずれをドリフト(drift)と呼びます。Terraform で管理するリソースは Terraform でしか変えない、という原則の理由がこれで、チーム規模でドリフトを扱う方法は運用シリーズで扱います。

state 操作コマンド — ファイルを直接編集しません #

state を調整する必要が出たら、テキストエディタではなく専用のコマンドを使います。ファイルを手で編集して JSON を壊したり serial をずらしたりするのが最悪の事故だからです。

確認
terraform state list                    # 管理中のリソースアドレスの一覧
terraform state show aws_s3_bucket.hello  # 1 つのリソースの属性すべて

この 2 つは読み取り専用なので安全で、デバッグの最初の道具です。次の 2 つは state を変更するコマンドなので、慎重に使う必要があります。

  • terraform state mv: リソースのアドレスを変えます。コードで aws_s3_bucket.helloaws_s3_bucket.logs に名前だけ変えると、Terraform は hello を消して logs を新しく作ろうとします。名前の変更が破壊になってしまいます。terraform state mv 'aws_s3_bucket.hello' 'aws_s3_bucket.logs' で state 側のアドレスを合わせてやれば、再作成なしで名前が変わります。最近のバージョンではコードに moved ブロックを書く宣言的な方法もあり、チーム作業ではレビューに残るこちらを推奨します。
  • terraform state rm: リソースを state からだけ削除します。実際のリソースは残して Terraform の管理対象から外すだけなので、「このリソースは今後別のコードが管理する」といった移管の場面で使います。誤って rm すると、次の apply が同じリソースをまた作ろうとして名前の衝突で失敗する、といった混乱が起きるので、使う前に必ず目的を明確にする必要があります。

state を失うと起きること #

terraform.tfstate が削除されると、Terraform は何も管理していないと思い込みます。インフラは AWS に無事に生きているのに、plan は全部を新しく作ると提案し、そのまま apply すると名前の衝突エラーか重複リソースが生まれます。復旧手段は terraform import で既存リソースを 1 つずつ state に接続し直すことですが、リソースが数十個あればかなりの労働です。詳しい手順は運用シリーズで扱いますが、基礎段階の結論は単純です。state は失ってはいけないファイルであり、ノート PC 1 台のローカルディスクはその保管場所として不適切です。この問題の答えであるリモート state は #9 で扱います。

まとめ #

今回扱った内容です。

  • state は Terraform が管理中のリソースの一覧と最後に確認した属性値を持つ JSON で、参照の文法が読む値の出どころです
  • すべての属性が平文で保存されるので、state はコミットしません。ただし .terraform.lock.hcl は state ではなくバージョン固定のファイルなのでコミットします
  • plan はコード、state、実際のインフラの三者比較です。コードの外の変更が生むずれがドリフトで、管理リソースは Terraform でしか変えないのが原則です
  • state の調整はファイル編集ではなくコマンドで行います。名前の変更は state mv または moved ブロック、管理からの除外は state rm です
  • state を失うと Terraform は全部を作り直そうとします。ローカルディスクは state の保管場所として不適切で、答えは #9 のリモート state です

次回(#6 依存関係と繰り返し)では、リソースが複数に増えるときの文法を扱います。Terraform が作成順序を決める仕組みと、count、for_each でリソースを繰り返し作る方法です。

X