Terraform Guide

Multi-environment Terraform: dev, staging, and prod on one diagram

By the engineer who builds Datadef, from client work on data platforms · Reviewed August 21, 2026

A repository that deploys the same code to three environments produces three nearly identical diagrams, and nobody can tell them apart at a glance. Which is worse than it sounds, because the whole reason to look is to find the places where the environments are not identical. The fix is to draw the shape once and put the differences on the nodes.

6 min readFor teams running one Terraform codebase across several environments

See it as a diagram

Everything below, as a diagram you can edit. Describe yours and see it in seconds.

157/20003 credits left
Try:

No account needed · Editable canvas, not a picture

Three layouts, one question

Environments are expressed in Terraform in three common ways. A directory per environment, each with its own backend and its own state. A single configuration with a tfvars file per environment. Or workspaces, where the same configuration is applied under different state keys selected at apply time.

The layout changes where the values live, not what a reader wants to know, which is always the same question: what is actually different between prod and the others. In practice the answer is small. The same modules, the same wiring, and a short list of divergences: instance sizes and SKUs, replica and node counts, a backup or DR resource that only exists in production, and sometimes a network peering that only exists in dev.

Draw the shape once, annotate the divergences

Datadef parses tfvars files per environment and reads the environment names out of the paths, so they are a property of the model rather than a label someone typed. The rule is one path segment: env, envs, environment, or environments, followed by the name, upper-cased. envs/dev/terraform.tfvars gives DEV, platform/environments/prod/main.tfvars gives PROD, and nothing has to be configured. When a resource or a module is instantiated per environment it stays one node and carries the real numbers, 10 catalogs in DEV and 1 in PROD, instead of appearing ten times or hiding behind a generic times-N.

Multiplicity comes from the source. A for_each over a literal map is counted by its top-level entries. A count expression like var.enable_dr ? 1 : 0 has its flag resolved per environment and prints not deployed (count 0) where it is false. A for_each over a comprehension is not evaluated, so the node says per projects rather than inventing a number, and a module instantiated that way stays one zone with its multiplicity written on it.

One rule keeps the annotations trustworthy. Where an attribute resolves to different literals in different environments and nothing decides which the node should carry, nothing is written: a single-environment value printed on a shared node would read as fact. Counts are the exception, because they can be attributed, which is why a count appears as DEV 10, QUAL 4, PROD 1 while a disputed SKU appears not at all.

What you get is a single canvas where the shared architecture reads normally and the divergences are visible as annotations on the specific nodes that diverge. That is the diagram people actually needed, and it is regenerated on every sync rather than re-drawn per release.

envs/
  dev.tfvars      catalog_count = 10   sku = "Standard"
  qual.tfvars     catalog_count = 4    sku = "Standard"
  prod.tfvars     catalog_count = 1    sku = "Premium"   geo_backup = true

parsed result on the canvas:
  Unity Catalog metastore    catalogs: DEV 10, QUAL 4, PROD 1
  Storage account            SKU: Standard (DEV, QUAL) / Premium (PROD)
  Geo backup vault           PROD only

When environments deserve their own diagram

Split when the topology genuinely diverges rather than when the values do. A production environment in two regions with failover, a dev environment that skips the private networking entirely, or a legacy environment mid-migration: those are different architectures wearing the same repository, and annotations stop helping once roughly a fifth of the nodes differ.

Split also when the audiences differ. A production diagram shown to auditors and a dev diagram used by engineers can legitimately be two artifacts with two levels of detail. Connecting the same repository twice, once per branch or tag, gives each its own synced project.

Do not split just because there are three environments. Three copies of the same picture create a maintenance problem and a trust problem at once, since the moment two of them disagree nobody knows which one was updated last.

FAQ

Should each environment get its own architecture diagram?

Only when the topology differs, not when just the values differ. Environments of one codebase share a shape, so one diagram with per-environment counts and annotations is easier to maintain and easier to read than three near-identical pictures that drift apart.

How does a diagram know which environment a value belongs to?

By parsing the tfvars files and the environment layout of the repository. Environment names are discovered from that structure, and values resolved per environment are attached to the node they configure, so a count of 10 in dev and 1 in prod appears on the same node.

What if we use Terraform workspaces instead of directories?

Workspaces select state at apply time, so the per-environment values usually still live in tfvars or in variable defaults in the repository. Those parse the same way. What workspaces add is a state key per environment, which is an operational fact rather than an architectural one.

How do we show a resource that exists only in production?

As a normal node annotated with the environments it applies to. A resource guarded by a count expression that evaluates to zero outside production is still declared in the source, so it is visible to a parser and can be labelled rather than silently dropped.

Does annotating environments make the diagram harder to read?

Less than duplication does. The annotations sit on the small number of nodes that actually diverge, while the shared architecture reads exactly as it would for a single environment. That inverts the usual problem: instead of comparing three pictures to find the differences, the reader sees the differences marked and the rest of the canvas quiet. Nodes with nothing to annotate carry no extra text at all.