Terraform 32: 5: Keeping environments DRY with shared modules
easy⏱ 5 mincourseterraform
Resources live in a shared module
Move the resources into modules/service/ once. Each environment directory becomes a thin caller that passes environment, location, sizing, etc. Fix a bug in the module and every environment inherits the fix on its next apply — no copy-paste.
# modules/service/main.tf — the reusable definition
variable "environment" { type = string }
variable "service" { type = string }
variable "vm_count" { type = number }
resource "azurerm_resource_group" "this" {
name = "${var.environment}-${var.service}-rg"
location = var.location
tags = { environment = var.environment, managed_by = "terraform" }
}
Environments just call the module
Each environment's main.tf is a module block with environment-specific inputs. The only things that differ between envs/dev and envs/prod are the tfvars values and the backend key — the resource logic is shared.
# envs/prod/main.tf — a thin caller
module "orders" {
source = "../../modules/service"
environment = var.environment # "prod"
service = "orders"
location = var.location
vm_count = var.vm_count # 3 in prod, 1 in dev
}
for_each a map to fan out many copies
Inside the module, drive repeated resources from a map with for_each. One map entry per instance; each.key names it and each.value configures it. Adding a service is a one-line map edit, not a copy-pasted resource block.
variable "buckets" {
type = map(object({ tier = string }))
# { logs = { tier = "Cool" }, uploads = { tier = "Hot" } }
}
resource "azurerm_storage_container" "this" {
for_each = var.buckets
name = "${var.environment}-${each.key}" # dev-logs, dev-uploads
storage_account_name = azurerm_storage_account.this.name
container_access_type = "private"
}
Compute the for_each names
Given env and a buckets map, write containerNames(env, buckets) returning the sorted array of "{env}-{key}" names Terraform would create. Log containerNames("dev", { logs: {}, uploads: {} }) — expect ["dev-logs", "dev-uploads"].