TL;DR
- Split by environment (dev/staging/prod) and by component (network, app, data) — never one giant root module.
- Use modules for reusable abstractions; pass variables in, expose outputs, never hard-code state references.
- State per environment, per component — small blast radius beats one big state file.
- Pin provider and module versions explicitly. Floating versions = surprise breakage.
What is structuring Terraform code?
Terraform code structure is the layout decisions that make a repo of .tf files manageable as it grows past one root module. Get it right and changes stay local and predictable; get it wrong and every change has to think about the whole infrastructure at once.
The dominant pattern in 2026 is environment-per-directory + component-per-subdirectory — for example live/prod/network/, live/prod/app/, modules/network/, modules/app/. State files mirror that structure, so a change in app can never accidentally touch network.
Prerequisites
- Terraform 1.5 or later installed.
- A version-control repo where this code will live.
- Decisions on backend (S3+DynamoDB, Terraform Cloud, GCS) and on naming convention before the first commit.
How to use this guide
The sections below walk through the practical commands and options. After the main content you’ll find a Verification block (sanity-check it actually worked), a Troubleshooting block (common error messages and what to do), and Related reading for follow-on topics.
Terraform represents a robust tool that automates codebase management by creating a private, version-controlled repository and configuring source control systems like Git to ensure synchronization. This article will elucidate the optimal approaches for setting up and utilizing Terraform in conjunction with Git.
By adhering to these guidelines, you will establish a folder structure that facilitates code management, rendering it more accessible while enhancing team productivity.
Why Structure Your Code?

Establishing a standardized structure for your code fosters cohesion, improves readability, and simplifies code creation. Commands remain consistent throughout your codebase. For instance, initializing a development repository can be accomplished using the same command each time:
terraform init --backend-conf=./config/dev/backend.conf
terraform init --backend-conf=./config/test/backend.conf
terraform init --backend-conf=./config/prod/backend.conf
It’s the same with var files.
terraform apply --var-file=./config/dev/vars.tfvars
terraform apply --var-file=./config/test/vars.tfvars
terraform apply --var-file=./config/prod/vars.tfvars
It makes everything much easier when using CI/CD tooling like Jenkins.
Tree Structure
Within this post, we will delve into the importance of maintaining a directory tree structure along with the associated best practices. Additionally, we will provide a helpful diagram illustrating the directory tree to illustrate our points effectively.
By employing a directory tree structure, you can easily locate and access the necessary files and folders. Furthermore, this structure facilitates efficient file and folder management. By adhering to best practices, you can establish a clean and organized directory tree.
.
├── .pre-commit-config.yaml
├── .terraform-docs.yml
├── .gitignore
├── jenkinsfile
├── README.md
├── bitbucket-pipelines.yml
└── tf
├── README.md
├── config
│ ├── dev
│ │ ├── backend.conf
│ │ └── vars.tfvars
│ ├── prod
│ │ ├── backend.conf
│ │ └── vars.tfvars
│ └── test
│ ├── backend.conf
│ └── vars.tfvars
├── main.tf
├── .terraform-version
├── .terraform.lock.hcl
├── outputs.tf
├── providers.tf
├── variables.tf
├── modules
└── module-1
└── README.md
Tree Structure Breakdown
| .terraform-version | Used by tfenv to set Terraform version automatically. Jenkins picks up this file to select the version |
| .terraform.lock.hcl | Used to lock versions of specific providers within Terraform; to upgrade this, use the Terraform init -upgrade command. More information available at Hashicorps Terraform Website |
| config | per environment directory for the backend.conf & vars.tfvars |
| backend.conf | Used by Jenkins to define specific terraform backends |
| vars.tfvars | environment-specific variable values |
| .pre-commit-config.yaml & .terraform-docs.yaml | Used for terraform documentation within README.md files |
| Jenkinsfile | Used by Jenkins to specify a branch |
| Modules Folder | All code should be modularized, and the main.tf file should reference each module. This keeps the code clean and portable. |
| Outputs.tf | Output values make information about your infrastructure available on the command line and can expose information for other Terraform configurations to use. Output values are similar to return values in programming languages. |
Module Versioning
When using Terraform, tracking which versions of your modules you are using is essential. This is especially important when upgrading or using modules in a collaborative environment.
“When using Terraform, the version of a module can be determined by the version number in the themodule’ss filename. For example, the terraform module” aws-ec” has a version number of”1.5.0″. “When using Terraform, the version of a module can be determined by the version number in the module’s filename. For example, the terraform module “aws-ec2” has a version number of “1.5.0”.
When using Terraform, tracking which versions of your modules you are using is important. This is especially important when upgrading or using modules in a collaborative environment.
v1.0.0 would be the first user-ready version of a module.
v1.0.0 to v1.0.1 would be for a bug fix
v1.0.0 to v1.2.0 would be for new features, with no breaking changes
v1.0.0 to v2.0.0 would be for new features, with breaking changes
A breaking change forces people using the module to make changes even if they’re not using the new features.
For Ansible Roles, versioning has to be in a semantic versioning format, which means it cannot start with a v like the above example.
Verification
Sanity-check the change actually worked:
terraform fmt -recursiveexits clean — formatting is consistent.terraform validatein each component directory exits clean.terraform planfrom one component shows no unexpected resources from other components — proves state isolation.
Troubleshooting
Module references break after restructuring — terraform state mv moves resources within a state; the docs have the exact syntax for nested modules.
Provider version conflicts between environments — Pin provider versions in required_providers; use ~> 5.0 for minor-only floats.
Plan shows drift on every run — Usually means a parameter is being set outside Terraform (manual console change, autoscaling resizing). Either ignore via lifecycle or bring the change into Terraform.
Authoritative sources
References: Terraform style guide (official), Module development.


Leave a Reply