Terraform の設計は「どこを root にして、何を child に切り出すか」で決まります。ここを外すと、状態管理や再利用、チーム開発で苦労します。
本稿は Terraform Associate(初級)の出題観点を押さえつつ、現場で破綻しにくい階層設計の原則を、最小限のパターンとコードで示します。
root モジュールは terraform init/plan/apply を実行するディレクトリ直下の .tf 群です。child モジュールは root から module ブロックで呼び出される側で、さらに下位の child を呼ぶこともできます。
実務と試験の要点は、プロバイダ設定・状態ファイル・入出力の境界を root に集約し、child は疎結合な再利用単位として小さく保つこと。プロバイダ設定は原則 root に置き、child には providers 引数で渡します。
| 観点 | root モジュール | child モジュール | 備考 |
|---|---|---|---|
| 実行場所 | terraform init/plan/apply を実行 | 呼び出されて実行される | CLI は常に root で動く |
| 状態管理 | backend 設定の所在。1つの state に紐づく | state は root に統合される | child 単体の state は通常持たない |
| プロバイダ | provider 設定を定義 | providers 引数で受け取って使用 | 意図的に差し替える場合のみ別名供給 |
| I/O | variables.tf, tfvars, outputs の集約点 | 最小限の variables/outputs を公開 | locals で内部表現を隠蔽 |
| 再利用 | 低い(環境固有が多い) | 高い(機能単位・汎用) | semantic versioning を推奨 |
最小の root から child を呼ぶ例
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.region
}
module "network" {
source = "./modules/network"
providers = { aws = aws }
cidr = var.vpc_cidr
}
単一レポジトリで modules を共通化し、環境別に root を分けるのが、学習コストと分離性のバランスが良い定番です。複数リポジトリに分けるのは、変更フローやリリース管理が固まってからで十分です。
命名は“動詞より名詞”。modules は network, compute, security のようにリソース境界で分け、root では環境(dev/stg/prod)ごとの差分だけを与えます。
推奨レイアウト(単一リポジトリ)
repo-root/
modules/
network/
main.tf
variables.tf
outputs.tf
compute/
main.tf
variables.tf
outputs.tf
environments/
dev/
main.tf
variables.tf
terraform.tfvars
backend.hcl
stg/
main.tf
terraform.tfvars
backend.hcl
prod/
main.tf
terraform.tfvars
backend.hcl
backend はファイル分離し CLI で渡す(変数は使えない)
# environments/dev/backend.hcl
bucket = "my-tfstate-bucket"
key = "env/dev/terraform.tfstate"
region = "ap-northeast-1"
dynamodb_table = "my-tf-lock"
# 初期化
# terraform -chdir=environments/dev init -backend-config=backend.hcl
child は入力境界を variables.tf、出力境界を outputs.tf で明確化します。内部表現は locals に閉じ込め、root からは見えないようにします。
root は tfvars で環境差分だけを与え、module 出力を別 module の入力に渡すことで依存を明示します。
入出力の最小例
# modules/network/variables.tf
variable "cidr" {
type = string
description = "VPC CIDR"
}
# modules/network/main.tf
resource "aws_vpc" "this" {
cidr_block = var.cidr
tags = { Name = "nlab-vpc" }
}
# modules/network/outputs.tf
output "vpc_id" {
value = aws_vpc.this.id
}
# environments/dev/main.tf
module "network" {
source = "../../modules/network"
cidr = var.vpc_cidr
}
module "compute" {
source = "../../modules/compute"
vpc_id = module.network.vpc_id
}
強い分離(権限・バックエンド・変更窓口まで分けたい)が必要な本番系は、環境ごとに root ディレクトリを分けるのが基本です。state も backend 設定も物理的に分かれます。
Terraform のワークスペースは同一コード・同一 backend で state ファイルを切り替える機能です。軽量なサンドボックスや短命なレビュー環境には有用ですが、環境ごとの差分が大きい場合はディレクトリ分離が安全です。
環境ごと root で差分を与える(tfvars 例)
# environments/dev/terraform.tfvars
region = "ap-northeast-1"
vpc_cidr = "10.10.0.0/16"
# environments/prod/terraform.tfvars
region = "ap-northeast-1"
vpc_cidr = "10.20.0.0/16"
# 実行例
# terraform -chdir=environments/dev plan -var-file=terraform.tfvars
レジストリ配布のモジュールは version 引数でセマンティックに固定します。Git 参照のモジュールは source に ref タグやハッシュを明示します。ローカル相対パスのモジュールに version は使えません。
更新時は terraform init -upgrade を使い、変更は plan で可視化したうえでレビューに掛けます。
registry と Git の指定例
# Registry から取得
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.5"
name = "nlab"
cidr = "10.30.0.0/16"
}
# Git から取得(タグ固定)
module "network" {
source = "git::https://github.com/example/network-module.git//modules/vpc?ref=v1.4.2"
cidr = "10.40.0.0/16"
}
# 更新時
# terraform init -upgrade
lint → validate → plan の順に自動化し、plan の差分を PR コメントに出すだけでも品質が上がります。apply は環境ごとに手動承認を挟むのが安全です。
モジュール単体は terraform validate と最小モックでの plan を行い、root はバックエンドを指した上で差分の可視化に徹します。
最小の CI ステップ例(GitHub Actions)
name: terraform
on: [pull_request]
jobs:
plan:
runs-on: ubuntu-latest
defaults:
run:
working-directory: environments/dev
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.5
- run: terraform fmt -check
- run: terraform init -backend-config=backend.hcl
- run: terraform validate
- run: terraform plan -var-file=terraform.tfvars -out=tfplan
Associate
問題 1
Terraform で環境ごとに異なる設定を安全に適用したい。root/child の役割分担として最も適切なのはどれか。
正解: A
プロバイダ設定は原則 root に集約し、child には providers で渡すのが公式の推奨パターン。強い分離が必要な環境は root を分け、tfvars と backend を環境単位で管理するのが安全。
root は環境ごとに必ず分けるべき?
本番系は分けるのが基本です。dev/stg のような同質な環境はワークスペース併用も可能ですが、権限やバックエンドまで分けたい場合は root ディレクトリを環境ごとに用意します。
child モジュールで provider ブロックを書いてもよい?
技術的には可能ですが、意図しない競合や差し替え困難さを招きやすいため、root で定義し providers 引数で渡すのが安全です。代替プロバイダ(別名)を使う場合も root で用意して map で渡します.
モジュールの更新はどう管理する?
レジストリ配布は version で固定し、更新時に terraform init -upgrade → plan で差分をレビューします。Git 参照は ref をタグ/コミットで固定し、ブランチ参照は避けます。
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を用いた既存リソース参照の基本、選択基準、評価順序、...