Wrap Terraform with guardrails
fmt, validate, plan, approve, apply — in that order, every time. Wrap your Terraform commands in a workflow so the safety steps aren't optional and the apply is gated behind a confirm and a dry-run.
blog/terraform-guardrailsEveryone knows the safe Terraform sequence: fmt, validate, plan, look at the plan, then apply. Everyone also knows the 11pm version where someone runs terraform apply straight, skips the plan review, and learns something new about their state file.
The sequence isn't hard. Keeping it non-optional is. A workflow makes the safe order the only order — and puts a human confirm and a dry-run between you and a production apply.
The pipeline as tasks
Each Terraform command is a task. plan calls fmt and validate first, so you physically cannot plan without formatting and validating. apply calls plan, then gates the real apply behind a prompt.
name: terraform
defaults:
stage: staging
workdir: infra
tasks:
fmt:
steps:
- name: fmt
func: shell
do: echo "terraform -chdir={{ vars.workdir }} fmt -check -recursive"
validate:
steps:
- name: init
func: shell
do: echo "terraform -chdir={{ vars.workdir }} init -backend=false"
- name: validate
func: shell
do: echo "terraform -chdir={{ vars.workdir }} validate"
# orchstep run plan --var stage=production
plan:
steps:
- name: fmt
task: fmt
- name: validate
task: validate
- name: plan
func: shell
do: echo "terraform -chdir={{ vars.workdir }} plan -out=tfplan ({{ vars.stage }})"
# orchstep run apply --var stage=production --dry-run
apply:
desc: "Plan, ask for approval, then apply"
steps:
- name: plan
task: plan
- name: approve
func: prompt
args:
message: "Apply this plan to {{ vars.stage }}?"
type: confirm
default: false
- name: gate
if: '{{ eq steps.approve.value "true" }}'
then:
- name: apply
func: shell
do: echo "terraform -chdir={{ vars.workdir }} apply tfplan"
else:
- name: hold
func: shell
do: echo "apply to {{ vars.stage }} not approved; nothing changed"The tasks compose. apply -> plan -> fmt + validate is wired by task: references, so the guardrails run in order whether you start at plan or at apply. The confirm prompt defaults to false: in CI, where nobody is at the keyboard, the gate takes the else: branch and refuses to apply rather than silently shipping.
The two-command habit
orchstep run apply --var stage=production --dry-runDry-run first, every time. It resolves the variables, walks the composed tasks, and shows you exactly which steps would run — including that the apply is the prompt-gated branch — without executing anything.
When the plan looks right, drop the flag:
orchstep run apply --var stage=productionNow it runs fmt, validate, plan, and stops at the confirm. You type y or you don't. There's no path where the apply happens because you forgot a step.
What you gained
| Concern | Bare terraform | OrchStep wrapper |
|---|---|---|
| Run order | discipline | enforced by task: composition |
| Skip the plan | one command away | impossible — apply calls plan |
| Apply guard | none | prompt confirm, defaults to no |
| CI safety | applies if invoked | else: branch holds |
| Preview | terraform plan | --dry-run over the whole pipeline |
Where this is too much
If you already run Terraform through Atlantis, Terraform Cloud, or a Spacelift pipeline, that is your guardrail layer — don't double-wrap it. This pattern is for the team running Terraform from laptops and a thin CI job, who want the safe sequence to be one command instead of a wiki page nobody reads.
Where to go next
- The prompt function — confirms, text, and CI-safe defaults
- Error Handling — conditionals and gating
- Previewing with Dry Run — preview the composed pipeline
The project above is a runnable, echo-only demo. Swap the echos for real terraform commands once the order is wired the way you want.
curl -fsSL https://orchstep.dev/install.sh | sh