はじめに
TerraformコードはGitで管理するのに、terraform.tfstate はS3などのBackendに置く。
最初は「StateにSecretが入っていなければ、Gitで管理してもよいのでは?」と疑問でした。
調べていくと、StateをGitに置かない理由は機密性だけではありません。
StateはTerraformが実行を続けるために、一貫した最新版を読み書きする管理データでした。
TL;DR
- Stateは、Terraform上のresourceと実物の対応関係などを保持する
- JSONを直接編集すると、AWSより先にTerraformの管理認識が変わる
- Stateは最新版共有と排他制御が必要なので、GitよりState管理向けのBackendと相性がよい
1. Stateは「AWSの現在状態を保存したファイル」だけではない
terraform.tfstate という名前を見ると、
「AWSの現在状態をJSONに保存しているファイル」と捉えたくなります。
実際、Stateにはresourceの属性値も保存されます。
ただ、Stateが必要な理由を理解するうえで先に知りたかったのは、
TerraformのConfigと実世界を結びつける役割でした。
1-1. aws_instance.web と i-0123... は別の名前
TerraformのConfigには、たとえば次のように書きます。
resource "aws_instance" "web" {
ami = "ami-xxxxxxxx"
instance_type = "t3.micro"
}
Terraform上では、このresourceを aws_instance.web として扱えます。
一方、AWS上にできるEC2には、
i-0123456789abcdef0
のようなinstance IDがあります。
AWS側に、
「このEC2はTerraformの aws_instance.web です」
という対応表が自動で存在するわけではありません。
そこでTerraformはStateに、
aws_instance.web
↕
i-0123456789abcdef0
というbindingを保持します。
HashiCorpもStateの目的を説明する中で、
Terraform configとreal worldを対応付けるために何らかのdatabaseが必要であり、
Stateがresourceとremote objectのmappingを持つと説明しています。
Purpose of Terraform State
1-2. mapping以外の情報も入っている
Stateにはbindingだけでなく、
resource dependencyなどのmetadataやresource attributeのcacheも保存されます。
たとえばConfigからresourceを削除した後でも、
TerraformはStateに残った情報を使って、
「どのremote objectを削除する必要があるか」
「どの順番で削除するか」
を判断できます。
なので、
State = AWSの現在状態を丸ごとコピーしたもの
と考えると少し狭いです。
自分の中では、
「Terraformが管理を継続するための運用DB」
と考えると一番整理しやすくなりました。
ここでいう「DB」は理解のための比喩です。RDBのようなデータベース製品という意味ではありません。
2. Stateを直接編集すると、先にTerraformの認識が変わる
StateがTerraformの管理情報なら、
「JSONファイルだからGitで編集してもよい」とは考えにくくなります。
ただし、Stateを書き換えた瞬間にEC2が消えるわけではありません。
ここは自分も最初に混乱したところでした。
2-1. Stateを書き換えても、AWSは即座には変わらない
たとえばStateに、
aws_instance.web ↔ i-0123456789abcdef0
というbindingがあるとします。
これを人間がJSONから手で消しても、
AWS上の i-0123456789abcdef0 はその場では残っています。
変わるのは、Terraform側が持っている管理情報です。
Configには aws_instance.web が存在する。
しかしStateには、それと結びつくremote objectがいない。
この状態で次に terraform plan を実行すると、
Terraformは変更後のStateを入力として判断します。
状況によっては、
+ aws_instance.web will be created
のように、新しいresourceを作るPlanが出ることもあります。
そのPlanを apply すれば、そこで初めて実インフラにも影響します。
つまり、
State手編集
↓
Terraformの管理認識が変わる
↓
planの判断が変わる
↓
applyで実物へ反映される
という順番です。
2-2. 直接JSONを編集せず、Terraformが用意した仕組みを使う
HashiCorpはStateについて、
JSON text fileではあるものの直接編集しないよう明記しています。
State | Terraform
Stateを変更する必要がある場合も、JSONを手で書き換えるのではなく、
Terraformが提供するCLIやconfiguration上の仕組みを使います。
確認だけなら、たとえば次のコマンドがあります。
terraform state list
terraform state show aws_instance.web
一方、resourceをTerraform管理から外したい場合、
現在のHashiCorpドキュメントでは terraform state rm より
removed blockを使う方法が推奨されています。
このときは、対象のresource blockを残したまま横に追加するのではなく、
既存のresource blockをremoved blockへ置き換えます。
removed {
from = aws_instance.web
lifecycle {
destroy = false
}
}
removed blockなら通常の plan / apply workflowの中で
「Stateから外す変更」を確認してから適用できます。
HashiCorpも、結果をpreviewできるため
terraform state rm より安全な方法として推奨しています。
Remove a resource from Terraform state
resourceやmoduleのaddressを変更するリファクタリングでも、
moved blockというconfiguration上の仕組みがあります。
Refactor modules
terraform state rm や terraform state mv 自体が間違いなわけではありません。
CLIが必要な場面もあります。
ここで持ち帰りたいのは、
Stateを変更する必要があっても、State JSONを直接編集するのではなく、
Terraformが提供する操作経路を使う
という境界です。
3. plan と apply の中でStateはどう扱われるのか
Stateを「Terraformが使う管理データ」と捉えると、
普段の plan と apply でいつ読まれ、いつ更新されるのかが気になります。
ここを時系列で見ると、
StateをGitの成果物として扱わない理由がさらに分かりやすくなりました。
3-1. plan はStateを手がかりに現在状態を確認する
通常の terraform plan では、
TerraformはStateにあるbindingを使って管理対象を特定します。
たとえばStateに、
aws_instance.web ↔ i-0123456789abcdef0
があれば、Provider経由でそのEC2のcurrent stateを読み取ります。
そのうえでConfigと照合し、
「変更なし」「update」「replace」「destroy」などの変更案を作ります。
Stateだけを見てPlanを決めるわけでも、
Configだけを見てPlanを決めるわけでもありません。
3-2. apply が終わるとStateも更新される
terraform apply はPlanに基づいてremote systemを変更します。
保存済みPlanを渡さない通常の terraform apply では、
Terraformはその場で新しいexecution planを作ってから実行します。
saved planを渡した場合は、そのPlanを実行します。
変更が完了すると、
Terraformはその結果をStateへ反映します。
prior State
↓
plan
↓
apply
↓
実インフラ変更
↓
new State
このStateが、また次回のTerraform実行で使われます。
なのでStateは、
「人間がレビューして完成させたらcommitするファイル」というより、
Terraformのrunとrunの間をつなぐ管理データ
と見る方が自然でした。
4. 2人が同時に apply したらどうなるのか
Stateをチームで共有するなら、
AさんとBさんが同時にTerraformを実行する可能性があります。
ここで必要になるのがState lockingです。
4-1. lockは「Stateを書き込む瞬間だけ」ではない
Backendがlockingをサポートしている場合、
TerraformはStateを書き得るoperationでlockを取得します。
HashiCorpの説明でも、
state lockingはstateへwriteし得るすべてのoperationで自動的に行われ、
lock取得に失敗した場合は処理を継続しません。
State: Locking
たとえばAさんが apply を開始してlockを取っている間に、
Bさんも同じStateへ apply しようとします。
Bさんはlockを取得できないため、
そのまま競合する処理を進められません。
-lock-timeout を指定すれば、
指定時間はlock取得をretryできます。
terraform apply -lock-timeout=30s
それでも取得できなければエラーになります。
Aさんの処理が終わった後に改めて実行すれば、
その時点の最新Stateから処理を始められます。
4-2. Git conflictで後から解決するのでは遅い
Gitなら、AさんとBさんが同じファイルを変更しても、
pushやmergeの段階でconflictを解消できます。
Stateは性質が違います。
| Gitで管理するコード | Terraform State |
|---|---|
| 複数branchで変更できる | 同じ管理対象では一貫した最新版が必要 |
| diffを見て人間がmergeする | Terraformの管理状態を人間がmergeするのは危険 |
| 変更後にconflictを解消できる | 競合するoperationを先に止めたい |
もしAさんとBさんがそれぞれ古いStateを持って、
両方ともAWSへ変更を加えた後に、
「StateのJSONがconflictしたのでmergeしよう」
となっても遅いです。
すでにremote system側の変更が走っているからです。
Terraform Stateでは、
競合した後にファイルをmergeすることより、
競合するwriterを同時に進めないこと
が必要になります。
4-3. 保存したPlanも永遠に使えるわけではない
terraform plan -out=tfplan で保存したPlanも、
Stateと無関係な静的ファイルではありません。
Planは作成時点のStateを前提にしています。
Aさんが別のrunを apply してStateが変わると、
それ以前のStateを前提にしたsaved planはstaleになり得ます。
ここでは内部の serial や lineage までは覚えなくても、
PlanとStateはセットで整合している必要がある
と分かれば十分だと思います。
5. GitとBackendの境界は「更新主体」だけでは決まらない
ここまでの話を、タイトルの言葉に戻して整理します。
Config側は「インフラをこうしたい」と人間が宣言し、
レビューや変更履歴を残す設計図に近いデータです。
一方のStateは、Terraformがrunをまたいで管理状態を引き継ぐ運用DBに近いデータです。
ただし、
「人間が編集するものはGit、Terraformが編集するものはBackend」
と覚えるだけでは .terraform.lock.hcl が説明できません。
.terraform.lock.hcl は terraform init などでTerraformが作成・更新しますが、
HashiCorpはGitへcommitすることを推奨しています。
一方、terraform.tfstate はcommitしないよう案内しています。
Terraform Style Guide
なので境界は、
「誰がファイルを書き換えるか」だけではありません。
自分は次の2つで考えると整理できました。
| 対象 | 欲しい管理特性 | 管理先 |
|---|---|---|
main.tf |
review・履歴・branchで変更したい | Git |
variables.tf |
review・履歴を残したい | Git |
| Backend設定 | 保存先設定を共有・reviewしたい | Git |
.terraform.lock.hcl |
Provider選択を再現・reviewしたい | Git |
terraform.tfstate |
一貫した最新版を共有したい | Backend |
terraform.tfstate |
競合する書き込みを排他したい | Backend |
| Secret実値 | アクセスを制御したい | Secret管理基盤 |
StateについてHashiCorpも、
lockingやsecure access controlを持たないversion control systemへ保存しないよう勧めています。
State | Terraform
そして、冒頭で触れたSecretもGitにStateを置かない理由の一つです。
Stateにはpasswordやprivate keyなどのsensitive dataが保存される場合があります。
sensitive = true にして画面表示を隠しても、
通常はその値がStateから消えるわけではありません。
HashiCorpのStyle Guideでも、
Stateにはsecretなどのsensitive informationが含まれ得ることと、
VCSにはState lockingがないことの両方を理由に
terraform.tfstate をcommit対象から外しています。
Terraform Style Guide
つまり、
Secretが入る可能性があるからGitを避ける
も正しいです。
ただしそれだけでは、
「SecretがないStateならGitでよいのか?」
「複数人が同時にapplyしたら?」
という問題までは説明できません。
この記事でStateを運用DBとして捉えてきたのは、
機密性に加えて、最新版共有と排他制御まで同じモデルで説明するためです。
5-1. 「StateはこのS3に置く」という設定はGitでよい
たとえば次のようなBackend設定です。
terraform {
backend "s3" {
bucket = "my-tfstate-bucket"
key = "production/terraform.tfstate"
region = "ap-northeast-1"
}
}
これはState本体ではありません。
「Stateをどこへ保存するか」というconfigurationなので、
変更履歴を残したりreviewしたりする性質を持っています。
そのためGit管理と相性があります。
一方、S3に保存される実際の terraform.tfstate は、
Terraformがrunごとに読み書きする管理データです。
5-2. S3 Backendにしただけではlockingは有効にならない
ここは2026年時点で気をつけたいところでした。
S3 BackendはState lockingをサポートしていますが、
lockingはopt-inです。
現在の公式ドキュメントでは、
S3 lockfileを使う場合は use_lockfile = true を設定します。
terraform {
backend "s3" {
bucket = "my-tfstate-bucket"
key = "production/terraform.tfstate"
region = "ap-northeast-1"
use_lockfile = true
}
}
use_lockfile のdefaultは false です。
また、以前よく使われていたDynamoDB-based lockingはdeprecatedになっています。
S3 backend
S3 Backendを使う理由を、
S3に置けば自動でlockingされる
と覚えるのは少し危ないです。
「共有Stateを置ける」ことと、
「lockingを有効にして競合を制御する」ことは分けて考える必要があります。
Backend configurationにaccess keyやsecret keyをハードコードする話は別です。
HashiCorpはBackendのcredentialsなどのsensitive dataを環境変数などから渡すことを推奨しています。
おわりに
StateをGitに置かない理由を、
「Secretが入る可能性があるから」だけで覚えると、
SecretがなければGitでもよさそうに見えました。
自分が一番納得できたのは、
StateをTerraformのrunとrunをつなぐ運用DBとして見ることでした。
だからStateには、
Gitのbranch・diff・mergeより、
最新版の共有・locking・アクセス制御
の方が必要になります。
「TerraformコードはGit、StateはBackend」という分け方も、
設計図はreview・履歴管理、運用DBは最新版共有・locking
と役割から見ると、ファイル名の暗記ではなく理由から理解できるようになりました。



