Terraform Code Organization
Introduction
Organizing your Terraform code is crucial for maintaining clean, scalable, and reusable infrastructure as code. As your infrastructure grows in complexity, well-structured code becomes essential for collaboration, testing, and long-term maintenance. This guide covers best practices for organizing Terraform code, from basic file structures to advanced modular designs.
Why Code Organization Matters
Good Terraform code organization provides several benefits:
- Maintainability: Easier to understand and modify the codebase
- Reusability: Promotes component reuse across projects
- Collaboration: Enables team members to work on different components simultaneously
- Scalability: Supports growing infrastructure without increasing complexity
- Testing: Facilitates testing of individual components
Basic File Structure
Let's start with a simple but effective file structure for a Terraform project:
project-root/
├── main.tf # Primary entry point
├── variables.tf # Input variables
├── outputs.tf # Output values
├── providers.tf # Provider configurations
├── versions.tf # Terraform and provider version constraints
└── terraform.tfvars # Variable values (not committed to version control)
Example: Basic File Structure
Here's how you might organize a simple AWS VPC deployment:
versions.tf
terraform {
required_version = ">= 1.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 4.0.0"
}
}
}
providers.tf
provider "aws" {
region = var.aws_region
}
variables.tf
variable "aws_region" {
description = "AWS region to deploy resources"
type = string
default = "us-west-2"
}
variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}
variable "environment" {
description = "Deployment environment"
type = string
default = "dev"
}
main.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
}
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, 1)
availability_zone = "${var.aws_region}a"
tags = {
Name = "${var.environment}-public-subnet"
Environment = var.environment
}
}
outputs.tf
output "vpc_id" {
description = "ID of the created VPC"
value = aws_vpc.main.id
}
output "public_subnet_id" {
description = "ID of the public subnet"
value = aws_subnet.public.id
}
Workspaces for Multiple Environments
For managing multiple environments (dev, staging, production), Terraform workspaces provide a simple solution:
# Create workspaces
terraform workspace new dev
terraform workspace new staging
terraform workspace new production
# Select workspace
terraform workspace select dev
Then, you can use the workspace in your configuration:
locals {
environment = terraform.workspace
# Environment-specific configurations
cidr_blocks = {
dev = "10.0.0.0/16"
staging = "10.1.0.0/16"
production = "10.2.0.0/16"
}
}
resource "aws_vpc" "main" {
cidr_block = local.cidr_blocks[local.environment]
tags = {
Name = "${local.environment}-vpc"
Environment = local.environment
}
}
Using Modules for Code Reuse
Modules are containers for multiple resources that are used together. A module can encapsulate a specific piece of infrastructure logic like a VPC, a database cluster, or a complete application stack.
Module Structure
modules/
├── vpc/
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ └── README.md
├── rds/
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ └── README.md
└── ec2/
├── main.tf
├── variables.tf
├── outputs.tf
└── README.md
Example: Creating and Using a VPC Module
First, let's create a VPC module:
modules/vpc/variables.tf
variable "cidr_block" {
description = "CIDR block for the VPC"
type = string
}
variable "environment" {
description = "Deployment environment"
type = string
}
variable "public_subnet_count" {
description = "Number of public subnets to create"
type = number
default = 2
}
modules/vpc/main.tf
resource "aws_vpc" "this" {
cidr_block = var.cidr_block
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
}
}
data "aws_availability_zones" "available" {}
resource "aws_subnet" "public" {
count = var.public_subnet_count
vpc_id = aws_vpc.this.id
cidr_block = cidrsubnet(var.cidr_block, 8, count.index)
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.environment}-public-subnet-${count.index + 1}"
Environment = var.environment
}
}
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
tags = {
Name = "${var.environment}-igw"
Environment = var.environment
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.this.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this.id
}
tags = {
Name = "${var.environment}-public-rt"
Environment = var.environment
}
}
resource "aws_route_table_association" "public" {
count = var.public_subnet_count
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
modules/vpc/outputs.tf
output "vpc_id" {
description = "ID of the created VPC"
value = aws_vpc.this.id
}
output "public_subnet_ids" {
description = "IDs of the public subnets"
value = aws_subnet.public[*].id
}
Now, use this module in your root configuration:
main.tf
module "vpc" {
source = "./modules/vpc"
cidr_block = "10.0.0.0/16"
environment = var.environment
public_subnet_count = 2
}
# Use the VPC outputs
resource "aws_security_group" "example" {
name = "${var.environment}-example-sg"
description = "Example security group"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.environment}-example-sg"
Environment = var.environment
}
}
Environment-Specific Configurations
For a more robust environment separation, use directory-based organization:
environments/
├── dev/
│ ├── main.tf
│ ├── variables.tf
│ └── terraform.tfvars
├── staging/
│ ├── main.tf
│ ├── variables.tf
│ └── terraform.tfvars
└── production/
├── main.tf
├── variables.tf
└── terraform.tfvars
Each environment directory uses the same modules but with different configurations:
environments/dev/main.tf
module "vpc" {
source = "../../modules/vpc"
cidr_block = var.vpc_cidr
environment = "dev"
public_subnet_count = 2
}
module "app" {
source = "../../modules/app"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.public_subnet_ids
environment = "dev"
instance_type = "t3.small"
instance_count = 2
}
environments/production/main.tf
module "vpc" {
source = "../../modules/vpc"
cidr_block = var.vpc_cidr
environment = "production"
public_subnet_count = 3
}
module "app" {
source = "../../modules/app"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.public_subnet_ids
environment = "production"
instance_type = "t3.large"
instance_count = 5
}
Terraform Project Structure Visualization
Here's a visualization of a comprehensive Terraform project structure:
Advanced Organization Techniques
Remote State Management
For team collaboration, use remote state storage:
backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "environments/dev/terraform.tfstate"
region = "us-west-2"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
Using Terragrunt for DRY Configurations
Terragrunt is a thin wrapper for Terraform that provides extra tools for keeping configurations DRY:
terragrunt.hcl
remote_state {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-west-2"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
inputs = {
aws_region = "us-west-2"
}
Component-Based Structure
For large projects, a component-based structure can be effective:
infrastructure/
├── networking/
│ ├── vpc/
│ ├── dns/
│ └── cdn/
├── data/
│ ├── rds/
│ ├── elasticache/
│ └── s3/
└── compute/
├── ecs/
├── ec2/
└── lambda/
Best Practices Summary
- Separate Configuration by Environment: Use separate directories or workspaces for dev, staging, and production.
- Use Modules for Reusability: Create reusable modules for common infrastructure components.
- Version Control Configuration: Store all Terraform code in version control, except sensitive values.
- Document Your Code: Add READMEs to explain the purpose and usage of modules.
- Use Remote State: Store state remotely for team collaboration and state locking.
- Keep It DRY: Avoid duplicating code and configurations.
- Validate and Format: Use
terraform fmt
andterraform validate
to ensure code quality. - Use Variables and Locals: Parameterize your code with variables and use locals for derived values.
- Implement Consistent Naming: Follow a consistent naming convention for resources.
- Test Your Modules: Verify that modules work as expected in isolation.
Implementing Terraform Code Organization in a Real Project
Let's walk through a real-world example of refactoring a monolithic Terraform configuration into a well-organized structure:
Before: Monolithic Configuration
# Single main.tf file with everything
provider "aws" {
region = "us-west-2"
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "public1" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
}
resource "aws_subnet" "public2" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
}
resource "aws_instance" "app" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
subnet_id = aws_subnet.public1.id
}
resource "aws_db_instance" "database" {
allocated_storage = 20
engine = "mysql"
engine_version = "5.7"
instance_class = "db.t3.micro"
name = "mydb"
username = "admin"
password = "password"
skip_final_snapshot = true
}
After: Well-Organized Structure
After refactoring, we'll have:
- A module for networking
- A module for the database
- A module for the application
- Environment-specific configurations
Directory Structure
project/
├── modules/
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── database/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── app/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── environments/
├── dev/
│ ├── main.tf
│ ├── variables.tf
│
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)