August 1, 2026 : 10 min read
Architecture Stack: Terraform Modules & Reusable Patterns
Understanding Terraform modules: composable, reusable infrastructure building blocks that enable DRY infrastructure at scale.
- Architecture
- Terraform
This is a conversation between Alex (Engineering Manager) and Jordan (Senior Architect) exploring Terraform modules and reusable patterns.
Part 1: The Code Duplication Problem
Alex: "Jordan, we have Terraform managing infrastructure across Landing Zones. But I see a lot of repeated code. We define a VPC in prod, then again in staging, again in dev. That's wasteful."
Jordan: "Great observation. That's where modules come in. Terraform modules are reusable infrastructure patterns."
Alex: "How do they work?"
Jordan: "Similar to Helm for applications. You define a pattern once (e.g., 'create a VPC with subnets and security groups'), then use it multiple times with different variables. Prod and dev use the same VPC module, but with different variable values."
Alex: "So it's DRY (Don't Repeat Yourself) for infrastructure?"
Jordan: "Exactly. Write once, use everywhere."
Part 2: What Is a Terraform Module?
Alex: "Define it."
Jordan: "A Terraform module is a reusable package of Terraform code. Contains resources, variables, and outputs. Can be used from other Terraform configurations.
Structure:
vpc-module/
├── main.tf # Resource definitions
├── variables.tf # Input variables
├── outputs.tf # Output values
└── README.md # Documentation
Usage:
module \"vpc\" {
source = \"./modules/vpc\"
cidr_block = \"10.0.0.0/16\"
region = \"us-east-1\"
availability_zones = [\"us-east-1a\", \"us-east-1b\"]
}
That's it. One block uses the entire VPC module."
Alex: "How is that different from writing resources directly?"
Jordan: "Encapsulation. Module hides complexity. Consumer just provides inputs, gets outputs. Doesn't need to know how VPC is built."
Part 3: Problems Modules Solve
Alex: "What real problems?"
Jordan: "Several:
Code Duplication: Write VPC once, use in multiple accounts. Save 80% of code.
Consistency: All prod VPCs are identical (same module). No accidental inconsistencies.
Maintainability: Bug in VPC creation? Fix module once. Automatically applies to all VPCs created from it.
Knowledge Sharing: Senior engineers write modules, junior engineers use them. Capture best practices in code.
Onboarding: New teams use existing modules. Don't need to learn infrastructure details.
Scalability: Create 100 VPCs by instantiating module 100 times. Much cleaner than copying code 100 times.
Versioning: Module version 1.0 works one way. Version 2.0 improves it. Existing deployments use v1.0, new ones use v2.0."
Alex: "This is all about efficiency and knowledge capture?"
Jordan: "Exactly."
Part 4: Module Design Principles
Alex: "How do you design a good module?"
Jordan: "Several principles:
Single Responsibility: One module does one thing. VPC module creates VPCs. RDS module creates databases. Not one module for everything.
Configurable: Provide variables for variations. Hard-coding is bad. Variables let users customize.
Sensible Defaults: Variables should have reasonable defaults. Novice users can use defaults. Experts can override.
Clear Outputs: Export values users might need. VPC module exports VPC ID, subnet IDs. Downstream modules use these.
Minimal Dependencies: Each module should be independent. VPC module doesn't depend on RDS module. Can use together but independently viable.
Well-Documented: README explains what module does, what variables, what outputs. Examples of usage."
Alex: "That's a lot of design thinking?"
Jordan: "Yes. Good modules require architecture. Bad modules become technical debt."
Part 5: Module Variables
Alex: "What kind of variables do modules have?"
Jordan: "Input variables allow customization. Example:
variable \"environment\" {
type = string
description = \"Environment name (dev, staging, prod)\"
validation {
condition = contains([\"dev\", \"staging\", \"prod\"], var.environment)
error_message = \"Environment must be dev, staging, or prod\"
}
}
variable \"instance_count\" {
type = number
default = 1
description = \"Number of instances\"
}
variable \"tags\" {
type = map(string)
default = {}
description = \"Tags to apply to all resources\"
}
Variables have types, descriptions, defaults, validation rules. Makes modules self-documenting."
Alex: "Validation is useful?"
Jordan: "Very. Catches misuse early. If someone passes 'prod-staging' (typo), validation fails with helpful message."
Part 6: Module Outputs
Alex: "What do modules export?"
Jordan: "Outputs. Values other modules need. Example:
output \"vpc_id\" {
value = aws_vpc.main.id
description = \"ID of the VPC\"
}
output \"private_subnet_ids\" {
value = aws_subnet.private[*].id
description = \"IDs of private subnets\"
}
output \"security_group_id\" {
value = aws_security_group.app.id
description = \"ID of security group\"
}
Other modules use these outputs. Application module needs security group ID? Get it from VPC module output."
Alex: "How?"
Jordan: "Reference module output:
module \"vpc\" {
source = \"./modules/vpc\"
environment = \"prod\"
}
module \"rds\" {
source = \"./modules/rds\"
vpc_id = module.vpc.vpc_id # Use VPC module output
security_group_id = module.vpc.security_group_id
}
Modules compose together via outputs."
Part 7: Module Composition
Alex: "Can modules call other modules?"
Jordan: "Yes. Modules can contain other modules. Example: Kubernetes cluster module might internally use VPC module and security group module.
# In eks-module/main.tf
module \"vpc\" {
source = \"../vpc\"
cidr_block = var.cidr_block
}
module \"security_groups\" {
source = \"../security-groups\"
vpc_id = module.vpc.vpc_id
}
resource \"aws_eks_cluster\" \"main\" {
vpc_id = module.vpc.vpc_id
# ...
}
Composing smaller modules into larger ones. Hierarchical architecture."
Alex: "That's powerful?"
Jordan: "Yes. You can build complex infrastructure from simple, composable pieces."
Part 8: Shared Module Registry
Alex: "Can you share modules across teams?"
Jordan: "Yes. Create central Terraform Module Registry. Internal registry (like Docker registry but for Terraform).
Structure:
terraform-registry/
├── vpc-module/
│ ├── v1.0.0/
│ └── v2.0.0/
├── rds-module/
│ └── v1.0.0/
├── eks-module/
│ └── v1.0.0/
└── alb-module/
└── v1.0.0/
Teams use like:
module \"vpc\" {
source = \"git::https://github.com/mycompany/terraform-registry//vpc-module?ref=v2.0.0\"
# ...
}
Platform team maintains registry. Application teams use modules. Standardization and knowledge share."
Alex: "Who maintains modules?"
Jordan: "Platform team (or infrastructure team). They own the registry."
Part 9: Public Module Repositories
Alex: "Are there public modules?"
Jordan: "Yes. Terraform Registry (registry.terraform.io) hosts public modules. AWS modules, GCP modules, Kubernetes modules, etc.
Example:
module \"vpc\" {
source = \"terraform-aws-modules/vpc/aws\"
version = \"3.0.0\"
name = \"main-vpc\"
cidr = \"10.0.0.0/16\"
}
Use public module, customize with variables. Most common infrastructure patterns are already implemented."
Alex: "Custom modules for proprietary patterns?"
Jordan: "Yes. Use public modules for generic (VPC, RDS). Custom modules for company-specific (security baseline, compliance policies)."
Part 10: Module Versioning
Alex: "How do you version modules?"
Jordan: "Semantic versioning. Major.Minor.Patch (e.g., 1.2.3).
1.0.0: Initial release 1.1.0: New feature (backward compatible) 1.2.0: Another feature (backward compatible) 2.0.0: Breaking change (requires update to consuming code)
When using modules, pin version:
source = \"terraform-aws-modules/vpc/aws\"
version = \"~> 3.0\" # Use 3.x, not 4.x
This prevents unexpected breaking changes."
Alex: "Upgrade strategy?"
Jordan: "Test new versions in dev/staging. When confident, update version constraint in prod. Terraform plan shows changes. Review before applying."
Part 11: Testing Modules
Alex: "How do you test modules?"
Jordan: "Several approaches:
Terraform Validate: Check syntax and consistency.
terraform validate
terraform plan: Dry-run to see what would be created.
terraform plan -var-file=test.tfvars
Integration Tests: Terratest (Go testing framework).
// Test creates resources, validates outputs
func TestVPCModule(t *testing.T) {
opts := &terraform.Options{
TerraformDir: \"../vpc-module\",
Vars: map[string]interface{}{
\"cidr_block\": \"10.0.0.0/16\",
},
}
defer terraform.Destroy(t, opts)
terraform.InitAndApply(t, opts)
vpcId := terraform.Output(t, opts, \"vpc_id\")
assert.NotEmpty(t, vpcId)
}
Policy Testing: Sentinel policies ensure compliance.
Documentation: Document expected inputs, outputs, and examples.**"
Alex: "This is comprehensive?"
Jordan: "Yes. Module testing ensures quality. Bad module affects many consumers."
Part 12: Module Maintenance & Updates
Alex: "How do you maintain modules?"
Jordan: "Module maintenance is ongoing:
Bug Fixes: Someone finds issue. Fix in module. Version bump (patch). Consumers update.
Feature Additions: New requirement (e.g., add encryption by default). Add variable. Version bump (minor). Backward compatible.
Breaking Changes: Restructure module. Version bump (major). Consumers must update code.
Security Patches: CVE in base AMI or package. Update module. All consumers get fix on next apply.
Documentation: Keep README updated as module changes."
Alex: "Who decides changes?"
Jordan: "Module owner (usually platform team). Reviews pull requests, maintains backward compatibility, releases new versions."
Part 13: Anti-Patterns to Avoid
Alex: "What mistakes happen?"
Jordan: "Common anti-patterns:
Monolithic Modules: One module does everything (VPC, RDS, EKS, monitoring). Too complex, hard to reuse.
Hard-Coded Values: Prod values in module. Not flexible. Solution: always use variables.
No Defaults: Every variable required. Painful to use. Solution: sensible defaults.
Poor Naming: Variables named 'x', 'val1', 'config'. Confusing. Solution: descriptive names.
Tight Coupling: Module A requires Module B. Can't use independently. Solution: minimal dependencies.
No Versioning: Using latest module master branch. Breaks on updates. Solution: always pin versions.
Over-Parameterization: Too many variables, hard to understand. Solution: balance flexibility with simplicity.
No Documentation: Module code is cryptic. Solution: good README and inline comments."
Alex: "Design discipline is required?"
Jordan: "Absolutely. Modules are shared infrastructure. Poor design hurts many teams."
Part 14: Integrating with Helm
Alex: "We use both Terraform and Helm. Do they interact?"
Jordan: "Yes. Terraform creates infrastructure (VPC, EKS cluster, RDS database). Helm deploys applications into that infrastructure.
Flow:
# Terraform creates EKS cluster
module \"eks\" {
source = \"terraform-aws-modules/eks/aws\"
cluster_name = \"production\"
}
# Output cluster info
output \"kubeconfig\" {
value = module.eks.kubeconfig
}
Then Helm uses that cluster:
# Get kubeconfig from Terraform outputs
export KUBECONFIG=$(terraform output kubeconfig)
# Deploy with Helm
helm install my-app ./my-app-chart
Or use Terraform Helm provider (deploy Helm from Terraform)."
Alex: "So they're separate tools but can integrate?"
Jordan: "Yes. Terraform for infrastructure. Helm for applications. But can orchestrate both from Terraform if needed."
Part 15: Terraform Modules & ArgoCD
Alex: "How do modules work with ArgoCD?"
Jordan: "ArgoCD deploys applications (via Helm). Terraform provisions infrastructure.
Separation:
Terraform (Infrastructure)
├── VPC
├── EKS Cluster
├── RDS Database
└── Load Balancer
ArgoCD (Applications)
├── Microservices (via Helm)
├── Databases (Kubernetes pods or RDS)
└── Networking
Terraform creates the foundation. ArgoCD deploys on top. Clean separation."
Alex: "Terraform runs first?"
Jordan: "Yes. Terraform creates EKS cluster. ArgoCD then deploys into that cluster."
Part 16: Community Modules
Alex: "What modules are available?"
Jordan: "Hundreds on Terraform Registry:
AWS: terraform-aws-modules/vpc, terraform-aws-modules/rds, terraform-aws-modules/eks, etc.
Kubernetes: terraform-helm (deploy Helm charts), kubernetes (manage Kubernetes resources directly).
Community: Gruntwork, Cloudposse, HashiCorp provide pre-built modules.
**Most common patterns are already implemented. Rarely need to write from scratch."
Alex: "Reduces development time?"
Jordan: "Massively. You're standing on shoulders of thousands of engineers."
Part 17: When to Write Custom Modules
Alex: "When do you need custom modules?"
Jordan: "Write custom when:
- Company-specific patterns (internal standards)
- Compliance requirements (encryption, logging, tagging)
- Complex composition (multiple AWS services with specific logic)
- Business logic (e.g., 'create a standard web service stack with all our policies')
Don't write custom when:
- Generic infrastructure (VPC, RDS) - use public modules
- Simple use (one-off project) - direct resources
- Rapidly changing (still experimenting) - lock after stable"
Alex: "Balance is key?"
Jordan: "Yes. Leverage existing. Create custom only when needed."
Part 18: Bridging to L4: Governance
Alex: "We've covered L3 (Infrastructure as Code). All three layers: Terraform, Landing Zones, Modules."
Jordan: "Right. L3 is how you define infrastructure at scale. L4 is governance: how you enforce policies, control costs, manage risk."
Alex: "So L3 is the tool. L4 is policy?"
Jordan: "Exactly. Terraform is the mechanism. Policy and governance ensure it's used correctly."
Part 19: The Bottom Line
Alex: "If I pitch this?"
Jordan: "Terraform modules are reusable infrastructure building blocks. Write once, use everywhere. Infrastructure becomes composable. Each module is independently tested, versioned, and maintained. Teams use pre-built modules instead of reinventing the wheel. Company-specific patterns are standardized in modules. Scaling to hundreds of accounts or infrastructure instances is clean. Knowledge is captured in code, not in people's heads."
Alex: "And the commitment?"
Jordan: "We're saying: no more one-off Terraform scripts. Everything goes into modules. Modules are reviewed, tested, versioned, and shared. Infrastructure quality and consistency improve dramatically."
Key Takeaways
| Aspect | Details | | --------------------- | ------------------------------------------------------------------------------------------- | | Core Concept | Reusable, composable Terraform code packages | | Primary Benefit | DRY infrastructure, consistency, maintainability, scalability | | Best For | Any infrastructure at scale, teams, multi-environment deployments | | Module Structure | main.tf (resources), variables.tf (inputs), outputs.tf (exports), README | | Design Principles | Single responsibility, configurable, sensible defaults, clear outputs, minimal dependencies | | Composition | Modules can contain other modules for complex infrastructure | | Registry | Public (Terraform Registry) and private (company-specific) | | Versioning | Semantic versioning, pin versions in consuming code | | Testing | terraform validate, plan, Terratest integration tests, policy testing | | Maintenance | Ongoing updates, bug fixes, feature additions, security patches |
L3 Series Complete
- Terraform & OpenTofu ✓ (infrastructure definition language)
- Landing Zones ✓ (account structure and guardrails)
- Modules & Reusable Patterns ✓ (DRY infrastructure)
Next up: L4 – Governance & Control
- Cloud Center of Excellence (CCoE) organization
- Policy as Code (enforcement)
- FinOps (cost optimization)
- Risk & Compliance Controls
You've now completed L3: Infrastructure as Code Foundation. Ready to explore L4 (Governance) or take a breath?