Terraform State管理のベストプラクティス:チーム開発で破綻しないための戦略と実践テクニック
Terraform を実務で使うインフラエンジニア / SRE 向けに、運用の要である State 管理を深掘り解説。State は「コードで定義したインフラ」と「実際のリソース状態」を結びつける 唯一の信頼できる情報源。その扱い方がスケーラビリティ、チーム効率、安定性を左右する。
本記事のゴール
- リモート State とロックで同時実行の破綻を防ぐ
- State 分離で巨大 Stateを回避し、速度と保守性を両立
-
moved/removedで 宣言的に安全なリファクタリングを行う
目次
1. リモート State と排他ロック
ローカル State は分岐を招きやすく、チーム開発では高確率で破綻する。共有場所に State を置く リモートバックエンドを採用し、加えて 排他ロックで同時実行を制御する。
ローカル管理が招く分岐の例
推奨構成(S3 + DynamoDB Lock)
# versions.tf or backend.tf
terraform {
backend "s3" {
bucket = "your-tfstate-bucket-name" # State を保管する S3 バケット
key = "path/to/project/terraform.tfstate" # バケット内のパス
region = "ap-northeast-1"
dynamodb_table = "your-terraform-lock-table" # 排他ロック用テーブル
}
}
実行フロー
terraform plan/apply 開始 → DynamoDB にロック取得 → 他の実行は待機 → 完了で解放
効果
- 単一の最新 State を全員で共有
- 同時書き込みによる破損を防止
- 個人 PC 依存からの脱却
2. 巨大 State を避ける分離戦略
State が肥大化すると plan/apply が遅い、state 操作が複雑、ミスの影響範囲が拡大。分離で回避する。
戦略A: ワークスペースで環境分離
単一コードで複数環境(dev/stg/prod)を管理。terraform.workspace で動的切替。
# main.tf
locals {
instance_settings = {
stg = { type = "t3.medium", ami = "ami-stg-xxxxxxxxxxxx" }
prod = { type = "r5.large", ami = "ami-prod-yyyyyyyyyyyy" }
}
}
resource "aws_instance" "app_server" {
instance_type = local.instance_settings[terraform.workspace].type
ami = local.instance_settings[terraform.workspace].ami
tags = { Name = "${terraform.workspace}-app-server", Env = terraform.workspace }
}
- 長所: コード重複が少ない。環境間でほぼ同一構成のとき効率的
- 短所: 環境差分が大きいと条件分岐が増え可読性が低下
戦略B: ディレクトリ分離
環境・コンポーネントごとにディレクトリを分ける。State も独立。
.
├── envs
│ ├── dev/
│ ├── stg/
│ └── prod/
└── modules/
- 長所: 独立性が高く明快。影響範囲が限定される
- 短所: 共通化しないと重複が増える。環境差異が広がりやすい
分離の判断軸
- 依存関係: 疎結合なら分ける(例: VPC と ECS サービス)
- ライフサイクル/変更頻度: 低頻度(VPC)と高頻度(ECS, ALB, Lambda)は分ける
3. コードによる State 操作: moved / removed
手作業の terraform state mv/rm は属人化しやすい。宣言的ブロックで Git レビュー可能な形にする。
moved で安全にアドレス変更
count → for_each などのリファクタで再作成を防ぐ。
変更前(count)
# main.tf (before)
locals { bucket_name = ["my", "your", "their"] }
resource "aws_s3_bucket" "sample_bucket" {
count = length(local.bucket_name)
bucket = "${local.bucket_name[count.index]}-bucket"
}
# State: aws_s3_bucket.sample_bucket[0],[1],[2]
変更後(for_each + moved)
# main.tf (after)
locals { bucket_name = ["my", "your", "their"] }
resource "aws_s3_bucket" "sample_bucket" {
for_each = toset(local.bucket_name)
bucket = "${each.value}-bucket"
}
moved { from = aws_s3_bucket.sample_bucket[0] to = aws_s3_bucket.sample_bucket["my"] }
moved { from = aws_s3_bucket.sample_bucket[1] to = aws_s3_bucket.sample_bucket["your"] }
moved { from = aws_s3_bucket.sample_bucket[2] to = aws_s3_bucket.sample_bucket["their"] }
- 効果: 実リソースには触れず State アドレスのみ移行
- メリット: Git 履歴で意図が追跡可能。レビュー可能
removed で管理対象から安全に外す
実体を消さずに Terraform 管理から外す。
# resource "aws_iam_role" "legacy_role" { ... } # ← これを削除しつつ
removed {
from = aws_iam_role.legacy_role
lifecycle {
destroy = false # 実リソースは削除しない
}
}
ユースケース
- State A から State B へ移管したい
- いったん
importしてコード化し、その後非管理に戻す
まとめ
- S3 + DynamoDB Lock のリモート State は必須。共有と同時実行制御で破綻を防ぐ
- 分離設計は依存関係とライフサイクルで決める。巨大 State を回避し plan/apply を高速化
-
moved/removedで State 変更を宣言的に。安全で再現性の高いリファクタリングを実現
適切な State 管理は設計思想。ヒューマンエラーを減らし、変更意図を可視化し、プロジェクトのスケールを可能にする。ここで示した戦略を適用すれば、チーム開発に耐える堅牢な IaC を実現できる。