はじめに
TerraformでALB+EC2(2台)の最小構成を作成したあと、「このままだと実務では使いづらいな」と感じ、既存構成をmodules化しました。
本記事では、
- なぜmodules化したのか
- どのように構成を分割したか
- 実際に詰まったポイント
を中心にまとめます。
Before:modules化前の構成
最初は以下のように、ほぼすべてのリソースを同一ディレクトリで管理していました。
terraform-alb-ec2-2az/
├── alb.tf
├── ec2.tf
├── outputs.tf
├── provider.tf
├── scripts/
│ └── check_infra.sh
├── security_group.tf
├── terraform.tfvars
├── variables.tf
└── vpc.tf
この構成でも 動作自体は問題なしでしたが、
- ファイル数が増えると全体像が見えづらい
- 再利用がしづらい
- 実務でよく見る構成と違う
といった課題を感じました。
modules化した理由
modules化を行った理由は主に以下です。
- 責務ごとに分離したかった(VPC / SG / ALB / EC2)
- 他の環境でも再利用できる形にしたかった
- 実務でよく見るTerraform構成に寄せたかった
特に「modulesを前提としたTerraform設計」に慣れておきたかったのが大きな理由です。
After:modules化後の構成
modules化後のディレクトリ構成は以下です。
terraform-alb-ec2-2az/
├── main.tf # 各moduleを組み立てるエントリーポイント
├── provider.tf # AWS Provider定義(ルートで1回のみ)
├── variables.tf # ルートmoduleの変数定義
├── outputs.tf # ルートmoduleの出力
├── terraform.tfvars # 環境ごとの変数値
│
├── scripts/ # EC2初期設定・動作確認用スクリプト
│ ├── user_data.sh # EC2起動時に実行されるUserData
│ └── check_infra.sh # ALB / EC2 疎通確認用スクリプト
│
└── modules/ # 各AWSリソースを責務ごとに分離
├── vpc/ # VPC / Subnet / IGW / RouteTable
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
│
├── security_group/ # ALB・EC2用セキュリティグループ
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
│
├── alb/ # ALB / TargetGroup / Listener
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
│
└── ec2/ # EC2(2台)/ IAM Role(SSM)/ TG紐付け
├── main.tf
├── variables.tf
└── outputs.tf
- ルートは 全体の組み立てのみ を担当
- 各modulesは 単一責務 に集中
という形に整理しました。
main.tf の役割
main.tf では modules を呼び出すだけにしています。
- provider はルートで1回だけ定義
- 各リソースの依存関係は outputs / inputs で接続
これにより、「全体構成は main.tf を見れば分かる」状態になりました。
user_data をファイルとして分離
modules化以前は、ec2.tf 内に直接 user_data を記述していました。
user_data = <<EOF
#!/bin/bash
yum install -y httpd
...
EOF
この方法でも動作はしますが、
- Terraformコードが読みにくくなる
- user_data が長くなると差分が追いづらい
- IaC と 初期設定スクリプト が混在する
といった課題を感じました。
そこで、user_data は scripts/user_data.sh としてファイルに切り出し、Terraform 側では file() 関数で読み込む形に変更しました。
user_data = file(var.user_data_file)
この構成にすることで、
- Terraformコードの可読性向上
- user_data の単体修正が容易
- 実務でよく見る構成に近づく
といったメリットを得られました。
terraform.tfvars による構成変更のしやすさ
modules化を進める中で、terraform.tfvars の値を変更するだけで構成を柔軟に切り替えられる点に大きなメリットを感じました。
例えば、
- EC2のインスタンスタイプ
- 利用するAZ
- サブネットのCIDR
- リソース名のプレフィックス
といった項目は、Terraformコードを修正せずにterraform.tfvars のみで変更できます。
aws_region = "ap-northeast-1"
vpc_cidr = "10.0.0.0/16"
vpc_name = "my-vpc"
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
azs = ["ap-northeast-1a", "ap-northeast-1c"]
name_prefix = "myproject"
ec2_instance_type = "t3.micro"
この構成により、
- レビュー時に変更点が分かりやすい
- Terraformコード自体を安定させられる
といった、実務で重要なメリットを実感しました。
また、今回は実装していませんが、terraform.tfvars を切り替えることで環境差分(dev / stg / prod)を簡単に管理できる点でも、大きなメリットがあると感じています。
詰まったポイント
modules にファイルを移動しただけでは動かない
modules化を始めた当初は、ec2.tf や vpc.tf をそのまま modules/ 配下に移動すれば動くものだと思っていました。
しかし実際には、
- modules 内では 変数は自動的に共有されない
- ルートで定義した resource は modules から直接参照できない
- outputs を通して値を受け渡す必要がある
といった点を理解しておらず、最初はうまく動きませんでした。
最終的には、
- modules 側で
variables.tfに必要な値を明示的に定義 -
outputs.tfで外部に公開したい値を出力 - ルートの
main.tfで modules 同士を接続
という形に整理することで、意図した構成を実現できました。
providerの二重定義エラー
modules化の途中で、
Duplicate provider configuration
というエラーに遭遇しました。
原因は、
- main.tf
- provider.tf
の両方に provider を定義していたことでした。
providerはルートモジュールで1回だけ定義するという基本を再確認しました。
まとめ
Terraformで一度動く構成を作ったあと、modules化して整理することで以下のメリットを実感しました。
- 構成が理解しやすくなる
- 実務に近い形になる
- 再利用性が高まる
「とりあえず動く」から一歩進んで、設計を意識したTerraform に取り組みたい人には、modules化はとても良い題材だと思います。