Terraform Tutorial — Project Directory Layout

3 minute read

A well-organized Terraform project is easier to read, review, and maintain. While Terraform doesn’t enforce a directory structure, the community has settled on a standard layout that most teams follow.


Minimal Single-File Project

For quick experiments, you can put everything in one file:

my-project/
└── main.tf

Terraform reads all .tf files in the current directory. Even with a single file, all resources, variables, and outputs work. This is fine for learning, but it doesn’t scale.


Standard Project Layout

For any real project, split your configuration into multiple files:

my-project/
├── main.tf              ← Primary resources
├── variables.tf         ← Input variable declarations
├── outputs.tf           ← Output value declarations
├── providers.tf         ← Provider configuration
├── versions.tf          ← Terraform and provider version constraints
├── terraform.tfvars     ← Variable values (often gitignored)
├── .terraform/          ← Provider cache (never commit)
├── .terraform.lock.hcl  ← Dependency lock file (commit this)
├── terraform.tfstate    ← State file (never commit)
└── .gitignore           ← Ignore state and cache

What Goes in Each File

File Contents
main.tf Resource and data source blocks — the core of your infrastructure
variables.tf All variable {} blocks — inputs your config accepts
outputs.tf All output {} blocks — values to display or pass to other modules
providers.tf provider {} blocks — how Terraform connects to AWS/GCP/Azure
versions.tf terraform { required_version, required_providers } block
terraform.tfvars Actual values for variables (e.g., region = "us-east-1")

💡 Tip: These filenames are conventions, not requirements. Terraform reads all .tf files in the directory regardless of their names. The standard names help your team find things quickly.


The .terraform/ Directory

When you run terraform init, Terraform creates a .terraform/ directory containing:

  • Downloaded provider binaries (e.g., the AWS provider plugin)
  • Downloaded module source code
  • Backend configuration cache

This directory is large (hundreds of MB with multiple providers) and is never committed to Git. Every developer runs terraform init to populate it locally.


The .terraform.lock.hcl File

This is Terraform’s dependency lock file. It records the exact versions and checksums of providers downloaded during terraform init.

provider "registry.terraform.io/hashicorp/aws" {
  version     = "5.31.0"
  constraints = "~> 5.0"
  hashes = [
    "h1:ltYPMnfPhat6NjBMBn0NpsRMwa2dBbKaYZkHlLBbH1A=",
    "zh:0cdb9c2083bf0902442c60b3a113e43b42fcee1e8b7... ",
  ]
}

You should commit this file to Git. It ensures everyone on your team uses the exact same provider versions, preventing “it works on my machine” issues.


State Files

Terraform creates two state files:

File Purpose
terraform.tfstate Current state — tracks every resource Terraform manages
terraform.tfstate.backup Backup of the previous state

Never commit state files to Git. They contain:

  • Resource IDs and attributes
  • Potentially sensitive data (database passwords, API keys)
  • Infrastructure details that could be a security risk

In production, use a remote backend (S3, Terraform Cloud) to store state securely.


Sample .gitignore for Terraform Projects

Every Terraform project should have this .gitignore:

# Local .terraform directories
**/.terraform/*

# .tfstate files
*.tfstate
*.tfstate.*

# Crash log files
crash.log
crash.*.log

# Exclude all .tfvars files, which are likely to contain sensitive data
*.tfvars
*.tfvars.json

# Ignore override files as they are usually used to override resources locally
override.tf
override.tf.json
*_override.tf
*_override.tf.json

# Ignore CLI configuration files
.terraformrc
terraform.rc

💡 Tip: If your .tfvars file doesn’t contain sensitive data (just region names, environment names), you can track it in Git. But when in doubt, .gitignore it and document the required variables in a README.md.


Files to Commit vs Files to Ignore

File Commit to Git? Why
*.tf ✅ Yes Your infrastructure code
.terraform.lock.hcl ✅ Yes Ensures consistent provider versions
README.md ✅ Yes Documentation
.gitignore ✅ Yes Defines what to ignore
.terraform/ ❌ No Large, auto-generated by init
*.tfstate ❌ No Contains sensitive data
*.tfvars ⚠️ Depends Ignore if contains secrets

Multi-Environment Layout (Preview)

As your project grows, you might structure directories per environment:

terraform/
├── modules/
│   ├── vpc/
│   ├── ec2/
│   └── rds/
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   ├── staging/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   └── prod/
│       ├── main.tf
│       ├── variables.tf
│       └── terraform.tfvars

This pattern is covered in detail in the Workspaces chapter.


What’s Next?

Your project is organized and ready to go. Let’s verify everything with the chapter’s practice exercises.


← VS Code & HashiCorp Extension Practice Exercises →