Terraform

Terraformモジュール設計ベストプラクティス:再利用性と保守性を両立する

2026-04-19
NicheeLab編集部

ハブ記事: Terraform モジュール 完全ガイド

設計・配布・運用まで Terraform モジュールの全体像を一望できるハブ記事

モジュールは再利用性を担保しつつ、変更に強い境界を切ることが重要です。ここでは公式ドキュメントに沿った安定的な設計指針に絞り、試験対策にも効く観点でまとめます。

特に入力・出力の最小化、プロバイダーの扱い、バージョン固定とセマンティックバージョニング、count/for_eachの選択は再現性と保守性を大きく左右します。

1. 設計原則:責務の単一化と明確な境界

モジュールは単一の責務に絞り、入力(variables)と出力(outputs)を最小限に保ちます。副作用の少ないAPI表面を作ることで、下位実装の入れ替えや将来の変更に強くなります。

プロバイダーの設定は原則としてルートモジュールで行い、子モジュールではrequired_providersのみ宣言します。これにより利用側での認証・リージョン選択などの自由度が保てます。

外部との結合は明示的に。外部リソースIDやタグ戦略などは入力に閉じ、モジュール内で名前解決(dataソース乱用)しないようにします。

  • 入出力は「必要十分」に限定(Breaking changeを避ける)
  • ルートでprovider設定、子は継承を前提(必要ならaliasを明示的に渡す)
  • 内部実装の隠蔽:resource名や構成は出力で抽象化
  • 試験観点:root moduleとchild moduleの役割、provider継承の既定動作は要暗記

モジュール境界とプロバイダー継承のイメージ

継承alias渡しRoot Moduleterraform { required_providers } / provider "aws"module "network"source = ./modules/network既定の aws provider を継承module "compute"providers = { aws = aws.us-west-2 }Root Module が provider を設定し、子モジュールへ継承 / alias を明示的に渡す

最小構成のモジュール骨子

# 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"
}

2. 入力変数設計:型、安全性、検証

入力は厳格な型を付け、意味のあるデフォルトだけを許容します。曖昧なany型は再利用性を下げ、将来の破壊的変更リスクを高めます。

validationで業務ルールを早期に検知し、sensitive = trueでシークレットの漏洩を防ぎます。タグや共通設定はmap(string)などで受け取り、localsで整形します。

リストでは順序が漂流しやすいため、インスタンス分岐に使うデータは原則map/setでキーを安定させる設計に寄せます。

  • 型は具体的に(string, number, bool, list(string), map(string), object({...}) など)
  • validationで形式・範囲を早期チェック
  • シークレットはsensitive、平文出力しない
  • 順序依存を避けるためfor_each前提のmap設計を優先

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 = "データベース接続パスワード(外部から安全に注入)"
}

3. 出力と依存の扱い:結合の最小化

出力は利用側に必要な識別子や接続情報に限定し、内部のリソース構造を漏らさないようにします。将来の実装変更に耐えるため、ARNやIDなどの安定した値を優先します。

sensitive出力で機密を守りつつ、必要に応じてdepends_onでモジュール間の順序制御を明示します。参照が存在する場合は参照による暗黙的依存が優先で、depends_onは最後の手段です。

試験では、出力の役割とモジュール呼び出しでのdepends_onの意味を正しく説明できることが問われます。

  • 出力は最小限(ID, ARN, エンドポイントなど)
  • 機密はsensitive = true
  • 暗黙依存がない場合のみmodule呼び出しに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]
}

4. プロバイダーとバージョニング:予測可能性の確保

terraformブロックでrequired_versionを設定し、チームで同一のTerraformバージョン帯を強制します。required_providersではプロバイダーのソースとバージョン制約を明記します。

子モジュールではプロバイダーを「設定」せず、required_providersで要求のみを表明し、ルートからの継承やalias指定で制御できるようにします。

モジュール自身はVCSタグでバージョン管理し、セマンティックバージョニングに従います。破壊的変更はメジャー、後方互換の機能追加はマイナー、バグ修正はパッチ。

  • required_versionでCLIの下限を固定
  • required_providersでソースとバージョン帯を固定
  • モジュールはVCSタグを用いSemVerで公開
  • 試験観点:~> 演算子の意味やプロバイダー継承の既定を説明できる

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" ブロックは原則ここでは定義しない(ルートから継承)

5. 構成・ドキュメント・品質チェック

ディレクトリはmodules/配下に機能単位で分け、examples/に最小の利用例を置きます。READMEに入力・出力・依存・バージョン対応範囲を明記します。

CIではterraform fmt -recursiveとterraform validateでスタイルと構文の整合性を担保し、planの差分を監視します。モジュールの公開はタグで行い、CHANGELOGで破壊的変更を明示します。

例は実機テストの最短経路になります。examples/は実際にplan/applyできる最小構成とし、プロバイダー設定は読み手が上書き可能にします。

  • modules/, examples/, README(入力/出力/互換性)をセットで整備
  • CIでfmt/validate/planの基本三点セット
  • バージョンタグと変更履歴で下流の安全性を担保

推奨レイアウトと品質チェック

# リポジトリ構成例
.
├── 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

6. 再利用を高めるパターンとアンチパターン

インスタンスの増減やキー安定性が重要な場面ではfor_eachを優先し、countによるインデックスゆらぎを避けます。ネストブロックの繰り返しはdynamic blockで生成します。

データソースに過度に依存して外部探索を行うと、適用順序や環境依存が強まり保守性が落ちます。必要なIDや設定は原則入力で受け取り、localsで標準化します。

アンチパターンは「なんでも受けるany」「内部実装を出力に晒す」「子モジュールでproviderを設定して固定化する」など。これらは下流の自由度を奪い、破壊的変更を誘発します。

  • リソース増減=for_each、ネスト繰り返し=dynamic、単純一意=countでも可
  • 外部探索は最小限。入力で境界を固定しlocalsで整形
  • 子モジュールでのprovider設定は避ける(required_providersのみ)
観点countfor_eachdynamic block
主な用途同種リソースの単純な複製(個数ベース)キー付きインスタンス(map/setベース)ネストブロックの動的生成
アドレス指定res.name[count.index]res.name["key"]親リソースの設定内のみ(アドレスなし)
変更の安定性順序変更で再作成が発生しやすいキーが安定すれば差分最小化リソースアドレスへ影響しない
削除の挙動高位インデックスから消える傾向該当キーのみ破棄該当ブロックのみ削除
入力データnumberset(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モジュールを設計します。再利用性と保守性を最大化するための方針として最も適切なのはどれですか?

  1. 子モジュールではrequired_providersのみ宣言し、provider設定はルートで行う。入力は型を厳格化しvalidationを付与。モジュールはVCSタグでSemVer管理し、破壊的変更はメジャー更新とする。
  2. 子モジュールでproviderを固定しリージョンをハードコードする。入力はmap(any)で受け、検証は行わない。モジュールのバージョンは最新ブランチを参照させる。
  3. リソースの増減はすべてcountで実装し、順序に依存してアドレスを管理する。プロバイダーのバージョン制約は設けない。
  4. 出力は一切設けず、利用側は内部リソース名を直接参照できるようにする。

正解: 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などを返し、内部構造を露出しないようにします。

この記事で学んだ内容を問題で確認しましょう

16,000問以上の問題で実力チェック

無料で問題を解いてみる
この記事の著者

NicheeLab編集部

データエンジニアリング・クラウド資格の専門家。Databricks・Snowflake等の認定資格を保有し、実務経験に基づいた問題作成・解説を行っています。NicheeLab運営。


関連記事
Terraform

Terraform HCL 構文の基礎:Block / Attribute / Expression を正しく使い分ける

Terraform Associate で頻出の HCL 構文を、ブロック・属性・式の3視点で整理。実務で迷いがちな書き...

Terraform

Terraform Authoring & Ops Pro: 上位資格の範囲と対策

上位レベルを想定したTerraformの設計・運用ドメインを整理し、実務で通用する対策を提示。モジュール設計、ステート運...

Terraform

Terraform Providers の基本: プラグイン型アーキテクチャを正しく使いこなす

Associate レベルで押さえるべき Provider の基礎、インストール、バージョニング、認証、エイリアス運用を...

Terraform

Terraform Resourceブロック徹底ガイド: 最小単位のリソース定義

Associateレベルで押さえるべきResourceブロックの構造、依存関係、メタ引数、ライフサイクル制御を実務目線で...

Terraform

Terraform Data Source徹底理解:既存リソースの参照で壊さず足す

Terraform Associate向けに、Data Sourceを用いた既存リソース参照の基本、選択基準、評価順序、...

Terraformの記事一覧 (102件)
© 2026 NicheeLab All rights reserved.