ハブ記事: Terraform Modules: Complete Guide →
Hub article covering the full picture of Terraform modules — design, distribution, and operations
Modules need to deliver reusability while drawing boundaries that hold up under change. This article focuses on stable design guidelines aligned with the official docs, organized so they double as exam prep.
In particular, minimizing inputs and outputs, handling providers correctly, pinning versions with SemVer, and choosing between count and for_each have the biggest impact on reproducibility and maintainability.
Keep each module focused on a single responsibility and keep its inputs (variables) and outputs minimal. A small API surface with few side effects makes it easier to swap implementations and to absorb future change.
As a rule, configure providers in the root module and only declare required_providers in child modules. This preserves consumers' freedom to choose authentication, region, and so on.
Make external coupling explicit. Keep things like external resource IDs and tagging strategy at the input boundary, and avoid resolving names inside the module (i.e. don't overuse data sources).
Module boundaries and provider inheritance, at a glance
Skeleton of a minimal module
# 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"
}Give inputs strict types and allow only meaningful defaults. Vague any types reduce reusability and increase the risk of future breaking changes.
Use validation to catch business-rule violations early, and sensitive = true to keep secrets from leaking. Receive shared things like tags as map(string) and normalize them in locals.
Because list order tends to drift, prefer designs that use map/set with stable keys for data that drives instance branching.
Concrete variables.tf examples (types and validation)
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 = "データベース接続パスワード(外部から安全に注入)"
}Limit outputs to the identifiers and connection info consumers actually need; don't leak internal resource structure. Prefer stable values like ARNs and IDs so your module survives future implementation changes.
Mark sensitive outputs to protect confidential data, and use depends_on between modules only when you need explicit ordering. When references exist, the implicit dependency they create takes precedence; depends_on is a last resort.
On the exam, you need to clearly articulate the role of outputs and the meaning of depends_on on a module call.
Examples of outputs and module-level 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]
}Set required_version in the terraform block to force the whole team onto the same Terraform version range. In required_providers, spell out each provider's source and version constraint.
Child modules should not configure providers; they should only declare requirements via required_providers, letting the root module control behavior via inheritance or explicit aliases.
Version the module itself with VCS tags and follow SemVer: breaking changes bump the major version, backward-compatible features bump the minor, and bug fixes bump the patch.
terraform, provider, and how to pass providers to a module
# ルートモジュール
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" ブロックは原則ここでは定義しない(ルートから継承)Split directories under modules/ by capability, and put minimal usage examples in examples/. The README should clearly list inputs, outputs, dependencies, and supported version ranges.
In CI, use terraform fmt -recursive and terraform validate to enforce style and syntax consistency, and watch plan diffs. Publish modules via tags and call out breaking changes in the CHANGELOG.
Examples are the fastest path to real-world testing. Keep examples/ as a minimal config that actually runs plan/apply, and make sure readers can override the provider setup.
Recommended layout and quality checks
# リポジトリ構成例
.
├── 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 planWhen you're adding/removing instances or key stability matters, prefer for_each over count to avoid index drift. Repeated nested blocks should be generated via dynamic blocks.
Over-relying on data sources for external lookups increases apply-order and environment dependencies, which hurts maintainability. As a rule, take required IDs and config via inputs and normalize them in locals.
Common anti-patterns: accepting any for everything, leaking internal implementation through outputs, and locking in providers by configuring them in child modules. They strip downstream consumers of flexibility and invite breaking changes.
| Aspect | count | for_each | dynamic block |
|---|---|---|---|
| Main use case | Simple replication of identical resources (count-based) | Keyed instances (map/set-based) | Dynamic generation of nested blocks |
| Addressing | res.name[count.index] | res.name["key"] | Only inside the parent resource's config (no address) |
| Change stability | Order changes tend to trigger recreation | Stable keys → minimal diffs | Does not affect the resource address |
| Deletion behavior | Tends to remove from the highest index down | Destroys only the matching key | Removes only the matching block |
| Input data | number | set(string)/map(T) etc. | Any collection via a for_each expression |
Address stability: for_each vs. 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
You are designing a VPC module to be reused across multiple accounts and regions. Which approach best maximizes reusability and maintainability?
正解: A
A matches the official recommendation. Configure providers in the root and have child modules only declare requirements via required_providers. Strict input types with validation stabilize the boundary, and VCS tags + SemVer make releases predictable. The other options undermine reusability and predictability.
Is it OK to define a provider block inside a child module?
Generally avoid it. Child modules should only declare dependencies via required_providers, while actual provider configuration (auth, region, endpoint, etc.) belongs in the root module and is inherited. When you need multiple configurations, use aliases and pass them explicitly via the providers argument on the module call.
When should you use depends_on on a module call?
Only in the special case where you need to control creation order but there is no implicit dependency via references. Normally, referring to another module's outputs as an input establishes the implicit dependency and depends_on is unnecessary.
How should I design collections to minimize breaking changes?
Prefer for_each with map/set so that the key identifying each resource instance is stable. Relying on count + list tends to trigger massive replacements when ordering shifts. Also expose only stable IDs/ARNs in outputs and avoid leaking internal structure.
Practice with certification-focused question sets
無料で問題を解いてみるNicheeLab Editorial Team
NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.
HCL Syntax: Terraform's Configuration Language (2026)
HCL2 fundamentals for Terraform — blocks, attributes, expres...
Terraform Authoring & Operations Pro: Complete Guide (2026)
Tactics for the Terraform Pro exam — module authoring, works...
Terraform Providers: Plugin Management Fundamentals (2026)
Provider mechanics — required_providers, versions, mirrors, ...
Terraform Resource Blocks: Declarative Infra Units (2026)
Resource block fundamentals — addresses, references, common ...
Terraform Data Sources: Read-Only External Data (2026)
Data source basics — declaration, refresh behavior, dependen...