Skip to content
Ravin Vasudev
Back to all articles

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

  1. Terraform & OpenTofu ✓ (infrastructure definition language)
  2. Landing Zones ✓ (account structure and guardrails)
  3. 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?