ハブ記事: Terraform モジュール 完全ガイド →
設計・配布・運用まで Terraform モジュールの全体像を一望できるハブ記事
モジュールは再利用性を担保しつつ、変更に強い境界を切ることが重要です。ここでは公式ドキュメントに沿った安定的な設計指針に絞り、試験対策にも効く観点でまとめます。
特に入力・出力の最小化、プロバイダーの扱い、バージョン固定とセマンティックバージョニング、count/for_eachの選択は再現性と保守性を大きく左右します。
モジュールは単一の責務に絞り、入力(variables)と出力(outputs)を最小限に保ちます。副作用の少ないAPI表面を作ることで、下位実装の入れ替えや将来の変更に強くなります。
プロバイダーの設定は原則としてルートモジュールで行い、子モジュールではrequired_providersのみ宣言します。これにより利用側での認証・リージョン選択などの自由度が保てます。
外部との結合は明示的に。外部リソースIDやタグ戦略などは入力に閉じ、モジュール内で名前解決(dataソース乱用)しないようにします。
モジュール境界とプロバイダー継承のイメージ
最小構成のモジュール骨子
# modules/network/
# main.tf
resource "aws_vpc" "this" {
cidr_block = var.cidr_block
}
# variables.tf
variable "cidr_block" {
type = string
}
# outputs.tf
output "vpc_id" {
value = aws_vpc.this.id
}
# ルート側使用例(providersはルートで設定)
module "network" {
source = "./modules/network"
cidr_block = "10.0.0.0/16"
}入力は厳格な型を付け、意味のあるデフォルトだけを許容します。曖昧なany型は再利用性を下げ、将来の破壊的変更リスクを高めます。
validationで業務ルールを早期に検知し、sensitive = trueでシークレットの漏洩を防ぎます。タグや共通設定はmap(string)などで受け取り、localsで整形します。
リストでは順序が漂流しやすいため、インスタンス分岐に使うデータは原則map/setでキーを安定させる設計に寄せます。
variables.tfの具体例(型と検証)
variable "name" {
type = string
validation {
condition = length(var.name) > 0 && length(var.name) <= 63
error_message = "nameは1〜63文字で指定してください。"
}
}
variable "tags" {
type = map(string)
description = "共通タグ"
default = {}
}
variable "ports" {
type = set(number)
validation {
condition = alltrue([for p in var.ports : p > 0 && p < 65536])
error_message = "portsは1〜65535の範囲で指定してください。"
}
}
variable "db_password" {
type = string
sensitive = true
description = "データベース接続パスワード(外部から安全に注入)"
}出力は利用側に必要な識別子や接続情報に限定し、内部のリソース構造を漏らさないようにします。将来の実装変更に耐えるため、ARNやIDなどの安定した値を優先します。
sensitive出力で機密を守りつつ、必要に応じてdepends_onでモジュール間の順序制御を明示します。参照が存在する場合は参照による暗黙的依存が優先で、depends_onは最後の手段です。
試験では、出力の役割とモジュール呼び出しでのdepends_onの意味を正しく説明できることが問われます。
outputsとmodule depends_onの例
# modules/network/outputs.tf
output "vpc_id" {
value = aws_vpc.this.id
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}
# ルートモジュール側:明示的な順序が必要なケース
module "network" {
source = "./modules/network"
cidr_block = "10.0.0.0/16"
}
module "app" {
source = "./modules/app"
vpc_id = module.network.vpc_id
# vpc_id参照があるため通常は暗黙依存で十分
}
# 参照が存在しないが順序制御したい特殊ケース
module "audit" {
source = "./modules/audit"
depends_on = [module.network]
}terraformブロックでrequired_versionを設定し、チームで同一のTerraformバージョン帯を強制します。required_providersではプロバイダーのソースとバージョン制約を明記します。
子モジュールではプロバイダーを「設定」せず、required_providersで要求のみを表明し、ルートからの継承やalias指定で制御できるようにします。
モジュール自身はVCSタグでバージョン管理し、セマンティックバージョニングに従います。破壊的変更はメジャー、後方互換の機能追加はマイナー、バグ修正はパッチ。
terraformとprovider、モジュールへのprovider引き渡し
# ルートモジュール
terraform {
required_version = ">= 1.3.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # 5.xに固定(将来の6.xを除外)
}
}
}
provider "aws" {
region = "us-east-1"
}
provider "aws" {
alias = "us_west_2"
region = "us-west-2"
}
module "compute" {
source = "./modules/compute"
providers = {
aws = aws.us_west_2
}
}
# 子モジュール側(modules/compute)
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}
# provider "aws" ブロックは原則ここでは定義しない(ルートから継承)ディレクトリはmodules/配下に機能単位で分け、examples/に最小の利用例を置きます。READMEに入力・出力・依存・バージョン対応範囲を明記します。
CIではterraform fmt -recursiveとterraform validateでスタイルと構文の整合性を担保し、planの差分を監視します。モジュールの公開はタグで行い、CHANGELOGで破壊的変更を明示します。
例は実機テストの最短経路になります。examples/は実際にplan/applyできる最小構成とし、プロバイダー設定は読み手が上書き可能にします。
推奨レイアウトと品質チェック
# リポジトリ構成例
.
├── modules/
│ ├── network/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── compute/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── examples/
│ └── simple/
│ ├── main.tf
│ └── providers.tf
└── README.md
# CIの基本(シェル例)
terraform fmt -recursive
terraform validate
terraform -chdir=examples/simple init
terraform -chdir=examples/simple planインスタンスの増減やキー安定性が重要な場面ではfor_eachを優先し、countによるインデックスゆらぎを避けます。ネストブロックの繰り返しはdynamic blockで生成します。
データソースに過度に依存して外部探索を行うと、適用順序や環境依存が強まり保守性が落ちます。必要なIDや設定は原則入力で受け取り、localsで標準化します。
アンチパターンは「なんでも受けるany」「内部実装を出力に晒す」「子モジュールでproviderを設定して固定化する」など。これらは下流の自由度を奪い、破壊的変更を誘発します。
| 観点 | count | for_each | dynamic block |
|---|---|---|---|
| 主な用途 | 同種リソースの単純な複製(個数ベース) | キー付きインスタンス(map/setベース) | ネストブロックの動的生成 |
| アドレス指定 | res.name[count.index] | res.name["key"] | 親リソースの設定内のみ(アドレスなし) |
| 変更の安定性 | 順序変更で再作成が発生しやすい | キーが安定すれば差分最小化 | リソースアドレスへ影響しない |
| 削除の挙動 | 高位インデックスから消える傾向 | 該当キーのみ破棄 | 該当ブロックのみ削除 |
| 入力データ | number | set(string)/map(T) 等 | for_each式で任意のコレクション |
for_eachとcountのアドレス安定性の違い
# count(順序ゆらぎに弱い)
variable "names" { type = list(string) }
resource "aws_iam_user" "u" {
count = length(var.names)
name = var.names[count.index]
}
# namesの順序を変えると多数の置換が発生しうる
# for_each(キーが安定すれば強い)
variable "users" { type = set(string) }
resource "aws_iam_user" "u2" {
for_each = var.users
name = each.value
}
# キー(各ユーザー名)が同一であれば差分は最小化されるPro
問題 1
複数アカウント・複数リージョンで再利用されるVPCモジュールを設計します。再利用性と保守性を最大化するための方針として最も適切なのはどれですか?
正解: A
公式推奨に沿うのはA。provider設定はルートで行い、子はrequired_providersで要件のみ宣言する。入力の型・validationで境界を安定化し、モジュールはVCSタグとSemVerで予測可能にリリースする。他の選択肢は再利用性や予測可能性を損なう。
子モジュールにproviderブロックを定義してもよいですか?
原則として避けます。子モジュールはrequired_providersで依存のみ表明し、実際のprovider設定(認証やリージョン、エンドポイントなど)はルートで行い継承させます。複数の設定が必要な場合はaliasを用い、module呼び出しのproviders引数で明示的に渡します.
module呼び出しでdepends_onを使うのはどんな場合ですか?
参照による暗黙的依存が存在しないが、作成順序を制御したい特別なケースに限定します。通常は入力で他モジュールの出力を参照すれば暗黙依存が成立するため、depends_onは不要です。
破壊的変更を最小化するためのコレクション設計は?
リソースの個体を識別するキーが安定するよう、for_eachでmap/setを用いる設計を優先します。countとlistに依存すると順序変更で大規模な置換が発生しがちです。また、出力は安定したID/ARNなどを返し、内部構造を露出しないようにします。
NicheeLab編集部
データエンジニアリング・クラウド資格の専門家。Databricks・Snowflake等の認定資格を保有し、実務経験に基づいた問題作成・解説を行っています。NicheeLab運営。
Terraform HCL 構文の基礎:Block / Attribute / Expression を正しく使い分ける
Terraform Associate で頻出の HCL 構文を、ブロック・属性・式の3視点で整理。実務で迷いがちな書き...
Terraform Authoring & Ops Pro: 上位資格の範囲と対策
上位レベルを想定したTerraformの設計・運用ドメインを整理し、実務で通用する対策を提示。モジュール設計、ステート運...
Terraform Providers の基本: プラグイン型アーキテクチャを正しく使いこなす
Associate レベルで押さえるべき Provider の基礎、インストール、バージョニング、認証、エイリアス運用を...
Terraform Resourceブロック徹底ガイド: 最小単位のリソース定義
Associateレベルで押さえるべきResourceブロックの構造、依存関係、メタ引数、ライフサイクル制御を実務目線で...
Terraform Data Source徹底理解:既存リソースの参照で壊さず足す
Terraform Associate向けに、Data Sourceを用いた既存リソース参照の基本、選択基準、評価順序、...