LabHub

ブログ

Terraform 状態管理とモジュール設計実践ガイド:Remote Backend・State Locking・モジュール化パターンとDrift Detection

한국어English日本語

Terraform の状態管理

はじめに

インフラをコードで管理する IaC (Infrastructure as Code) ツールの中で、Terraform は最も広く使われているツールだ。しかし Terraform をプロダクション環境で運用するとき、最も複雑で重要な領域が状態 (State) 管理である。状態ファイルが破損したり衝突が発生したりすると、インフラ運用全体が麻痺しかねず、誤ったモジュール設計は保守コストを指数関数的に増加させる。

本記事では、Terraform 状態ファイルの内部構造から Remote Backend の設定、State Locking のメカニズム、モジュール設計パターン、Drift Detection 戦略、そして実際の障害事例と復旧手順まで、プロダクション環境で必要なすべてを深く扱う。特に Terraform 1.10+ 以降に導入された S3 ネイティブロックのような最新の変化も併せて反映した。

Terraform State のアーキテクチャ

状態ファイルの役割

Terraform は terraform.tfstate という JSON ファイルに、現在管理しているインフラリソースの状態を記録する。このファイルは次のような中核的な役割を果たす。

状態ファイルの構造

状態ファイルの内部構造は次のとおりだ。

{
  "version": 4,
  "terraform_version": "1.10.3",
  "serial": 42,
  "lineage": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "outputs": {
    "vpc_id": {
      "value": "vpc-0abc123def456789",
      "type": "string"
    }
  },
  "resources": [
    {
      "mode": "managed",
      "type": "aws_vpc",
      "name": "main",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 1,
          "attributes": {
            "id": "vpc-0abc123def456789",
            "cidr_block": "10.0.0.0/16",
            "tags": {
              "Name": "production-vpc"
            }
          }
        }
      ]
    }
  ]
}

Plan の動作原理

terraform plan は 3 つの情報を比較して実行計画を生成する。

  1. 設定ファイル (.tf): ユーザーが定義した望ましい状態 (Desired State)
  2. 状態ファイル (.tfstate): 最後に適用された状態 (Known State)
  3. 実際のインフラ: クラウドプロバイダーから取得した現在の状態 (Actual State)

Terraform はまず実際のインフラを参照 (refresh) して状態ファイルを更新し、更新された状態と設定ファイルを比較して変更計画を算出する。

Remote Backend の構成

ローカル Backend の限界

デフォルトでは、Terraform は状態ファイルをローカルファイルシステムに保存する。これは個人プロジェクトでは問題ないが、チーム環境では次のような深刻な限界がある。

S3 + DynamoDB Backend の設定

AWS 環境で最も一般的な構成だ。S3 は状態ファイルの保存先として、DynamoDB は状態ロック用として使われる。

まず Backend のインフラをブートストラップする。

# backend-bootstrap/main.tf
resource "aws_s3_bucket" "terraform_state" {
  bucket = "my-company-terraform-state"

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.terraform_state.arn
    }
  }
}

resource "aws_s3_bucket_public_access_block" "terraform_state" {
  bucket                  = aws_s3_bucket.terraform_state.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-state-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

resource "aws_kms_key" "terraform_state" {
  description             = "KMS key for Terraform state encryption"
  deletion_window_in_days = 30
  enable_key_rotation     = true
}

その後、プロジェクトで Backend を設定する。

# terraform block
terraform {
  required_version = ">= 1.10.0"

  backend "s3" {
    bucket         = "my-company-terraform-state"
    key            = "production/network/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    kms_key_id     = "arn:aws:kms:ap-northeast-2:123456789012:key/abcd-1234"
    dynamodb_table = "terraform-state-locks"
  }
}

S3 ネイティブロック (Terraform 1.10+)

Terraform 1.10.0 から S3 ネイティブの状態ロックが導入され、DynamoDB はもはや不要になった。S3 の条件付き書き込み (Conditional Write) 機能を活用する。

terraform {
  backend "s3" {
    bucket       = "my-company-terraform-state"
    key          = "production/network/terraform.tfstate"
    region       = "ap-northeast-2"
    encrypt      = true
    use_lockfile = true
  }
}

use_lockfile = true の設定だけで、DynamoDB なしに S3 で直接ロックを管理する。既存の DynamoDB ベースのロックから移行するときは、dynamodb_table の設定を削除し use_lockfile = true を追加したうえで terraform init -migrate-state を実行すればよい。

GCS Backend の設定

GCP 環境では Google Cloud Storage を使う。GCS Backend はロック機能が内蔵されているため、別途ロックテーブルは必要ない。

terraform {
  backend "gcs" {
    bucket = "my-company-terraform-state"
    prefix = "production/network"
  }
}

Terraform Cloud / HCP Terraform

HashiCorp のマネージドサービスで、状態管理だけでなく実行環境、ポリシー管理、監査ログなども提供する。

terraform {
  cloud {
    organization = "my-company"

    workspaces {
      name = "production-network"
    }
  }
}

State Locking と同時実行制御

ロック機構が必要な理由

複数のユーザーや CI/CD パイプラインが同時に terraform apply を実行すると、状態ファイルの衝突が発生し、インフラの不整合、リソースの重複作成、最悪の場合は状態ファイルの破損が起こりうる。State Locking はこれを防ぐ中核的なメカニズムだ。

DynamoDB ベースのロックの動作フロー

  1. terraform plan または terraform apply を実行する
  2. Terraform が DynamoDB テーブルにロック項目を作成する (LockID = 状態ファイルのパス)
  3. ロックの取得に成功したら作業を実行する
  4. 別のユーザーが同じ状態に対して作業を試みると、ロック衝突エラーが発生する
  5. 作業完了後にロックを解放する

ロック衝突の解消

ロックがかかった状態で作業を試みると、次のようなエラーが発生する。

Error: Error acquiring the state lock
Lock Info:
  ID:        a1b2c3d4-e5f6-7890
  Path:      my-company-terraform-state/production/network/terraform.tfstate
  Operation: OperationTypeApply
  Who:       user@hostname
  Version:   1.10.3
  Created:   2026-03-11 09:15:30.123456 +0000 UTC

正常な場合は作業が完了するまで待つ必要がある。プロセスのクラッシュやネットワーク断などで異常にロックが残っている場合は、強制解除を行う。

# ロックの強制解除 (必ず他の作業が実行中でないことを確認してから)
terraform force-unlock a1b2c3d4-e5f6-7890

注意: 実際に別のユーザーが apply 中の状態で強制解除すると状態ファイルが破損しうるため、必ずそのロックの所有者と確認してから実行しなければならない。

モジュール設計パターン

モジュール構造の標準

HashiCorp が推奨する標準的なモジュール構造は次のとおりだ。

modules/
  vpc/
    main.tf          # リソース定義
    variables.tf     # 入力変数
    outputs.tf       # 出力値
    versions.tf      # プロバイダーおよび Terraform バージョン制約
    README.md        # モジュール利用ドキュメント
    examples/
      simple/
        main.tf
      complete/
        main.tf
    tests/
      vpc_test.go    # Terratest ベースのテスト

Composition パターン

小さな単位のモジュールを組み合わせて上位のインフラを構成するパターンだ。各モジュールは単一責任の原則に従い、依存関係は引数として注入される。

# environments/production/main.tf
module "vpc" {
  source  = "../../modules/vpc"
  name    = "production"
  cidr    = "10.0.0.0/16"
  azs     = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
}

module "security_groups" {
  source = "../../modules/security-groups"
  vpc_id = module.vpc.vpc_id
  environment = "production"
}

module "eks" {
  source             = "../../modules/eks"
  cluster_name       = "production-cluster"
  vpc_id             = module.vpc.vpc_id
  subnet_ids         = module.vpc.private_subnet_ids
  security_group_ids = [module.security_groups.eks_sg_id]
}

module "rds" {
  source             = "../../modules/rds"
  identifier         = "production-db"
  vpc_id             = module.vpc.vpc_id
  subnet_ids         = module.vpc.database_subnet_ids
  security_group_ids = [module.security_groups.rds_sg_id]
}

Facade パターン

複雑な内部モジュールの組み合わせを、単純なインターフェースの背後に隠すパターンだ。消費者 (Consumer) に簡潔な API を提供しつつ、内部の複雑さをカプセル化する。

# modules/web-application/main.tf
# 内部で VPC、ALB、ECS、RDS モジュールを組み合わせる
module "vpc" {
  source = "../vpc"
  cidr   = var.vpc_cidr
}

module "alb" {
  source    = "../alb"
  vpc_id    = module.vpc.vpc_id
  subnet_ids = module.vpc.public_subnet_ids
}

module "ecs" {
  source       = "../ecs"
  cluster_name = var.app_name
  vpc_id       = module.vpc.vpc_id
  alb_arn      = module.alb.alb_arn
}

# 消費者はシンプルに使う
# environments/production/main.tf
module "web_app" {
  source   = "../../modules/web-application"
  app_name = "my-web-app"
  vpc_cidr = "10.0.0.0/16"
}

Registry によるバージョン管理

Private Registry を活用すると、モジュールのバージョン管理とチーム間の共有が容易になる。

module "vpc" {
  source  = "app.terraform.io/my-company/vpc/aws"
  version = "~> 3.2.0"

  name = "production"
  cidr = "10.0.0.0/16"
}

バージョン制約のパターンは次のとおりだ。

Drift Detection と Remediation

Drift とは何か

Drift (ドリフト) とは、Terraform が管理する実際のインフラが状態ファイルと一致しない状態を指す。主な原因は次のとおりだ。

terraform plan による検知

最も基本的な Drift Detection の方法は、terraform plan を定期的に実行することだ。

# 状態だけを更新して drift を確認 (インフラ変更なし)
terraform plan -refresh-only

# 詳細出力で変更内容を確認
terraform plan -refresh-only -detailed-exitcode
# 終了コード: 0 = 変更なし、1 = エラー、2 = drift 検知

CI/CD パイプラインへの Drift Detection の統合

#!/bin/bash
# drift-detection.sh
set -euo pipefail

SLACK_WEBHOOK_URL="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXX"

echo "Running drift detection..."
terraform init -backend=true -input=false

# -detailed-exitcode: 終了コード 2 は変更が存在することを示す
if terraform plan -refresh-only -detailed-exitcode -input=false > plan_output.txt 2>&1; then
  echo "No drift detected."
  exit 0
fi

EXIT_CODE=$?

if [ "$EXIT_CODE" -eq 2 ]; then
  echo "Drift detected! Sending notification..."
  DRIFT_SUMMARY=$(grep -E "^  #|^  ~|^  -|^  \+" plan_output.txt | head -20)

  curl -X POST "$SLACK_WEBHOOK_URL" \
    -H 'Content-Type: application/json' \
    -d "{\"text\": \"Drift detected in production infrastructure:\n\`\`\`\n${DRIFT_SUMMARY}\n\`\`\`\"}"
  exit 2
else
  echo "Error running terraform plan"
  exit 1
fi

Drift Remediation の戦略

Drift を発見したときの対応方法は大きく 3 つある。

  1. Terraform で元に戻す: terraform apply を実行し、コードに定義された状態にインフラを戻す
  2. コードに反映する: 手動変更が意図されたものであれば、HCL コードを更新して現在のインフラ状態を反映する
  3. 状態ファイルの更新: terraform apply -refresh-only を実行し、状態ファイルだけを現在のインフラに合わせて更新する

State 移行戦略

ローカルから Remote Backend への移行

# 1. backend 設定を追加した後
terraform init -migrate-state

# 2. 移行確認のプロンプトに yes と入力
# 3. ローカル状態ファイルを削除
rm terraform.tfstate terraform.tfstate.backup

terraform state mv の活用

リソース名の変更やモジュールのリファクタリング時に使う。

# リソース名の変更
terraform state mv aws_instance.old_name aws_instance.new_name

# モジュールへ移動
terraform state mv aws_vpc.main module.network.aws_vpc.main

# 別の状態ファイルへ移動
terraform state mv -state-out=other.tfstate aws_s3_bucket.data aws_s3_bucket.data

terraform import による既存リソースの取り込み

# 既存リソースを Terraform の管理下に置く
terraform import aws_instance.web i-1234567890abcdef0

# モジュール内リソースの import
terraform import module.vpc.aws_vpc.main vpc-0abc123def456789

Terraform 1.5+ では import ブロックを使った宣言的な Import も可能だ。

import {
  to = aws_instance.web
  id = "i-1234567890abcdef0"
}

moved ブロックを使ったリファクタリング (Terraform 1.1+)

moved {
  from = aws_instance.old_name
  to   = aws_instance.new_name
}

moved {
  from = aws_vpc.main
  to   = module.network.aws_vpc.main
}

moved ブロックを使うと、terraform state mv と違ってリファクタリングの履歴をコードに宣言的に残せ、チームメンバーが terraform plan を実行したときに自動で状態が移行される。

比較分析

Backend 比較表

項目S3 + DynamoDBS3 ネイティブ (1.10+)GCSTerraform Cloud
ロック方式DynamoDB テーブルS3 条件付き書き込みGCS オブジェクトロック内蔵
追加インフラS3 バケット + DynamoDB テーブルS3 バケットのみGCS バケットのみなし (SaaS)
暗号化SSE-S3/SSE-KMSSSE-S3/SSE-KMSGoogle 管理キー/CMEKHashiCorp Vault
バージョン管理S3 バージョン管理S3 バージョン管理GCS オブジェクトバージョン自動バージョン管理
アクセス制御IAM ポリシーIAM ポリシーIAM ポリシーチーム/組織ベースの RBAC
コストS3 + DynamoDB のコストS3 のコストのみGCS のコスト無料枠 + 有料
設定の複雑さ中程度低い低い非常に低い
マルチクラウドAWS 専用AWS 専用GCP 専用クラウド非依存

モジュール設計パターンの比較

パターン適した状況長所短所
Flat (単一構成)小規模プロジェクト、プロトタイプ単純で立ち上げが速い再利用不可、コードの重複
Composition中・大規模プロジェクト、チーム協業再利用性、テストが容易初期設計のコスト
Facade複雑なインフラ、セルフサービスプラットフォーム使いやすさ、一貫性柔軟性の低下、抽象化の維持コスト
Registry大規模組織、マルチチームガバナンス、バージョン管理運用オーバーヘッド

運用上の注意事項

状態ファイルには機密情報が含まれる

Terraform の状態ファイルには、データベースのパスワード、API キー、証明書といった機密情報が平文で保存されうる。必ず次を守らなければならない。

output "database_password" {
  value     = aws_db_instance.main.password
  sensitive = true
}

状態ファイルの分割戦略

大規模インフラでは、単一の状態ファイルですべてのリソースを管理すると次のような問題が発生する。

推奨される分割構造は次のとおりだ。

environments/
  production/
    network/          # VPC、サブネット、NAT GW (インフラチーム)
    security/         # IAMKMS、Security Group (セキュリティチーム)
    database/         # RDS、ElastiCache (DBA チーム)
    application/      # ECS、Lambda、API GW (開発チーム)
    monitoring/       # CloudWatch、Datadog (SRE チーム)
  staging/
    ...

各ディレクトリは独立した状態ファイルを持ち、terraform_remote_state データソースを通じて他の状態の出力値を参照する。

data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "my-company-terraform-state"
    key    = "production/network/terraform.tfstate"
    region = "ap-northeast-2"
  }
}

resource "aws_instance" "web" {
  subnet_id = data.terraform_remote_state.network.outputs.private_subnet_ids[0]
}

terraform plan の自動実行には注意が必要だ

CI/CD で terraform plan を自動実行することは推奨されるが、terraform apply -auto-approve を自動実行するのはプロダクション環境では非常に危険だ。必ず承認プロセスを経なければならない。

障害事例と復旧手順

事例 1: 状態ファイルの破損

状態ファイルが破損する最も多い原因は、ネットワーク障害中に terraform apply が中断されることだ。

症状: terraform plan 実行時に JSON パースエラーが発生する。

復旧手順:

# 1. S3 バージョン管理から以前のバージョンを復元
aws s3api list-object-versions \
  --bucket my-company-terraform-state \
  --prefix production/network/terraform.tfstate

# 2. 正常な以前のバージョンをダウンロード
aws s3api get-object \
  --bucket my-company-terraform-state \
  --key production/network/terraform.tfstate \
  --version-id "VERSION_ID_HERE" \
  restored-state.tfstate

# 3. 状態ファイルの検証
terraform show restored-state.tfstate

# 4. 復元した状態ファイルをアップロード
aws s3 cp restored-state.tfstate \
  s3://my-company-terraform-state/production/network/terraform.tfstate

# 5. refresh で最新状態を同期
terraform apply -refresh-only

事例 2: ロックが解除されない (Stuck Lock)

CI/CD パイプラインが異常終了してロックが残ってしまった場合だ。

症状: Error acquiring the state lock エラーが継続的に発生する。

復旧手順:

# 1. 実際に他の作業が実行中かどうかを確認
# DynamoDB でロック項目を照会
aws dynamodb get-item \
  --table-name terraform-state-locks \
  --key '{"LockID": {"S": "my-company-terraform-state/production/network/terraform.tfstate"}}'

# 2. ロックの所有者と時刻を確認してから強制解除
terraform force-unlock LOCK_ID_HERE

# 3. 状態の整合性を確認
terraform plan

事例 3: State の衝突 (Serial Mismatch)

2 人のユーザーがほぼ同時に apply を実行し、serial 番号が一致しなくなった場合だ。

症状: Error saving state: serial number mismatch エラーが発生する。

復旧手順:

# 1. 現在のリモート状態をダウンロード
terraform state pull > remote-state.json

# 2. serial 番号を確認
python3 -c "import json; print(json.load(open('remote-state.json'))['serial'])"

# 3. refresh で状態を同期
terraform apply -refresh-only

# 4. 変更を再適用
terraform plan
terraform apply

プロダクションチェックリスト

Terraform をプロダクションに導入するときに必ず確認すべき項目をまとめる。

Remote Backend の設定

State Locking

モジュール管理

Drift Detection

運用プロセス

参考資料

おわりに

Terraform の状態管理は IaC 運用の根幹だ。状態ファイルが正しく管理されなければ、どれほどよく書かれた HCL コードでもプロダクション環境で安定して動作させることはできない。本記事で扱った内容をまとめると次のようになる。

第一に、Remote Backend は必須である。ローカルの状態ファイルはチーム環境では使えず、S3、GCS または Terraform Cloud を通じて安全に状態を管理しなければならない。Terraform 1.10 以上であれば、S3 ネイティブロックを活用して DynamoDB への依存を取り除ける。

第二に、State Locking は同時実行制御の核である。ロックなしに複数のユーザーが同時に apply すると、状態ファイルの破損は避けられない。ロックの強制解除は、現在実行中の作業がないことを必ず確認してから行わなければならない。

第三に、モジュール設計は再利用性と保守性の基盤である。Composition パターンで小さなモジュールを組み合わせ、Registry を通じてバージョンを管理すれば、大規模な組織でも一貫したインフラを維持できる。

第四に、Drift Detection は継続的に実行しなければならない。手動変更はいつでも起こりうるもので、これを早期に検知してコードに反映しなければ、Terraform と実際のインフラの乖離が蓄積し、最終的には Terraform 自体を使えなくなる。

最後に、障害は必ず発生するという前提のもとで、復旧手順をあらかじめ検証しておかなければならない。S3 バージョン管理による状態ファイルの復元、ロックの強制解除、serial 衝突の解消といった手順をチーム全体が把握していてこそ、実際の障害時に素早く対応できる。

Terraform の状態管理を堅牢に構築すれば、インフラ変更を自信を持って実行できる基盤ができあがる。これは DevOps 文化が目指す、速く安全なデプロイの中核的な前提条件だ。

コメント

まだコメントはありません。

ログインするとコメントできます