KiaMation

Terraform Best Practices for Beginners

Terraform Workflow Diagram

Essential patterns and anti-patterns when starting with infrastructure as code, learned from managing personal projects.

Introduction

Managing infrastructure as code (IaC) with Terraform can streamline deployments, improve reproducibility, and reduce manual errors. However, beginners often fall into common pitfalls that lead to unmaintainable code or security risks. Here are essential best practices to follow from day one.

1. Use a Consistent File & Directory Structure

A well-organized structure improves readability and scalability.

Recommended Structure:

📁 terraform-project/
├── 📁 modules/          # Reusable components
│   ├── 📁 vpc/
│   ├── 📁 ec2/
├── 📁 environments/     # Environment-specific configs
│   ├── 📁 dev/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── terraform.tfvars
│   ├── 📁 prod/
├── 📄 README.md         # Documentation

Do:

  • Separate environments (dev/staging/prod) to avoid accidental changes
  • Use modules for reusable components (e.g., VPC, databases)

Avoid:

  • Monolithic main.tf files with hundreds of lines
  • Hardcoding values directly in .tf files

2. Leverage Variables & terraform.tfvars

Hardcoding values makes your code inflexible and harder to maintain.

Proper Variable Usage:

# variables.tf
variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.micro"
}

# terraform.tfvars (environment-specific)
instance_type = "t3.small"

Anti-Pattern Example:

resource "aws_instance" "example" {
  instance_type = "t3.micro"  # Hardcoded value
}

3. Always Use State Management

Terraform state (terraform.tfstate) tracks resource mappings. Losing it can cause chaos.

Remote State with S3 and Locking:

terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "dev/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-lock"
  }
}

Avoid:

  • Committing .tfstate to Git (contains secrets!)
  • Ignoring state locking (risks corruption with multiple users)

4. Implement Least-Privilege IAM Policies

Over-permissive IAM roles are a major security risk.

Secure IAM Example:

resource "aws_iam_role" "ec2_role" {
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Action = "sts:AssumeRole",
      Effect = "Allow",
      Principal = { Service = "ec2.amazonaws.com" }
    }]
  })
}

Dangerous Anti-Pattern:

resource "aws_iam_role_policy_attachment" "admin_access" {
  role       = aws_iam_role.ec2_role.name
  policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"  # 🚨 Dangerous!
}

5. Plan & Review Before Applying

Always preview changes to avoid surprises.

terraform plan -out=tfplan
terraform apply tfplan

Avoid:

terraform apply --auto-approve # Skips review!

6. Use Version Constraints

Unpinned provider versions can break your setup.

Proper Version Pinning:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"  # Allows patches, not major upgrades
    }
  }
}

7. Document with README.md & Comments

Future you (or teammates) will thank you.

Good Documentation Example:

# This NACL rule allows SSH only from the office IP
ingress {
  from_port   = 22
  to_port     = 22
  cidr_blocks = ["192.0.2.0/24"]
}

Final Thoughts

Following these practices early prevents technical debt and security issues. Start small, automate wisely, and always plan before applying!

Further Reading: