Terraform 43: CI/CD — plan in PR, gated apply
easy⏱ 5 mincourseterraform
Remote state is the foundation
CI has no local terraform.tfstate. A remote backend gives every run the same state plus a lock so two pipelines can't apply at once. On Azure that's a Storage Account + container; the key isolates each stack/env.
terraform {
backend "azurerm" {
resource_group_name = "myorg-tfstate-dev-rg"
storage_account_name = "myorgtfstatedev"
container_name = "tfstate"
key = "orders/dev.tfstate"
}
}
Pull requests get a plan, not an apply
On every PR the pipeline runs fmt -check, validate, and plan, then posts the plan for review. Reviewers approve the diff, not just the code. Nothing changes in the cloud from a PR.
# pr-validation pipeline (runs on pull_request)
steps:
- terraform fmt -check -recursive
- terraform init -input=false
- terraform validate
- terraform plan -input=false -out=tfplan # posted to the PR
Apply is gated and merge-only
The apply stage triggers only on merge to main, applies the saved plan, and sits behind a manual environment approval for uat/prod. Same artifact you reviewed gets applied — no drift between plan and apply.
# apply pipeline (runs on push to main)
environment: prod # requires manual approval
steps:
- terraform init -input=false
- terraform apply -input=false -auto-approve tfplan
No local applies. Ever.
A laptop terraform apply bypasses review, uses whoever's personal creds, and races the pipeline for the state lock. CI holds the only credentials that can mutate infra; humans open PRs and approve — they don't apply.
Decide the pipeline stage
Write pipelineStage(event) where event = { type: "pull_request" | "push"; branch: string; env: string }. Return "plan" for a pull_request; for a push to main return "gated-apply" when env is uat/prod, else "apply"; any other push returns "skip". Log the three provided cases. A PR must print plan; a push to main on prod must print gated-apply.