5 Tips Before You Create Your First Terraform Module
Planning your first Terraform module? Discover 5 quick tips on structure, variables, versioning, testing & reuse to build clean, maintainable code.
The Module Design Reality Check
Here's what nobody tells you about Terraform modules: the first version is always wrong. Not because you're bad at design, but because you can't predict how infrastructure patterns evolve. The teams that succeed embrace this reality and build accordingly.
Take Deutsche Bank's approach. According to HashiCorp case studies, they reduced deployment time from weeks to 30 minutes by creating opinionated modules that encoded their organizational standards and security policies. The key insight? They didn't try to build infinitely flexible modules.
# Don't do this: Over-engineered flexibility
module "network" {
source = "./modules/network"
enable_nat_gateway = var.environment == "prod" ? true : false
enable_vpn = var.enable_vpn != null ? var.enable_vpn : (var.environment == "prod" ? true : false)
subnet_count = var.subnet_count != null ? var.subnet_count : (var.environment == "prod" ? 6 : 2)
# 20 more conditional parameters...
}
# Do this: Clear, opinionated modules
module "production_network" {
source = "./modules/production-network"
region = var.region
cidr_block = var.cidr_block
# That's it. Production standards are baked in.
}
The most sustainable architectures follow a three-layer pattern documented in HashiCorp's module composition guidelines:
- Base modules: Individual resources (VPC, security groups)
- System modules: Logical groupings (complete network stack)
- Environment modules: Full deployments (dev, staging, prod)
This hierarchy works because it matches how teams actually think about infrastructure. And when you need to manage these modules at scale? That's where platforms like Scalr shine, providing the governance layer that makes module reuse practical across multiple teams.
Testing Strategies That Actually Work
Let's be honest: most teams don't test their Terraform modules. They run terraform plan
and hope for the best. But at scale, hope isn't a strategy.
Terratest has emerged as the standard for module testing, enabling teams to write Go-based tests that deploy real infrastructure:
// Integration test with Terratest
func TestProductionNetworkModule(t *testing.T) {
t.Parallel()
terraformOptions := &terraform.Options{
TerraformDir: "../modules/production-network",
Vars: map[string]interface{}{
"region": "us-west-2",
"cidr_block": "10.0.0.0/16",
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
// Validate the network was created correctly
vpcId := terraform.Output(t, terraformOptions, "vpc_id")
assert.NotEmpty(t, vpcId)
// Check that private subnets exist
privateSubnets := terraform.OutputList(t, terraformOptions, "private_subnet_ids")
assert.Equal(t, 3, len(privateSubnets))
}
But integration tests are expensive. Smart teams layer their testing:
- Static analysis with tools like tfsec or Trivy
- Plan testing with Terraform 1.6+'s native test framework
- Integration testing with Terratest for critical modules
- Drift detection in production
The game-changer? Platforms that integrate these tests into your workflow automatically. When every module change triggers the full test suite, quality becomes automatic rather than aspirational.
Scaling Beyond the First Team
The transition from individual Terraform usage to enterprise-scale adoption follows predictable stages:
Stage 1: Chaos - Everyone writes their own Terraform Stage 2: Sharing - Teams copy each other's code Stage 3: Modules - Shared modules emerge organically Stage 4: Platform - Central team provides governed modules
The platform team model has emerged as the dominant pattern. According to the research, successful organizations establish clear ownership: platform teams create core modules, domain teams own specific infrastructure areas, and application teams consume these modules.
# Module registry structure that scales
terraform-modules/
├── networking/
│ ├── vpc/
│ │ ├── v1.0.0/
│ │ ├── v2.0.0/
│ │ └── v2.1.0/
│ └── security-groups/
├── compute/
│ ├── ecs-cluster/
│ └── kubernetes/
└── data/
├── rds/
└── dynamodb/
Organizations implementing module registries with built-in policy enforcement see significantly faster adoption than those relying on documentation alone. The key is making module discovery and consumption self-service while maintaining governance.
Refactoring Without Breaking Production
Terraform 1.1's introduction of moved
blocks transformed module refactoring from a risky manual process to declarative configuration. Before this innovation, teams relied on error-prone terraform state mv
commands:
# Extract resources into a module safely
moved {
from = aws_instance.web_server
to = module.web_tier.aws_instance.server
}
moved {
from = aws_security_group.web_sg
to = module.web_tier.aws_security_group.main
}
moved {
from = aws_lb.web_lb
to = module.web_tier.aws_lb.main
}
module "web_tier" {
source = "./modules/web-tier"
instance_type = "t3.large"
min_instances = 3
max_instances = 10
}
Real refactoring success requires:
- Discovery - Find all module consumers
- Communication - Notify teams of changes
- Migration path - Provide clear upgrade instructions
- Rollback plan - Because things go wrong
When you can see every module consumer and their versions in one place, refactoring becomes manageable instead of terrifying.
Common Pitfalls and How to Avoid Them
Based on documented failures and anti-patterns, here are the mistakes to avoid:
The God Module
# Don't create modules that do everything
module "everything" {
source = "./modules/kitchen-sink"
create_vpc = true
create_rds = true
create_ecs = true
create_lambda = true
# 50 more flags...
}
Hardcoded Values
# This will haunt you
resource "aws_s3_bucket" "data" {
bucket = "my-company-prod-data-bucket" # Never hardcode environment-specific values
region = "us-east-1" # Never hardcode regions
}
Missing Path Context
# Breaks when used as a module
data "template_file" "config" {
template = file("templates/config.json") # Wrong
}
# Works everywhere
data "template_file" "config" {
template = file("${path.module}/templates/config.json") # Right
}
Circular Dependencies According to GitHub issues and community discussions, circular dependencies remain one of Terraform's most challenging limitations. The solution is to design modules with clear boundaries where dependencies flow in one direction.
Building Your Module Strategy
Start where you are. If you're just beginning with Terraform, build local modules for patterns you repeat more than twice. Structure your repository to support growth:
infrastructure/
├── modules/
│ ├── networking/
│ ├── compute/
│ └── data/
├── environments/
│ ├── dev/
│ ├── staging/
│ └── prod/
└── policies/
├── security.rego
└── cost.rego
For teams already using Terraform, audit your existing code. Look for:
- Repeated patterns across environments
- Copy-pasted configurations with minor changes
- Resources that always get created together
These are your module candidates. Extract them incrementally using moved blocks. Version them properly from day one using semantic versioning principles.
For enterprise organizations, the challenge isn't technical—it's organizational. Based on experiences from companies like GitHub and Deutsche Bank, you need:
- Clear ownership model for modules
- Automated testing and publishing pipelines
- Policy-as-code enforcement
- Self-service discovery and consumption
This is where purpose-built platforms make the difference. While you can build this infrastructure yourself, platforms like Scalr provide these capabilities out of the box, letting you focus on your actual infrastructure rather than the infrastructure for your infrastructure.
Summary: Key Decisions at Scale
Here's what matters when building Terraform modules at scale:
Decision Point | Small Teams | Growing Organizations | Enterprise Scale |
---|---|---|---|
Module Design | Simple, focused modules | Layered architecture (base/system/env) | Domain-driven modules with clear contracts |
Testing Strategy | Manual plan review | Automated plan tests + Terratest | Full pyramid: static analysis → integration → drift detection |
Versioning | Git tags | Semantic versioning | Automated compatibility testing + deprecation policies |
Registry | Git repositories | Private module registry | Registry with dependency tracking and policy enforcement |
Governance | Code reviews | CODEOWNERS + PR templates | Policy-as-code with automated enforcement |
Refactoring | terraform state mv | Moved blocks + documentation | Automated migration tools with rollback capabilities |
Team Model | Shared ownership | Platform team + consumers | Federated model with domain expertise |
The organizations that succeed with Terraform modules share three characteristics:
- They invest in modules early, before they "need" them
- They choose boring technology—standard patterns over clever abstractions
- They use platforms that handle the undifferentiated heavy lifting
Your infrastructure is too important to leave to chance. Whether you build your module platform yourself or adopt an existing solution, make the investment. Your future self (and your ops team at 3 AM) will thank you.
Want to see how leading organizations manage Terraform modules at scale? Platforms like Scalr provide enterprise-grade module registries with built-in governance, making it possible to achieve rapid deployment speeds without building the platform yourself.
