January 15, 2024 · 3 min read
Terraform Module Design: Outputs, Inputs, and Versioning
Learn how to build reusable, maintainable Terraform modules with proper input validation, output design, and semantic versioning strategies.
January 15, 2024 · 3 min read
Learn how to build reusable, maintainable Terraform modules with proper input validation, output design, and semantic versioning strategies.
Building Terraform modules that your team actually wants to use requires more than just wrapping resources. It requires thoughtful API design, proper validation, and a versioning strategy that doesn't break production deployments.
Most internal Terraform modules fail for the same reasons: inconsistent interfaces, missing validation, unclear outputs, and no versioning discipline. Teams end up copy-pasting instead of reusing.
Let's fix that.
# Bad: What does 'size' mean?
variable "size" {
type = string
}
# Good: Clear and specific
variable "instance_type" {
type = string
description = "EC2 instance type (e.g., t3.medium, m5.large)"
}
variable "environment" {
type = string
description = "Deployment environment"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "instance_type" {
type = string
description = "EC2 instance type"
validation {
condition = can(regex("^[a-z][0-9][.][a-z]+$", var.instance_type))
error_message = "Instance type must be a valid AWS format (e.g., t3.medium)."
}
}
# Instead of multiple separate variables
variable "vpc_config" {
type = object({
vpc_id = string
subnet_ids = list(string)
security_group_ids = list(string)
})
description = "VPC configuration for the deployment"
}
Outputs are your module's public API. Think carefully about what consumers need.
output "instance_id" {
description = "The ID of the EC2 instance"
value = aws_instance.main.id
}
output "instance_arn" {
description = "The ARN of the EC2 instance"
value = aws_instance.main.arn
}
output "private_ip" {
description = "Private IP address of the instance"
value = aws_instance.main.private_ip
}
output "security_group_id" {
description = "ID of the security group created for this instance"
value = aws_security_group.main.id
}
output "database" {
description = "Database connection details"
value = {
endpoint = aws_db_instance.main.endpoint
port = aws_db_instance.main.port
database = aws_db_instance.main.db_name
username = aws_db_instance.main.username
}
sensitive = true
}
git tag -a v1.2.0 -m "Add support for custom IAM policies"
git push origin v1.2.0
module "vpc" {
source = "git::https://github.com/org/terraform-aws-vpc.git?ref=v2.1.0"
}
Use Terratest or native Terraform testing:
# tests/main.tftest.hcl
run "verify_instance_type" {
command = plan
variables {
instance_type = "t3.medium"
environment = "dev"
}
assert {
condition = aws_instance.main.instance_type == "t3.medium"
error_message = "Instance type mismatch"
}
}
terraform-aws-app/
├── main.tf # Primary resources
├── variables.tf # Input variables
├── outputs.tf # Output values
├── versions.tf # Provider requirements
├── README.md # Documentation
├── examples/
│ ├── basic/
│ └── complete/
└── tests/
└── main.tftest.hcl
Good modules reduce cognitive load. Your team should be able to use them without reading the source code.