Terraform Modules

A module is a directory of Terraform files treated as one unit. Every configuration is already a module (the root module), and it can call child modules to reuse infrastructure patterns: a standard VPC, a hardened S3 bucket, a Kubernetes cluster with company defaults. Modules are to Terraform what functions and libraries are to application code. They package complexity behind inputs and outputs so it's written once and reused everywhere.

Well-designed modules are how platform engineering teams offer self-service infrastructure with guardrails built in. Poorly designed ones become wrappers with a hundred pass-through variables that nobody can upgrade.

TL;DR

Quick Example

A small module for a private, encrypted S3 bucket:

Every bucket created through the module gets public-access blocking and versioning automatically.

Core Concepts

Module Structure

The conventional layout:

Child modules shouldn't configure providers themselves. They declare required_providers, and the root module passes provider configuration in (implicitly, or via providers = { aws = aws.us_west } for aliases).

Inputs, Outputs, and Locals

Module Sources and Versions

Local paths (./modules/x) aren't versioned; they change with the repo. Registry and Git sources should always pin a version or tag so upgrades are deliberate.

Composition

Keep modules shallow and compose them in the root module, passing one module's outputs into another's inputs:

Deeply nested modules (modules calling modules calling modules) are hard to debug and to refactor.

Designing Good Modules

Best Practices

Use Well-Maintained Public Modules for Commodity Infrastructure

Community modules like terraform-aws-modules/vpc encode years of edge cases. Pin a version, read the changelog before upgrading, and wrap them only if you need to enforce organization defaults.

Validate Inputs Early

validation blocks, precondition/postcondition checks on resources, and precise types catch mistakes at plan time with clear error messages instead of cryptic provider errors mid-apply.

Prefer for_each Over count

for_each keys instances by stable names (module.buckets["logs"]), so removing one doesn't shift the others. With count, removing index 0 renumbers everything and can trigger destroy/create churn.

Document With Examples

A README with a minimal and a complete example, plus generated input and output tables (terraform-docs), makes modules usable without reading their source.

Common Mistakes

Pass-Through Wrapper Modules

Either add real value (defaults, companion resources, policy), or use the resource directly.

Unpinned Module Versions

A registry module without version, or a Git source without ref, pulls whatever is latest on the next init, and can change infrastructure without any code change on your side.

Configuring Providers Inside Child Modules

Modules containing provider blocks can't be used with for_each/count and can't be removed cleanly, because Terraform needs the provider to destroy their resources. Configure providers in the root module and pass them down.

FAQ

When should I create a module?

When the same group of resources appears in several places, or when you want to enforce a standard (security defaults, tagging, naming) across teams. Don't modularize a resource used once "for tidiness". Extract when duplication or policy makes it worthwhile.

How do I version internal modules?

Put them in a dedicated repo (or monorepo path) and tag releases with semver (v1.4.0), referenced via ?ref=. Or publish them to a private registry (HCP Terraform, GitLab, Artifactory) for version constraints and discovery.

Can modules have their own state?

No. A module's resources live in the state of the root configuration that calls it. To isolate state, use separate root configurations that call the same module.

What's the difference between a module and a workspace?

A module is reusable code. A workspace is a separate state instance for the same root configuration. You might use one root configuration (calling many modules) with workspaces for dev and staging, or separate root directories per environment calling shared modules.

Related Topics

References