ハブ記事: Terraform Modules: Complete Guide →
Hub article covering the full picture of Terraform modules: design, distribution, and operations
Modules are Terraform's unit of reuse, but their real value shows up when you compose them. Safely combining multiple modules — network, security, application platform — hinges on a handful of non-negotiables: expressing dependencies, propagating providers, designing inputs and outputs, and pinning versions.
This article walks through composition patterns that hold up under change, grounded in the behavior described in the official documentation. It also covers the points exams tend to focus on: depends_on between modules, the stability of for_each keys, propagating provider aliases, and how to write version constraints.
Composition of multiple modules normally happens in the root module. Each child module has a clear boundary (input variables and output outputs), and the root wires them together. Define input types strictly, and keep outputs to the minimal shape downstream modules need.
Use a registry, VCS, or local path for module source, and pin version explicitly for stable operations. Initialize providers at the root, and map aliases through to children when needed.
| Pattern | Overview | Strengths | Caveats |
|---|---|---|---|
| Root composition (Flat) | Wire multiple child modules directly from the root | Easy to read and debug | The root tends to bloat |
| Wrapper module (Facade) | Bundle a series of constructions into a single reusable module | High reusability and standardization | Strict version management is essential when swapping internals |
| Per-environment stack split | Repeat the same composition in per-env directories | Clear separation of state and responsibility | Absorb duplicated definitions with templating or a shared wrapper |
A typical module composition (wired at the root)
Root composition example (pinning source and wiring outputs)
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.region
}
module "network" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = var.name
cidr = var.vpc_cidr
}
module "security" {
source = "git::https://example.com/org/security-group.git?ref=v1.4.2"
vpc_id = module.network.vpc_id
}
module "app" {
source = "./modules/app"
subnet_ids = module.network.private_subnets
security_groups = [module.security.sg_id]
}
output "app_endpoint" {
value = module.app.endpoint
}Modules connect to each other by passing outputs into inputs. As long as the references are there, Terraform resolves implicit dependencies and orders the graph for you. Use locals for intermediate shaping so the data matches the downstream module's input spec.
Marking an output sensitive suppresses display but does not change the fact that it is stored in plaintext in state. For secrets, consider provider-side secret management or an external secret store.
Using locals to wire outputs through
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
name = var.name
cidr = var.vpc_cidr
}
locals {
app_subnet_ids = slice(module.vpc.private_subnets, 0, 2)
}
module "db" {
source = "./modules/rds"
subnet_ids = local.app_subnet_ids
}
output "db_endpoint" {
value = module.db.endpoint
sensitive = true
}Terraform builds an implicit dependency graph from references (e.g., to resource or module outputs). Only when a procedural ordering is required without any reference should you add an explicit dependency via the depends_on meta-argument on the module block.
Data sources are read-only and evaluated at plan time. Adding unnecessary depends_on to a data source for side effects or ordering just adds complexity. First check whether implicit dependency through a reference is enough.
Explicit dependency between modules (when no implicit reference exists)
module "iam" {
source = "./modules/iam-baseline"
}
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 20.0"
cluster_name = var.name
# This module does not directly reference iam outputs, but iam must be applied first
depends_on = [module.iam]
}When instantiating the same module multiple times, you can use either count or for_each. From the perspective of stable-key addressing and diff control, real-world practice usually favors for_each.
Use identifiers that will stay stable into the future (logical names or IDs) as for_each keys to avoid destructive replacements when items are reordered or removed.
Replicate a subnet module with for_each and aggregate the outputs
variable "subnets" {
type = map(object({
cidr = string
az = string
}))
}
module "subnet" {
source = "./modules/subnet"
for_each = var.subnets
name = each.key
cidr = each.value.cidr
az = each.value.az
}
# Bundle outputs with their keys so the caller can work with them easily
output "subnet_ids" {
value = { for k, m in module.subnet : k => m.id }
}Child modules inherit providers from the caller. When passing aliased providers to children, map them via the providers argument on the module block. Assign the caller's definitions to the provider names (including aliases) the child expects.
Declare provider version ranges with required_providers and pin module versions on the source. Use range operators like ~> according to your release compatibility policy.
Propagating provider aliases and constraining versions
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0, < 6.0"
}
}
}
provider "aws" {
alias = "use1"
region = "us-east-1"
}
provider "aws" {
alias = "usw2"
region = "us-west-2"
}
# Assume the child module expects the default provider name 'aws'
module "logs" {
source = "./modules/logs"
providers = {
aws = aws.usw2
}
}
# Pin registry modules with version
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.1"
name = var.name
cidr = var.vpc_cidr
}Separating state by directory or backend for production and staging environments is the practical default. Workspaces are useful for lightweight variants, but they are not a substitute for environment boundaries or permission separation.
When applying the same composition across environments, building a shared wrapper module and only varying the input values and backend configuration per env makes management easy. During refactors, use moved blocks to make resource address moves explicit and avoid breaking state.
Minimal example of splitting by environment directory
prod/
main.tf
variables.tf
backend.hcl # remote backend config for prod
terraform.tfvars
staging/
main.tf
variables.tf
backend.hcl # for staging
terraform.tfvars
# Example: supply backend.hcl at init time with -backend-config=backend.hcl
# Example of a moved block (when moving a resource into a wrapper)
moved {
from = aws_iam_role.app
to = module.security.aws_iam_role.app
}Pro
問題 1
You are composing three modules — network, iam, and eks — in the root module. eks does not reference iam's outputs directly, but iam must be applied first in creation order. Which approach is the most appropriate Terraform best practice?
正解: A
When there is no reference but a procedural order is required, add an explicit dependency with depends_on on the module block. Creating a pseudo-dependency through a dummy reference is an anti-pattern; lifecycle controls destroy ordering, not dependency resolution; and refresh only updates state and is not appropriate for ordering.
Should I use count or for_each to replicate a module?
for_each is recommended because stable identifiers minimize diffs. count is fragile against index shifts and can trigger unintended replacements when items are inserted or removed. Exams also frequently test the stability of for_each keys.
How do I make a child module use a provider for a different region?
Define an aliased provider in the parent and map it to the provider name the child expects via the providers argument on the module block. Example: providers = { aws = aws.usw2 }. The child then uses the provider under that name.
How should I manage versions for registry modules versus providers?
Pin modules via the version argument on the module block, and constrain providers with version ranges in required_providers. Declaring both explicitly produces highly reproducible plans.
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...