Skip to content
Ravin Vasudev
Back to all articles

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.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

AspectDetails
Core ConceptReusable, composable Terraform code packages
Primary BenefitDRY infrastructure, consistency, maintainability, scalability
Best ForAny infrastructure at scale, teams, multi-environment deployments
Module Structuremain.tf (resources), variables.tf (inputs), outputs.tf (exports), README
Design PrinciplesSingle responsibility, configurable, sensible defaults, clear outputs, minimal dependencies
CompositionModules can contain other modules for complex infrastructure
RegistryPublic (Terraform Registry) and private (company-specific)
VersioningSemantic versioning, pin versions in consuming code
Testingterraform validate, plan, Terratest integration tests, policy testing
MaintenanceOngoing updates, bug fixes, feature additions, security patches