Terraform Stacks Explained for Teams Drowning in Workspaces
Workspaces and copy-pasted root modules don't scale to dozens of environments. Terraform Stacks rethink the unit of deployment. Here's how they actually work.
- #terraform
- #stacks
- #iac
- #deployments
- #hcp
- #scaling
If you’ve managed more than a handful of environments with Terraform, you’ve felt the wall. You start with one root module, then you need staging and prod, so you reach for workspaces. Then you need three regions, so you copy the root module into directories. Then a new team wants the same stack with different inputs, and now you’re maintaining a fragile mesh of terraform.tfvars files, wrapper scripts, and a CI matrix that nobody fully understands.
Terraform Stacks are HashiCorp’s answer to that wall. They change the unit of deployment from “a single state file you run apply against” to “a declared set of components deployed across many instances.” I’ve been running them in anger for a few months now, and they genuinely fix the multiplication problem — but only if you understand what they’re actually doing.
The problem Stacks solve
The traditional model gives you one state file per terraform apply. Want the same infrastructure in us-east-1 and eu-west-1? That’s two apply targets, two state files, two backend configs, and usually two copies of the wiring that passes outputs from your network module into your app module.
Stacks invert this. You describe your infrastructure once as a set of components with explicit dependencies, and then you describe deployments — the instances of that whole stack you want to exist. Terraform handles the fan-out.
The two new file types
A Stack is defined by two kinds of files. Components live in .tfstack.hcl, and deployments live in .tfdeploy.hcl.
Here’s a minimal component file:
# network.tfstack.hcl
component "network" {
source = "./modules/network"
inputs = {
cidr_block = var.cidr_block
}
providers = {
aws = provider.aws.this
}
}
component "app" {
source = "./modules/app"
inputs = {
subnet_ids = component.network.subnet_ids
region = var.region
}
providers = {
aws = provider.aws.this
}
}
Notice component.app reads component.network.subnet_ids directly. There’s no terraform_remote_state data source, no manually published outputs, no race between two separate applies. The dependency is a first-class reference, and Terraform builds the dependency graph across components the same way it does across resources inside a single module.
Deployments: where the fan-out happens
The deployment file declares the instances:
# deployments.tfdeploy.hcl
deployment "staging" {
inputs = {
region = "us-east-1"
cidr_block = "10.0.0.0/16"
}
}
deployment "production_us" {
inputs = {
region = "us-east-1"
cidr_block = "10.1.0.0/16"
}
}
deployment "production_eu" {
inputs = {
region = "eu-west-1"
cidr_block = "10.2.0.0/16"
}
}
Three deployments, one component definition. Add a fourth region and it’s five lines, not a new directory. This is the entire value proposition: the thing you change when you scale out is data, not code.
Identity tokens instead of long-lived credentials
Stacks lean hard on OIDC. Instead of stuffing static cloud credentials into each deployment, you define an identity token and let the cloud provider trust it:
identity_token "aws" {
audience = ["aws.workload.identity"]
}
deployment "production_us" {
inputs = {
region = "us-east-1"
role_arn = "arn:aws:iam::111111111111:role/stacks-deploy"
identity_token = identity_token.aws.jwt
}
}
This matters more than it looks. In the old model, every environment directory was a place a credential could leak. Stacks push you toward short-lived, per-deployment tokens by default, which is the single biggest security upgrade most Terraform shops can make.
Orchestration rules
The feature I didn’t expect to love is orchestration rules — declarative conditions that decide when a deployment auto-applies versus waiting for a human:
orchestrate "auto_approve" {
check {
condition = context.plan.deployment.name == "staging"
reason = "Staging changes auto-apply."
}
}
You get to say “staging applies itself, production always pauses for review” without writing a single line of pipeline glue. The promotion logic lives next to the infrastructure instead of in a YAML file three repos away.
What to watch out for
A few hard-won notes:
- Stacks are not a free lunch for existing config. You don’t
importa workspace into a Stack. You restructure your root module into components, which is real refactoring work. Budget for it. - The component is the blast radius. A change to a shared component re-plans every deployment that uses it. That’s the point, but it means you want components scoped thoughtfully — not one giant component, not a thousand tiny ones.
- Tooling is still maturing. Local plan output is more verbose than classic Terraform, and some third-party tools that parse plan JSON don’t understand Stacks yet. Check your policy and cost tooling before you commit.
- Review the generated plans like any other change. A Stack plan can touch dozens of deployments at once, which is exactly when an automated review pass earns its keep. Running plans through a code review workflow catches the “this innocuous variable change re-creates every database” surprises before they apply.
When Stacks are worth it
If you have one or two environments, Stacks are overkill — stick with directories. The break-even point is when you’re maintaining the same infrastructure across many axes: multiple regions, multiple tenants, multiple environments, or all three at once. That’s where the workspace-and-directory sprawl becomes a maintenance tax, and where declaring deployments as data pays for the migration almost immediately.
For more patterns on structuring Terraform as your footprint grows, browse our other Terraform guides. Stacks are the biggest shift in the deployment model since remote state, and for teams genuinely drowning in environment copies, they’re the first thing in years that actually changes the math.
Terraform Stacks are evolving quickly. Verify feature availability and syntax against the current release before adopting them in production.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.