Terraform Best Practices for Beginners

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.
Avoid:
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: