Terraform入門:state管理とplan/applyの違いを、事故らず覚える
Terraformのstate管理(リモートstate・ロック)とplan/applyの違いを、クラウド非依存のHCLとコピペで動く例で解説。module化とマルチクラウドの考え方、Claude Codeでの差分レビューまで。
僕が初めてTerraformで本番を壊しかけたのは、terraform apply を「保存ボタンみたいなもの」だと思っていた日でした。
ローカルでちょっと書き換えて、緑色のログが流れて、よし反映された——その数分後に同僚から「DBのセキュリティグループ、消えてない?」と連絡が来ました。planを読まずにapplyしたら、for ループの書き換えが既存リソースを丸ごと作り直す(replace)扱いになっていたんです。手元のコードは正しかった。でも、コードと実環境のズレを記録した「state」を僕がまったく理解していなかった。
Terraformは賢いツールです。けれど賢さと安全は別物で、事故の9割は「stateとplanを軽く見たこと」から来ます。今日はそこを、専門用語をなるべく日本語に置き換えながら、転んでもケガしない順番で解説します。AWS専用のコードは書きません。**どのクラウドでも同じように効く「クラウド非依存のTerraform」**として進めます。
この記事の要点
- Terraformは「あるべき姿」をHCLという設定言語で書くと、実環境をそこへ近づけてくれる道具。命令を1行ずつ書くシェルスクリプトとは発想が逆。
planは変更の下見、applyは実行。planを読まずにapplyしない。これだけで事故の大半は防げる。- state(実環境とコードの対応表)はチームの共有資産。ローカルやGitに置かず、ロック付きのリモートに置く。
- 似た設定の塊は module(再利用できる部品)にまとめる。env(dev/prod)はファイルで分け、stateの保存先も分ける。
- providerを差し替えればAWS・Google Cloud・Azureを同じ書き方で扱える。Claude Codeはコード生成より「planの危ない差分を拾う相棒」として使うと強い。
Terraformって、要するに何をしている?
一言でいうと、Terraformは「願い事を書くと、その通りに環境を寄せてくれる」道具です。
普通のスクリプトは「VPCを作れ」「次にサブネットを作れ」と手順を並べます。Terraformは違って、vpc が1つ、subnet が2つ、ある状態 というゴールの絵を書くだけ。今の環境がその絵とどう違うかをTerraformが計算して、足りないものを作り、余ったものを消します。この「今と理想の差を埋める」考え方を宣言的(declarative)と呼びます。
ここで主役になるのがstateです。Terraformは「自分が作ったリソースは何で、どんなIDだったか」を terraform.tfstate という台帳に書き残します。次に走るとき、Terraformは台帳・実環境・コードの3つを見比べて差分を出す。だから台帳が壊れたり、人によって中身が違ったりすると、平気で「作り直し」や「二重作成」が起きます。冒頭の僕の事故も、要はこの台帳の読み方を知らなかっただけでした。
用語を先にならしておきます。
- HCL: TerraformのためのDSL(設定専用の小さな言語)。
{}で囲ってリソースを書く。 - provider: AWSやGoogle Cloudなど、相手のAPIを叩くプラグイン。これを差し替えるとクラウドが変わる。
- state: 実環境とコードの対応表。Terraformの記憶そのもの。
- backend: stateの保存先。ローカルか、S3やGCSのようなリモートか。
- module: 何度も使う設定の塊を部品化したもの。
plan と apply の違い(ここだけは絶対に外さない)
Terraformでいちばん大事な2語が plan と apply です。混同すると本番が燃えます。
terraform plan は下見です。コードとstateと実環境を見比べて、「これからこういう変更をするよ」という予定表を出すだけ。何も実行しません。予定表には記号が付きます。
| 記号 | 意味 | 警戒度 |
|---|---|---|
+ | 新規作成(create) | 低 |
~ | その場で変更(update in place) | 中 |
- | 削除(destroy) | 高 |
-/+ | 一度消して作り直す(replace) | 最高 |
terraform apply は、その予定表を実行します。-/+(replace)が出ているのに気づかずapplyすると、稼働中のDBやロードバランサーが一瞬消えて再作成される、なんてことが起きます。だから僕は、planを必ずファイルに保存して、それを目で読んでからapplyする癖をつけました。
# 1. 下見:予定表をファイル(tfplan)に保存する
terraform plan -out=tfplan
# 2. 人間が読む。-/+ や - が無いか目で確認する
terraform show -no-color tfplan
# 3. 「この予定表だけ」を実行する(planとapplyの間で環境が変わってもズレない)
terraform apply tfplan
ポイントは -out=tfplan です。これを付けると、applyが「今この瞬間に計算し直した変更」ではなく「さっき僕が確認した予定表そのもの」を実行してくれます。レビューした内容と実行内容が必ず一致する。CIで自動化するときも、この形が基本になります。
コピペで動く最小構成(クラウド非依存)
説明より動かすほうが早いです。クラウドの認証もお金もいらない、local(手元にファイルを作るだけ)と random(ランダム値を作るだけ)のproviderで、Terraformの一連の流れを体験してみましょう。Terraform本体だけ入っていれば動きます。
まず空のフォルダに main.tf を1つ置きます。
# main.tf — クラウド不要。手元でstateとplan/applyを体験する最小例
terraform {
required_version = ">= 1.10.0"
required_providers {
random = {
source = "hashicorp/random"
version = ">= 3.6"
}
local = {
source = "hashicorp/local"
version = ">= 2.5"
}
}
}
# 変数:あとから -var で差し替えられる
variable "greeting" {
type = string
description = "ファイルに書き込むあいさつ"
default = "hello terraform"
}
# ランダムな接尾辞(実クラウドの「一意な名前づくり」の練習)
resource "random_pet" "name" {
length = 2
separator = "-"
}
# 手元にファイルを1つ作るだけのリソース
resource "local_file" "note" {
filename = "${path.module}/output/${random_pet.name.id}.txt"
content = "${var.greeting}\n"
}
output "created_file" {
value = local_file.note.filename
description = "作成したファイルのパス"
}
あとは下の順で叩くだけです。Terraformが初めての人でも、この3コマンドで「初期化 → 下見 → 実行」の流れが体に入ります。
# providerをダウンロードして初期化する
terraform init
# 何が作られるか下見する(+ が2つ出るはず)
terraform plan -out=tfplan
# 予定表を実行する。output/ にファイルが1つできる
terraform apply tfplan
ここで terraform.tfstate というファイルがフォルダに生まれます。中を開くと、さっき作ったファイル名やランダム値が記録されています。これがstateの正体です。試しに greeting を変えてもう一度 terraform plan すると、Terraformが「ファイルの中身を ~(変更)するよ」と予定表を出してくれます。stateがあるから、Terraformは「何を前回作ったか」を覚えていられるわけです。片付けは terraform destroy で一発です。
state管理:チームで事故らないための置き場所
ここが本記事の核心です。stateは「ただのキャッシュ」ではなく、チームの共有記憶です。扱いを間違えると、2人が同時にapplyしてstateが壊れる、といった事故が起きます。
ローカルのstateには2つの致命的な弱点があります。
- 共有できない: あなたのPCにしかstate台帳が無いと、同僚のTerraformは「リソースが1つも無い」と勘違いして、同じVPCをもう1つ作ろうとします。
- ロックが無い: 2人が同時にapplyすると、台帳の書き込みがぶつかって壊れます。
だから本番では、stateをリモート(S3・GCS・Azure Blobなど)に置き、さらにロックをかけます。ロックは「誰かがapply中は、他の人を待たせる」鍵のことです。例としてAWSのS3 backendを挙げますが、考え方はどのクラウドでも同じです。
# backend.tf — stateをS3に置き、ロックをかける(クラウドが変わっても発想は同じ)
terraform {
backend "s3" {
bucket = "your-tfstate-bucket" # 事前に作っておく
key = "myapp/dev/terraform.tfstate" # 環境ごとに分ける(後述)
region = "ap-northeast-1"
encrypt = true # stateを暗号化
use_lockfile = true # S3ネイティブのロック
}
}
use_lockfile = true がロックの肝です。以前はロック用にDynamoDBのテーブルを別途用意するのが定番でしたが、2026年6月時点のTerraform S3 backend公式ドキュメントでは、DynamoDBによるロックはdeprecated(非推奨・将来削除予定)になっています。新規に組むなら use_lockfile を使ってください。Google CloudならGCS backend、AzureならazurermのようにbackendはほぼAPI差分だけで差し替えられます。
stateを安全に保つコツを3つ。
- stateにはIDだけでなく一部の属性値(時にパスワードらしき値)も入る。だからversioning(履歴保存)と暗号化を有効にし、アクセス権を最小に絞る。
- stateをGitにコミットしない。
.gitignoreに*.tfstate*を必ず入れる。 - stateを手で書き換えない。リソースを動かしたいときは
terraform state mv、消したいだけならterraform state rmのように専用コマンドを使う。
module化:似たコードのコピペをやめる
VPCを3環境ぶん手書きしていると、必ずどこかでコピペミスが起きます。そこで似た塊を module(部品)にまとめます。
考え方はシンプルで、関数に切り出すのと同じです。「入力(variable)を受け取り、リソースを作り、出力(output)を返す」フォルダを1つ用意し、それを呼び出す。下は完全にクラウド非依存な、local_file を量産するだけのmodule例です。仕組みだけ掴んでください。
# modules/notes/main.tf — 「メモファイルを量産する」部品
variable "names" {
type = list(string)
description = "作るメモの名前リスト"
}
variable "body" {
type = string
description = "各メモの中身"
}
# for_each で名前ごとに1ファイルずつ作る(順番に依存しないのがミソ)
resource "local_file" "memo" {
for_each = toset(var.names)
filename = "${path.module}/../../output/${each.key}.md"
content = "# ${each.key}\n\n${var.body}\n"
}
output "files" {
value = [for f in local_file.memo : f.filename]
}
呼び出し側はこれだけです。
# main.tf から部品を呼ぶ
module "team_notes" {
source = "./modules/notes"
names = ["alice", "bob", "carol"]
body = "今日のタスクメモ"
}
ここで覚えてほしいのが for_each です。僕が冒頭で本番を壊しかけた原因は、リストのインデックス(count.index)でリソースを並べていたことでした。リストの途中に1要素足すと番号が全部ずれて、Terraformが「2番は別物になった」と判断し、replace(作り直し)の嵐になる。for_each と名前(キー)で管理すれば、要素を足しても既存リソースは動きません。moduleは「責務を1つに絞る」のが鉄則で、ネットワークの部品にDBやロードバランサーまで詰め込むと、少し直すだけで巨大なplanが出るようになります。
マルチクラウド:providerを差し替えるだけ
Terraformの強みは「クラウドに縛られない」ことです。同じHCLの書き方で、providerブロックを変えるだけで相手が変わります。
# AWSを使うとき
provider "aws" {
region = "ap-northeast-1"
}
# Google Cloudを使うとき
provider "google" {
project = "my-gcp-project"
region = "asia-northeast1"
}
# Azureを使うとき
provider "azurerm" {
features {}
}
注意したいのは、リソースの「書き方」は共通でも「中身」はクラウドごとに違う点です。AWSの aws_s3_bucket とGoogle Cloudの google_storage_bucket は別物で、自動変換はされません。Terraformが共通化してくれるのは「plan/apply/state/moduleという作法」であって、各クラウドの仕様そのものではない。ここを誤解すると「Terraformなら一度書けばどこでも動く」と期待してハマります。
AWS固有のIaCをCloudFormationやCDK(AWS専用の道具)と比べたい人は、AWS CDK入門:CloudFormationとの違いと差分レビューを読むと住み分けが見えます。ざっくり言うと、AWSしか使わないならCDK/CloudFormation、複数クラウドや将来の乗り換えを見据えるならTerraform、というのが僕の使い分けです。AWSの権限設計だけ深掘りしたいならClaude Code × AWS IAMガイドもあわせてどうぞ。
Claude CodeをTerraformの相棒にする
Terraformは便利ですが、HCLは手で書くと地味に疲れます。ここでClaude Codeに手伝ってもらうのですが、コツは「生成させる」より「planの危ない差分を一緒に読ませる」ことです。
依頼は短くても、触ってよい範囲・禁止事項・検証コマンドまで添えます。
claude -p "
Terraformを書いてください。クラウドはAWSではなくGoogle Cloud(provider: google)です。
対象は Cloud Storage bucket を作る module だけ。既存ファイルは削除しない。
要件:
- Terraform 1.10以降
- module は modules/bucket に分ける
- dev/prod は tfvars で分離し、backend key も分ける
- secrets や鍵を tfvars に書かない
- 仕上げに terraform fmt -recursive と terraform validate の手順も出す
- plan に destroy / replace が出たら apply しない前提で、レビュー観点を箇条書きにする
"
そしてplanを保存したら、その予定表をClaude Codeに読ませて「最悪のケース」を拾わせます。AIは便利ですが、最後の安全弁は人間です。
terraform plan -out=tfplan
terraform show -no-color tfplan > plan.txt
claude -p "
plan.txt をレビューしてください。
destroy / replace / force replacement / 権限の拡大 / 公開設定の変更 / state key の変更を最優先で指摘。
削除や作り直しが含まれる場合は『承認しない』と明言してください。
安全に進めるための確認質問を箇条書きで出してください。
"
CIに組み込むなら、PRごとに fmt -check → validate → plan を回し、planの結果をレビュー材料にします。設計の全体像はClaude Code CI/CD設定ガイド、鍵の扱いはClaude Codeで始めるシークレット管理が参考になります。Claude Code側の作法はClaude Code公式ドキュメント、module設計の原典はTerraform Modules公式ドキュメントを見てください。
僕がやらかしたTerraformの失敗3つ
正直に書きます。最初の数ヶ月は事故だらけでした。
ひとつ目は、planを読まずにapplyしたこと。冒頭のセキュリティグループ消失がこれです。緑のログ=成功、ではありません。緑のログは「予定表どおり実行した」だけで、その予定表が間違っていたら忠実に間違いを実行します。今は -out=tfplan で保存して必ず目を通します。
ふたつ目は、stateをローカルに置いたまま2人で作業したこと。僕のPCのstateと同僚のstateがズレて、同じリソースが二重に作られました。リモートstate+ロックに移したら、この手の事故はぴたりと止まりました。
みっつ目は、count でリソースを並べたこと。リストの真ん中に1つ足しただけで番号が全部ずれて、無関係なリソースまでreplace判定。for_each と名前キーに直してからは、要素の増減で既存が壊れなくなりました。
よくある質問
Q. terraform plan と terraform apply の違いは?
planは「これからこう変えるよ」という下見(予定表)で、何も実行しません。applyがその予定表を実際に実行します。terraform plan -out=tfplan で保存し、中身を確認してから terraform apply tfplan する流れが安全です。
Q. state(tfstate)はGitにコミットしてもいい?
だめです。中にIDや一部の機微な値が入るうえ、複数人で更新するとすぐ壊れます。.gitignore に *.tfstate* を入れ、S3やGCSなどのリモートに、ロックと暗号化を付けて置いてください。
Q. リモートstateのロックって何のため?
2人が同時に apply してstateが壊れるのを防ぐためです。誰かが実行中は他の人を待たせます。S3 backendなら use_lockfile = true で有効になり、以前必要だったDynamoDBは非推奨になりました。
Q. TerraformとCloudFormation/CDKはどう使い分ける? AWSだけで完結し、AWSの最新機能をすぐ使いたいならCloudFormation/CDK。複数クラウドや将来の乗り換え、ツールの統一を重視するならTerraform、というのが基本線です。詳しくはAWS CDK入門の記事を参照してください。
Q. dev と prod はどう分ける?
変数ファイルを envs/dev.tfvars・envs/prod.tfvars に分け、backendのstate keyも myapp/dev/...・myapp/prod/... のように分けます。同じstate keyを共有すると、devの変更がprodに紛れ込む典型的な事故になります。
実際に試した結果
この記事の最小構成(random + local)は、クラウド認証なしで init → plan → apply まで数十秒で通りました。greeting を変えて再planすると、ちゃんと ~(変更)の予定表が出て、stateが「前回の記憶」を持っていることを目で確認できます。
そして実クラウドでの肌感覚はこうです。Terraform自体は驚くほど素直で、事故は道具ではなく僕の運用から来ていました。planを必ず保存して読む、stateはロック付きリモートに置く、for_each で名前管理する。この3つを徹底しただけで、ヒヤッとする回数が激減しました。Claude Codeはコードを書かせる相手というより、planの destroy と replace を最優先で拾わせる「もう一組の目」として置くのがいちばん効きます。
賢いツールを使いこなそうとするより、転んでもケガしない順番を先に決める。Terraformでも結局これがいちばん速い、というのが今の実感です。チームでレビュー基準を整えたいなら教材一覧もどうぞ。まずは上の最小構成を手元で動かして、plan と apply の違いを体で覚えるところから始めてみてください。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
制作会社がClaude Codeに触らせる前に決める権限チェックリスト
クライアントサイトを壊さずにAI編集を使うための、制作会社向け権限と確認の型です。
SaaSサポートのバグ報告をClaude Codeで再現手順に変える実務フロー
問い合わせ文をそのまま開発へ投げず、再現手順、証拠、次の一手に整えるサポート向け手順です。
Obsidianの古いメモをClaude Codeの指示書に変える10分ルーチン
Obsidianに溜めたメモが毎回ゴミになる人へ。事実・決定・未確認に仕分けして、Claude Codeがそのまま動ける指示書に変える朝の10分の型を紹介します。