Infrastructure as Code: Terraform Best Practices for 2024
Infrastructure as Code: Terraform Best Practices for 2024
Terraform has become the de facto standard for infrastructure as code. Here are the essential best practices for writing production-grade Terraform configurations.
Project Structure
Recommended Directory Layout
terraform/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ └── production/
├── modules/
│ ├── networking/
│ ├── compute/
│ └── database/
└── global/
└── backend.tf
State Management
Remote Backend Configuration
terraform {
backend "s3" {
bucket = "mycompany-terraform-state"
key = "production/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
State Locking
Always enable state locking to prevent concurrent modifications:
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Module Design
Create Reusable Modules
# modules/vpc/main.tf
variable "environment" {
type = string
}
variable "cidr_block" {
type = string
default = "10.0.0.0/16"
}
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
ManagedBy = "terraform"
}
}
output "vpc_id" {
value = aws_vpc.main.id
}
Use Module Sources
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
name = "production-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
}
Variable Management
Use terraform.tfvars
# variables.tf
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
validation {
condition = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
error_message = "Instance type must be t3.micro, t3.small, or t3.medium."
}
}
# terraform.tfvars
instance_type = "t3.medium"
Sensitive Variables
variable "database_password" {
description = "RDS master password"
type = string
sensitive = true
}
output "db_endpoint" {
value = aws_db_instance.main.endpoint
sensitive = false
}
Security Best Practices
Secrets Management
Never commit secrets to version control:
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "production/db/password"
}
resource "aws_db_instance" "main" {
password = data.aws_secretsmanager_secret_version.db_password.secret_string
}
Resource Tagging
locals {
common_tags = {
Environment = var.environment
Project = "myproject"
ManagedBy = "terraform"
CostCenter = "engineering"
}
}
resource "aws_instance" "web" {
tags = merge(
local.common_tags,
{
Name = "web-server"
Role = "frontend"
}
)
}
Testing & Validation
Pre-commit Hooks
# .pre-commit-config.yaml
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.83.5
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_docs
- id: terraform_tflint
Terratest for Integration Testing
func TestVPCCreation(t *testing.T) {
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: "../examples/vpc",
})
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
vpcId := terraform.Output(t, terraformOptions, "vpc_id")
assert.NotEmpty(t, vpcId)
}
CI/CD Integration
GitHub Actions Workflow
name: Terraform
on:
push:
branches: [main]
pull_request:
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.6.0
- name: Terraform Format
run: terraform fmt -check -recursive
- name: Terraform Init
run: terraform init
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
run: terraform plan -out=tfplan
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: terraform apply tfplan
Performance Optimization
Use Data Sources Efficiently
# Cache AMI lookups
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
}
locals {
ami_id = data.aws_ami.ubuntu.id
}
Parallel Resource Creation
resource "aws_instance" "web" {
count = 10
# Terraform creates these in parallel
}
Common Pitfalls
- Not using terraform.lock.hcl: Always commit this file
- Direct resource deletion: Use
terraform destroyinstead - Hardcoded values: Use variables and data sources
- No state backup: Enable versioning on state buckets
- Ignoring provider versions: Pin to specific versions
Advanced Patterns
Dynamic Blocks
resource "aws_security_group" "main" {
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}
For_each vs Count
# Prefer for_each for better state management
resource "aws_instance" "servers" {
for_each = toset(["web", "app", "db"])
tags = {
Name = "${each.key}-server"
}
}
Monitoring & Alerts
Terraform Cloud Integration
terraform {
cloud {
organization = "mycompany"
workspaces {
name = "production"
}
}
}
Conclusion
Following these best practices will help you:
- Write maintainable code
- Avoid common pitfalls
- Scale infrastructure safely
- Collaborate effectively
The key is consistency across your team and continuous improvement of your IaC processes.
Need help with Terraform implementation? Let's connect!