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 fmt and validate in CI.
  • Always read the plan before applying.

Avoid

  • Editing infrastructure by hand outside Terraform (causes drift).
  • Committing state files or secrets to Git.
  • Overusing provisioners — treat local-exec/remote-exec as a last resort, since they are imperative and fragile.

Example

Example · hcl
# 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.

Example · bash
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.tf

Commit the lock file

Explains that .terraform.lock.hcl must be committed so all team members and CI use provider binaries with verified checksums.

Example · bash
# 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 ignore

Detailed exit code in CI

Uses -detailed-exitcode to distinguish three plan outcomes in a CI script: no changes, error, or pending changes requiring apply.

Example · bash
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

  • Be the first to comment on this lesson.