Writing and using modules

Terraform · Practical · 12 min

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_providers and let the root pass configurations (via providers = { 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.this

Remember

  • 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.

Check yourself

  1. How does a root module read a value created inside a child module?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.hashicorp.com/terraform/language/modules