Terraform によるマルチ環境(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の本質的な価値だ。コンソールの手作業では永遠に実現できない。
この記事をシェア