ハブ記事: Terraform Modules: Complete Guide →
Hub article giving a complete view of Terraform modules from design through distribution and operations
As Terraform code multiplies across teams and business units, differences in naming, tagging, security baselines, and network design quickly become operational debt. A strategically curated set of shared modules lets you achieve both cross-cutting reuse and governance.
This article walks through design principles, provider and dependency handling, distribution and versioning, and testing and release from a practitioner's perspective — all grounded in stable concepts from the official documentation. We also flag the points most likely to appear on Professional-level exams.
The primary goal of shared modules is to deliver repeating infrastructure elements safely and consistently. The top design priority is an API that consuming teams cannot easily misuse. Building on Terraform's stable concepts — root/child modules, input variables, outputs, required_providers, version constraints, and the Private Registry — yields modules that survive long-term operation.
From an exam perspective, the key points are: modules must not embed state or backends; providers are defined at the root and explicitly passed to children; and compatibility is managed via semantic versioning.
Minimal shared-module skeleton example
terraform {
required_version = ">= 1.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
# Set version constraints as needed (e.g. ">= 4.0")
}
}
}
# main.tf
# Example of enforcing standard organizational tags
variable "name" {
type = string
description = "Logical name of the resource"
}
variable "tags" {
type = map(string)
description = "Additional tags (merged with standard tags)"
default = {}
}
locals {
standard_tags = {
owner = "platform"
managed-by = "terraform"
}
merged_tags = merge(local.standard_tags, var.tags)
}
# Define resources here and apply tags = local.merged_tags
output "id" {
value = "example-id"
description = "ID of the created resource"
}Always define type, validation, and description for inputs so consumers immediately understand the meaning and constraints of each value. The sensitive flag prevents accidental exposure of confidential values, and nullable and defaults should be handled carefully. Maintain outputs with naming and schemas that do not break backward compatibility.
Map and object types are flexible for future extension, but excessive nesting raises the learning curve. A realistic design makes the 80% use case easy and absorbs the remaining 20% through extension points such as optional tags or accepting a policy document.
Example of variable typing, validation, and sensitive marking
variable "cidr_block" {
type = string
description = "VPC CIDR; specify within RFC1918"
validation {
condition = can(cidrnetmask(var.cidr_block))
error_message = "Please specify a valid CIDR."
}
}
variable "enable_public_subnets" {
type = bool
description = "Whether to create public subnets"
default = false
}
variable "admin_password" {
type = string
description = "Administrative password"
sensitive = true
}
output "vpc_id" {
value = aws_vpc.this.id
description = "ID of the created VPC"
}Child modules declare required_providers, but concrete provider configuration (credentials, region, aliases) belongs in the root module and is passed in through the providers argument. This concentrates responsibility for credentials and endpoints at the root, boosting reusability and safety.
Backend configuration is always the responsibility of the root module. Keeping backend blocks out of child modules aligns with the official recommendation.
Defining providers at the root and passing them to child modules
provider "aws" {
region = "us-east-1"
alias = "use1"
}
module "network" {
source = "app/network/aws"
version = "~> 1.2"
providers = {
aws = aws.use1
}
name = "core"
cidr_block = "10.0.0.0/16"
}
Terraform Cloud/Enterprise's Private Registry and VCS integration let you publish modules versioned by SemVer tags within the organization. Standardize the naming convention (<namespace>/<name>/<provider>) and always include a README, usage examples, and a compatibility policy.
Repository layout breaks down into monorepo vs. multi-repo. Choose based on dependency isolation, release granularity, CI efficiency, and access control — and enforce tag-driven version management either way.
| Aspect | Monorepo (multiple under modules/) | Multi-repo (one module per repo) | Exam point to remember |
|---|---|---|---|
| Change-impact isolation | Watch for cross-impact; CI must detect affected scope | Easy to isolate completely | Manage compatibility consistently with SemVer; distribute on tags |
| Release granularity | Batch operations; tagging strategy tends to get complex | Clear per-module unit | Private Registry treats tags as versions |
| CI efficiency | Can optimize together; needs careful diff detection | Simple but tends to multiply CI pipelines | Standardize validate / fmt / plan |
| Access control | Coarse-grained; supplement with CODEOWNERS etc. | Fine-grained; permissions are easy to design | Avoid exposing sensitive modules |
Overall picture of shared-module distribution
Tag-driven release (example)
# Add a SemVer tag after committing changes
$ git tag v1.2.0
$ git push origin v1.2.0
# The VCS-connected Private Registry detects the tag and publishes module v1.2.0Modules follow semantic versioning, including breaking changes only in major releases. Minor releases add backward-compatible features; patches are limited to bug fixes. Document diffs and migration steps in CHANGELOG, and set an advance-notice period for deprecations.
Consumers specify constraints with the version argument. ~> is handy for varying only the rightmost component, while combining >= with < lets you strictly manage both lower and upper bounds.
Example of version constraints on the consumer side
module "network" {
source = "app/network/aws"
version = "~> 1.4" # Pin to 1.4.x (future 2.0 series not picked up)
# Or strictly
# version = ">= 1.4.0, < 2.0.0"
}
Gate releases on the module passing terraform validate/fmt standalone, and on a minimal usage example in the examples directory producing a successful plan. CI should prioritize stable runs of the official validate / fmt / init plus plan against examples — even before linters.
Recent Terraform versions add testing helpers, but adopt them gradually in line with your organization's compatibility policy. First lock the pipeline on validation via the stable official commands, then complement breaking-change detection with CHANGELOG and review.
Minimal validation steps for module CI (example)
#!/usr/bin/env bash
set -euo pipefail
# 1) Formatting and basic validation
terraform -chdir=. fmt -check -diff
terraform -chdir=. init -backend=false
terraform -chdir=. validate
# 2) Plan against the minimal usage example (do not write state)
terraform -chdir=examples/minimal init -backend=false
terraform -chdir=examples/minimal plan -input=false -lock=false -refresh=false -var "name=ci-test"Pro
問題 1
Which combination is the most appropriate design for Terraform modules reused across an organization?
正解: B
Terraform's recommendation is to configure providers at the root module and have child modules declare dependencies with required_providers. Passing aliased providers via the providers argument is the safe design. Distribute through SemVer tags in the Private Registry and have consumers manage updates with version constraints. Avoid putting backend blocks in children, pinning to latest, or skipping tags.
Is it okay to put a provider block inside a child module?
Generally avoid it. Child modules should only declare dependencies via the terraform required_providers block, while concrete credentials, regions, and aliases belong in the root and are passed through the module's providers argument. This cleanly separates credential responsibility and improves reusability.
How should breaking changes be handled?
Introduce them only in major releases following semantic versioning. Document the impact and migration steps in CHANGELOG, and phase out deprecated items gradually with a grace period. Consumers should pin upper bounds with version constraints and roll out updates in stages.
How can we share modules across multiple clouds or accounts?
Split provider-specific implementations into separate modules, then apply common policies, naming, and tags in a higher-level composition module. Define multiple provider aliases at the root and pass each child only the providers it needs.
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...