Q: Explain your Terraform module structure multiple environments setup for Dev, Stage, and Prod.
Best-practice terraform module structure multiple environments blueprint: directory-per-environment vs workspaces, reusable module design, state isolation, and Terragrunt DRY patterns.
Want to master this scenario in a live sandbox? KodeKloud's HashiCorp Certified Terraform Associate (003) Interactive Labs covers this exact problem with hands-on terminal drills.
🛠️ Production Runbook & Step-by-Step Resolution
Direct Answer: In enterprise cloud operations, designing a resilient terraform module structure multiple environments is critical. Code duplication and excessive blast radius are the two biggest failure vectors in Terraform. In an interview, I explain why we avoid single-state workspaces for multiple environments, how we structure directory-per-environment hierarchies with dedicated S3 backends, and how reusable modules enforce corporate standards across Dev, Staging, and Production.
Why Workspaces Fail for Multi-Environment Separation
Terraform CLI provides 'workspaces', but they share the exact same backend configuration, IAM permissions, and directory structure. A mistake in production variables can destroy production resources while testing in Dev.
- Shared Blast Radius: Workspaces use the same AWS account and backend bucket; accidental state overwrites are easy.
- Access Control Impossibility: You cannot grant a developer write access to dev workspace without granting write access to prod.
- Enterprise Standard: Use physical Directory-Per-Environment or Terragrunt rather than CLI workspaces.
Directory-Per-Environment & Module Repository Layout
We enforce a strict separation between Reusable Modules (the blueprint) and Environment Live Configurations (the instantiation).
# Enterprise Terraform Directory Structure:
├── modules/ # Shared reusable blueprints (versioned in Git)
│ ├── networking-vpc/ # main.tf, variables.tf, outputs.tf
│ ├── compute-eks/
│ └── data-aurora/
└── environments/ # Live environments (distinct AWS accounts & state)
├── dev/
│ ├── backend.tf # S3 bucket in Dev AWS Account
│ ├── terraform.tfvars # Small instance sizes, single AZ
│ └── main.tf # Sources modules/networking-vpc
├── staging/
│ ├── backend.tf
│ ├── terraform.tfvars # Prod-like configuration
│ └── main.tf
└── prod/
├── backend.tf # S3 bucket in Locked Prod AWS Account
├── terraform.tfvars # Multi-AZ, deletion protection = true
└── main.tf
- Isolated Backends: Dev state is stored in the Dev AWS account; Prod state is stored in a locked Prod account.
- Blast Radius Containment: Running
terraform applyin Dev cannot physically touch Production.
Layered State Architecture: Decoupling Blast Radius
Even within a single environment (e.g. Production), we never put all resources into a single monolithic state file. We partition the environment into decoupled lifecycle layers.
# Layered state structure inside environments/prod/:
├── 01-networking/ # VPC, Subnets, NAT Gateways (changed once a year)
├── 02-security/ # IAM Roles, KMS keys, WAF rules
├── 03-databases/ # RDS Aurora, Redis clusters (stateful, high risk)
└── 04-applications/ # EKS node groups, ingress, services (changed daily)
- Layer Independence: Deploying an application update never risks mutating the foundational VPC or dropping a database.
- Inter-Layer Data Passing: Layers share data using
terraform_remote_statedata sources or AWS SSM Parameter Store keys.
Terraform Module Structure Multiple Environments Architectural Matrix
Comparison of structural patterns for managing multiple environments in Terraform:
Versioned Module Sourcing & Terragrunt DRY Patterns
In production, live environments must never source modules from a local relative path. Instead, modules are hosted in dedicated Git repositories and sourced using immutable semantic version tags.
module "vpc" {
source = "git::https://github.com/my-corp/terraform-aws-vpc.git?ref=v2.4.1"
cidr_block = var.vpc_cidr
availability_zones = var.azs
enable_nat_gateway = var.enable_nat
}
- Safe Upgrades: Dev can upgrade to
v2.5.0for testing while Production remains pinned to testedv2.4.1. - Changelog & Semantic Versioning: All breaking changes are isolated behind major version bumps.
- Avoid Workspaces: CLI workspaces share backend storage and lack IAM boundary isolation; use physical directory-per-environment instead.
- Module vs Live Separation: Decouple reusable blueprint modules from environment live instantiations (dev, stage, prod).
- Layered State Files: Split infrastructure into lifecycle layers (01-network, 02-databases, 03-compute) to minimize blast radius and speed up plans.
- Semantic Versioning: Source modules via Git tags (ref=v2.1.0) so Dev can validate new module versions before Prod upgrades.
- Terragrunt for DRY: Use Terragrunt to automatically generate backend and provider blocks, eliminating 90% of boilerplate code.
Senior hiring managers and bar raisers evaluate how you prioritize during outages. Avoid these common anti-patterns:
- Managing all infrastructure across all environments in a single giant state file A single typo or corrupted resource locks the entire company infrastructure, and terraform plan takes 30+ minutes to execute.
- Sourcing modules via relative local file paths in Production A developer editing the local module file immediately mutates production the next time CI runs, bypassing version governance.
- Hardcoding account IDs, CIDRs, or passwords inside reusable module code Reusable modules must be completely environment-agnostic, receiving all parameters via input variables.
- Failing to enable deletion_protection on production stateful resources A stray terraform destroy in production will permanently delete databases unless explicit deletion protection flags are enforced.