Modules are how you stop copy-pasting the same VPC or service setup across environments.
Every Terraform directory is a module. The one you run commands in is the root module; others are child modules called with a module block:
module "network" { source = "./modules/network" cidr = "10.0.0.0/16" }
A child module's interface is its variable blocks (inputs) and output blocks (return values). The caller reads outputs as module.network.vpc_id. Resources inside are addressed as module.network.aws_vpc.this. A module can't see the caller's variables or resources except what is passed in.
Good module design:
- One clear purpose ("an ECS service with its load balancer target and alarms"), not "everything for team X".
- Small, typed, documented inputs with sensible defaults; validation for important ones.
- Output IDs and ARNs that callers will need.
- Don't configure providers inside reusable modules; declare
required_providersand let the root pass configurations (viaproviders = { aws = aws.us }when needed). - Avoid thin wrappers around a single resource; they add indirection without value.
Typical repo layout: modules/ with reusable modules, and one root directory per environment or component (envs/prod/network, envs/prod/app) that calls them with environment values.
Run terraform init after adding a module; terraform get refreshes them.
Example
# modules/bucket/main.tf
variable "name" { type = string }
variable "versioning" {
type = bool
default = true
}
resource "aws_s3_bucket" "this" { bucket = var.name }
resource "aws_s3_bucket_versioning" "this" {
bucket = aws_s3_bucket.this.id
versioning_configuration { status = var.versioning ? "Enabled" : "Suspended" }
}
output "arn" { value = aws_s3_bucket.this.arn }
# envs/prod/main.tf
module "logs" {
source = "../../modules/bucket"
name = "acme-logs-prod"
}
output "logs_arn" { value = module.logs.arn }
# address in state: module.logs.aws_s3_bucket.thisRemember
- A module = a directory; inputs are variables, results are outputs
- Callers use module.<name>.<output>
- Don't put provider configuration in reusable modules
Watch out for
- Giant do-everything modules with dozens of boolean flags.