はじめに
tfstateの直接編集を実際にやったことがないため、EC2を1台だけデプロイした簡単な環境で試してみることにしました。
検証用の環境を作る
まず、検証用のTerraformコードを用意します。
EC2を1台デプロイするだけのシンプルな構成で、stateはローカルに保存します。
# main.tf
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
data "aws_ami" "amazon_linux_2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
resource "aws_instance" "test" {
ami = data.aws_ami.amazon_linux_2023.id
instance_type = "t3.small"
tags = {
Name = "tfstate-edit-test"
}
}
terraform init と terraform apply を実行して、EC2を作成します。
terraform init
terraform apply
tfstateの中身を覗いてみる
apply が完了すると、カレントディレクトリに terraform.tfstate が生成されます。
中身はJSON形式で、大まかにこんな構造になっています。
{
"version": 4,
"terraform_version": "1.13.1",
"serial": 1,
"lineage": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"outputs": {},
"resources": [...]
}
各フィールドの役割はこちらです。
| フィールド | 役割 |
|---|---|
version |
tfstateのフォーマットバージョン。現行は 4 固定 |
terraform_version |
このstateを最後に更新したTerraformのバージョン |
serial |
stateが更新されるたびに+1される通番。競合検知に使われる |
lineage |
stateファイルの一意なID。別のstateを誤って上書きしないためのもの |
outputs |
output ブロックで定義した値の格納先。未定義なら空 |
resources |
管理しているリソースの一覧。ここに各リソースの属性値が入る |
resources の中にある aws_instance.test を見ると、attributes にインスタンスタイプなどの値が記録されています。
{
"mode": "managed",
"type": "aws_instance",
"name": "test",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"attributes": {
"ami": "ami-0292622b22bd52948",
"instance_type": "t3.small",
"id": "i-XXXXXXXXXXXXXXXXXXXX",
"tags": {
"Name": "tfstate-edit-test"
},
...
},
"dependencies": [
"data.aws_ami.amazon_linux_2023"
]
}
]
}
terraform plan はattributes の値と .tf ファイルの定義を比較して差分を出します。
ただし、単純に比較しているわけではなく、事前にrefreshという処理が挟まります。
terraform planのrefresh動作を知っておく
tfstateを直接編集する前に、terraform plan の動作を理解しておく必要があります。
refreshとは: AWSなどのプロバイダに問い合わせて、実際のリソースの現在の状態を取得する処理です。tfstateに記録された値と実際のインフラの間にズレがないかを確認するために行われます。
terraform plan は実行時に以下の順序で動きます。
- AWSの実際の状態を読み取る(refresh)
- 読み取った値でstateをメモリ上で更新する
- その更新後のstateと**
.tfファイルを比較**して差分を出す
ここで注意したいのが、plan と apply でrefresh結果の扱いが異なる点です。
-
terraform plan: refresh結果はメモリ上に保持される。tfstateファイルは更新されない -
terraform apply: refresh結果がtfstateファイルにも書き込まれる
つまり、tfstateの instance_type を手で書き換えても、planの最初のステップでAWSから実際の値を取得してメモリ上で上書きしてしまいます。
例えば、tfstateだけ t3.micro に書き換えて terraform plan を実行しても、AWSの実インスタンスが t3.small のままなら、refreshで t3.small に戻されて差分は出ません。
ただし、これはメモリ上の話なので、tfstateファイル自体は t3.micro のまま残ります。
この動作をスキップするオプションが -refresh=false です。
terraform plan -refresh=false
これならAWSへの問い合わせをせず、tfstateファイル上の値と .tf を直接比較します。
以降の検証ではこのオプションも活用していきます。
図解すると下記のような形。
またrefreshで上書きされない属性もあるため、必ず実リソースの全ての情報がメモリ上のstateに反映されるわけではないです。
やってみる: tfstateを直接編集して差分を消す
AWSコンソールから手動でインスタンスタイプを t3.small から t3.micro に変更してしまった、という状況を想定します。.tf ファイルも t3.micro に修正済みです。
# main.tf の該当箇所を修正
instance_type = "t3.micro"
この状態で terraform plan を実行すると、refreshによってAWSから実際の値(t3.micro)が取得されます。refreshed後のstateと .tf はどちらも t3.micro なので、適用すべき変更(planned changes)はありません。
一見すると差分がないように見えますが、tfstateファイルの中身は t3.small のまま更新されていません(plan はファイルを書き換えないため)。
-refresh=false をつけてrefreshをスキップすると、tfstateファイルの値がそのまま使われるので差分が見えます。
terraform plan -refresh=false
普通なら terraform apply すれば解決しますが、今回はあえてtfstateの直接編集で差分を消してみます。
手順
1. バックアップを取る
cp terraform.tfstate terraform.tfstate.backup
2. tfstateの instance_type を書き換える
terraform.tfstate をエディタで開き、instance_type の値を t3.micro に変更します。
"instance_type": "t3.micro",
3. serial を+1する
"serial": 2,
serial を更新しないと、リモートバックエンド(S3など)を使っている場合に書き込みが拒否される可能性があります。ローカルstateでも念のため上げておきます。
※追記)ローカルではserialは更新しなくてもエラーがでませんでした。
4. terraform plan -refresh=false で確認
terraform plan -refresh=false
差分が消えました。JSONの構文を壊していたり値が間違っていれば、この時点でエラーや差分として検出されるので、失敗してもすぐ気づけます。もしうまくいかなければ、バックアップから terraform.tfstate を復元すれば大丈夫です。
まとめ
tfstateの直接編集、やってみると意外とシンプルでした。
JSONの該当箇所を書き換えて serial を上げるだけです。
壊しても terraform plan ですぐ気づけますし、バックアップがあれば復元もできます。
ただし、普段の運用では terraform state rm + terraform import ほうが当たり前ですが安全で確実です。
直接編集は「仕組みを理解するため」や「他の手段だとリスクが高いケース」に限定するのがよさそうです。




