- Published on
Terraform モジュール設計パターン完全ガイド:状態管理・ワークスペース・Atlantis 自動化
- Authors
- Name
- はじめに
- Terraform モジュールの基本構造と設計原則
- モジュール設計パターン
- 変数設計と出力値戦略
- リモート状態管理
- ワークスペース戦略 vs ディレクトリ分離
- Atlantis を活用した GitOps 自動化
- モジュールバージョン管理とレジストリ
- 比較表
- 障害事例と復旧手順
- 運用チェックリスト
- まとめ

はじめに
Infrastructure as Code(IaC)エコシステムにおいて、Terraform はマルチクラウド環境を支援するデファクトスタンダードツールとしての地位を確立した。しかし、Terraform プロジェクトの規模が拡大するにつれて、モジュール設計、状態管理、チーム協業ワークフローの複雑性が指数関数的に増大する。
単一の main.tf ファイルに数百のリソースを列挙する初期のアプローチは、メンテナンス不可能な「スパゲッティインフラ」へと急速に変質する。モジュール化されたコードであっても、状態ファイルが一つに集中していると terraform plan に10分以上かかり、チームメンバー間の状態競合が頻繁に発生する。
本記事では、Terraform モジュール設計の3大パターン(Composition、Facade、Factory)を実際の HCL コードとともに解説し、リモート状態管理(S3+DynamoDB、GCS、Terraform Cloud)、ワークスペース戦略、そして Atlantis を活用した GitOps ベースの自動化まで包括的に取り上げる。実運用で直面する状態ロック競合、ドリフト検知、循環依存などの障害事例と復旧手順も含めた。
Terraform モジュールの基本構造と設計原則
モジュールディレクトリ構造
適切に設計された Terraform モジュールは明確なファイル構造に従う。HashiCorp 公式ガイドラインと Google Cloud Best Practices に基づく標準構造は以下の通りである。
modules/
networking/
main.tf # コアリソース定義
variables.tf # 入力変数宣言
outputs.tf # 出力値定義
versions.tf # provider/terraform バージョン制約
README.md # モジュール使用法ドキュメント
examples/
simple/
main.tf # シンプルな使用例
complete/
main.tf # 全オプション活用例
tests/
networking_test.go # terratest テスト
コア設計原則
1. 単一責任原則(Single Responsibility)
一つのモジュールは一つの論理的な機能のみを担当すべきである。「モジュールの機能や目的を一文で説明するのが難しい場合、そのモジュールは複雑すぎる」というのが HashiCorp の基準である。
2. 疎結合(Loose Coupling)
モジュール間の直接的な依存関係を最小化する。terraform plan 実行時に一つのモジュールの変更が他の複数のモジュールの状態を予期せず変更する場合、モジュール間の結合度が高すぎるという信号である。
3. 共有モジュールでのプロバイダー設定禁止
共有モジュールでは provider ブロックや backend ブロックを直接設定しない。プロバイダー設定は常にルートモジュールで行う。
# 悪い例 - モジュール内部でプロバイダーを設定
# modules/vpc/main.tf
provider "aws" {
region = "ap-northeast-1" # ハードコードされたリージョン
}
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
}
# 良い例 - ルートモジュールでプロバイダーを設定
# environments/prod/main.tf
provider "aws" {
region = "ap-northeast-1"
}
module "vpc" {
source = "../../modules/vpc"
cidr_block = "10.0.0.0/16"
}
4. 出力値の必須化
モジュールで作成するすべてのリソースに対して、最低一つの出力値を定義する。出力値がなければモジュール間の依存関係推論が不可能であり、他のモジュールからリソースを参照できない。
モジュール設計パターン
1. Composition パターン(コンポジション)
小さな単位のモジュールを組み合わせて複雑なインフラを構成するパターンである。ソフトウェア工学の「Composition over Inheritance」原則をインフラコードに適用したもので、最も推奨されるパターンである。
# environments/prod/main.tf - Composition パターン
module "vpc" {
source = "../../modules/networking/vpc"
cidr_block = "10.0.0.0/16"
azs = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
}
module "security_group" {
source = "../../modules/networking/security-group"
vpc_id = module.vpc.vpc_id
ingress_rules = [
{
port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
]
}
module "eks" {
source = "../../modules/compute/eks"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
security_group_id = module.security_group.sg_id
cluster_version = "1.31"
}
module "rds" {
source = "../../modules/database/rds"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.database_subnet_ids
security_group_id = module.security_group.sg_id
engine = "postgres"
engine_version = "16.4"
}
各モジュールは独立してテスト、バージョン管理、再利用が可能であり、出力値を通じてモジュール間でデータを受け渡す。
2. Facade パターン(ファサード)
複雑な内部実装を隠蔽し、利用者にシンプルなインターフェースを提供するパターンである。テレビのリモコンのように、一つのボタン(変数)で内部の複雑な動作(複数リソースの作成)を制御する。
# modules/platform/main.tf - Facade パターン
variable "environment" {
type = string
}
variable "app_name" {
type = string
}
variable "instance_type" {
type = string
default = "t3.medium"
}
# 内部で複数のサブモジュールを組み合わせ
module "networking" {
source = "../networking/vpc"
cidr_block = var.environment == "prod" ? "10.0.0.0/16" : "10.1.0.0/16"
environment = var.environment
}
module "compute" {
source = "../compute/eks"
vpc_id = module.networking.vpc_id
subnet_ids = module.networking.private_subnet_ids
instance_type = var.instance_type
cluster_name = "cluster-name-placeholder"
}
module "monitoring" {
source = "../observability/cloudwatch"
cluster_id = module.compute.cluster_id
alarm_sns = module.compute.alarm_topic_arn
}
# 利用者はシンプルに使用
# environments/prod/main.tf
module "platform" {
source = "../../modules/platform"
environment = "prod"
app_name = "my-service"
instance_type = "m5.xlarge"
}
3. Factory パターン(ファクトリー)
for_each を活用して、同一構造のリソースをデータ駆動で大量生成するパターンである。
# modules/multi-region/main.tf - Factory パターン
variable "regions" {
type = map(object({
cidr_block = string
instance_type = string
replicas = number
}))
}
module "regional_stack" {
source = "../regional-stack"
for_each = var.regions
region = each.key
cidr_block = each.value.cidr_block
instance_type = each.value.instance_type
replicas = each.value.replicas
}
# 使用例
module "global_infra" {
source = "../../modules/multi-region"
regions = {
"ap-northeast-1" = {
cidr_block = "10.0.0.0/16"
instance_type = "m5.xlarge"
replicas = 3
}
"us-east-1" = {
cidr_block = "10.1.0.0/16"
instance_type = "m5.large"
replicas = 2
}
}
}
変数設計と出力値戦略
変数設計ガイドライン
効果的な変数設計がモジュールの再利用性と安定性を決定する。
# modules/vpc/variables.tf
variable "cidr_block" {
type = string
description = "VPC CIDR block (e.g., 10.0.0.0/16)"
validation {
condition = can(cidrnetmask(var.cidr_block))
error_message = "Must be a valid CIDR block."
}
}
variable "environment" {
type = string
description = "Environment name (dev, staging, prod)"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "enable_nat_gateway" {
type = bool
default = true
description = "Whether to create NAT Gateways for private subnets"
}
variable "tags" {
type = map(string)
default = {}
description = "Additional tags to apply to all resources"
}
核心原則: 環境ごとに変わるべき値(CIDR、インスタンスサイズ、名前、タイムアウト等)のみを変数として公開し、内部実装の詳細(IAM ポリシー構造、ログ設定、タグ体系等)はモジュール内部にカプセル化する。
出力値設計
# modules/vpc/outputs.tf
output "vpc_id" {
value = aws_vpc.main.id
description = "The ID of the VPC"
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
description = "List of private subnet IDs"
}
output "database_subnet_ids" {
value = aws_subnet.database[*].id
description = "List of database subnet IDs"
}
output "nat_gateway_ips" {
value = aws_eip.nat[*].public_ip
description = "Elastic IPs of NAT Gateways"
}
リモート状態管理
S3 + DynamoDB バックエンド(AWS)
AWS 環境で最も広く使用されるリモート状態管理構成である。S3 は状態ファイルの保存、DynamoDB は状態ロックを担当する。なお、AWS は DynamoDB ベースのロックから S3 ネイティブロックへの移行を進めているため、最新バージョンでは use_lockfile = true オプションを確認する必要がある。
# backend.tf - S3 + DynamoDB リモート状態設定
terraform {
backend "s3" {
bucket = "my-company-terraform-state"
key = "prod/networking/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
# use_lockfile = true # S3 ネイティブロック(最新バージョン)
}
}
状態バケットブートストラップスクリプト:
#!/bin/bash
# bootstrap-backend.sh - 状態保存インフラの作成
BUCKET_NAME="my-company-terraform-state"
DYNAMODB_TABLE="terraform-state-lock"
REGION="ap-northeast-1"
# S3 バケット作成
aws s3api create-bucket \
--bucket "$BUCKET_NAME" \
--region "$REGION" \
--create-bucket-configuration LocationConstraint="$REGION"
# バージョニング有効化
aws s3api put-bucket-versioning \
--bucket "$BUCKET_NAME" \
--versioning-configuration Status=Enabled
# パブリックアクセスブロック
aws s3api put-public-access-block \
--bucket "$BUCKET_NAME" \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
# KMS 暗号化設定
aws s3api put-bucket-encryption \
--bucket "$BUCKET_NAME" \
--server-side-encryption-configuration '{
"Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "aws:kms"}}]
}'
# DynamoDB テーブル作成(状態ロック用)
aws dynamodb create-table \
--table-name "$DYNAMODB_TABLE" \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region "$REGION"
echo "Backend infrastructure created successfully"
GCS バックエンド(Google Cloud)
terraform {
backend "gcs" {
bucket = "my-company-tf-state"
prefix = "prod/networking"
}
}
Terraform Cloud / HCP Terraform
terraform {
cloud {
organization = "my-company"
workspaces {
name = "prod-networking"
}
}
}
リモート状態データソース(クロススタック参照)
あるスタックの出力値を別のスタックから参照するには、terraform_remote_state データソースを使用する。
# compute スタックから networking スタックの状態を参照
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "my-company-terraform-state"
key = "prod/networking/terraform.tfstate"
region = "ap-northeast-1"
}
}
resource "aws_instance" "app" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
subnet_id = data.terraform_remote_state.networking.outputs.private_subnet_ids[0]
}
ワークスペース戦略 vs ディレクトリ分離
ワークスペース方式
Terraform ワークスペースは同一の .tf ファイルを共有しながら、環境ごとに独立した状態ファイルを管理する。
# ワークスペースの作成と切り替え
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
terraform workspace select prod
# 現在のワークスペース確認
terraform workspace show
HCL でのワークスペース参照:
resource "aws_instance" "app" {
instance_type = terraform.workspace == "prod" ? "m5.xlarge" : "t3.medium"
tags = {
Environment = terraform.workspace
}
}
ディレクトリ分離方式
infrastructure/
modules/
vpc/
eks/
rds/
environments/
dev/
main.tf
terraform.tfvars
backend.tf
staging/
main.tf
terraform.tfvars
backend.tf
prod/
main.tf
terraform.tfvars
backend.tf
ワークスペース vs ディレクトリの比較
| 基準 | ワークスペース | ディレクトリ分離 |
|---|---|---|
| コード重複 | なし(コード共有) | 一部重複発生 |
| 環境間分離 | 弱い(同一バックエンド) | 強い(別バックエンド可) |
| IAM 権限分離 | 困難 | 環境別に設定可能 |
| 爆発半径 | 広い(コード共有) | 狭い(独立的) |
| 運用複雑度 | 低い | 中程度 |
| 適した用途 | 一時的環境、テスト | 本番環境 |
推奨事項: 本番環境にはディレクトリ分離を、短期テスト環境にはワークスペースを使用する。多くの成功しているチームは両方のアプローチを組み合わせて使用している。
Atlantis を活用した GitOps 自動化
Atlantis とは
Atlantis は Pull Request ベースで Terraform の plan と apply を自動化する GitOps ツールである。開発者がインフラ変更の PR を作成すると、Atlantis が自動的に terraform plan を実行し、その結果を PR コメントに表示する。レビュアーが承認すると、atlantis apply コメントで適用できる。
主要なメリット
- 一貫した実行環境: すべての Terraform 実行が専用サーバーで行われ、「自分の PC では動くのに」問題を防止
- 自動状態ロック: PR がオープンしている間、該当プロジェクトの状態ファイルをロックし同時変更を防止
- コードレビューとの統合: plan 結果を PR で直接確認し、インフラ変更の可視性を確保
- 監査ログ: すべての変更が PR 履歴に記録
atlantis.yaml 設定
# atlantis.yaml - リポジトリルートに配置
version: 3
automerge: false
parallel_plan: true
parallel_apply: false
projects:
- name: prod-networking
dir: environments/prod/networking
workspace: default
terraform_version: v1.9.0
autoplan:
when_modified:
- '*.tf'
- '*.tfvars'
- '../../../modules/networking/**/*.tf'
enabled: true
apply_requirements:
- approved
- mergeable
- name: prod-compute
dir: environments/prod/compute
workspace: default
terraform_version: v1.9.0
autoplan:
when_modified:
- '*.tf'
- '*.tfvars'
- '../../../modules/compute/**/*.tf'
enabled: true
apply_requirements:
- approved
- mergeable
- name: dev-networking
dir: environments/dev/networking
workspace: default
terraform_version: v1.9.0
autoplan:
when_modified:
- '*.tf'
- '*.tfvars'
enabled: true
Atlantis ワークフローのカスタマイズ
# atlantis.yaml - カスタムワークフロー
workflows:
custom:
plan:
steps:
- run: terraform fmt -check -recursive
- run: tflint --init
- run: tflint
- init
- plan
apply:
steps:
- apply
モジュールバージョン管理とレジストリ
セマンティックバージョニング
Terraform モジュールはセマンティックバージョニング(SemVer)に従うことが推奨される。
- メジャーバージョン: 必須入力変数の追加、出力値の削除など互換性が壊れる変更
- マイナーバージョン: オプション入力変数の追加、新しい出力値の追加
- パッチバージョン: バグ修正、ドキュメント更新
# バージョン制約の指定
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0" # 5.x 範囲内で最新バージョン
}
module "eks" {
source = "git::https://github.com/my-org/terraform-aws-eks.git?ref=v3.2.1"
}
プライベートモジュールレジストリ
Terraform Cloud や自前のレジストリを使用して組織内部のモジュールを管理できる。
# Terraform Cloud プライベートレジストリの使用
module "vpc" {
source = "app.terraform.io/my-org/vpc/aws"
version = "2.1.0"
}
比較表
状態バックエンドの比較
| 機能 | S3 + DynamoDB | GCS | Terraform Cloud | Azure Blob |
|---|---|---|---|---|
| 状態ロック | DynamoDB / S3 ネイティブ | 標準搭載 | 標準搭載 | Blob Lease |
| 暗号化 | KMS | Google KMS | 標準搭載 | Azure KeyVault |
| バージョン管理 | S3 Versioning | Object Versioning | 標準搭載 | Blob Snapshots |
| アクセス制御 | IAM Policy | IAM | Teams/RBAC | Azure RBAC |
| コスト | S3 + DynamoDB 課金 | GCS 課金 | 無料枠制限あり | Blob 課金 |
| 設定難易度 | 中程度 | 低い | 低い | 中程度 |
IaC ツール比較
| 特性 | Terraform/OpenTofu | Pulumi | Crossplane | CloudFormation |
|---|---|---|---|---|
| 言語 | HCL | TypeScript/Python/Go | YAML/CRD | JSON/YAML |
| 状態管理 | 外部バックエンド必要 | 自前/外部 | Kubernetes etcd | AWS マネージド |
| マルチクラウド | 優秀 | 優秀 | 優秀 | AWS 専用 |
| 学習曲線 | 中程度 | 低い(既存言語) | 高い | 低い(AWS ユーザー) |
| コミュニティ | 非常に大きい | 成長中 | 成長中 | AWS エコシステム |
| ドリフト検知 | plan で手動 | preview で手動 | 自動(reconciliation) | Drift Detection |
障害事例と復旧手順
事例 1: 状態ロック競合
症状: terraform plan または apply 実行時に "Error acquiring the state lock" エラーが発生
原因: 前回の Terraform 操作が異常終了(ネットワーク切断、CI ランナータイムアウト、Ctrl+C 強制中断)し、ロックが解除されていない状態
復旧手順:
# 1. ロック状態を確認 - 他のユーザーが実行中でないかまず確認
# エラーメッセージから Lock ID を確認
# 2. 他の操作が実行中でないことを確認した後、強制解除
terraform force-unlock LOCK_ID
# 3. -force オプションで確認なしに即時解除(注意: 他の操作がないことを必ず確認)
terraform force-unlock -force LOCK_ID
予防措置:
- CI/CD パイプラインに適切なタイムアウトを設定
- 同時実行制御(concurrency control)を適用
- Atlantis 使用時は PR ベースの自動ロックで競合を防止
事例 2: 状態ドリフト(Drift)
症状: terraform plan で予期しない変更が表示される。コンソールで手動変更したリソースが Terraform 状態と不整合
復旧手順:
# 1. 現在の実際のインフラ状態で状態ファイルを更新
terraform refresh
# 2. または plan でドリフトを確認し、選択的に import
terraform plan
# 3. 手動変更をコードに反映するか元に戻す
terraform apply # コード基準でインフラを復元
事例 3: 循環依存
症状: terraform plan 時に "Cycle" エラーが発生
原因: モジュール A がモジュール B の出力を参照し、モジュール B が再びモジュール A の出力を参照するケース
解決方法:
- 共通の依存関係を別モジュールに分離
depends_onを使用して明示的な依存関係を指定- データソースを使用して間接参照に切り替え
事例 4: 大規模状態ファイルのパフォーマンス低下
症状: terraform plan が10分以上かかる、API レートリミティングが発生
解決方法:
# 特定のモジュールのみを対象に plan/apply を実行
terraform plan -target=module.eks
terraform apply -target=module.eks
# 状態ファイルの分割(state の移動)
terraform state mv module.monitoring module.monitoring
根本的な解決: 状態ファイルをコンポーネントごとに分割して、各状態ファイルのサイズを削減する。ネットワーキング、コンピュート、データベース、モニタリングを別々の状態ファイルで管理し、terraform_remote_state データソースで相互参照する。
運用チェックリスト
モジュール設計チェックリスト
- モジュールが単一責任原則に従っているか
- すべてのリソースに対する出力値が定義されているか
- 変数に type、description、validation が含まれているか
- provider と backend がルートモジュールのみに設定されているか
- README.md と examples ディレクトリが含まれているか
- セマンティックバージョンでタグが管理されているか
状態管理チェックリスト
- リモートバックエンドが設定されているか(ローカル状態ファイルの使用禁止)
- 状態ロックが有効化されているか
- 状態ファイルが暗号化されているか
- S3 バケットにバージョニングが有効化されているか
- パブリックアクセスがブロックされているか
- コンポーネントごとに状態ファイルが分離されているか
Atlantis / CI-CD チェックリスト
- atlantis.yaml がリポジトリルートに設定されているか
- apply 前に approved + mergeable 要件が設定されているか
- モジュール変更時に依存するプロジェクトが自動的に plan されるか
- Webhook シークレットが安全に管理されているか
- 認証情報(credentials)のローテーションが定期的に実行されているか
まとめ
Terraform モジュール設計と状態管理は、インフラコードの規模が拡大するほどその重要性が指数関数的に増大する。Composition パターンで小さなモジュールを組み合わせ、リモート状態をコンポーネントごとに分離し、Atlantis で GitOps ワークフローを自動化することが、2026年現在最も成熟した運用モデルである。
核心は 「小さく始めて、必要な時に分離する」 という原則である。最初から完璧なモジュール構造を設計しようとするのではなく、単一モジュールから始めて重複が発生した時にリファクタリングし、状態ファイルが大きくなったら分離し、チームメンバーが増えたら Atlantis を導入する段階的なアプローチが最も現実的である。
インフラコードもアプリケーションコードと同じエンジニアリング規律(コードレビュー、テスト、バージョン管理、CI/CD)を適用すべきであり、本記事で紹介したパターンとツールがその道のりの実質的な助けとなれば幸いである。