August 1, 2026 : 9 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.xThis 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 validateterraform plan: Dry-run to see what would be created.
terraform plan -var-file=test.tfvarsIntegration 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-chartOr 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 |
Related articles
July 25, 2026 : 11 min read
Architecture Stack: Terraform & OpenTofu Fundamentals
August 29, 2026 : 11 min read
Architecture Stack: Risk Controls
August 23, 2026 : 9 min read
Architecture Stack: Policy as Code
August 8, 2026 : 8 min read
Architecture Stack: Cloud Center of Excellence