Terraform Tutorial · Modules and Quality · Lesson 24 of 25

Terraform CI/CD and Best Practices

Run Terraform safely in a team: plans on pull requests, applies from CI, formatting and security checks, and the habits that keep infrastructure code healthy.

A typical pipeline

  1. On every pull request: terraform fmt -check, terraform validate, terraform test, a security scan (Checkov, Trivy or tfsec), then terraform plan, with the plan posted as a PR comment.
  2. Review the plan, not just the code.
  3. On merge: terraform apply of the saved plan file (plan -out=tfplan, then apply tfplan), with a manual approval for production.
terraform init -input=false
terraform plan -input=false -out=tfplan
terraform apply -input=false tfplan

CI should authenticate with short-lived credentials (for example GitHub Actions OIDC to an AWS IAM role), not stored access keys.

Best practices checklist

  • Remote state with locking and versioning; one state per environment.
  • Pin Terraform, provider and module versions; commit .terraform.lock.hcl.
  • Small, composable modules; no copy-paste between environments.
  • Consistent naming and tags (ManagedBy = "terraform").
  • Secrets come from a secrets manager, never from .tf or .tfvars in Git.
  • Never change Terraform-managed resources by hand; if you must, plan -refresh-only afterwards.
  • Keep plans small: many small applies are safer than one huge one.
A tiny policy check in plain Terraform
locals {
  required_tags = ["Project", "Environment", "ManagedBy"]
  tags = {
    Project     = "shop"
    Environment = "prod"
    ManagedBy   = "terraform"
  }
  missing = [for t in local.required_tags : t if !contains(keys(local.tags), t)]
}

output "tag_policy" {
  value = length(local.missing) == 0 ? "all required tags present" : "missing: ${join(", ", local.missing)}"
}

Output

tag_policy = "all required tags present"

Run this on your computer — see “Get Started” for setup.

Test yourself

Why apply a saved plan file in CI?

  1. It is faster
  2. So exactly the reviewed changes are applied
  3. It skips state locking
  4. It avoids init
Show answer

Answer: So exactly the reviewed changes are applied