- Authors
- Name

- 概要
- モジュール設計原則
- モジュールコンポジションパターン
- State管理戦略
- リモートバックエンド構成
- Stateロックと同時実行性
- Stateマイグレーション
- Terraform vs OpenTofu比較
- Terratestテスト
- トラブルシューティング
- 運用チェックリスト
- 障害事例と復旧手順
- 参考資料
概要
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 + DynamoDB | GCS (Google Cloud Storage) | Azure Blob Storage |
|---|---|---|---|
| State保存先 | S3 Bucket | GCS Bucket | Blob Container |
| Stateロック | DynamoDB Table | GCS組み込みロック | Azure Blob Lease |
| 暗号化(保存時) | SSE-S3、SSE-KMS | Google-managed、CMEK | Microsoft-managed、CMEK |
| 暗号化(転送時) | TLS 1.2+ | TLS 1.2+ | TLS 1.2+ |
| バージョン管理 | S3 Versioning | Object Versioning | Blob Versioning |
| 追加コスト | DynamoDB別途課金 | 追加コストなし | 追加コストなし |
| 設定の複雑さ | 高い(2サービス) | 低い(1サービス) | 中程度 |
| IAM統合 | AWS IAM | Google IAM | Azure 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変更が必要なすべての操作(plan、apply、destroy、stateサブコマンドなど)でロックを取得する。ロックがすでに存在する場合は待機するかエラーを返す。
# ロック競合時に表示されるメッセージ
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 Values | 1.10+サポート | 独自実装 |
| Write-Only Attributes | 1.11+サポート | 1.11同等機能(2025年7月リリース) |
| プロバイダー互換性 | 完全互換 | 99%以上互換 |
| レジストリ | registry.terraform.io | registry.opentofu.org + 互換 |
| 商用サポート | HCP Terraform、Terraform Enterprise | Spacelift、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運用の各段階で必ず確認すべき項目を整理する。
モジュールリリース前チェックリスト
- すべての変数に
descriptionとtypeが定義されているか validationブロックで入力値を検証しているかoutputs.tfに必要な出力値のみ公開しているかversions.tfにrequired_versionとrequired_providersが明記されているか- README.mdに使用例と入出力の説明があるか
- CHANGELOG.mdに変更履歴が記録されているか
- Terratestまたはterraform testが通過するか
terraform fmtとterraform validateが成功するか
State管理チェックリスト
- リモートバックエンドが構成されているか(ローカルState使用禁止)
- State Lockingが有効化されているか
- State保存先でバージョニングが有効化されているか
- State保存先が暗号化(KMS/CMEK)されているか
- パブリックアクセスがブロックされているか
- 環境別Stateが完全に分離されているか(Directory方式推奨)
- StateアクセスIAMポリシーが最小権限の原則に従っているか
- CI/CDで同時実行防止が設定されているか
変更適用前チェックリスト
terraform planの出力を必ずレビューしたか- destroy対象のリソースが意図したものか
- 機密情報がコードにハードコードされていないか
movedブロック使用時にfromとtoが正確か- 本番変更は別途承認プロセスを経たか
障害事例と復旧手順
事例1:誤ったterraform destroy
あるエンジニアがdev環境で作業中に、prodワークスペースが選択された状態でterraform destroyを実行してしまった事故である。Workspace方式の致命的な弱点を示す事例である。
復旧手順:
- 直ちに
Ctrl+Cでdestroyを中止(実行中の場合) - S3バージョニングで以前のStateバージョンを確認
- 以前のStateにロールバック
terraform planで削除されたリソースを特定terraform applyで削除されたリソースを再作成- 根本対策:Workspace方式からDirectory方式に移行
この事例がDirectory方式が推奨される最大の理由である。prodディレクトリで作業するには明示的にcd environments/prodを実行する必要があり、CI/CDパイプラインも環境別に分離される。
事例2:State Lockデッドロック
DynamoDB Lockが解除されず、すべてのチームメンバーがapplyを実行できない状況である。
復旧手順:
- DynamoDBでLock項目の
Infoカラムを確認(誰が、いつ、どの操作を行ったか) - 該当エンジニアに作業状態を確認(Slack、メッセンジャーなど)
- 作業がすでに終了していることが確認できたら
terraform force-unlockを実行 terraform planでState整合性を検証- 根本対策:CI/CDパイプラインにタイムアウトを設定し、graceful shutdownハンドラーを追加
事例3:Stateファイルに機密情報が漏洩
RDSパスワードがStateファイルに平文で保存されていたことが監査で発見された事例である。
復旧手順:
- S3バケットのアクセスログを確認し、不正アクセスの有無を把握
- 漏洩したパスワードを直ちに変更
- Terraform 1.10+ Ephemeral Resourcesまたは1.11+ Write-Only Attributesに移行
- 以前のStateファイルバージョンにも機密情報が含まれるため、S3 Lifecycle Policyで旧バージョンの削除または有効期限を設定
- 根本対策:OpenToFuのState暗号化機能の導入を検討、またはTerraform 1.11 Write-Only Attributesを全面適用
参考資料
- Terraform 1.10 - Ephemeral ValuesによるStateの秘密管理改善
- Terraform 1.11 - Write-Only ArgumentsによるManaged ResourcesへのEphemeral Values適用
- HashiCorp - Terraformモジュール作成の推奨パターン
- Terraform movedブロックによるリファクタリング
- Terratest - インフラコード自動テストライブラリ
- Spacelift - OpenTofu vs Terraform比較
- AWS - Terraformバックエンドのベストプラクティス
- Google Cloud - Terraformスタイルと構造のベストプラクティス
- Terraform Stateリファクタリング公式ドキュメント