Terraform によるマルチ環境(Dev/Prd)の疎結合なモジュール設計

|
Terraform IaC AWS Module Dev/Prd インフラ設計

TerraformでDev/Prd環境を疎結合なモジュール設計で管理する。共通モジュールの分離・環境変数の差分管理・State分離戦略まで、LP構築の実例で解説する。

はじめに:なぜ「コンソール手作業」は技術的負債になるか

AWSコンソールからポチポチとリソースを作成するのは、最初は速い。しかし2つ目の環境(ステージング・本番)を作る瞬間に、その方法の限界が露呈する。

手作業インフラの問題:

  dev環境  ──(コンソール手作業)──→  設定A
  prd環境  ──(コンソール手作業)──→  設定B  ← 気づかない差分が生まれる

  問題1: 環境ドリフト
    dev と prd の設定差分が「誰も把握していない」状態になる
    「devでは動くがprdでは動かない」の原因になる

  問題2: 再現不能
    「なぜこの設定にしたか」の記録が残らない
    担当者が離れると全てブラックボックスになる

  問題3: レビュー不能
    変更を加える前に「何が変わるか」を確認できない
    本番環境の変更が常にぶっつけ本番になる

Terraformは「インフラの状態をコードとして記述し、差分を可視化してから適用する」ツールだ。しかしTerraformを導入しただけでは問題は解決しない。 モジュール設計が悪ければ、コード化されたカオスができあがるだけだ。

この記事では、LP案件の実例を元に「疎結合なマルチ環境設計」の構造と判断根拠を解説する。


Part 1:「疎結合」とは何か ── 密結合との比較

密結合な設計(よくあるアンチパターン)

# ❌ 密結合: dev/prd を if 分岐で管理するパターン
variable "env" {
  type    = string
  default = "dev"
}

resource "aws_s3_bucket" "lp" {
  bucket = var.env == "prd" ? "company-lp-prod" : "company-lp-dev"

  # prd のみバージョニング有効
  dynamic "versioning" {
    for_each = var.env == "prd" ? [1] : []
    content {
      enabled = true
    }
  }
}

resource "aws_cloudfront_distribution" "lp" {
  # prd のみカスタムドメイン
  aliases = var.env == "prd" ? ["example.com"] : []
  
  # dev のみ price_class を下げる
  price_class = var.env == "prd" ? "PriceClass_All" : "PriceClass_100"
}

一見整理されているように見えるが、このパターンには致命的な問題がある。環境が増えるたびに if 分岐が増殖し、コード全体が「どの条件が何に影響するか」不透明になる。

疎結合な設計(推奨パターン)

疎結合の原則は**「モジュールは変数を受け取るだけで、環境を知らない」**ことだ。

疎結合の構造:

  environments/
  ├── dev/
  │   ├── main.tf       ← モジュールを呼ぶだけ
  │   └── terraform.tfvars  ← dev固有の変数値
  └── prd/
      ├── main.tf       ← 同じモジュールを呼ぶだけ
      └── terraform.tfvars  ← prd固有の変数値

  modules/
  └── lp-stack/
      ├── main.tf       ← 環境を知らない。変数だけ受け取る
      ├── variables.tf  ← 入力変数の定義
      └── outputs.tf    ← 出力値の定義

Part 2:ディレクトリ構成の実例 ── LP案件の全体像

実際のLP案件で使用したディレクトリ構成を示す。

terraform/
├── modules/
│   ├── lp-stack/           # S3 + CloudFront + ACM + Route53
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── contact-form/       # API Gateway + Lambda + SES
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   └── monitoring/         # CloudWatch + SNS アラート
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
│
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf      # dev用のS3 backend設定
│   └── prd/
│       ├── main.tf
│       ├── variables.tf
│       ├── terraform.tfvars
│       └── backend.tf      # prd用のS3 backend設定
│
└── shared/
    └── backend-bootstrap/  # tfstate管理用S3バケット・DynamoDBの初期構築
        └── main.tf

設計の意図:

  • modules/ は環境の概念を持たない純粋な部品
  • environments/ が「どの部品をどの設定で組み合わせるか」を決める
  • shared/ はState管理インフラを独立させ、鶏と卵問題を回避する

Part 3:モジュールの実装 ── variables.tf の設計が全て

モジュールの品質は variables.tf の設計で決まる。変数の粒度・型・バリデーションが「モジュールの再利用性と安全性」を支配する。

# modules/lp-stack/variables.tf

variable "project_name" {
  type        = string
  description = "プロジェクト名。リソース名のプレフィックスに使用する"

  validation {
    condition     = can(regex("^[a-z][a-z0-9-]{2,30}$", var.project_name))
    error_message = "project_name は小文字英数字とハイフンのみ、3〜31文字で指定してください"
  }
}

variable "env" {
  type        = string
  description = "環境識別子"

  validation {
    condition     = contains(["dev", "stg", "prd"], var.env)
    error_message = "env は dev / stg / prd のいずれかで指定してください"
  }
}

variable "domain_name" {
  type        = string
  description = "CloudFrontのカスタムドメイン。devではnullを渡してデフォルトドメインを使用"
  default     = null
}

variable "enable_versioning" {
  type        = bool
  description = "S3バケットのバージョニング有効化。prd環境では true を推奨"
  default     = false
}

variable "price_class" {
  type        = string
  description = "CloudFrontのPrice Class"
  default     = "PriceClass_100"   # dev default: 最小コスト

  validation {
    condition     = contains(["PriceClass_100", "PriceClass_200", "PriceClass_All"], var.price_class)
    error_message = "price_class は PriceClass_100 / PriceClass_200 / PriceClass_All のいずれかです"
  }
}

variable "tags" {
  type        = map(string)
  description = "全リソースに付与するタグ"
  default     = {}
}

ポイント: validation ブロックで不正な値を terraform plan の時点で弾く。本番適用前にエラーが出るので、誤った設定が環境に入り込まない。


Part 4:環境ごとの差分管理 ── tfvars の実例

# environments/dev/terraform.tfvars

project_name      = "my-lp"
env               = "dev"
domain_name       = null        # devはCloudFrontデフォルトドメインを使用
enable_versioning = false       # devはコスト節約のためOFF
price_class       = "PriceClass_100"

tags = {
  Project     = "my-lp"
  Environment = "dev"
  ManagedBy   = "terraform"
  Owner       = "tono"
}
# environments/prd/terraform.tfvars

project_name      = "my-lp"
env               = "prd"
domain_name       = "example.com"    # prdはカスタムドメイン
enable_versioning = true             # prdはバージョニングON(誤削除対策)
price_class       = "PriceClass_All" # prdはグローバル配信

tags = {
  Project     = "my-lp"
  Environment = "prd"
  ManagedBy   = "terraform"
  Owner       = "tono"
}

モジュール側のコードは一切変えずに、tfvarsの値だけで環境の振る舞いが変わる。これが「疎結合」の実体だ。


Part 5:State 分離戦略 ── workspace vs ディレクトリ分離

Terraformのマルチ環境管理には2つのアプローチがある。

Terraform Workspace(非推奨)

terraform workspace new dev
terraform workspace new prd
terraform workspace select prd
terraform apply

Workspaceは同一バックエンドで複数のStateを管理する。一見便利だが、本番環境の操作中に誤って別Workspaceにいることに気づかないリスクがある。

# 最悪のシナリオ
$ terraform workspace select dev  # devに切り替えたつもり
# (実はprdのまま)
$ terraform destroy               # prd環境を全消去...

ディレクトリ分離 + 独立Stateバックエンド(推奨)

# environments/prd/backend.tf
terraform {
  backend "s3" {
    bucket         = "my-lp-tfstate-prd"      # prd専用バケット
    key            = "lp-stack/terraform.tfstate"
    region         = "ap-northeast-1"
    encrypt        = true
    dynamodb_table = "my-lp-tflock-prd"       # prd専用ロックテーブル
  }
}
# environments/dev/backend.tf
terraform {
  backend "s3" {
    bucket         = "my-lp-tfstate-dev"      # dev専用バケット(完全分離)
    key            = "lp-stack/terraform.tfstate"
    region         = "ap-northeast-1"
    encrypt        = true
    dynamodb_table = "my-lp-tflock-dev"
  }
}

ディレクトリ分離の利点:

比較軸 Workspace ディレクトリ分離
誤操作リスク 高い(workspace切り忘れ) 低い(cdでしか環境が変わらない)
State の独立性 同一バケット内で分離 バケットレベルで完全分離
IAM権限の分離 困難 環境ごとにRoleを分けられる
CI/CDとの統合 煩雑 ディレクトリごとにpipeline定義が自然
操作の安全性の違い:

  Workspace 方式:
  $ cd terraform/          # どこにいても同じディレクトリ
  $ terraform workspace select prd  # ← ここを忘れると事故
  $ terraform apply

  ディレクトリ分離方式:
  $ cd terraform/environments/prd/  # ← いる場所が環境を決める
  $ terraform apply                 # 間違えようがない

Part 6:terraform plan をレビューとして使う

Terraformの最大の価値は terraform plan にある。変更を適用する前に「何が変わるか」を人間が読める形で出力する。

$ cd terraform/environments/prd
$ terraform plan

Terraform will perform the following actions:

  # aws_cloudfront_distribution.lp will be updated in-place
  ~ resource "aws_cloudfront_distribution" "lp" {
      ~ price_class = "PriceClass_100" -> "PriceClass_All"
    }

  # aws_s3_bucket_versioning.lp will be updated in-place
  ~ resource "aws_s3_bucket_versioning" "lp" {
      ~ versioning_configuration {
          ~ status = "Suspended" -> "Enabled"
        }
    }

Plan: 0 to add, 2 to change, 0 to destroy.

この出力をプルリクエストのコメントに自動投稿することで、インフラ変更をコードレビューと同じプロセスに乗せられる。GitHub Actionsのワークフローでは actions/github-script を使ってPRコメントを自動生成できる。


Conclusion:「コードが環境の唯一の正典」という状態を作る

疎結合なモジュール設計が目指すのは「コードを読めば環境の全てが分かる」という状態だ。

目指す状態:

  新メンバーが加入したとき:
  $ git clone <repo>
  $ cd terraform/environments/prd
  $ terraform plan  # ← これだけで現在の環境の全構成が分かる

  障害対応時:
  「このリソースはなぜこの設定になっているか」
  → git log で変更履歴を追える
  → プルリクエストにplanの差分が残っている
  → 意思決定の根拠まで追跡できる

「誰が何をいつ変えたか」が全てコードとgit履歴に残る——これがIaCの本質的な価値だ。コンソールの手作業では永遠に実現できない。

この記事をシェア

Twitter / X LinkedIn
記事一覧に戻る
🤖
Cloud Assistant
IBM Watson powered

こんにちは!クラウドエンジニアのポートフォリオサイトへようこそ。AWS構成・副業サービス・お仕事のご相談など、何でも聞いてください 👋