TerraformPilot

Terraform

How to Structure a Terraform Project - Best Practices

Learn how to structure Terraform projects for teams. File organization, module layout, environment separation, naming conventions, and monorepo vs multi-repo.

LLuca Berton1 min read

Quick Answer

#

Separate files by purpose (main.tf, variables.tf, outputs.tf), use modules for reusable components, and split environments with separate directories or workspaces. Keep each root module focused on one concern (network, compute, database).

Basic File Structure

#
project/
├── main.tf           # Resources
├── variables.tf      # Input variable declarations
├── outputs.tf        # Output value declarations
├── locals.tf         # Local value computations
├── providers.tf      # Provider configuration
├── versions.tf       # Required versions
├── terraform.tfvars  # Variable values (don't commit secrets)
└── README.md

Small Project (Single Environment)

#
infrastructure/
├── main.tf
├── variables.tf
├── outputs.tf
├── providers.tf
├── terraform.tfvars
└── modules/
    └── vpc/
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

Medium Project (Multiple Environments)

#
infrastructure/
├── modules/
│   ├── networking/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── compute/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   └── database/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
├── environments/
│   ├── dev/
│   │   ├── main.tf        # Calls modules
│   │   ├── variables.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   ├── staging/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   └── prod/
│       ├── main.tf
│       ├── variables.tf
│       ├── terraform.tfvars
│       └── backend.tf
└── README.md

Large Project (Component-Based)

#
infrastructure/
├── modules/                    # Shared modules
│   ├── vpc/
│   ├── eks-cluster/
│   ├── rds-postgres/
│   └── monitoring/
├── live/                       # Deployed infrastructure
│   ├── production/
│   │   ├── networking/         # Separate state per component
│   │   │   ├── main.tf
│   │   │   └── backend.tf     # s3://state/prod/networking
│   │   ├── cluster/
│   │   │   ├── main.tf
│   │   │   └── backend.tf     # s3://state/prod/cluster
│   │   └── database/
│   │       ├── main.tf
│   │       └── backend.tf     # s3://state/prod/database
│   └── staging/
│       ├── networking/
│       ├── cluster/
│       └── database/
└── README.md

Module Structure

#
# modules/vpc/main.tf
resource "aws_vpc" "this" {
  cidr_block           = var.cidr_block
  enable_dns_hostnames = true
  tags                 = merge(var.tags, { Name = var.name })
}
 
resource "aws_subnet" "public" {
  count             = length(var.public_subnets)
  vpc_id            = aws_vpc.this.id
  cidr_block        = var.public_subnets[count.index]
  availability_zone = var.azs[count.index]
  tags              = merge(var.tags, { Name = "${var.name}-public-${count.index}" })
}
 
# modules/vpc/variables.tf
variable "name"           { type = string }
variable "cidr_block"     { type = string }
variable "public_subnets" { type = list(string) }
variable "azs"            { type = list(string) }
variable "tags"           { type = map(string) default = {} }
 
# modules/vpc/outputs.tf
output "vpc_id"     { value = aws_vpc.this.id }
output "subnet_ids" { value = aws_subnet.public[*].id }
# environments/dev/main.tf
module "network" {
  source         = "../../modules/vpc"
  name           = "dev-vpc"
  cidr_block     = "10.0.0.0/16"
  public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
  azs            = ["us-east-1a", "us-east-1b"]
  tags           = { Environment = "dev" }
}

Naming Conventions

#
ItemConventionExample
Resourcessnake_caseaws_instance.web_server
Variablessnake_casevar.instance_type
Outputssnake_caseoutput "vpc_id"
Moduleskebab-case (dirs)modules/eks-cluster/
Filessnake_case or purposemain.tf, variables.tf
TagsPascalCase (AWS)Name, Environment

Environment Separation: Directories vs Workspaces

# #
cd environments/prod && terraform apply
cd environments/dev && terraform apply
  • ✅ Different configs per environment
  • ✅ Separate state files
  • ✅ Clear Git history per environment
  • ❌ Code duplication (mitigated with modules)

Workspaces

#
terraform workspace new dev
terraform workspace new prod
terraform workspace select prod
terraform apply -var-file="prod.tfvars"
  • ✅ Less code duplication
  • ❌ Same config for all environments
  • ❌ Easy to apply to wrong environment
#

Conclusion

#

Start simple (single directory), grow into modules when you reuse patterns, and separate environments with directories for production workloads. Split state by component (network, compute, database) so changes to one don't risk another. Use consistent naming and always include README.md.

#Terraform#DevOps#Infrastructure as Code

Share this article