Terraform Modules: Best Practices for Production
Structure reusable Terraform modules with proper state management, versioning, and CI/CD integration.
As your cloud footprint grows, writing plain, copy-pasted Terraform files quickly becomes a maintenance nightmare. Terraform modules are the key to building dry (Don't Repeat Yourself), scalable, and reusable infrastructure configurations.
In this guide, we'll explore production best practices for structuring, versioning, and consuming Terraform modules.
Recommended Module Directory Layout
A standard, reusable module should have its own git repository and follow this structure:
terraform-aws-vpc/
├── LICENSE
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
├── examples/
│ └── simple-vpc/
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
└── test/
└── vpc_test.gomain.tf: Contains the actual resource declarations (e.g.,aws_vpc,aws_subnet).variables.tf: Input variables with descriptive descriptions and validation rules.outputs.tf: Key outputs needed by parent modules (e.g., VPC ID, subnet IDs).examples/: Working examples showing users how to call the module.
Writing a Clean Module Interface
Always validate your input variables using custom validations. This guarantees that misconfigurations are caught locally during terraform plan instead of failing mid-apply.
Here is an example in HCL for a subnet configuration block with strict tag validation:
# variables.tf
variable "vpc_cidr" {
type = string
description = "The CIDR block for the VPC"
default = "10.0.0.0/16"
validation {
condition = can(regex("^10\\.\\d+\\.\\d+\\.\\d+/\\d+$", var.vpc_cidr))
error_message = "The VPC CIDR block must be a valid 10.x.x.x block."
}
}
variable "environment" {
type = string
description = "Application deployment environment"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "The environment variable must be either dev, staging, or prod."
}
}Module Versioning Best Practices
Never reference a module using a local directory relative path (source = "../modules/vpc") for production deployments. Instead, use a versioned git source reference:
# main.tf in target environment
module "production_vpc" {
source = "github.com/awslearn/terraform-aws-vpc?ref=v1.4.2"
vpc_cidr = "10.100.0.0/16"
environment = "prod"
}By lock-stepping module versions with git tags, you can make updates to modules in isolated branches without breaking production environments.