Skip to content
Published on

Terraform モジュール設計パターン完全ガイド:状態管理・ワークスペース・Atlantis 自動化

Authors
  • Name
    Twitter
Terraform Module Design

はじめに

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 の planapply を自動化する 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 + DynamoDBGCSTerraform CloudAzure Blob
状態ロックDynamoDB / S3 ネイティブ標準搭載標準搭載Blob Lease
暗号化KMSGoogle KMS標準搭載Azure KeyVault
バージョン管理S3 VersioningObject Versioning標準搭載Blob Snapshots
アクセス制御IAM PolicyIAMTeams/RBACAzure RBAC
コストS3 + DynamoDB 課金GCS 課金無料枠制限ありBlob 課金
設定難易度中程度低い低い中程度

IaC ツール比較

特性Terraform/OpenTofuPulumiCrossplaneCloudFormation
言語HCLTypeScript/Python/GoYAML/CRDJSON/YAML
状態管理外部バックエンド必要自前/外部Kubernetes etcdAWS マネージド
マルチクラウド優秀優秀優秀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)を適用すべきであり、本記事で紹介したパターンとツールがその道のりの実質的な助けとなれば幸いである。