Skip to main content
  1. Posts/

Terraform Modules That Scale

·2 mins· loading · loading · ·
Alhassan Ibrahim
Author
Alhassan Ibrahim
Passionate about software reliability, availability, resilience, security, and automation.
Table of Contents

Terraform is easy to get started with and easy to make unmaintainable. The difference between a codebase that scales to a multi-person infra team and one that turns into a plan-and-pray minefield usually comes down to three habits.

Keep root modules thin
#

A root module should mostly be a list of module calls and variable wiring, not raw resource blocks. Logic belongs in reusable modules; the root module is configuration, not implementation.

module "payments_vpc" {
  source          = "../../modules/vpc"
  name            = "payments-prod"
  cidr_block      = "10.20.0.0/16"
  azs             = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
  enable_nat_gateway = true
}

module "payments_eks" {
  source       = "../../modules/eks"
  cluster_name = "payments-prod"
  vpc_id       = module.payments_vpc.vpc_id
  subnet_ids   = module.payments_vpc.private_subnet_ids
}

Give every module an explicit, minimal interface
#

A module’s variables.tf and outputs.tf are its contract. Resist the temptation to pass through every possible AWS provider argument “just in case” — every extra variable is a thing every caller now has to understand, and a thing you can’t change without a breaking-change conversation.

variable "name" {
  type        = string
  description = "Name prefix applied to all resources created by this module."
}

variable "cidr_block" {
  type        = string
  description = "CIDR block for the VPC."
}

output "vpc_id" {
  value       = aws_vpc.this.id
  description = "ID of the created VPC, for wiring into downstream modules."
}

Isolate state per environment, not per team
#

One state file per environment (dev/staging/prod), not one giant state file for the whole organisation and not one state file per engineer. Remote state with locking (S3 + DynamoDB, or Terraform Cloud) is non-negotiable once more than one person applies changes.

terraform {
  backend "s3" {
    bucket         = "acme-terraform-state"
    key            = "payments/prod/terraform.tfstate"
    region         = "eu-west-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

The common thread across all three habits is the same one that shows up in good software architecture generally: make interfaces explicit, keep blast radius small, and don’t let convenience today become someone else’s incident next quarter.

Related

Designing Resilient Kubernetes Deployments

·2 mins· loading · loading
Most Kubernetes outages I’ve debugged in production weren’t caused by Kubernetes itself — they were caused by a deployment manifest that never told Kubernetes what “healthy” and “safe to disrupt” actually meant. A handful of fields close most of that gap.