Table of Contents

How Should You Structure a Terraform Project?

A well-structured Terraform project separates concerns, enables reuse, and scales with your team. Here is the recommended structure used by production teams at scale.

terraform-project/
├── modules/
│   ├── networking/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── compute/
│   └── database/
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   ├── staging/
│   └── prod/
├── modules.tf
├── variables.tf
├── outputs.tf
└── versions.tf

Key Principles

1. Separate Environments

Each environment (dev, staging, prod) should have its own directory with its own state file. This prevents accidental changes to production when working on development.

2. Use Modules for Reusable Components

Modules encapsulate related resources. A networking module might include VPC, subnets, route tables, and NAT gateways. This promotes consistency and reduces duplication.

3. Pin All Versions

Always pin Terraform version, provider versions, and module versions:

terraform {
  required_version = ">= 1.5.0, < 2.0.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

4. Use Remote State

Store state in a remote backend (S3, GCS, Terraform Cloud) with locking enabled:

backend "s3" {
  bucket         = "company-terraform-state"
  key            = "env/dev/terraform.tfstate"
  region         = "us-east-1"
  dynamodb_table = "terraform-locks"
  encrypt        = true
}

5. Naming Conventions

Use consistent, descriptive names: resource_type-project-environment-region-purpose.

Anti-Patterns to Avoid

  • Monolithic configurations — one giant main.tf with hundreds of resources
  • Hardcoded values — use variables and locals instead
  • Shared state across environments — always separate state per environment
  • No version pinning — leads to unexpected breaking changes

Learn More