Skip to main content
Practical patterns for Terraform modules at scale: versioning, composition, testing, and avoiding the monolith trap.

Terraform Modules Done Right: Lessons from Managing 50+ Services

KU
Kiril Urbonas
6 months ago • 3 min read•23 views

Practical patterns for Terraform modules at scale: versioning, composition, testing, and avoiding the monolith trap.

Key takeaways

Practical patterns for Terraform modules at scale: versioning, composition, testing, and avoiding the monolith trap.

Terraform Modules Done Right: Lessons from Managing 50+ Services#

After managing infrastructure for 50+ microservices with Terraform, we've learned which module patterns scale and which become nightmares. Here's what works.

The Monolith Trap#

Our first approach was one massive Terraform repo with everything in it. Plan took 12 minutes. A typo in a dev variable once triggered a production change. We split it up.

Pattern 1: Layered Modules#

We organize modules in three layers:

code
modules/
  base/          # VPC, subnets, DNS zones
  platform/      # EKS cluster, RDS, ElastiCache
  service/       # Per-service: ALB, task def, IAM role

Each layer depends only on the layer below via remote state data sources:

hcl.hcl
data "terraform_remote_state" "platform" {
  backend = "s3"
  config = {
    bucket = "terraform-state-prod"
    key    = "platform/terraform.tfstate"
    region = "us-east-1"
  }
}

resource "aws_lb_target_group" "service" {
  vpc_id = data.terraform_remote_state.platform.outputs.vpc_id
  # ...
}

Pattern 2: Versioned Module Registry#

We publish reusable modules to a private registry with semantic versioning:

hcl.hcl
module "service" {
  source  = "app.terraform.io/ourorg/service/aws"
  version = "~> 2.0"

  name        = "payment-api"
  environment = "production"
  cpu         = 512
  memory      = 1024
}

Rules we follow:

  • Breaking changes = major version bump
  • New optional variables = minor version bump
  • Bug fixes = patch version bump
  • Teams pin to major version (~> 2.0), not exact

Pattern 3: Composition Over Configuration#

Instead of one module with 40 variables and 15 conditional blocks, we compose small modules:

hcl.hcl
module "alb" {
  source = "./modules/alb"
  # ...
}

module "ecs_service" {
  source          = "./modules/ecs-service"
  target_group_arn = module.alb.target_group_arn
  # ...
}

module "monitoring" {
  source       = "./modules/cloudwatch-alarms"
  service_name = module.ecs_service.name
  # ...
}

Each module does one thing. Connecting them is explicit, not hidden behind flags.

Pattern 4: Automated Testing#

We test modules with terraform validate, tflint, and integration tests:

bash.bash
# In CI pipeline
cd modules/service
terraform init -backend=false
terraform validate
tflint --init
tflint

# Integration test (creates real resources, then destroys)
cd tests/
go test -v -timeout 30m ./...

Best Practices Summary#

  1. Keep blast radius small: one service per state file
  2. Version your modules: semantic versioning with a changelog
  3. Compose, don't configure: small modules > one mega-module
  4. Test in CI: validate + lint + integration tests
  5. Use remote state carefully: read-only data sources, not cross-stack references
  6. Document inputs/outputs: a module without docs is a module no one trusts

Terraform at scale is a software engineering problem, not just an infrastructure problem. Treat your modules like libraries.

React

Get the DevOps Troubleshooting Cheat Sheet

Subscribe and get our free one-page reference for the errors that eat an afternoon — CrashLoopBackOff, OOMKilled, Terraform state locks, and more — plus new guides as we publish them.

Share this post
KU

About Kiril Urbonas

DevOps Engineer

550 articles
View all articles by Kiril Urbonas

You might have missed

Evergreen posts worth revisiting.