Terraform workspaces and environments: how to structure dev, staging, and production
"Workspace" means two different things in Terraform and OpenTofu, and mixing them up leads to the most common environment layout mistakes. Here is the difference and the layouts that hold up.
Two kinds of workspace
| CLI workspace | Platform workspace | |
|---|---|---|
| Created with | tofu workspace new staging | The platform's dashboard or API |
| What it is | Another state file for the same backend and configuration | A configuration with its own state, variables, credentials, policy, and run queue |
| Separate credentials | No, same backend and same shell | Yes |
| Separate access control | No | Yes |
CLI workspaces are handy for short-lived copies, such as a feature environment you tear down after testing. They are a poor boundary between staging and production, because both share the same backend credentials and one wrong tofu workspace select applies to the wrong environment.
Layouts that hold up
One directory per environment
modules/
network/
service/
environments/
staging/
main.tf # module "network" { source = "../../modules/network" }
production/
main.tfEach environment is its own root module and its own platform workspace. Differences between environments are visible in code review. The cost is some repetition, which shared modules keep small.
One configuration, many platform workspaces
If environments really are identical apart from inputs, point several platform workspaces at the same directory and set different variables on each. Keep the differences in variables, not in count = var.env == "production" ? 1 : 0 scattered through the code.
Split by layer as well as environment
Large environments get slow and risky as one state. Split by layer and rate of change: network, data, then services. A change to a service then can't plan a change to the VPC, and each plan stays fast.
Sharing values between workspaces
Lower layers expose outputs; higher layers read them. Read only what you need:
tfe_outputsreads another workspace's outputs and works with platforms that speak the Terraform Cloud API, including StackShip.terraform_remote_statereads the full state, including every sensitive attribute. Use it only when you must.
Many workspaces, one repository
With several workspaces in one repository, start runs only for the workspaces a change touches. In StackShip each workspace has a working directory and optional trigger paths such as environments/production/** and modules/**, so a change to a shared module plans every environment that uses it, and a change to staging plans only staging.
Workspaces in StackShip
- Workspaces are grouped into projects, such as
PlatformorPayments. - Each has its own state history, variables, credential profiles, policy attachments, OpenTofu version, and execution region.
- Runs in a workspace go one at a time, so two applies never race.
- By default no workspace can read another's state. Grant the Outputs consumer role to share only non-sensitive outputs.
Production and staging then differ in the places that matter: their cloud role, their variables, and which policies are hard mandatory. See the platform overview.