Skip to content

Terraform Modules from First Principles: Deterministic, Declarative, Idempotent

Published: at 04:20 PMSuggest Changes

Terraform Modules from First Principles: Deterministic, Declarative, Idempotent

Terraform modules are often treated like “just a folder of .tf files”. But when a module gets reused across projects and environments, it behaves more like a function:

From first principles, a “good” module is one you can reason about. That means it should behave predictably when you run it today, tomorrow, or from CI.

Three principles help you get there:

They’re related, but not identical. Terraform nudges you toward all three—but module design decides whether you actually get them.

A quick map of the three

You can violate one while keeping the others. For example:

The goal isn’t “purity”. The goal is: default to these properties so that breaking them is a conscious choice.


1) Determinism (reproducibility)

What it means

A module is deterministic when:

If you’ve ever asked “why did this change?” without changing code, you’ve felt what non-determinism looks like.

Why it matters for modules

Modules exist to be reused. Reuse implies:

Non-determinism creates:

Practical module patterns for determinism

Example: stable for_each keys

If your identity is “the thing itself”, the key should be the thing itself.

variable "subnet_ids" {
  type = list(string)
}

# Good: stable keys based on subnet IDs
resource "aws_route_table_association" "this" {
  for_each       = toset(var.subnet_ids)
  subnet_id      = each.value
  route_table_id = aws_route_table.this.id
}

Common ways modules accidentally become non-deterministic

Good reasons to break determinism

Sometimes “reproducible” is not the top priority.

If you do this, make it explicit:


2) Declarative module design (intent over steps)

What it means

Terraform is declarative: you describe desired state; Terraform computes the dependency graph.

A module is declarative when its interface is “describe what you want”, not “tell me what steps to run”.

Why it matters for modules

Declarative modules are:

A useful test

If someone uses your module, do they need a runbook like:

If yes, the module is probably carrying hidden procedural requirements.

Example: intent-driven inputs

Prefer modeling desired outcomes:

variable "logging" {
  type = object({
    enabled        = bool
    retention_days = number
  })
}

Your module can then decide which resources to create and how to wire them, based on the intent.

Common ways modules become non-declarative

Good reasons to be less declarative

There are legitimate “bootstrap” style operations:

Mitigations:


3) Idempotency (safe repetition)

What it means

A module is idempotent when:

This is what makes “apply from CI” safe.

Why it matters for modules

Non-idempotent modules create:

Example: making “force redeploy” explicit

If you want an escape hatch to force a change, model it as an explicit input.

variable "deployment_id" {
  type        = string
  default     = ""
  description = "Change this to force a redeploy. Keep constant for idempotent applies."
}

The important design idea: idempotency should be the default, and non-idempotent behavior should be a deliberate toggle.

Common designs that break idempotency

Good reasons to sacrifice idempotency

Mitigations:


A small “anti-pattern catalog” (what to watch for)

If you spot these in a module, it doesn’t mean it’s “bad”—it means the module is paying interest in operational complexity.


Closing: design for reasoning

If I had to compress the whole post into one sentence:

A Terraform module should behave like a predictable function; if it can’t, make the unpredictability explicit.

Default to determinism, declarative intent, and idempotent convergence.

Then, if you need to break the rules, do it for a reason—and encode that reason into the module interface so your future self doesn’t have to rediscover it.


Previous Post
AWS "Pending" States Are a Blueprint for Async Deployments
Next Post
VPN Not Working? Here's How MTU and Protocol Settings Can Fix It