Stop Mixing Environments: Why Your Dev and Prod States Should Never Touch
You have three directories: dev, staging, and prod. Each one holds configuration files, state records, and resource definitions. When you need to update a firewall rule, you open all three directories and make the same change three times. One afternoon, you forget to update staging. Two weeks later, a deployment to staging fails because the firewall blocks a connection that works everywhere else. Nobody notices until the release is blocked.
This situation is common. Teams start with one environment, then add another, then another. The separation feels clear because the folders have different names. But the separation is only cosmetic. The real problem is that the same person, the same tool, and the same access pattern can touch every environment without any guardrails.
The Real Problem with Environment Separation
Environment separation is not about folder names. It is about ensuring that changes in development never accidentally affect production, and that testing in staging actually reflects production conditions.
When you use the same state file for all environments, you create a single point of failure. A mistake in development can delete a production resource. A misconfigured CI pipeline can apply staging values to production infrastructure. These are not theoretical risks. They happen when teams share state files, share backends, or share access credentials across environments.
The goal is to make it impossible to accidentally modify production while working on development. That requires more than discipline. It requires structural separation.
Three Approaches to Environment Separation
1. Separate Directories
The following diagram maps each approach to its structure and key characteristics:
This is the simplest approach. You create a folder for each environment: dev/, staging/, prod/. Each folder contains its own configuration files and its own state file. When you run a command, you specify which folder to use.
This works well for small teams with few environments. You can see the differences between environments by comparing files side by side. The structure is easy to understand. New team members can find what they need without asking.
The problem appears when environments share most of their configuration. Development and staging might differ only in server size and database name. When you need to change a security setting, you must update three files. Forgetting one creates an invisible drift. Over time, the environments diverge until staging no longer represents production.
2. Shared Structure with Separate Configuration Files
Instead of duplicating the entire configuration, you keep one set of resource definitions and separate files for environment-specific values. The main files define the logic and structure. The configuration files hold values like server names, instance sizes, and connection strings.
This approach reduces duplication. You write the resource definitions once. When you change a firewall rule, you change it in one place. The environment-specific files only contain the values that actually differ.
The trade-off is complexity. You need to understand how the configuration files merge with the main definitions. You need to ensure that every environment has the correct configuration file. Missing a value can cause a deployment to fail or, worse, to use a default value that is wrong for that environment.
3. Separate State Backends
This is the most mature approach. Each environment uses a different backend to store its state file. The state file records every resource that has been created and managed. When the backends are separate, development state cannot mix with production state by accident.
Here is a concrete example of how to configure separate state backends in Terraform using S3:
# backend configuration for the production environment
terraform {
backend "s3" {
bucket = "my-company-tfstate"
key = "prod/terraform.tfstate"
region = "us-east-1"
}
}
For the staging environment, you would use the same bucket but a different key:
terraform {
backend "s3" {
bucket = "my-company-tfstate"
key = "staging/terraform.tfstate"
region = "us-east-1"
}
}
And for development:
terraform {
backend "s3" {
bucket = "my-company-tfstate"
key = "dev/terraform.tfstate"
region = "us-east-1"
}
}
Each environment's state file is stored under a separate prefix in the same S3 bucket. You can then enforce access policies at the bucket or prefix level, ensuring that developers can only read and write the dev/ prefix while production access is restricted to your CI/CD pipeline.
You can enforce access policies at the backend level. Development team members can only access the development backend. Operations or SRE teams hold access to the production backend. A developer running a command from their laptop cannot touch production resources because their credentials do not have access to the production backend.
This separation is critical because state files contain sensitive information. Production state typically records server IP addresses, database connections, and security configurations. If production and development states are stored in the same place, a leak or accidental change affects both environments. With separate backends, you can apply different security policies to each environment.
When to Use Each Approach
Choose separate directories when your project is small, you have few environments, and the resources differ significantly between environments. This is a good starting point, but plan to move away from it as the team grows.
Choose shared structure with separate configuration files when your environments have the same structure but different values. This works well for teams that have standardized their infrastructure and only need to vary parameters like size, region, or naming.
Choose separate state backends when your team is large, production must be protected strictly, or you need an audit trail for every change. This is the approach that scales with organizational complexity.
The Policy Side of Environment Separation
Technical separation is only half the solution. You can have perfect backend separation, but if every team member has access to the production backend from their laptop, the separation is meaningless.
Environment separation requires policy. Who can read the production state? Who can modify it? Who can approve changes to production infrastructure? These questions must be answered and enforced through access controls, not through verbal agreements.
Some teams use a separate CI/CD pipeline for production deployments. The production pipeline runs only from a specific branch, requires approvals, and uses credentials that are not available to developers. This adds another layer of separation beyond the backend itself.
Practical Checklist
- Each environment uses a different state backend
- Production backend is only accessible from a CI/CD pipeline, not from local machines
- Development and staging backends are accessible to developers
- State files are encrypted at rest
- Access to production state requires approval and is logged
- Configuration values are validated before they reach production
The Concrete Takeaway
Environment separation is not a folder structure problem. It is a state isolation problem. When you treat each environment as a completely separate system with its own state backend, its own access controls, and its own deployment pipeline, you remove the possibility of accidental cross-environment changes. Start with separate directories if you must, but move to separate backends before your team grows beyond five people. The cost of fixing a production outage caused by shared state is always higher than the cost of setting up proper separation from the start.