The Terraform Folder Structure That Scales Across Multiple Environments Without Breaking Down
The right Terraform folder structure for dev, staging, and prod. Organize modules, separate state files, and prevent catastrophic mistakes.
We ran terraform apply from the wrong directory at 3 PM on a Wednesday. Our working directory was pointing to prod state, but we thought it was dev. Terraform printed the plan: delete 47 resources including the production database. We stared at the screen for 2 seconds before hitting Ctrl+C. That 2-second pause cost us 10 years of aging. A proper folder structure prevents this mistake entirely.
The Problem
A flat Terraform structure with all .tf files in one directory is chaos. One state file. One backend config. No environment separation. A junior engineer runs terraform apply and accidentally impacts production. Or a senior engineer tired at the end of a long day makes the same mistake. The blast radius is unlimited. Disasters are catastrophic.
Teams that scale to 8+ environments (dev, staging, prod, sometimes multiple regions) with flat structure either have religious discipline (work in a sacred directory, never make mistakes) or they have incidents (they make mistakes). Religious discipline fails eventually. Everyone makes mistakes.
Why This Happens
Terraform tutorials show a single directory with main.tf, variables.tf, and backend.tf. This works for learning. It does not work for production systems with multiple environments. Teams copy the tutorial pattern and carry it forward. By the time they add the 5th environment, the folder structure is a mess. Refactoring seems expensive, so teams add documentation: "Be careful when running terraform apply in the prod directory." Documentation is a band-aid. Proper structure is a vaccine.
The Solution — The Scalable Terraform Folder Structure
Here is the folder structure we recommend for teams managing infrastructure across dev, staging, and prod:
/infrastructure
├── modules/
│ ├── vpc/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── README.md
│ ├── eks/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── README.md
│ ├── rds/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── alb/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── terraform.tfvars
│ │ ├── backend.tf
│ │ └── README.md
│ ├── staging/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── terraform.tfvars
│ │ ├── backend.tf
│ │ └── README.md
│ └── prod/
│ ├── main.tf
│ ├── variables.tf
│ ├── terraform.tfvars
│ ├── backend.tf
│ └── README.md
├── global/
│ ├── iam/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── route53/
│ ├── main.tf
│ └── variables.tf
└── terraform.tfvars (optional, for shared variables)
What Each Directory Does
/modules: Reusable infrastructure components. A VPC module is written once, called from dev, staging, and prod with different parameters. Each module is self-contained: main.tf defines resources, variables.tf defines inputs, outputs.tf exports values for other modules to use.
/environments: Environment-specific configuration. Each environment is a separate Terraform working directory with its own state file, backend config, and tfvars values. No shared state between environments.
/global: Infrastructure shared across all environments. IAM roles, Route53 hosted zones, KMS keys. These are provisioned once, not replicated per environment.
Environment-Specific Backend Configuration
# environments/prod/backend.tf
terraform {
backend "s3" {
bucket = "skillzmist-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true
}
}
# environments/staging/backend.tf
terraform {
backend "s3" {
bucket = "skillzmist-terraform-state"
key = "staging/terraform.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true
}
}
# environments/dev/backend.tf
terraform {
backend "s3" {
bucket = "skillzmist-terraform-state"
key = "dev/terraform.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true
}
}
Three separate state files. Prod state is completely isolated from staging state. A terraform destroy in dev cannot touch prod.
Calling Modules with Environment-Specific Variables
# environments/prod/main.tf
module "vpc" {
source = "../../modules/vpc"
vpc_name = var.vpc_name
vpc_cidr = var.vpc_cidr
enable_nat_gateway = var.enable_nat_gateway
tags = {
Environment = "production"
CostCenter = "engineering"
}
}
module "eks" {
source = "../../modules/eks"
cluster_name = var.cluster_name
cluster_version = var.cluster_version
vpc_id = module.vpc.vpc_id
private_subnets = module.vpc.private_subnets
desired_node_count = var.desired_node_count
tags = {
Environment = "production"
}
}
Different Values Per Environment
# environments/prod/terraform.tfvars
vpc_name = "prod-vpc"
vpc_cidr = "10.0.0.0/16"
enable_nat_gateway = true
cluster_name = "prod-eks"
cluster_version = "1.29"
desired_node_count = 5
# environments/staging/terraform.tfvars
vpc_name = "staging-vpc"
vpc_cidr = "10.1.0.0/16"
enable_nat_gateway = true
cluster_name = "staging-eks"
cluster_version = "1.28"
desired_node_count = 2
# environments/dev/terraform.tfvars
vpc_name = "dev-vpc"
vpc_cidr = "10.2.0.0/16"
enable_nat_gateway = false # Save costs
cluster_name = "dev-eks"
cluster_version = "1.28"
desired_node_count = 1 # Single node for testing
Prod has more nodes, staging is smaller, dev is minimal. The same module code, parameterized by environment.
Terraform Workspaces vs Directories — When to Use Each
Workspaces: Same infrastructure code, different state file per workspace. Good for ephemeral environments (branch-specific test environments that live for 1 week). Example: workspace=feature-auth, workspace=feature-billing.
Directories: Separate code per environment. Good for permanent environments where configuration differs (prod/staging/dev are NOT interchangeable). Example: prod has 5 nodes, dev has 1 node. Different configurations require different code.
Our recommendation: Use directories for permanent environments. Use workspaces for ephemeral test environments. For most teams, directories are the right choice.
Common Mistakes to Avoid
- Sharing one state file across all environments. A terraform destroy in one environment destroys the other. Separate state files are mandatory.
- Putting all resources in the root directory with no modules. The code becomes unmaintainable at scale. Modules are enforced separation of concerns.
- Hardcoding AWS account IDs and region names in .tf files. Use variables. Different environments use different account IDs and regions.
- No README.md in module directories. New team members cannot figure out how to use a module. Document inputs, outputs, and usage examples in every module.
- Variables without descriptions and types. Terraform defaults to string type. Be explicit: is this an integer, a list, a map? Write descriptions.
- Committing .terraform directory or .tfstate files to git. .terraform is generated locally. .tfstate contains secrets. Both belong in .gitignore.
Key Takeaways
- Separate directories per environment: One working directory for prod, one for staging, one for dev. State files never mix.
- Modules are the building blocks: Write modules once, call from multiple environments. DRY principle applied to infrastructure.
- Separate backend configs per environment: prod/backend.tf, staging/backend.tf, dev/backend.tf. Each environment has its own S3 key and potential separate AWS account.
- Environment-specific tfvars files: The same module code parameterized by environment. Prod gets more resources, dev gets less.
- Global infrastructure shared across environments: Route53, KMS keys, IAM roles live in global/, not duplicated per environment.
Struggling with Terraform folder organization or afraid of accidental production changes? The Skillzmist team has solved this exact problem for engineering teams across the US, UK, and Europe. Reach out for a free technical consultation — we respond within 24 hours.
Related: Goodbye DynamoDB: S3 Native State Locking | 7 Terraform Problems Every DevOps Engineer Faces