Skip to content

Directory-per-Environment Pattern

Directory-per-environment gives every environment a genuinely separate directory with its own backend configuration, while every environment’s root config calls the same shared module with different input values.

A real directory, a real backend, per environment

Section titled “A real directory, a real backend, per environment”

Instead of one workspace-selected state file, this pattern uses one real directory per environment, each with its own root-level .tf files:

Terminal window
environments/
├── dev/
└── main.tf
├── staging/
└── main.tf
└── prod/
└── main.tf
modules/
└── app/
└── main.tf

The key difference from copy-pasting a whole environment is what lives inside each main.tf: not a full copy of the application’s resources, but a small root configuration with its own backend block and a single module call into one shared module:

environments/dev/main.tf
terraform {
backend "s3" {
bucket = "acme-terraform-state-dev"
key = "app/terraform.tfstate"
region = "us-east-1"
use_lockfile = true
}
}
provider "aws" {
region = "us-east-1"
profile = "acme-dev"
}
module "app" {
source = "../../modules/app"
environment = "dev"
instance_type = "t3.micro"
instance_count = 1
}
environments/prod/main.tf
terraform {
backend "s3" {
bucket = "acme-terraform-state-prod"
key = "app/terraform.tfstate"
region = "us-east-1"
use_lockfile = true
}
}
provider "aws" {
region = "us-east-1"
profile = "acme-prod"
}
module "app" {
source = "../../modules/app"
environment = "prod"
instance_type = "m5.large"
instance_count = 3
}

Notice each backend points at a completely separate S3 bucket, tied to a separate profile (and, in a real setup, often a genuinely separate AWS account) — dev and prod cannot accidentally share state, because there is no shared state to accidentally touch. The actual application resources live once, in modules/app, and every environment simply calls that module with different instance_type and instance_count values.

This directly fixes workspaces’ weak isolation. Each environment directory can point at its own AWS account, its own region, and its own backend bucket, with nothing tying them together at the Terraform level. There is no shared state file for a mistyped workspace name to accidentally touch, and no shared provider block for the wrong credentials to sneak through. If dev and prod genuinely need to live in different AWS accounts for blast-radius isolation, this pattern can express that; workspaces cannot.

Look again at those two main.tf files. The instance_type, instance_count, and environment inputs are the only lines that meaningfully differ between them — three lines out of roughly fifteen. Everything else — the backend "s3" block’s shape, the provider "aws" block, and the module "app" block’s source argument — is boilerplate, repeated nearly verbatim in every environment directory. Bump the module to a new version, add a new required provider argument, or rename the state key convention, and that change has to be hand-applied to dev/main.tf, staging/main.tf, and prod/main.tf individually — the exact same manual-repetition failure mode from the copy-paste-a-whole-directory approach, just narrowed down to a smaller, but still real, slice of each file.

flowchart LR
  mod["modules/app (shared source)"]
  devEnv["environments/dev/main.tf"] -->|module call with dev inputs| mod
  stagingEnv["environments/staging/main.tf"] -->|module call with staging inputs| mod
  prodEnv["environments/prod/main.tf"] -->|module call with prod inputs| mod
  devEnv -.->|near-identical backend + provider + source blocks| stagingEnv
  stagingEnv -.->|near-identical backend + provider + source blocks| prodEnv
Three isolated environment directories, one shared module, but near-identical boilerplate repeated in every directory
What does the directory-per-environment pattern solve that Terraform workspaces do not
What stays DRY (written once, reused everywhere) in the directory-per-environment pattern
What is still being duplicated across environment directories in this pattern, despite solving isolation
Why can environments/dev and environments/prod safely use two entirely different AWS accounts in this pattern