
The first two parts of this series stayed mostly conceptual. Part 1 covered why structure matters and introduced the basic building blocks. Part 2 went through module design and the choice between monorepos and polyrepos. Part 3 is where it gets concrete. We'll look at how to lay out your Terraform and OpenTofu code on disk, and how to handle multiple environments and the state files that go with them.
How you arrange your directories and files has a real effect on how easy your IaC is to read and maintain. There's no single right answer, but a few patterns show up again and again. Which one fits depends on your repository strategy (monorepo vs. polyrepo), how complex the infrastructure is, and how your team likes to work.
Common Folder Structure Patterns:
Pros: Clear separation of concerns per environment; easy to manage environment-specific configurations and state.
Cons: Can lead to some duplication of main.tf structure if not carefully managed with modules; deploying a single component across all environments requires changes in multiple directories.
Pros: Good for service-oriented architectures; promotes component ownership.
Cons: Managing environment-specific nuances within each component can become complex if not handled with clear variable strategies or workspace configurations.
Hybrid Approaches: Many organizations adopt a hybrid, for instance, organizing by business unit, then by application, then by environment. The key is consistency and clarity.
Structuring Module Sources:
modules/ directory within the same repository. Root configurations then reference these using relative paths (e.g., source = "../../modules/vpc").Interaction with Repository Strategy:
modules/ directory would also reside here.Component-First (Top-Level Components/Services): Organizes code by logical service or infrastructure component, with environments as subdirectories or managed via workspaces/variable files.
├── components/
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ ├── dev.tfvars
│ │ ├── prod.tfvars
│ │ └── backend-configs/ # Or manage backend per workspace
│ │ ├── dev.s3.tfbackend
│ │ └── prod.s3.tfbackend
│ ├── application_A/
│ │ ├── ... (similar structure)
│ └── database_cluster/
│ ├── ...
├── modules/
│ ├── ...
Environment-First (Top-Level Environments): A common approach, especially for managing distinct deployment environments.
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf # Specific backend config for dev state
│ ├── staging/
│ │ ├── ... (similar structure)
│ └── prod/
│ ├── ... (similar structure)
├── modules/
│ ├── vpc/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── ec2_instance/
│ │ ├── ...
└── global/ # Optional: for resources shared across all environments
├── iam_roles/
│ ├── ...
└── s3_buckets_shared/
├── ...
Keeping infrastructure consistent across multiple environments (say development, staging, production), and sometimes across multiple cloud regions or accounts, is one of the harder problems good structure helps with.
.tfvars Files: The most common method. Define variables in variables.tf and provide environment-specific values in separate .tfvars files (e.g., dev.tfvars, prod.tfvars). Use terraform apply -var-file="dev.tfvars" to target an environment.terraform.workspace in your code to introduce conditional logic or naming differences (e.g., name = "my-resource-${terraform.workspace}"). This is often suitable for simpler environment distinctions within a single configuration directory.Terraform state is critical. How you handle it across different environments, regions, and components is what keeps your IaC operations safe and reliable.
terraform apply operations from corrupting the state file.key (in S3/Azure) or path/prefix (in GCS/Consul) within your remote backend determines where the state file is stored. Structure these paths logically and consistently.terraform-state/<PROJECT_NAME>/<ENVIRONMENT>/<REGION>/<COMPONENT>/terraform.tfstate (e.g., terraform-state/my-app/prod/us-east-1/vpc/terraform.tfstate)Put these pieces together and you end up with an IaC setup that holds up as the platform grows: a sensible folder layout, environment differences driven by variables rather than forked code, isolated state files, and a remote backend you can actually trust. The structure then stays out of your way instead of fighting you every time you add a service or a region.
Next in the Series (Part 4): Scaling Structures and Advanced IaC Patterns.
