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. Clarity - 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:

# The multi-instance root modules maps a bunch of tfvars values into child module inputs
└── 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 single-instance root modules set the child module inputs directly
└── 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

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. Clarity

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.

Terraform workspaces share a single backend block, so state isolation for multi-instance root modules wasn’t natively supported until the release of OpenTofu 1.8.0 (released 2024-07-29), which introduced the ability to use variables and locals within the backend block.

Prior to that release, the only ways to achieve true state isolation were to either:

  1. Use single instance root modules.
  2. Use a Terraform wrapper such as Terragrunt/Terramate to dynamically generate the backend at runtime.
Bottom Line

For Terraform, single-instance root modules are the only native 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:

Bottom Line

For an infrastructure configuration that will be repeated many times, e.g. deploying a bunch of single tenant infrastructure stacks for different customers, I would lean towards the multi-instance root module approach.

For an infrastructure configuration that has a fixed set of environments, e.g. a company with a staging and production environment, I would lean towards single-instance root modules organized by the file layout.