Most teams split Terraform state the way they split org charts: one state per team, or one state per repo. It feels tidy. Then someone runs terraform apply on a Friday and the plan touches the network, the database and three services at once, because they all live in the same state.
A better question to ask when you draw the boundaries: if this apply goes wrong, what is the most it can break?
What goes wrong with one big state
- Every
planhas to refresh every resource, so it gets slower as the state grows. - A state lock held by one person blocks everyone else, even when their changes are unrelated.
- A typo in a small change can still sit next to resources you would never want to touch by accident.
- Reviewers see a long plan and start skimming, which is exactly when mistakes get through.
Split by blast radius
Group resources by how much damage a bad change to them can do, and how often they change:
- Foundations (accounts, networking, DNS zones, IAM baselines). Change rarely, hurt a lot when wrong. Own state, very careful review.
- Shared platform (the Kubernetes cluster, shared databases, registries). Change occasionally.
- Per-environment application stacks (dev, staging, prod each in their own state). Change often, and a mistake in dev must not be able to reach prod.
The test is simple: a bad apply in one state should only be able to hurt one thing, and you should be able to say which thing before you press the button.
Sharing values between states
Splitting does not mean copy-pasting IDs. Two common options:
- Read what you need with a data source (look up the VPC by tag, for example). This keeps the consumer decoupled from how the producer is managed.
- Use
terraform_remote_stateand read the producer's outputs. It works, but it couples the consumer to the producer's state location and output names, so keep the outputs small and treat them as an interface.
Make the boundaries safe
- Use a remote backend with locking, one state per boundary.
- Give each state its own credentials with only the access it needs, so a prod state cannot be applied with dev credentials, and the reverse.
- Pin provider and module versions (
required_providers, moduleversion), so an upgrade is a change you review rather than a surprise. - Keep
planoutput in the pull request, so the reviewer sees exactly what will change.
The trade-off
More states means more places to manage: more backends, more pipelines, more outputs to keep stable. Do not split until a boundary earns it. A good trigger is when two things change at very different speeds, or when you catch yourself being nervous about applying one change because of something unrelated that lives next to it.
If you take one thing from this: draw state boundaries around failure, not around people.