Skip to main content

Command Palette

Search for a command to run...

Day06: Terraform Project Structure

Updated
4 min readView as Markdown

Complete Hands-On Guide

Transform your single main.tf into a production-ready, multi-file Terraform project. This follows HashiCorp best practices for organizing AWS infrastructure code across multiple files.

Why Multiple Files Matter

Single file → Multiple files evolution - Started with everything in main.tf (Day 4) to learn basics, now improving for real-world use.

Key Benefits:

  • Single Responsibility - Each file has one clear purpose

  • Team Collaboration - Multiple developers work simultaneously

  • Better Git History - Focused, reviewable changes

  • Scalable Foundation - Ready for modules (coming later)


Complete Project Structure

day-06-terraform-project/
├── main.tf                 # All resource definitions (S3, VPC, EC2)
├── variables.tf            # Input variable declarations
├── locals.tf               # Local computed values
├── outputs.tf              # Output values
├── providers.tf            # AWS provider configuration
├── versions.tf             # Terraform & provider versions
├── backend.tf              # S3 remote state backend
├── terraform.tfvars        # Environment variables (NEVER commit!)
├── terraform.tfvars.example # GitHub-safe variable template
├──.gitignore              # Exclude sensitive files
└── README.md               # Project documentation

Root directory = Root Module - Terraform auto-detects all .tf files.


File-by-File Implementation Guide

1. main.tf - Infrastructure Resources

Purpose: Define all AWS resources (S3, VPC, EC2, etc.)


// create S3 bucket
resource "aws_s3_bucket" "first_bucket" {
  bucket = "anjali-gupta-bucket-76"
#   region = "eu-north-1"
region = var.region

  tags = {
    Name        = "My bucket"
    Environment = var.environment
  }
}

// create one sample resource
resource "aws_vpc" "sample" {
    cidr_block = "10.0.1.0/24"
    # region = "eu-north-1"
    region = var.region
    tags = {
      Environment=var.environment
    #   Name= "${var.environment}-VPC"
        Name= local.vpc_name
    } 
}



// create one ec2 instance
resource "aws_instance" "example" {
  ami           = "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"  
  instance_type = "t2.micro"
#   region = "eu-north-1"
    region = var.region

  tags = {
    Environment=var.environment

    # Name = "${var.environment}-Instance"
    Name = local.instance_name
  }
}

When to split further: s3.tf, vpc.tf, ec2.tf for 100+ resources.

2. variables.tf - Input Configuration

Purpose: Declare all configurable parameters with types and descriptions.


// variable named it environment
variable "environment" {
  type        = string
  default     = "dev"
}


variable "region" {
  type        = string
  default     = "eu-north-1"
}

3. locals.tf - Reusable Computed Values

Purpose: Complex expressions used across resources.


// locals
# locals {
#     // can use like this also 
#  env = var.environment
#  vpc_name = "${local.env}-VPC"
#  instance_name = "${local.env}-Instance"
# }


locals {
  vpc_name = "${var.environment}-VPC"
  instance_name = "${var.environment}-Instance"
}

4. outputs.tf - Resource Information

Purpose: Expose key values for other modules/CLI.


output "instance_id" {
    value = aws_instance.example.id
}

output "vpc_id" {
    value = aws_vpc.sample.id
}

5. providers.tf - AWS Provider Setup

Purpose: Provider configuration and aliases.

 terraform {
    required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
 }

# Configure the AWS Provider
provider "aws" {
  region = "eu-north-1"
}

6. versions.tf - Version Constraints

Purpose: Ensure reproducible builds.

terraform {
  required_version = ">= 1.5.0"

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

7. backend.tf - Remote State Management

Purpose: S3 backend with DynamoDB locking.

terraform {

    backend "s3" {
        bucket = "anjali-gupta-bucket-76"
        key    = "terraform.tfstate"
        region = "eu-north-1"
        use_lockfile = true
    }
}

Git Security Checklist (.gitignore)

Critical: Never commit sensitive data to GitHub.

.terraform*
*.tfstate
*.tfstate.backup    
.terraform.lock.hcl
crash.log
*.log
terraform.tfvars
*.tfvars.json
.terraform/

terraform.tfvars.example (GitHub Safe)

aws_region     = "us-east-1"
environment    = "dev"
bucket_name    = "my-app-bucket-123"
vpc_cidr       = "10.0.0.0/16"
ami_id         = "ami-0abcdef1234567890"
instance_type  = "t3.micro"

Workflow:

  1. cp terraform.tfvars.example terraform.tfvars

  2. Edit sensitive values in terraform.tfvars

  3. Commit only .example file

Demo sequence:

  1. Cut backend blockbackend.tf

  2. Cut provider blockproviders.tf

  3. Cut variable declarationsvariables.tf

  4. Cut locals blocklocals.tf

  5. Cut outputsoutputs.tf

  6. Create .gitignore + terraform.tfvars.example

Result: Clean, green VSCode - Terraform auto-detects all files!

Advanced Multi-Environment Structure

terraform-project/
│
├── README.md                  # Project documentation
├── .gitignore                 # Git ignore file for Terraform
├── .terraform-version         # Terraform version specification
│
└── environments/              # Environment specific configurations
    │
    ├── dev/
    │   ├── main.tf            # Main configuration for dev
    │   ├── variables.tf       # Variable declarations
    │   ├── terraform.tfvars   # Variable values
    │   ├── outputs.tf         # Output values
    │   └── backend.tf         # Backend configuration
    │
    └── staging/
        ├── main.tf
        ├── variables.tf
        ├── terraform.tfvars
        ├── outputs.tf
        └── backend.tf

Module Based Approach:

Two strategies:

  1. Separate main.tf per environment - Different resources

  2. Shared main.tf + environment tfvars - Same infra, different values


Quick Start Commands

# Initialize (downloads providers)
terraform init

# Plan (shows what will change)
terraform plan

# Apply (creates infrastructure)
terraform apply

# Destroy (removes everything)
terraform destroy

Key Takeaways

  • Root Module = Root directory with .tf files

  • Flexible naming - outputs.tf or output.tf both work

  • Modules later - For complex projects with 100+ resources

  • S3 Backend = Team collaboration essential

  • Gitignore = Security first, never commit state files

  • tfvars.example = Shareable template for GitHub

Video:

#30daysTerraformwithAWS

More from this blog

Terraform lac with AWS challenge

27 posts