Best Practices
Follow proven conventions for structure, state, and safety to keep Terraform maintainable at scale.
A few habits keep Terraform projects healthy as they grow.
Do
- Use remote state with locking for any shared project.
- Split code into
main.tf,variables.tf,outputs.tf. - Pin provider and module versions.
- Run
fmtandvalidatein CI. - Always read the
planbefore applying.
Avoid
- Editing infrastructure by hand outside Terraform (causes drift).
- Committing state files or secrets to Git.
- Overusing provisioners — treat
local-exec/remote-execas a last resort, since they are imperative and fragile.
Example
# A provisioner is a last resort; prefer user_data or a native resource.
resource "aws_instance" "web" {
ami = "ami-0c02fb55956c7d316"
instance_type = "t3.micro"
# Preferred: declarative bootstrap via user_data
user_data = <<-EOF
#!/bin/bash
yum install -y nginx
systemctl enable --now nginx
EOF
}When to use it
- A team enforces a standard module layout (main.tf, variables.tf, outputs.tf, versions.tf) across all repositories so any engineer can navigate any module instantly.
- An organization locks the .terraform.lock.hcl file in Git so all engineers and CI pipelines use identical provider checksums, preventing supply-chain surprises.
- An SRE uses terraform plan -detailed-exitcode in CI so the pipeline distinguishes between 'no changes' (exit 0), 'changes exist' (exit 2), and 'error' (exit 1).
More examples
Standard repository layout
Shows a directory layout that separates reusable modules from environment-specific root configurations.
infrastructure/
modules/
vpc/ # main.tf, variables.tf, outputs.tf, versions.tf
rds/
ecs-service/
environments/
dev/
main.tf # calls modules with dev-specific vars
backend.tf
prod/
main.tf # calls same modules with prod-specific vars
backend.tfCommit the lock file
Explains that .terraform.lock.hcl must be committed so all team members and CI use provider binaries with verified checksums.
# Always commit the provider lock file
git add .terraform.lock.hcl
git commit -m "Lock provider checksums"
# .gitignore: never ignore the lock file
# .terraform/ <- ignore this (local cache)
# *.tfvars <- ignore sensitive var files
# .terraform.lock.hcl <- DO NOT ignoreDetailed exit code in CI
Uses -detailed-exitcode to distinguish three plan outcomes in a CI script: no changes, error, or pending changes requiring apply.
set +e
terraform plan -detailed-exitcode -out=tfplan
EXIT_CODE=$?
set -e
case $EXIT_CODE in
0) echo "No changes. Infrastructure is up to date." ;;
1) echo "Error during plan"; exit 1 ;;
2) echo "Changes detected. Proceeding to apply."
terraform apply tfplan ;;
esac
Discussion