The graveyard of every infrastructure codebase is full of modules that someone wrote once, used once, and never touched again. The code that actually gets reused shares a few patterns worth naming.
Make the interface obvious
Variables are your module's API. If someone has to read your main.tf to understand what the module does, the interface has failed. Every variable should have a description. Required variables should be few.
variable "environment" {
description = "Deployment environment: dev, staging, or prod"
type = string
validation {
condition = contains(["dev","staging","prod"], var.environment)
error_message = "Must be dev, staging, or prod."
}
}
Outputs as contracts
Every module should output everything a caller might need. If you're building a VPC module, output the VPC ID, all subnet IDs, the default security group, and the NAT gateway IPs.
Version your modules
Pin module versions in callers. Use Git tags. This is the single change that prevented the most incidents on my team.
module "vpc" {
source = "git::https://github.com/sujanmagar/tf-modules//vpc"
ref = "v1.4.0" # pinned — never use main/master
}