Terraformのディレクトリ構成を解説|チーム開発・マルチ環境に対応した設計パターン

Terraformでインフラをコード化する際、最初に悩む点のひとつが「ディレクトリ構成をどうするか」ではないでしょうか。

小規模なプロジェクトであれば単一ディレクトリで始めることもできますが、チーム開発・複数環境(dev/prod)・マイクロサービスやコンポーネントごとの管理といった現実的な運用を考えると、適切なディレクトリ体系の設計が後の保守性に大きく影響します。

本記事では、Terraformのディレクトリ構成の基本から、よく使われる設計パターン、そして実際の運用で役立つベストプラクティスまでを解説します。

Terraformのファイル・ディレクトリに関する基礎知識

まず、Terraformが扱う主要ファイルを整理します。

ファイル名役割
main.tfリソース定義のメインファイル
variables.tf変数の宣言
outputs.tf出力値の定義
providers.tfプロバイダ(AWSなど)の設定
terraform.tfvars変数の実際の値を記述
versions.tfTerraformおよびプロバイダのバージョン固定
backend.tftfstateの保存先(S3など)の設定

これらのファイルをどのディレクトリにどう配置するかが、ディレクトリ構成設計の重要なポイントです。

よく使われるディレクトリ構成パターン

パターン1:フラット構成(小規模・個人プロジェクト向け)

最もシンプルな構成です。すべてのリソース定義を1つのディレクトリに置きます。

terraform/
├── main.tf
├── variables.tf
├── outputs.tf
├── providers.tf
├── versions.tf
└── terraform.tfvars

メリット

  • 構成がシンプルで学習コストが低い
  • 小規模プロジェクトでは導入しやすい

デメリット

  • リソースが増えるにつれ main.tf が肥大化する
  • 複数環境への対応が難しい

PoC(概念実証)や個人学習、管理リソースが10件程度の小規模プロジェクトなどの用途に向いています。

パターン2:環境×サブシステムで分割する構成(チーム開発・プロダクション向け)★推奨

チーム開発や中〜大規模な運用で最もおすすめなのが、環境ディレクトリの直下に、サブシステム(機能・コンポーネント)ごとのディレクトリを配置する構成です。

terraform/
├── dev/                  # 開発環境
│   ├── subsystem1/       # サブシステム1(例: ネットワーク・基盤層)
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   └── subsystem2/       # サブシステム2(例: アプリ・ECS層)
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
├── prod/                 # 本番環境
│   ├── subsystem1/
│   │   ├── main.tf
│   │   └── ...
│   └── subsystem2/
│       ├── main.tf
│       └── ...
└── modules/              # 共通モジュール(固定値と変数の定義)
    ├── network/
    │   ├── main.tf
    │   └── ...
    └── app_server/
        ├── main.tf
        └── ...

各サブシステム(subsystem1、subsystem2)の main.tf から、modules/ 配下の共通モジュールを呼び出す形をとります。

メリット

  • 影響範囲(Blast Radius)の最小化
    subsystem2(アプリ層)を変更・適用する際に、subsystem1(ネットワーク基盤)の tfstate を変更しないため、誤操作によるインフラ全損リスクを抑えることができます。
  • 独立した tfstate 運用
    環境×サブシステムごとに完全に独立した状態ファイルを持てます。
  • CI/CDとの相性がよい
    Gitの差分検知を使って、「dev/subsystem2/ のコードが変わったときだけ、そのディレクトリで terraform apply を実行する」といったスマートな自動化が容易です。

デメリット

  • 共通部分の記述の重複
    ディレクトリごとに backend(S3バケットやキーの定義)や provider を書く必要があり、共通部分の記述が一部重複します。

モジュール設計のポイント

どの構成パターンでも、モジュールの設計品質が全体の保守性を左右します。特に「何を固定値として定義し、何を変数にするか」の切り分けが重要です。

1. モジュール内は「固定値」、環境差分は「変数(variables.tf)」で管理する

モジュール(modules/ 配下)を設計する際は、「環境やサブシステムによらず共通の設定はモジュール内に固定値として記述し、環境ごとに変えたい値だけを variables.tf で変数化する」のが基本です。

すべてを変数化してしまうとモジュールを呼び出す側の記述が肥大化し、逆にすべてを固定値にすると再利用ができなくなります。

  • モジュール内に固定してよい値(例)
    • セキュリティグループのプロトコル(”tcp” など)
    • 暗号化の有効化フラグ(原則常に true にすべきもの)
    • 共通の命名規則(プレフィックスなど)
  • variables.tf で変数化すべき値(例)
    • 環境ごとにスケールが変わる値(EC2のインスタンスタイプ、RDSのインスタンスクラスなど)
    • ネットワーク帯(VPCやサブネットのCIDRブロック)
    • 環境名(dev / prod などの識別子)

2. モジュールの粒度

モジュールは「変更の単位」や「再利用する単位」で細かく分けることを意識します。

modules/
├── network/          # VPC・サブネット・Internet Gateway
├── security_group/   # セキュリティグループ
└── app_server/       # EC2やECS、付随するインスタンス定義

実践:標準機能で組む「環境×サブシステム」の main.tf コード例

外部ラッパーツールを使わず、標準のコード共通化を適用した具体的な記述例です。ここでは、開発環境(dev)のサブシステム2(app_serverを構築するレイヤー) を例に解説します。

① 共通モジュール側(modules/app_server/main.tf)

共通のテンプレートを作ります。プロトコルなどは固定値にし、スペックなどは外から注入できるよう変数にします。

Terraform
# modules/app_server/main.tf
resource "aws_instance" "web" {
  ami           = var.ami_id
  instance_type = var.instance_type # 変数

  # 全環境で統一したい設定は固定値としてハードコード
  monitoring    = true
  ebs_optimized = true

  tags = {
    Name = "my-app-${var.env}-${var.subsystem}"
    Env  = var.env
  }
}

② 開発環境のサブシステム2(terraform/dev/subsystem2/main.tf)

モジュールを呼び出し、開発環境(dev)かつこのサブシステム用の変数を注入します。

Terraform
# terraform/dev/subsystem2/main.tf
terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  # tfstateを【環境/サブシステム名】専用のパスに隔離
  backend "s3" {
    bucket = "my-tfstate-bucket"
    key    = "dev/subsystem2/terraform.tfstate"
    region = "ap-northeast-1"
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

# 共通モジュールを呼び出し
module "app_server" {
  # 階層が2つ深くなっているため、「../../../」でmodulesを参照
  source = "../../../modules/app_server"

  env           = "dev"
  subsystem     = "subsystem2"
  ami_id        = "ami-0abc123456789def0"
  instance_type = "t3.micro" # 開発環境用の低コストスペック
}

③ 本番環境のサブシステム2(terraform/prod/subsystem2/main.tf)

構造はdevと同じですが、バックエンドの保存先キー(prod/subsystem2/…)と、注入するスペック値を本番環境用に変更します。

Terraform
# terraform/prod/subsystem2/main.tf
terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  # 本番環境用の個別パスを指定
  backend "s3" {
    bucket = "my-tfstate-bucket"
    key    = "prod/subsystem2/terraform.tfstate"
    region = "ap-northeast-1"
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

module "app_server" {
  source = "../../../modules/app_server"

  env           = "prod"
  subsystem     = "subsystem2"
  ami_id        = "ami-0abc123456789def0"
  instance_type = "m6i.large" # 本番環境用の高スペック
}

サブシステム間でデータを連携する方法

サブシステムを分割すると、例えば「subsystem1 で作ったVPCのIDを、subsystem2 のEC2で使いたい」という場面が出てきます。

この場合は、terraform_remote_state データソースを使って、他のサブシステムの tfstate から output された値を安全に読み込みます。

Terraform
# terraform/dev/subsystem2/main.tf 内での記述例

# subsystem1 の tfstate を読み込む
data "terraform_remote_state" "subsystem1" {
  backend = "s3"
  config = {
    bucket = "my-tfstate-bucket"
    key    = "dev/subsystem1/terraform.tfstate"
    region = "ap-northeast-1"
  }
}

# 読み込んだVPC IDを別のリソースやモジュールに渡す
module "app_server" {
  source = "../../../modules/app_server"
  # ...
  vpc_id = data.terraform_remote_state.subsystem1.outputs.vpc_id
}

まとめ

Terraformのディレクトリ構成を terraform/{環境}/{サブシステム}/ の形に落とし込むことで、変更の影響範囲を最小化し、チーム開発における衝突(マルチロックや意図しない差分バッティング)を防ぎやすくなります。

最初はシンプルな「フラット構成」から始めても問題ありませんが、規模の拡大を見据えて「モジュール内を共通化して固定値で守り、環境・サブシステムごとの差分はvariables.tfで管理する」 という原則を意識することが重要です。

プロジェクトの特性に合わせて最適な切り口を見つけ、安全なIaC運用を目指しましょう。