Chapter 9: Organizing Larger Codebases
Topic 55

DRY Configuration Patterns

PatternsDRY

Don't-Repeat-Yourself is a goal in infrastructure code too, but it competes with isolation and explicitness, and over-applying it produces clever abstractions that are harder to operate than the duplication they replaced. The patterns worth knowing remove genuine repetition — shared modules, common-tag locals, generated config — and the judgment worth developing is knowing when a little duplication is the right call.

DRY is a tool, not a virtue in itself. The question is never "is this repeated?" but "does removing this repetition make the code easier or harder to change safely?" — and for infrastructure, where a wrong change can take down production, the answer leans toward explicitness more often than it does in application code.

Where Repetition Comes From

Three things repeat most. Provider and backend boilerplate gets copy-pasted into every root module. Tag maps — owner, cost-center, environment — get re-typed on every resource. And near-identical environment configs get duplicated across directories. The first is structural and the hardest to remove with native tools; the second and third are exactly what locals, modules, and tfvars are for.

The Native DRY Tools

Plain Terraform already removes most real repetition. A shared module collapses a repeated resource pattern into one definition. A common_tags local defines the tag map once and merges it into every resource. for_each turns a repeated resource into one block driven by a map. And per-environment tfvars let one module serve dev and prod by varying inputs rather than copying code. Reach for these before reaching for any extra tool.

A common-tags local merged into every resource
locals {
  common_tags = {
    Environment = "prod"
    Owner       = "platform"
    ManagedBy   = "terraform"
  }
}

resource "aws_instance" "web" {
  ami           = var.ami_id
  instance_type = "t3.small"
  tags          = merge(local.common_tags, { Name = "web" })
}

The Limits of DRY

There is a point where removing duplication makes the result harder to understand. A module so deeply parameterized that every resource is configurable becomes harder to call correctly than writing the resources outright — its interface is the new complexity. Worse, eliminating duplication can destroy a boundary that was protecting you: sharing one config across prod and dev to be DRY couples their fates, so a change meant for dev rolls into prod. Some repetition is load-bearing.

Templated Config and Deliberate Duplication

The repetition the native tools cannot remove cleanly is the structural backend and provider boilerplate across many roots — and that is where teams start generating Terraform or reaching for a wrapper like Terragrunt (the next topic). That is a real signal, not a failure. But before adopting a wrapper, weigh the alternative: a little deliberate copy-paste. Isolating prod often justifies duplicating a config rather than sharing it, because the duplication is what keeps a dev mistake out of prod. Abstract on the second or third real occurrence, not the first — a thing used in exactly one place does not need an abstraction.

Common Mistakes
  • Building a deeply parameterized "one module to rule them all" whose interface is harder to use correctly than writing the resources directly.
  • Eliminating duplication that provided safe isolation — sharing one config across prod and dev to be DRY, coupling their fates.
  • Copy-pasting backend and provider boilerplate across dozens of roots instead of generating it or adopting a wrapper.
  • Treating DRY as an absolute and abstracting things used in exactly one place, adding indirection that buys nothing.
  • Abstracting on the first occurrence before you know the second one will even look the same, then bending the abstraction to fit.
Best Practices
  • Remove real repetition with shared modules, common_tags locals, and per-environment tfvars.
  • Accept small, deliberate duplication where it buys isolation, especially around production.
  • Reach for a wrapper or generation tool like Terragrunt only when backend and provider boilerplate repetition becomes the actual pain.
  • Abstract on the second or third real occurrence, not the first, so the abstraction fits real cases instead of a guess.
  • Keep module interfaces narrow — expose the inputs callers genuinely vary, not every internal attribute.
Comparable tools Terragrunt exists specifically to DRY backend and provider config Pulumi uses host-language functions and classes for DRY CloudFormation macros and nested stacks remove some template repetition

Knowledge Check

Which native Terraform tools remove genuine repetition?

  • Shared modules, common_tags locals, for_each, and per-environment tfvars
  • workspace select, taint, and force-unlock run against the existing state
  • Provisioner blocks running remote-exec and local-exec scripts on each resource
  • There are none; removing repetition always requires Terragrunt

When does DRY actively hurt an infrastructure codebase?

  • When removing duplication destroys an isolation boundary — sharing one config across prod and dev couples their fates
  • Whenever any two resources are assigned the same tag value, because Terraform rejects duplicate tags during apply
  • Only inside reusable modules, since root configurations are immune to over-abstraction
  • DRY never hurts; more abstraction is always safer for infrastructure

Why is some duplication a deliberate, correct choice?

  • Duplicating a config to isolate prod keeps a dev change from rolling into prod — the duplication is the boundary
  • Duplicated resource blocks always plan faster than the equivalent shared module call, because Terraform skips module resolution and reads the inline blocks directly from the root
  • Terraform bills a per-call fee for every module invocation, so inlining the duplicated code into the root keeps the monthly charge lower
  • Duplication is never deliberate; it is always accidental technical debt that should be factored out the moment it appears

What is the signal that you need a wrapper or generation tool?

  • Structural backend and provider boilerplate repeated across many roots that native tools cannot cleanly remove
  • Any time the same tag map appears across more than one resource in the configuration, since a repeated map is the clearest sign that a generation layer has become necessary
  • When a single shared module gets called more than once inside the same root, which is the point at which native composition can no longer keep the configuration tidy
  • When a plan takes longer than ten seconds to finish, since that delay is the threshold at which a wrapper tool starts to earn its keep

You got correct