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:
-
multi-instance root module
: A root module with multiple state files associated with it. This is done either with TF workspaces or with dynamic backend configurations, aka "Dynamic Backends". -
single-instance root module
: A root module directory with only oneΒ state file associated with it.Sources: Blog, LinkedIn Post
Yevgeniy "Jim" Brikman (https://www.gruntwork.io/) describes the different approaches:
-
Workspaces
: Terraform workspaces are one method of achieving a multi-instance root module. Note: OpenTofu 1.8.0 (released 2024-07-29) introduced the ability to use variables/locals in backend configurations. -
Branches
: Git branches can be used to store different static backend configurations for different environments within the same root module. Similar to managing long lived released branches, this will lead to pain and suffering. Please don't do this π. -
File Layout
(optionally using Terragrunt or similar): Having separate root modules allows for configuring different backends by having separate files, organized via a logical directory structure.Sources: Terraform Up and Running (Book), Workspaces Blog, Branches Blog, Terragrunt Blog
I like the single-instance
vs multi-instance
terminology and will use it for the remainder of the article.
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:
- Safety - Guarantee the Infrastructure as Code changes execute only in their intended environment (
staging βΉ staging
,production βΉ production
). - Consistency - Prevent accidental configuration drift between environments.
- Upgradability - Control the flow of provider and module upgrades across environments.
- Visibility - Make it obvious to readers which environments exist and what resources they contain.
- 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.
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:
- A backend configuration
- 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.
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.
multi-instance
root modules require upgrading these versions all at once. You can still apply them separately (i.e. apply to non-prod first), but until you have applied the upgrade across all environments, the configuration in version control will be out of date with reality.single-instance
root modules can specify different version of providers and child modules. This allows for a more controlled upgrade path, where new versions can be validated in lower environments before a change is made to production.
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:
- What environments exist?
- What resources are deployed in each environment?
- 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.
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).
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:
- Security & isolation: OpenTofuβs dynamic backends eliminate the last hard blocker for
multiβinstance
layouts, allowing a unique bucket and IAM role per workspace. If you are on Terraform,singleβinstance
still offers the most straightforward state segregation. - Upgrade cadence & visibility: Separate directories make it trivial to stage provider or module upgrades in lower environments before promoting to production, and they double as living documentation of what runs where.
- Operational consistency: A
multiβinstance
root module guarantees every environment uses the exact same codebase, reducing drift and simplifying CI pipelines, but moving logic into child modules mostly accomplishes the same thing forsingle-instance
configurations.