Velocity Stream LogoVelocity Stream Logo
Back to Insights
Infrastructure as Code

Structuring Terraform Modules for Multi-Environment Deployments

Our exact folder structure and state management strategy for deploying complex AWS infrastructure across Dev, Staging, and Prod without drift.

As infrastructure scales, managing Terraform state across multiple environments (Development, Staging, Production) becomes a critical failure point. A poor repository structure leads to configuration drift, accidental prod-level changes in dev, and a fear of running terraform apply.

At Velocity Stream, we have standardized a Terraform repository structure that enforces isolation, promotes code reusability, and integrates perfectly with CI/CD pipelines. Here is exactly how we do it.


The Anti-Pattern: Workspaces

The official HashiCorp documentation often introduces Terraform Workspaces as the solution for multi-environment deployments. We strongly advise against using Workspaces for anything other than ephemeral environments (like feature branches).

Why? Because Workspaces use the exact same backend configuration and code. If you make a mistake in your main.tf while testing in the dev workspace, that broken code is now sitting in the repository. Furthermore, navigating workspaces is invisible in the codebase—you have to run terraform workspace list just to know what you are looking at.

The Velocity Stream Standard Structure

We advocate for complete physical separation of state and code per environment, relying heavily on Terraform Modules for DRY (Don't Repeat Yourself) code.

.
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   ├── staging/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   └── prod/
│       ├── main.tf
│       ├── variables.tf
│       ├── terraform.tfvars
│       └── backend.tf
├── modules/
│   ├── vpc/
│   ├── eks/
│   └── rds/
└── README.md

1. The modules/ Directory

This is where the actual resources are defined. Modules should be completely environment-agnostic. They should not contain hardcoded environment names or state configurations. Everything should be parameterized.

2. The environments/ Directory

Each environment is an independent Terraform root module. The main.tf inside an environment strictly calls the modules defined in the modules/ directory.

# environments/prod/main.tf
module "vpc" {
  source = "../../modules/vpc"

  environment = "prod"
  vpc_cidr    = var.vpc_cidr
  # Multi-AZ for prod
  azs         = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

module "rds" {
  source = "../../modules/rds"

  environment = "prod"
  vpc_id      = module.vpc.vpc_id
  # High-availability enabled
  multi_az    = true 
}

State Isolation (The Golden Rule)

Notice the backend.tf in each environment folder. This is crucial. Development, Staging, and Production must use different S3 state buckets and DynamoDB lock tables.

If your dev state is compromised or corrupted, it should have zero impact on your prod state. Furthermore, this allows you to apply strict IAM permissions: Developers might have write access to the dev state bucket, but only the CI/CD pipeline role has write access to the prod state bucket.

The CI/CD Advantage

"This structure maps perfectly to GitHub Actions. A push to the main branch triggers a terraform apply exclusively in the environments/prod/ directory. A PR against main triggers a plan in prod, but an apply in staging."

Conclusion

By physically separating environments into distinct folders and relying on parameterized local modules, you gain visibility, security, and a robust CI/CD integration path. If you are struggling with Terraform drift or fear applying changes, auditing your repository structure is the first step.

Chat with an Engineer