Skip to content
Published on

Terraformモジュール設計パターンとState管理運用プレイブック 2026

Authors
  • Name
    Twitter
Terraformモジュール設計パターンとState管理運用プレイブック 2026

概要

2026年現在、Terraformは1.11バージョンまでリリースされ、Ephemeral ValuesやWrite-Only Attributesなどのセキュリティ重視の機能が大幅に強化された。一方、OpenTofu はState暗号化を独自にサポートし、累計ダウンロード数が1,000万件に迫っている。本記事では、Terraformモジュールを実務でどのように設計・組み合わせるか、そしてStateをチーム単位で安全に運用するためのプレイブックを扱う。単純なRemote Backend設定レベルを超え、モジュールコンポジションアーキテクチャからTerratestベースの検証、Stateマイグレーションのシナリオ別復旧手順まで、IaC運用のライフサイクル全体を網羅する。

本記事が扱う範囲は以下の通りである。

  • モジュール単一責任原則とインターフェース設計
  • Root Module、Child Module、Composition Moduleの階層構造
  • S3、GCS、Azure Blobなどリモートバックエンドの比較と選択基準
  • Workspace vs Directoryベースの環境分離戦略
  • moved、import、removedブロックを活用したStateマイグレーション
  • Terratestとterraform testを併用するテスト戦略
  • 実際の障害事例に基づくトラブルシューティングと復旧手順

モジュール設計原則

Terraformモジュールを正しく設計するには、ソフトウェアエンジニアリングのコア原則をHCLコードに適用する必要がある。最も重要な原則は**単一責任原則(Single Responsibility Principle)**である。1つのモジュールは、ネットワーキング、コンピュート、ストレージのうち1つのドメインのみを担当すべきである。

モジュールディレクトリ構造

適切に設計されたTerraformモジュールは標準的な構造に従う。HashiCorpの公式ガイドラインで推奨される構造を基に、実務で検証されたレイアウトを紹介する。

# 標準モジュールディレクトリ構造
modules/
  networking/
    main.tf          # VPC、Subnet、Route Tableなどコアリソース
    variables.tf     # 入力変数定義(CIDR、AZ、タグなど)
    outputs.tf       # 出力値:VPC ID、Subnet IDなど
    versions.tf      # required_providers、required_version
    README.md        # モジュール使用方法のドキュメント
    tests/           # terraform testまたはTerratestコード
      networking_test.go
  compute/
    main.tf
    variables.tf
    outputs.tf
    versions.tf
    iam.tf           # コンピュート専用IAM Role/Policy
    userdata.tf      # Launch Template、User Data
  database/
    main.tf
    variables.tf
    outputs.tf
    versions.tf
    security_group.tf  # DB専用Security Group

変数バリデーション(Variable Validation)

Terraform 1.9以降では、変数に複雑なバリデーションルールを設定できる。不正な入力値がplan段階で早期にブロックされるため、運用安定性が大幅に向上する。

# variables.tf - 変数バリデーションルールの例
variable "environment" {
  type        = string
  description = "デプロイ環境(dev、staging、prod)"

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "environmentはdev、staging、prodのいずれかでなければなりません。"
  }
}

variable "vpc_cidr" {
  type        = string
  description = "VPC CIDRブロック"

  validation {
    condition     = can(cidrhost(var.vpc_cidr, 0))
    error_message = "有効なCIDR形式でなければなりません(例:10.0.0.0/16)。"
  }

  validation {
    condition     = tonumber(split("/", var.vpc_cidr)[1]) >= 16 && tonumber(split("/", var.vpc_cidr)[1]) <= 24
    error_message = "VPC CIDRのプレフィックス長は/16から/24の間でなければなりません。"
  }
}

variable "instance_type" {
  type        = string
  default     = "t3.medium"
  description = "EC2インスタンスタイプ"

  validation {
    condition     = can(regex("^(t3|t3a|m5|m6i|c5|c6i)\\.", var.instance_type))
    error_message = "承認されたインスタンスファミリーのみ使用可能です:t3、t3a、m5、m6i、c5、c6i。"
  }
}

モジュールインターフェース設計原則

モジュールの入出力はAPIコントラクトと同様である。以下の原則を守ることで、モジュールの再利用性と保守性が最大化される。

原則説明
最小限の公開必要な出力のみをエクスポートするVPC IDは出力するが、内部Route Table IDは隠す
明示的な依存性暗黙の依存ではなく変数で注入するvpc_id = module.networking.vpc_id
デフォルト提供オプション変数には合理的なデフォルト値を設定default = "t3.medium"
型制約object、map、listなど具体的な型を使用するtype = map(object(...))
説明必須すべての変数と出力にdescriptionを記述する新メンバーのオンボーディング時間を短縮する

モジュールコンポジションパターン

実務ではインフラは単一モジュールで構成されない。複数のモジュールを組み合わせて完成した環境を作る**コンポジション(Composition)**パターンが核心である。

階層別モジュール構成

Terraformモジュールは大きく3つの階層に分けられる。

Leaf Module(基本モジュール):単一リソースまたは密接に関連するリソースグループを管理する。例えば、VPCモジュールはVPC、Subnet、Internet Gateway、NAT Gatewayを含む。このモジュールは他のモジュールに依存せず、独立してテスト可能でなければならない。

Composition Module(合成モジュール):複数のLeaf Moduleを組み合わせて1つのサービススタックを構成する。Webサービススタックであれば、networking + compute + database + monitoringモジュールを組み合わせる。Composition Module自体はリソースを直接作成せず、下位モジュールの出力を他のモジュールの入力に接続する役割のみを担う。

Root Module(ルートモジュール):実際にterraform applyを実行する最上位のエントリポイントである。環境別(dev、staging、prod)ディレクトリに配置され、Composition Moduleを呼び出して環境固有の変数を注入する。

# environments/prod/main.tf - Root Moduleの例
module "web_service" {
  source = "../../compositions/web-service"

  environment    = "prod"
  vpc_cidr       = "10.1.0.0/16"
  instance_type  = "m6i.xlarge"
  min_size       = 3
  max_size       = 10
  db_instance_class = "db.r6g.xlarge"
  multi_az       = true

  tags = {
    Team        = "platform"
    CostCenter  = "engineering"
    ManagedBy   = "terraform"
  }
}

# compositions/web-service/main.tf - Composition Moduleの例
module "networking" {
  source = "../../modules/networking"

  vpc_cidr    = var.vpc_cidr
  environment = var.environment
  tags        = var.tags
}

module "compute" {
  source = "../../modules/compute"

  vpc_id         = module.networking.vpc_id
  subnet_ids     = module.networking.private_subnet_ids
  instance_type  = var.instance_type
  min_size       = var.min_size
  max_size       = var.max_size
  environment    = var.environment
  tags           = var.tags
}

module "database" {
  source = "../../modules/database"

  vpc_id            = module.networking.vpc_id
  subnet_ids        = module.networking.database_subnet_ids
  instance_class    = var.db_instance_class
  multi_az          = var.multi_az
  security_group_id = module.compute.app_security_group_id
  environment       = var.environment
  tags              = var.tags
}

Terraform 1.10/1.11 Ephemeral Valuesの活用

Terraform 1.10で導入されたEphemeral Valuesと1.11のWrite-Only Attributesは、モジュール間での機密情報の受け渡し方法を根本的に変えた。従来はデータベースパスワードなどの秘密情報がStateファイルに平文で保存されていたが、現在はStateに記録しない方法で受け渡すことができる。

# Terraform 1.10+ Ephemeral Resourceの例
ephemeral "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "prod/db/master-password"
}

resource "aws_db_instance" "main" {
  identifier     = "prod-primary"
  engine         = "postgres"
  engine_version = "16.2"
  instance_class = "db.r6g.xlarge"

  # write_only属性 - Stateに保存されない(Terraform 1.11+)
  password_wo         = ephemeral.aws_secretsmanager_secret_version.db_password.secret_string
  password_wo_version = 1
}

このアプローチの核心は、password_woの値がplanファイルやstateファイルに一切記録されないことである。パスワードを変更する必要がある場合は、password_wo_versionの値を増やすとTerraformがアップデートをトリガーする。

State管理戦略

State管理はTerraform運用における最も重要な領域である。適切な戦略なしではチームコラボレーションが不可能であり、誤った運用はインフラ全体を危険にさらす可能性がある。

Workspace vs Directory環境分離

環境(dev、staging、prod)を分離する2つの主要戦略がある。それぞれの長所と短所を明確に理解して選択する必要がある。

比較項目Workspace方式Directory方式
Stateファイルの場所同じバックエンド、異なるキー完全に分離されたバックエンド
コードの重複なし(単一コードベース)あり(環境別ディレクトリ)
環境差異の表現条件文、tfvarsファイル各ディレクトリで直接設定
誤操作の影響範囲prodワークスペースでdev変更可能物理的に分離されており安全
チーム規模の適合性小規模チーム(5名以下)中〜大規模チーム
CI/CDパイプライン単一パイプライン+workspace変数環境別独立パイプライン
推奨シナリオ個人プロジェクト、小規模サービス本番運用、エンタープライズ

実務ではDirectory方式が圧倒的に推奨される。Workspace方式では、誤ってterraform workspace select prodを実行した状態でdev変更をapplyするとプロダクションに影響を及ぼす可能性があるためである。Directory方式は物理的な分離によりこのような誤操作を根本的に防止する。

# Directoryベースの環境分離構造
infrastructure/
  modules/          # 再利用モジュール
  environments/
    dev/
      main.tf       # module source = "../../modules/..."
      backend.tf    # S3 key = "dev/terraform.tfstate"
      terraform.tfvars
    staging/
      main.tf
      backend.tf    # S3 key = "staging/terraform.tfstate"
      terraform.tfvars
    prod/
      main.tf
      backend.tf    # S3 key = "prod/terraform.tfstate"
      terraform.tfvars

リモートバックエンド構成

クラウド別リモートバックエンド比較

比較項目AWS S3 + DynamoDBGCS (Google Cloud Storage)Azure Blob Storage
State保存先S3 BucketGCS BucketBlob Container
StateロックDynamoDB TableGCS組み込みロックAzure Blob Lease
暗号化(保存時)SSE-S3、SSE-KMSGoogle-managed、CMEKMicrosoft-managed、CMEK
暗号化(転送時)TLS 1.2+TLS 1.2+TLS 1.2+
バージョン管理S3 VersioningObject VersioningBlob Versioning
追加コストDynamoDB別途課金追加コストなし追加コストなし
設定の複雑さ高い(2サービス)低い(1サービス)中程度
IAM統合AWS IAMGoogle IAMAzure RBAC

S3 + DynamoDBバックエンド構成(本番推奨)

本番環境で最も広く使用されているS3バックエンドの完全なブートストラップ構成である。

# bootstrap/main.tf - Stateバックエンドインフラのブートストラップ
terraform {
  required_version = ">= 1.9.0"
}

provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_s3_bucket" "terraform_state" {
  bucket = "mycompany-terraform-state-prod"

  lifecycle {
    prevent_destroy = true
  }

  tags = {
    Name      = "Terraform State"
    ManagedBy = "bootstrap"
  }
}

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
    }
    bucket_key_enabled = true
  }
}

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_kms_key" "terraform_state" {
  description             = "KMS key for Terraform state encryption"
  deletion_window_in_days = 30
  enable_key_rotation     = true
}

resource "aws_dynamodb_table" "terraform_lock" {
  name         = "terraform-state-lock"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

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

  tags = {
    Name      = "Terraform State Lock"
    ManagedBy = "bootstrap"
  }
}

# 利用側のbackend.tf
# terraform {
#   backend "s3" {
#     bucket         = "mycompany-terraform-state-prod"
#     key            = "prod/networking/terraform.tfstate"
#     region         = "ap-northeast-2"
#     encrypt        = true
#     kms_key_id     = "arn:aws:kms:ap-northeast-2:123456789:key/xxx"
#     dynamodb_table = "terraform-state-lock"
#   }
# }

Stateロックと同時実行性

Stateロック(Locking)は、複数のユーザーが同時にterraform applyを実行する状況でStateファイルの破損を防止する重要なメカニズムである。ロックなしで2人のエンジニアが同時にapplyを実行すると、一方の変更がもう一方によって上書きされる可能性がある。

ロックの動作原理

Terraformは、State変更が必要なすべての操作(planapplydestroystateサブコマンドなど)でロックを取得する。ロックがすでに存在する場合は待機するかエラーを返す。

# ロック競合時に表示されるメッセージ
Error: Error acquiring the state lock
Lock Info:
  ID:        a1b2c3d4-e5f6-7890-abcd-ef1234567890
  Path:      s3://mycompany-terraform-state-prod/prod/terraform.tfstate
  Operation: OperationTypeApply
  Who:       engineer@workstation
  Version:   1.11.0
  Created:   2026-03-04 09:15:23.456789 +0000 UTC

# 強制ロック解除(緊急時のみ使用)
terraform force-unlock a1b2c3d4-e5f6-7890-abcd-ef1234567890

# 強制ロック解除前に必ず確認すべき事項
# 1. 該当Lock IDの操作が本当に終了したか確認
# 2. 他のチームメンバーに現在applyを実行中か確認(Slackなど)
# 3. DynamoDBテーブルで該当Lock項目を直接確認
aws dynamodb get-item \
  --table-name terraform-state-lock \
  --key '{"LockID": {"S": "mycompany-terraform-state-prod/prod/terraform.tfstate"}}'

同時実行制御のベストプラクティス

Stateロックだけでは完全な同時実行制御は不可能である。CI/CDパイプラインでは以下の追加対策が必要である。

第一に、直列実行の保証である。GitHub Actionsではconcurrencyキーを使用して、同じ環境への同時デプロイを防止する。concurrency: group: terraform-prodのように設定すれば、1つのワークフローのみが実行される。第二に、PlanとApplyの分離である。PRではplanのみ実行し、マージ後にapplyを実行するパターンを適用する。第三に、Stateアクセス権限の最小化である。本番Stateへの書き込み権限を持つIAMロールは、CI/CDパイプラインのapply段階でのみAssumeできるよう制限する。

Stateマイグレーション

Stateマイグレーションは、リソース名の変更、モジュールの再構成、バックエンドの切り替えなどの場面で必ず発生する。Terraform 1.1で導入されたmovedブロックとTerraform 1.5で導入されたimportブロックにより、マイグレーション作業が大幅に簡素化された。

movedブロックを活用したリファクタリング

movedブロックは、リソースのアドレスが変更されたときにStateを自動的に更新する。従来のterraform state mvコマンドと異なり、コードに宣言的にマイグレーション意図を表現するため、チーム全体が同じマイグレーションパスに従うことになる。

# リソース名変更:aws_instance.web -> aws_instance.web_server
moved {
  from = aws_instance.web
  to   = aws_instance.web_server
}

resource "aws_instance" "web_server" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.medium"
}

# モジュールへの移動:aws_vpc.main -> module.networking.aws_vpc.main
moved {
  from = aws_vpc.main
  to   = module.networking.aws_vpc.main
}

# モジュール名の変更
moved {
  from = module.old_networking
  to   = module.networking
}

# for_eachキーの変更
moved {
  from = aws_subnet.private["az-a"]
  to   = aws_subnet.private["ap-northeast-2a"]
}

HashiCorpは、共有モジュールの場合、movedブロックをコードに永続的に保持することを推奨している。これにより、さまざまなバージョンのモジュールを使用するコンシューマーが安全にアップグレードできることが保証される。

importブロックを活用した既存リソースの取り込み

# importブロック(Terraform 1.5+) - 宣言的インポート
import {
  to = aws_s3_bucket.existing_logs
  id = "my-existing-log-bucket"
}

resource "aws_s3_bucket" "existing_logs" {
  bucket = "my-existing-log-bucket"
}

# terraform planで差分を事前確認
# terraform applyでStateに反映

バックエンドマイグレーション手順

ローカルバックエンドからS3リモートバックエンドへ切り替える場合は、以下の手順に従う。

# 1. 現在のStateをバックアップ
cp terraform.tfstate terraform.tfstate.backup.$(date +%Y%m%d_%H%M%S)

# 2. backend.tfファイルを作成/修正
cat > backend.tf << 'HEREDOC'
terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "services/web/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"
  }
}
HEREDOC

# 3. terraform initでマイグレーションを実行
terraform init -migrate-state

# 4. マイグレーションの確認
terraform state list

# 5. リモートStateの検証
terraform plan  # No changes expected

# 6. ローカルStateファイルのクリーンアップ(検証完了後)
rm terraform.tfstate terraform.tfstate.backup

Terraform vs OpenTofu比較

2024年にHashiCorpがTerraformのライセンスをBSL(Business Source License)に変更して以降、Linux Foundationが管理するOpenToFuフォークが急速に成長している。2026年初頭時点でOpenToFuはGitHubリリースだけで累計約980万件のダウンロードを記録し、本番環境での導入が拡大している。

比較項目Terraform(HashiCorp)OpenTofu(Linux Foundation)
ライセンスBSL 1.1(商用制限あり)MPL 2.0(完全なオープンソース)
ガバナンスHashiCorp単独コミュニティ投票ベース
State暗号化非対応(外部ツール必要)ネイティブサポート
Ephemeral Values1.10+サポート独自実装
Write-Only Attributes1.11+サポート1.11同等機能(2025年7月リリース)
プロバイダー互換性完全互換99%以上互換
レジストリregistry.terraform.ioregistry.opentofu.org + 互換
商用サポートHCP Terraform、Terraform EnterpriseSpacelift、env0、Scalrなど
パフォーマンス同一アーキテクチャ同一アーキテクチャ(差異はわずか)

選択基準をまとめると次のようになる。ライセンスの制約がなく、既存のHashiCorpエコシステム(Vault、Consulなど)を使用中であれば、Terraformが安全な選択である。一方、オープンソースライセンスが必須であるか、State暗号化のネイティブサポートが必要であるか、コミュニティ主導の開発を好む場合は、OpenToFuが適している。両ツールのHCL構文とCLIワークフローはほぼ同一であるため、移行コストは低い。

Terratestテスト

インフラコードも、アプリケーションコードと同様に自動化テストが必須である。TerratestはGruntworkが開発したGoベースのテストライブラリで、実際のクラウドリソースをプロビジョニングし、検証した後にクリーンアップするエンドツーエンドテストをサポートする。

Terratest vs terraform test比較

Terraform 1.6から組み込まれたterraform testコマンドとTerratestは、異なるテスト領域をカバーする。実務では両ツールを併用するのが効果的である。

  • terraform test:HCLで記述し、planレベルの単体テストに適している。別言語の学習が不要で実行が速い。
  • Terratest:Goで記述し、実際のリソースをデプロイしてHTTPリクエスト、SSH接続などの統合テストを実施する。テスト範囲は広いが、実行時間が長くコストが発生する。

Terratestコード例

// test/networking_test.go
package test

import (
	"testing"

	"github.com/gruntwork-io/terratest/modules/aws"
	"github.com/gruntwork-io/terratest/modules/terraform"
	"github.com/stretchr/testify/assert"
)

func TestNetworkingModule(t *testing.T) {
	t.Parallel()

	awsRegion := "ap-northeast-2"

	terraformOptions := &terraform.Options{
		TerraformDir: "../modules/networking",
		Vars: map[string]interface{}{
			"environment": "test",
			"vpc_cidr":    "10.99.0.0/16",
		},
		EnvVars: map[string]string{
			"AWS_DEFAULT_REGION": awsRegion,
		},
	}

	// テスト終了時にリソースをクリーンアップ
	defer terraform.Destroy(t, terraformOptions)

	// インフラをデプロイ
	terraform.InitAndApply(t, terraformOptions)

	// 出力値を検証
	vpcID := terraform.Output(t, terraformOptions, "vpc_id")
	assert.NotEmpty(t, vpcID)

	// AWS APIで実際のリソースを検証
	vpc := aws.GetVpcById(t, vpcID, awsRegion)
	assert.Equal(t, "10.99.0.0/16", vpc.CidrBlock)

	// Subnet数を検証
	privateSubnetIDs := terraform.OutputList(t, terraformOptions, "private_subnet_ids")
	assert.Equal(t, 3, len(privateSubnetIDs))

	// タグを検証
	actualTags := aws.GetTagsForVpc(t, vpcID, awsRegion)
	assert.Equal(t, "test", actualTags["Environment"])
}

terraform testの例

# tests/networking.tftest.hcl
provider "aws" {
  region = "ap-northeast-2"
}

variables {
  environment = "test"
  vpc_cidr    = "10.99.0.0/16"
}

run "vpc_creation" {
  command = plan

  assert {
    condition     = aws_vpc.main.cidr_block == "10.99.0.0/16"
    error_message = "VPC CIDRブロックが期待値と異なります。"
  }

  assert {
    condition     = aws_vpc.main.tags["Environment"] == "test"
    error_message = "Environmentタグが正しくありません。"
  }
}

run "subnet_count" {
  command = plan

  assert {
    condition     = length(aws_subnet.private) == 3
    error_message = "Private Subnetは3つでなければなりません。"
  }
}

トラブルシューティング

Terraform運用中に最も頻繁に発生する問題とその解決方法を整理する。

State Lockが解除されない場合

CI/CDパイプラインが途中で終了したり、terraform apply実行中にネットワーク切断が発生した場合、State Lockが残ることがある。

# 1. 現在のLock状態を確認
aws dynamodb scan \
  --table-name terraform-state-lock \
  --filter-expression "attribute_exists(LockID)"

# 2. Lock所有者を確認後、安全に解除
terraform force-unlock <LOCK_ID>

# 注意:force-unlockは他のapplyが実行中でないことを確認してから使用すること

Stateと実際のインフラの不一致

AWS Consoleで手動でリソースを変更すると、Stateと実際のインフラの間にドリフトが発生する。

# ドリフトの検出
terraform plan -detailed-exitcode
# Exit code 0: 変更なし
# Exit code 1: エラー
# Exit code 2: 変更あり(ドリフト検出)

# 特定リソースのState更新(実際のインフラに基づいてStateを更新)
terraform apply -refresh-only -target=aws_instance.web_server

# StateからリソースをRemove(実際のインフラは維持)
terraform state rm aws_instance.legacy_server

Stateファイルの破損

極めてまれだが、Stateファイルが破損することがある。S3バージョニングを有効にしていれば、以前のバージョンへの復旧が可能である。

# S3でのStateファイル過去バージョン一覧の確認
aws s3api list-object-versions \
  --bucket mycompany-terraform-state-prod \
  --prefix prod/terraform.tfstate

# 特定バージョンへの復旧
aws s3api get-object \
  --bucket mycompany-terraform-state-prod \
  --key prod/terraform.tfstate \
  --version-id "abc123def456" \
  terraform.tfstate.recovered

# 復旧したStateの検証
terraform show terraform.tfstate.recovered

# Stateファイルの置き換え(DynamoDB Lock確認後)
aws s3 cp terraform.tfstate.recovered \
  s3://mycompany-terraform-state-prod/prod/terraform.tfstate

運用チェックリスト

Terraform IaC運用の各段階で必ず確認すべき項目を整理する。

モジュールリリース前チェックリスト

  • すべての変数にdescriptiontypeが定義されているか
  • validationブロックで入力値を検証しているか
  • outputs.tfに必要な出力値のみ公開しているか
  • versions.tfrequired_versionrequired_providersが明記されているか
  • README.mdに使用例と入出力の説明があるか
  • CHANGELOG.mdに変更履歴が記録されているか
  • Terratestまたはterraform testが通過するか
  • terraform fmtterraform validateが成功するか

State管理チェックリスト

  • リモートバックエンドが構成されているか(ローカルState使用禁止)
  • State Lockingが有効化されているか
  • State保存先でバージョニングが有効化されているか
  • State保存先が暗号化(KMS/CMEK)されているか
  • パブリックアクセスがブロックされているか
  • 環境別Stateが完全に分離されているか(Directory方式推奨)
  • StateアクセスIAMポリシーが最小権限の原則に従っているか
  • CI/CDで同時実行防止が設定されているか

変更適用前チェックリスト

  • terraform planの出力を必ずレビューしたか
  • destroy対象のリソースが意図したものか
  • 機密情報がコードにハードコードされていないか
  • movedブロック使用時にfromtoが正確か
  • 本番変更は別途承認プロセスを経たか

障害事例と復旧手順

事例1:誤ったterraform destroy

あるエンジニアがdev環境で作業中に、prodワークスペースが選択された状態でterraform destroyを実行してしまった事故である。Workspace方式の致命的な弱点を示す事例である。

復旧手順:

  1. 直ちにCtrl+Cでdestroyを中止(実行中の場合)
  2. S3バージョニングで以前のStateバージョンを確認
  3. 以前のStateにロールバック
  4. terraform planで削除されたリソースを特定
  5. terraform applyで削除されたリソースを再作成
  6. 根本対策:Workspace方式からDirectory方式に移行

この事例がDirectory方式が推奨される最大の理由である。prodディレクトリで作業するには明示的にcd environments/prodを実行する必要があり、CI/CDパイプラインも環境別に分離される。

事例2:State Lockデッドロック

DynamoDB Lockが解除されず、すべてのチームメンバーがapplyを実行できない状況である。

復旧手順:

  1. DynamoDBでLock項目のInfoカラムを確認(誰が、いつ、どの操作を行ったか)
  2. 該当エンジニアに作業状態を確認(Slack、メッセンジャーなど)
  3. 作業がすでに終了していることが確認できたらterraform force-unlockを実行
  4. terraform planでState整合性を検証
  5. 根本対策:CI/CDパイプラインにタイムアウトを設定し、graceful shutdownハンドラーを追加

事例3:Stateファイルに機密情報が漏洩

RDSパスワードがStateファイルに平文で保存されていたことが監査で発見された事例である。

復旧手順:

  1. S3バケットのアクセスログを確認し、不正アクセスの有無を把握
  2. 漏洩したパスワードを直ちに変更
  3. Terraform 1.10+ Ephemeral Resourcesまたは1.11+ Write-Only Attributesに移行
  4. 以前のStateファイルバージョンにも機密情報が含まれるため、S3 Lifecycle Policyで旧バージョンの削除または有効期限を設定
  5. 根本対策:OpenToFuのState暗号化機能の導入を検討、またはTerraform 1.11 Write-Only Attributesを全面適用

参考資料