terraformiacpatterns

The shape of a Terraform module that survives the second engineer

Five small habits that keep your modules usable after the original author moves on.

Most Terraform modules don’t die from bad code. They die from being unloved — inherited by someone who doesn’t know the assumptions, can’t tell the required inputs from the decorative ones, and finds the README two commits out of date. Here are five small habits that, in my experience, keep that from happening.

1. Inputs go in two buckets

Split variables.tf into required and optional sections, with a comment header. The required section should be small.

# -----------------------------------------------------------------------------
# REQUIRED
# -----------------------------------------------------------------------------
variable "name" {
  description = "Name prefix used for all resources in this module."
  type        = string
}

variable "vpc_id" {
  description = "VPC the module attaches into."
  type        = string
}

# -----------------------------------------------------------------------------
# OPTIONAL — sensible defaults; override only when you have a reason.
# -----------------------------------------------------------------------------
variable "tags" {
  description = "Extra tags merged into the module's defaults."
  type        = map(string)
  default     = {}
}

Anyone reading the file in 6 months can see the contract in 10 seconds.

2. Outputs describe intent, not internals

Bad:

output "aws_lb_arn" {
  value = aws_lb.this.arn
}

Better:

output "load_balancer_arn" {
  description = "ARN of the load balancer fronting the service. Use this when wiring DNS."
  value       = aws_lb.this.arn
}

The point of an output is to be a handle for another module. Name it like one.

3. One module = one thing

If your module README starts with “this module creates a VPC, an EKS cluster, an ALB, and the IAM roles for…”, you’ve made four modules wearing one hat.

A useful smell test: can a new engineer guess what the module produces from its folder name alone? If not, it’s doing too much.

4. Pin everything that can move

terraform {
  required_version = "~> 1.7"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.40"
    }
  }
}

Floating versions feel ergonomic until the day a terraform init on CI pulls a new provider major and breaks every PR. Pin tightly; bump deliberately.

5. The README starts with a working example

Not a sentence. Not a feature list. A copy-paste-runnable block:

## Usage

```hcl
module "blog" {
  source = "git::https://github.com/your-org/tf-modules.git//web-app?ref=v0.4.1"

  name   = "blog"
  vpc_id = data.aws_vpc.main.id
}
```

If a stranger can copy that block, paste it into a fresh repo, and get a working stack on terraform apply, the module is doing its job.


None of this is clever. It’s the boring version of “design for the maintainer, not the author.” But the gap between modules that survive and modules that get silently rewritten almost always comes down to these five things.