Terraformを使い始めると、最初はうまくいく。VPCを作り、EC2を作り、サブネットを定義する。しかし3つ目、4つ目の環境を作り始めたとき、コードが爆発的に膨らんでいることに気づく。
この記事では、terraform moduleの使い方と、for_each・countを組み合わせた構成の再利用設計パターンを解説します。moduleの基本構造から、countとfor_eachの使い分け基準、ネストmoduleの設計まで、実務で通用するパターンを段階的に説明します。
この記事のポイント
・terraform moduleでリソース定義を再利用可能な単位にまとめられる
・countは数値で繰り返し、for_eachはマップで繰り返す
・削除が伴う変更にはfor_eachの方がリスクが低い
・inputとoutputを設計すればmodule間を疎結合につなげる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜterraform moduleが必要なのか
Terraformを書き始めたエンジニアが最初にぶつかる壁は「コードの重複」だ。開発環境と本番環境、Tokyo/Osaka/Singaporeの3リージョンなど、似た構成を複数作ろうとすると、コピー&ペーストしかないのかという疑問が生まれる。その答えがmoduleだ。Terraformのmoduleとは、関連するリソース定義をひとまとまりにして「部品」として扱えるようにする仕組みだ。プログラミングでいう関数に近い概念で、引数(input variables)と戻り値(outputs)を持ち、何度でも呼び出せる。
moduleを使わない場合、例えばEC2+SecurityGroup+EIPのセットを3環境分作ると、ほぼ同じコードが3箇所に存在する。moduleを使えば、同じセットを1箇所に定義して3回呼び出すだけになる。
・変更が1箇所で済む(保守性向上)
・テスト済みの定義を使い回せる(品質安定)
・環境ごとの差分が変数だけになる(差分管理の明確化)
moduleの基本構造とディレクトリ設計
1. ディレクトリ構造の基本
moduleはディレクトリ=1moduleが基本の設計思想だ。以下が一般的なディレクトリ構成だ。# プロジェクトルート構成 . ├── main.tf # ルートモジュール(moduleを呼び出す側) ├── variables.tf ├── outputs.tf └── modules/ ├── ec2/ # EC2モジュール │ ├── main.tf │ ├── variables.tf │ └── outputs.tf └── vpc/ # VPCモジュール ├── main.tf ├── variables.tf └── outputs.tf
# ルートのmain.tf module "web_server" { source = "./modules/ec2" instance_type = "t3.micro" ami_id = "ami-0c7217cdde317cfec" name = "web" } module "app_server" { source = "./modules/ec2" instance_type = "t3.small" ami_id = "ami-0c7217cdde317cfec" name = "app" }
2. moduleの内側(variables.tf / outputs.tf)
modules/ec2/variables.tf でmoduleが受け取る引数を定義する。# modules/ec2/variables.tf variable "instance_type" { description = "EC2インスタンスタイプ" type = string default = "t3.micro" } variable "ami_id" { description = "AMI ID" type = string } variable "name" { description = "リソース名(Nameタグに使用)" type = string }
# modules/ec2/outputs.tf output "instance_id" { value = aws_instance.this.id } output "private_ip" { value = aws_instance.this.private_ip }
countによる繰り返し
3. countの基本
同じリソースを指定した個数だけ作りたい場合は count を使う。最もシンプルな繰り返し方法だ。# 3台のEC2インスタンスを作る例 resource "aws_instance" "worker" { count = 3 ami = "ami-0c7217cdde317cfec" instance_type = "t3.micro" tags = { Name = "worker-${count.index}" # worker-0, worker-1, worker-2 } }
変数から個数を制御する使い方もよく使われる。
variable "worker_count" { type = number default = 3 } resource "aws_instance" "worker" { count = var.worker_count ami = "ami-0c7217cdde317cfec" instance_type = "t3.micro" tags = { Name = "worker-${count.index}" } }
4. countをmoduleに適用する
moduleブロックにも count を指定できる。module "web_server" { count = 3 source = "./modules/ec2" instance_type = "t3.micro" ami_id = "ami-0c7217cdde317cfec" name = "web-${count.index}" } # 参照はインデックスで行う output "web_server_ids" { value = module.web_server[*].instance_id }
for_eachによる繰り返し
5. for_eachの基本(マップを使う)
for_each はマップ(map)またはセット(set)を使って繰り返す。各要素に意味のある名前(キー)をつけられることが特徴だ。variable "servers" { type = map(object({ instance_type = string ami_id = string })) default = { "web" = { instance_type = "t3.micro" ami_id = "ami-0c7217cdde317cfec" } "app" = { instance_type = "t3.small" ami_id = "ami-0c7217cdde317cfec" } "db" = { instance_type = "t3.medium" ami_id = "ami-0c7217cdde317cfec" } } } resource "aws_instance" "servers" { for_each = var.servers ami = each.value.ami_id instance_type = each.value.instance_type tags = { Name = each.key # "web", "app", "db" } }
6. for_eachをmoduleに適用する
for_each はmoduleブロックにも使える。環境(dev/staging/prod)ごとに構成を変えたいときに便利だ。locals { environments = { dev = { instance_type = "t3.micro" min_size = 1 } staging = { instance_type = "t3.small" min_size = 2 } prod = { instance_type = "t3.large" min_size = 3 } } } module "env" { for_each = local.environments source = "./modules/app-env" env_name = each.key instance_type = each.value.instance_type min_size = each.value.min_size } # 環境ごとのURLを出力する例 output "env_urls" { value = { for k, v in module.env : k => v.app_url } }
countとfor_eachの使い分け判断基準
どちらを使うべきか迷うことが多い。以下の基準で判断してほしい。| 状況 | 推奨 | 理由 |
|---|---|---|
| 数だけ決まっていて、個々に意味がない | count |
シンプルに書ける |
| 各要素に意味のある名前がある | for_each |
リソース管理がキー名で明確になる |
| 途中の要素を削除する可能性がある | for_each |
インデックスがずれないため安全 |
| マップ/セット型の変数がある | for_each |
変数の構造をそのまま活用できる |
| シンプルな同種リソースのコピー | count |
コードが短くなる |
count で3台([0][1][2])作った後、[1]を削除したとする。Terraformはインデックスを詰め直すため、[2]だったリソースが[1]になり、既存リソースが「削除して再作成」と判断されることがある。
for_each の場合はキー名で管理するため、ひとつのキーを削除しても他のリソースには影響しない。これが「変更が多い構成にはfor_each」と言われる主な理由だ。
実践的なmodule設計パターン
7. ネストmodule(moduleからmoduleを呼ぶ)
moduleの中からさらに別のmoduleを呼び出すことができる。ネストmoduleと呼ぶ。# modules/app-env/main.tf # このmodule内でvpcモジュールを呼び出す module "vpc" { source = "../vpc" cidr_block = var.vpc_cidr env_name = var.env_name } module "ec2" { source = "../ec2" subnet_id = module.vpc.public_subnet_id instance_type = var.instance_type name = "${var.env_name}-app" }
8. 入力変数の型定義を丁寧に書く
moduleの品質は入力変数(variables.tf)の設計で決まると言っても過言ではない。型を明確にし、descriptionを書き、validationを追加するとmoduleの使いやすさが格段に上がる。# modules/ec2/variables.tf (丁寧な定義の例) variable "instance_type" { description = "EC2インスタンスタイプ(t3.micro / t3.small / t3.medium)" type = string default = "t3.micro" validation { condition = contains(["t3.micro", "t3.small", "t3.medium", "t3.large"], var.instance_type) error_message = "許可されるインスタンスタイプ: t3.micro, t3.small, t3.medium, t3.large" } } variable "tags" { description = "リソースに付与するタグのマップ" type = map(string) default = {} }
9. outputの設計(必要な値だけ外に出す)
outputは「moduleの外から何を参照できるか」を定義する。すべての値を外に出す必要はない。呼び出し元が実際に使う値に絞ることで、moduleのインターフェースがシンプルになる。# modules/ec2/outputs.tf output "instance_id" { description = "EC2インスタンスID" value = aws_instance.this.id } output "private_ip" { description = "プライベートIPアドレス" value = aws_instance.this.private_ip } # sensitive=trueで機密値をplan/applyの出力に表示させない output "connection_string" { description = "DB接続文字列(機密)" value = local.db_connection_string sensitive = true }
よくあるトラブルと解決法
10. 「Error: Duplicate resource」が出る
for_each のキーに重複がある場合に発生する。同じキーを2つのマップに使っていないか確認する。# エラーになる例(重複キー) variable "servers" { default = { "web" = { ... } "web" = { ... } # NG: 同じキー "web" が2つある } } # 実際の確認コマンド terraform plan 2>&1 | grep -i "duplicate"
11. moduleを削除するとリソースが消える
moduleブロックをコメントアウトしたり削除したりすると、そのmodule内のリソースが全て削除される。これは意図通りの動作だが、誤って消してしまう事故が多い。削除前に必ず `terraform plan` でdestroy対象を確認すること。本番環境での削除は `lifecycle { prevent_destroy = true }` を設定しておくと安全だ。
# 削除を防ぐlifecycleの設定 resource "aws_db_instance" "main" { ... lifecycle { prevent_destroy = true } } # terraformplanで削除対象を必ず確認する terraform plan -out=tfplan terraform show -json tfplan | jq '.resource_changes[] | select(.change.actions[] == "delete")'
12. for_each でセットを使う場合の注意点
set型のfor_eachを使う場合、要素の順序が保証されない。また、セットの要素はキーとしてそのまま使われるため、要素の値が変わると「削除して再作成」になる。# setを使う例(シンプルなケース) variable "availability_zones" { type = set(string) default = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"] } resource "aws_subnet" "public" { for_each = var.availability_zones vpc_id = aws_vpc.main.id availability_zone = each.key cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, index(tolist(var.availability_zones), each.key)) }
13. moduleのsourceを変更すると既存リソースが削除される
moduleのsourceパスを変更(例:ローカルパス→Terraform Registryへの移行)した場合、Terraformはそれを「別のmodule」として認識し、既存リソースを削除して再作成しようとする。これを防ぐには `terraform state mv` コマンドで既存リソースを新しいmoduleのアドレスに移動してから plan/apply する。
# stateを移動してリソースを保護する例 # 旧アドレス → 新アドレスに移動 terraform state mv \ 'module.old_name.aws_instance.web' \ 'module.new_name.aws_instance.web' # 確認 terraform state list | grep module
本記事のまとめ
terraform moduleとfor_each・countの設計パターンを改めて整理する。| やりたいこと | 使い方 |
|---|---|
| リソース定義を部品として再利用する | module "name" { source = "..." } |
| 指定した数だけリソースを作る | count = 3 で count.index を参照 |
| マップ/セットで繰り返す | for_each = var.map で each.key/each.value |
| moduleにfor_eachを使う | module "env" { for_each = local.envs } |
| 途中の要素を削除しても安全に | countではなくfor_eachを選ぶ |
| moduleの出力を別moduleに渡す | module.module_name.output_name で参照 |
| 誤削除を防ぐ | lifecycle { prevent_destroy = true } |
| stateのリソースを移動する | terraform state mv 旧アドレス 新アドレス |
Terraformでのインフラ設計をさらに深めたい方は、tfstateのチーム運用や変数の設計パターンも合わせて学ぶと、より堅牢な構成管理ができるようになる。
・postfix mynetworks の書き方はこちら
Terraform実践セミナーの詳細を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
登録10秒/合わなければ解除3秒 / 詳細はこちら
- 次のページへ:TerraformとAnsibleの違いと使い分け|IaCツール選定の判断軸
- 前のページへ:Terraformのtfstate管理とS3バックエンド設定|チーム運用で壊さないための基礎
- この記事の属するカテゴリ:Terraformへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は1分、解除もいつでも可。
登録無料・いつでも解除できます