Organizing Terraform Configurations (Single-Instance vs. Multi-Instance Root Modules)

There are a few primary approaches when it comes to organizing terraform configurations to be deployed across multiple environments (e.g. dev/staging/prod).

I have seen a bunch of strong opinions about this on this floating around the interwebs recently, and wanted to lay out my thinking on the matter.

Terminology

Before we can have an informed discussion about this topic, first we should agree on terminology and review existing resources:

Matt Gowie (https://masterpoint.io/) uses the terms:

Yevgeniy "Jim" Brikman (https://www.gruntwork.io/) describes the different approaches:

I like the single-instance vs multi-instance terminology and will use it for the remainder of the article.

Important

There are multiple ways to implement each of these approaches and those differences are important. For example, Terraform doesn't have dynamic backends but OpenTofu does. This is a critical difference as to whether a multi-instance root module can meet your organizations security requirements.

What are we trying to achieve?

Rather than jump in and claim one approach is better than the other, we should first lay out what goals we want to achieve with our infrastructure configurations:

  1. Safety - Guarantee the Infrastructure as Code changes execute only in their intended environment (staging ⟹ staging, production ⟹ production).
  2. Consistency - Prevent accidental configuration drift between environments.
  3. Upgradability - Control the flow of provider and module upgrades across environments.
  4. Visibility - Make it obvious to readers which environments exist and what resources they contain.
  5. Security - Isolate production state from non-production state for compliance rules and reduce the blast radius of any change.

1. Safety

It is critical that we apply Infrastructure as Code configurations to the desired environment and ONLY the desired environment. An accidental application the wrong configuration to a production environment could be catastrophic.

The proper way to achieve this type of safety is to avoid applying configurations to production manually and limit production applies to a Terraform Automation and COllaboration Software (TACOS) tool such as Atlantis, Spacelift, or Env0. Once configured, these automations will ensure applies end up targeting the right environment.

What about in a break glass scenario?! πŸ’₯

In the heat of an incident, teams will often resort to overriding existing automations. I believe having single-instance root modules (as opposed to terraform workspaces) makes it slightly more clear which environment you are targeting.

Bottom Line

The difference between approaches here is small and in my opinion does not tip the scale one way or another.

2. Consistency

Another key consideration for IaC configurations is keeping environments consistent with one another.

A multi-instance root module by definition achieves this (it is a single module).

multi-instance supporters often claim that single-instance root modules will cause drift and duplication due to the additional files. In my opinion this is mostly a straw man argument.

Why? Because regardless of which approach you chose, the logic for your configurations should live in SEPARATE CHILD MODULES! In the single-instance approach, the root modules are primarily:

  1. A backend configuration
  2. Consuming a bunch of child modules (populating their input variables)

If the logic lives in separate modules, the difference between whether you store the input variables in environment specific .tfvars files and pass them into a multi-instance root module or store them in an environment specific root module directly is minimal from a consistency standpoint.

Let's look at set of files required by each approach for a hypothetical config:

└── multi-instance
    β”œβ”€β”€ live
    β”‚   β”œβ”€β”€ compute
    β”‚   β”‚   β”œβ”€β”€ main.tf
    β”‚   β”‚   β”œβ”€β”€ production.tfvars
    β”‚   β”‚   └── staging.tfvars
    β”‚   β”œβ”€β”€ global
    β”‚   β”‚   └── main.tf
    β”‚   └── networking
    β”‚       β”œβ”€β”€ main.tf
    β”‚       β”œβ”€β”€ production.tfvars
    β”‚       └── staging.tfvars
    └── modules # ALL OF THE LOGIC LIVES HERE!
        β”œβ”€β”€ compute
        β”‚   β”œβ”€β”€ main.tf
        β”‚   └── variables.tf
        └── networking
            β”œβ”€β”€ main.tf
            └── variables.tf

The multi-instance root module maps a bunch of tfvars into child modules

└── single-instance
    β”œβ”€β”€ live
    β”‚   β”œβ”€β”€ global
    β”‚   β”‚   └── main.tf
    β”‚   β”œβ”€β”€ production
    β”‚   β”‚   β”œβ”€β”€ compute
    β”‚   β”‚   β”‚   └── main.tf
    β”‚   β”‚   └── networking
    β”‚   β”‚       └── main.tf
    β”‚   └── staging
    β”‚       β”œβ”€β”€ compute
    β”‚       β”‚   └── main.tf
    β”‚       └── networking
    β”‚           └── main.tf
    └── modules # ALL OF THE LOGIC LIVES HERE!
        β”œβ”€β”€ compute
        β”‚   β”œβ”€β”€ main.tf
        β”‚   └── variables.tf
        └── networking
            β”œβ”€β”€ main.tf
            └── variables.tf

The single-instance root modules set those child module inputs directly

Note: Tools like Terragrunt and Terramate help avoid the duplication of the backend blocks and reduce opportunities for configuration drift.

Bottom Line

A multi-instance root module has an edge in this category, but it is much smaller than its proponents claim.

3. Upgradability

Within a root module, you can only specify a single version for each usage of a particular provider and module.

All workspaces share the same required_providers and child module source/version constraints.

Bottom Line

single-instance root modules allow for testing upgrades of providers and child modules in a more controlled fashion than a multi-instance root module.

4. Visibility

Another consideration is the speed with which a developer can read and understand an IaC configuration. This includes things like:

  1. What environments exist?
  2. What resources are deployed in each environment?
  3. How to validate my changes for a particular environment?

For a multi-instance root module these questions are answered by examining the root module and mentally mapping values from corresponding .tfvars files into it. For single-instance root modules, you navigate to the corresponding directory and everything is right there.

Bottom Line

In my opinion, single-instance root modules are easier to reason about and quickly understand exactly what is deployed to a given environment. Note: This is subjective and may be due me having more experience working with them.

5. Security

For both practical and compliance reasons, it is useful to isolate the state of production environments from the state of non-production environments.

Prior to OpenTofu 1.8.0, this was not possible for multi-instance root modules because Terraform workspaces share a backend configuration (and use prefixes to manage each workspace's state).

Bottom Line

For Terraform, single-instance root modules are the only way to achieve true state isolation. For OpenTofu, a multi-instance root module with a dynamic backend configuration can achieve this as well.

Closing thoughts

Both directory‑basedΒ single‑instanceΒ roots and workspace/dynamic‑backendΒ multi‑instanceΒ roots are viable patterns in 2025. The right choice hinges on which trade‑offs map to your team’s priorities: