Table of Contents

Introduction

A well-structured Terraform project is easier to maintain, debug, and scale. This guide covers proven patterns for organizing your Terraform code, from simple single-environment setups to complex multi-environment architectures.

Basic Project Structure

For small projects or getting started:

project/
├── main.tf           # Primary resources
├── variables.tf      # Input variable declarations
├── outputs.tf        # Output declarations
├── terraform.tfvars  # Variable values
├── providers.tf      # Provider configuration
└── versions.tf       # Version constraints

main.tf

resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr
  tags       = local.common_tags
}

resource "aws_subnet" "public" {
  count             = length(var.public_subnets)
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.public_subnets[count.index]
  availability_zone = var.azs[count.index]
}

providers.tf

provider "aws" {
  region = var.region
}

versions.tf

terraform {
  required_version = ">= 1.5"
  
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

Multi-Environment Structure

For projects with dev, staging, and production:

project/
├── 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
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   ├── staging/
│   │   ├── main.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   └── prod/
│       ├── main.tf
│       ├── terraform.tfvars
│       └── backend.tf
└── README.md

Module Best Practices

Create Reusable Modules

# modules/networking/main.tf
resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true
  tags = merge(var.tags, { Name = "${var.name}-vpc" })
}

# modules/networking/variables.tf
variable "vpc_cidr" {
  type        = string
  description = "CIDR block for the VPC"
}

variable "name" {
  type        = string
  description = "Name prefix for resources"
}

# modules/networking/outputs.tf
output "vpc_id" {
  value = aws_vpc.this.id
}

Use Modules in Environments

# environments/prod/main.tf
module "networking" {
  source   = "../../modules/networking"
  vpc_cidr = "10.0.0.0/16"
  name     = "prod"
  tags     = local.common_tags
}

module "compute" {
  source        = "../../modules/compute"
  vpc_id        = module.networking.vpc_id
  instance_type = "t3.large"
}

Naming Conventions

Consistent naming makes code readable:

# Resources: snake_case, descriptive
resource "aws_instance" "web_server" {}
resource "aws_s3_bucket" "application_logs" {}

# Variables: snake_case
variable "instance_type" {}
variable "vpc_cidr_block" {}

# Outputs: snake_case, prefixed by resource
output "vpc_id" {}
output "web_server_public_ip" {}

# Modules: snake_case
module "networking" {}
module "web_application" {}

File Organization Rules

  1. One resource type per file for large projects
  2. Group related resources in smaller projects
  3. Always separate variables, outputs, and providers
  4. Use data sources in a dedicated data.tf file
  5. Keep locals in locals.tf

Hands-On Courses

Conclusion

Good project structure pays dividends as your infrastructure grows. Start simple, use modules for reusability, and maintain consistent conventions across your team.