Terraformのディレクトリ構成ベストプラクティス|environments分割とmodules共通化のリポジトリ設計

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)Terraform > Terraformのディレクトリ構成ベストプラクティス|environments分割とmodules共通化のリポジトリ設計
「単一のmain.tfにすべてのリソースを書いていたら、環境ごとにファイルをコピーするようになってしまい、どれが最新か分からなくなった」
「terraform planを実行するたびに全リソースの差分確認に追われ、本当に変えたい設定に集中できない」

Terraformを使い始めたエンジニアが必ずぶつかるこの壁の根本原因は、terraform ディレクトリ構成が適切に設計されていないことにあります。

この記事では、単一ファイル構成(脱・単一main.tf)の限界を整理したうえで、environments分割とmodules共通化を組み合わせたリポジトリ設計のベストプラクティスを解説します。3つの設計パターンの比較・environments/dev/stg/prodの実装例・modules/の設計指針・よくある失敗パターンと対策まで、リポジトリ構成軸で体得できます。

実行環境:Terraform 1.8.x(RHEL 9.4 / Ubuntu 24.04 LTSで動作確認済み)

この記事のポイント

・単一main.tfはリソースが増えると「全リソースplan」「環境コピー増殖」で必ず破綻する
・environments分割型はdev/stg/prodをディレクトリで分離しtfstateも環境ごとに独立させる
・modules/共通化の基準は「2環境以上で同じ構成を使うリソース群」を抽出することだけ
・tfstateのS3バックエンドはenvironments/dev・stg・prodで必ずkeyパスを分ける


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
図解60P/登録10秒/解除も3秒 / 詳細はこちら

なぜ単一main.tfでは破綻するのか

Terraformを学び始めたとき、最初はすべてのリソースを1つのmain.tfに書くことが多いでしょう。VPC・EC2・RDS・IAMを1ファイルに詰め込んだ状態です。小さなプロジェクトなら動きますが、リソースが増え、環境が増えると次の3つの問題が必ず顕在化します。

1. terraform plan/applyが全リソース対象になる

VPCもEC2もRDSもS3も、すべてが1つのtfstateで管理されます。EC2の設定を1行変えたいだけでも、RDSや他のリソースに意図しない差分がないかplan結果をすべて目視確認しなければなりません。リソースが増えるほどplanは遅くなり、確認コストが上がります。

2. 環境ごとのファイルコピーが増殖する

開発(dev)と本番(prod)では使用するAMI IDやインスタンスタイプが異なります。「main.tfをコピーしてハードコードを書き換える」運用に陥りがちです。コピーが2つ3つと増えるにつれ、どちらが最新か分からなくなります。本番のAMI IDを誤ってdevに書いた、といった事故の温床になります。

3. コードが肥大化して見通しが悪くなる

VPC・EC2・RDS・IAMを1ファイルに書くと、数百行のmain.tfになります。新しいメンバーがオンボーディングするとき、どのリソースがどの設定と対応しているかを把握するだけで時間がかかります。変更時のレビューコストも増大します。

これらを解決するのが、terraform ディレクトリ構成の設計です。環境をディレクトリで分離し、共通構成をモジュール化することで、これらの問題を根本から解消できます。

ディレクトリ構成の3つの設計パターン

Terraformのリポジトリ構成には、規模やチーム体制に応じた代表的な3つのパターンがあります。

1. フラット型(小規模・1環境)

リソースが5~10個程度、環境が1つだけのケースに向いています。

terraform-repo/ ├── main.tf # リソース定義 ├── variables.tf # 入力変数 ├── outputs.tf # 出力値 ├── providers.tf # プロバイダー設定 └── terraform.tfvars # 変数の値

シンプルで学習コストが低く、「まず動かす」フェーズに適しています。ただし環境が増えた瞬間にファイルコピー運用に陥るため、本番運用には向きません。

2. environments分割型(中規模チーム・複数環境)

最も多くの現場で採用されるパターンです。環境(dev/stg/prod)をディレクトリで分離し、共通化できるリソース定義をmodules/にまとめます。

terraform-repo/ ├── environments/ │ ├── dev/ │ │ ├── main.tf # modulesを呼び出すだけ │ │ ├── variables.tf │ │ ├── outputs.tf │ │ ├── terraform.tfvars # dev用の値 │ │ └── backend.tf # S3バックエンド(devパス) │ ├── stg/ │ │ ├── main.tf │ │ ├── variables.tf │ │ ├── outputs.tf │ │ ├── terraform.tfvars # stg用の値 │ │ └── backend.tf # S3バックエンド(stgパス) │ └── prod/ │ ├── main.tf │ ├── variables.tf │ ├── outputs.tf │ ├── terraform.tfvars # prod用の値 │ └── backend.tf # S3バックエンド(prodパス) └── modules/ ├── vpc/ │ ├── main.tf │ ├── variables.tf │ └── outputs.tf ├── ec2/ │ ├── main.tf │ ├── variables.tf │ └── outputs.tf └── rds/ ├── main.tf ├── variables.tf └── outputs.tf

terraform planはenvironments/dev/、environments/prod/の各ディレクトリで個別に実行するため、影響範囲を環境単位に限定できます。

3. モノレポ型(大規模・マルチプロダクト)

複数サービスのインフラを1つのリポジトリで管理するパターンです。共有インフラ(ネットワーク・セキュリティグループ等)をshared/に置き、サービス別インフラをproducts/配下に分離します。

terraform-repo/ ├── shared/ │ ├── network/ # VPC・サブネット・NATゲートウェイ │ └── security/ # セキュリティグループ・IAMポリシー └── products/ ├── product-a/ │ ├── environments/ │ │ ├── dev/ │ │ └── prod/ │ └── modules/ └── product-b/ ├── environments/ │ ├── dev/ │ └── prod/ └── modules/

shared/のoutputをterraform_remote_stateでproducts/から参照する設計が一般的です。小~中規模では管理が複雑になりすぎるため、5人以上のチームかつ複数サービスが前提となる場合に限定することをすすめます。

environments分割型の実装例(dev/stg/prod)

中規模チームで最も使われるenvironments分割型を具体的に実装します。

1. backend.tf ── tfstateの保存先を環境ごとに分離する

terraform ディレクトリ構成設計で最も重要な判断がここです。S3のkeyパスに環境名を含めることで、devのstateとprodのstateを完全に分離します。

# environments/dev/backend.tf terraform { backend "s3" { bucket = "mycompany-terraform-state" key = "environments/dev/terraform.tfstate" region = "ap-northeast-1" dynamodb_table = "terraform-state-lock" encrypt = true } }

# environments/prod/backend.tf(keyパスのみ異なる) terraform { backend "s3" { bucket = "mycompany-terraform-state" key = "environments/prod/terraform.tfstate" region = "ap-northeast-1" dynamodb_table = "terraform-state-lock" encrypt = true } }

S3バケットは共有でも問題ありませんが、keyのパスは必ず環境ごとに分けます。同じキーを使うと、devのapplyがprodのstateを上書きする重大事故が起きます。DynamoDBによる状態ロック(dynamodb_table)も必ず設定し、複数人が同時にapplyする競合状態を防ぎます。

2. environments/dev/main.tf ── modulesを呼び出すだけにする

各環境のmain.tfはmodules/のモジュールを呼び出すだけのシンプルな構成にします。リソースの実体はmodules/に集約します。

# environments/dev/main.tf module "vpc" { source = "../../modules/vpc" env = var.env vpc_cidr = var.vpc_cidr az_list = var.az_list } module "ec2" { source = "../../modules/ec2" env = var.env vpc_id = module.vpc.vpc_id subnet_ids = module.vpc.private_subnet_ids instance_type = var.instance_type ami_id = var.ami_id }

# environments/dev/terraform.tfvars env = "dev" vpc_cidr = "10.0.0.0/16" az_list = ["ap-northeast-1a", "ap-northeast-1c"] instance_type = "t3.micro" ami_id = "ami-0abcdef1234567890" # environments/prod/terraform.tfvars(値だけが異なる) env = "prod" vpc_cidr = "10.1.0.0/16" az_list = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"] instance_type = "t3.large" ami_id = "ami-0abcdef9876543210"

environments/prod/のmain.tfはenvironments/dev/のmain.tfと構造が同一です。terraform.tfvarsの値だけが異なります。これが「構成の同一性」と「値の差分化」を両立するポイントです。

modules共通化の設計指針

「何をmodule化するか」は悩みやすいポイントです。判断基準はシンプルです。「2環境以上で同じ構成を使うリソース群」をmodule化するだけです。

1. modules/の内部構造

modules/ └── vpc/ ├── main.tf # VPC・サブネット・IGW・ルートテーブルの定義 ├── variables.tf # 入力変数(env・cidr・az_list等) └── outputs.tf # 出力値(vpc_id・subnet_ids等を呼び元に公開)

各モジュールはmain.tf・variables.tf・outputs.tfの3ファイル構成を基本とします。この構成を守ることで、modules/vpc/やmodules/ec2/をそれぞれ独立した再利用単位として扱えます。

2. module化の判断基準

・dev・stg・prodの3環境で同じVPC構成を作る → module化の対象
・特定環境にしか存在しないリソース(例:devにしかない踏み台EC2) → その環境のmain.tfに直接書く
・1つのmoduleに変数が10を超えてきた → モジュールの分割を検討する

3. 過度な抽象化を避ける

「汎用的にしよう」とすべてをmodule化しようとすると、environments/のmain.tfが変数パススルーだらけになり、かえって読みにくくなります。モジュールの変数が増えすぎると、呼び出し側でどの変数が何をするか追いにくくなります。

「2環境以上で使う」という基準を守ることで、必要十分な粒度に収まります。
Terraformのリポジトリ設計から実際の運用フローまで、「なぜそう設計するのか」を体系的に習得したい方へ。environments分割・modules共通化・tfstate分離の設計判断を、実務に即したハンズオン形式で身につけられるセミナーを開催しています。
>> Terraform実践セミナーの詳細はこちら

よくある失敗パターンとその対策

1. tfstateを環境間で共有してしまう

失敗パターン:S3バックエンドのkeyをすべての環境で同じパスにしてしまう。

# NG:全環境で同じキーを使っている terraform { backend "s3" { bucket = "mycompany-terraform-state" key = "terraform.tfstate" # 環境名がない } }

対策:keyパスに必ず環境名を含めます。`environments/dev/terraform.tfstate`のように階層化することで、devとprodのstateを物理的に分離します。

2. backendブロックでvar.を使おうとしてエラーになる

失敗パターン:backendの設定を動的にしようとしてHCL変数を参照しようとする。

# NG:backendブロック内でvar.は使えない terraform { backend "s3" { key = "environments/${var.env}/terraform.tfstate" # エラー } } # 実行するとこのエラーが出る Error: Variables not allowed on backend.tf line 4, in terraform: Variables may not be used here.

対策1:環境ごとのbackend.tfを個別に置き、keyパスをハードコードします(environments分割型の標準アプローチ)。

対策2:`terraform init -backend-config`オプションで外部ファイルから注入します。

# -backend-configで外部ファイルから設定を注入する方法 $ terraform init -backend-config=../../config/dev.backend.hcl # dev.backend.hcl bucket = "mycompany-terraform-state" key = "environments/dev/terraform.tfstate" region = "ap-northeast-1"

3. environments/のmain.tfにリソースを直書きしてしまう

失敗パターン:「急いでいたから」とenvironments/dev/main.tfにリソースを直書きし、後からstg・prodでも同じリソースが必要になってコピーが増える。

対策:environments/のmain.tfのルールを「module呼び出しのみ」と明文化します。リソース直書きが混入したらコードレビューで指摘する運用を最初から徹底することが重要です。

4. modules/を分割しすぎてモジュールが細かすぎる

失敗パターン:「1リソース1モジュール」にして、セキュリティグループだけのモジュール・EIPだけのモジュール…と細分化しすぎる。

対策:モジュールの粒度は「一緒にデプロイ・削除するリソースのまとまり」を基準にします。VPC・サブネット・IGW・ルートテーブルはまとめてmodules/vpcにする、EC2・キーペア・セキュリティグループはまとめてmodules/ec2にするのが現実的な粒度です。

本記事のまとめ

terraform ディレクトリ構成のポイントをまとめます。

設計要素 内容
パターン選択 小規模=フラット型・中規模複数環境=environments分割型・大規模マルチプロダクト=モノレポ型
environments/分離 dev・stg・prodをディレクトリで分離し、terraform planの影響範囲を環境単位に限定する
tfstate分離 S3バックエンドのkeyに環境名を含め(例:environments/dev/terraform.tfstate)環境間でstateを混在させない
modules/共通化 「2環境以上で同じ構成を使うリソース群」を抽出してmodule化する。過度な抽象化は避ける
backendの制約 backendブロック内ではvar.は使えない。環境別にbackend.tfを置くか-backend-configを使う

terraform ディレクトリ構成の設計は一度決めると後から変えにくい部分です。まずenvironments分割型でdev・stg・prodを分離し、S3バックエンドで環境別にtfstateを管理する構成を作ることから始めましょう。modules/への抽出は「2環境以上で同じ構成を使う」ブロックが見えてきてから行うのが順序として正しいです。

次に読む記事:

Terraformのtfstate管理とS3バックエンド設定|チーム運用で壊さないための基礎
・Terraformのworkspaceと-var-fileで環境を分離する方法|dev・stg・prodを1つのコードで管理する設計
・Terraformのremote_stateでモジュール間データを受け渡す方法|tfstate分離設計とoutput参照の実践
TerraformのHCL変数設計|variable・locals・output・data sourceで構成を整理する方法
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、Terraformのディレクトリ設計・environments分割・modules共通化・tfstate分離まで、実務で使えるIaCスキルをハンズオン形式で習得できるセミナーを開催しています。
>> Terraform実践セミナーの詳細はこちら

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は1分、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

登録10秒/合わなければ解除3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。