Terraform

Automating Terraform with scripts

AGAnurag Gupta04 Apr 2025 Β· Updated 04 Oct 2026 Β· 7 min read
Automating Terraform with scripts

Quick answer: Automating Terraform with scripts means wrapping the init β†’ fmt β†’ validate β†’ plan β†’ apply sequence in a shell script, Makefile or CI pipeline so every run is identical, non-interactive and logged. The key ingredients are the -input=false and -auto-approve flags, saved plan files, environment-specific variable files, and strict error handling so a failed step stops the run instead of silently continuing.

Typing the same five commands by hand works for a demo and falls apart the moment two people share an environment. In this guide you will build a production-quality Bash wrapper, a Makefile with per-environment targets, and see how the same script becomes a CI/CD job β€” along with the pitfalls that catch most teams when they first automate Terraform.

Why script Terraform at all?

Terraform is already declarative, so why add another layer? Because the commands around Terraform are where mistakes happen:

  • Someone forgets terraform init after a provider change and gets a confusing error.
  • Someone applies against production with the dev .tfvars file.
  • Someone approves a plan, gets distracted, and applies a different plan twenty minutes later.
  • Formatting and validation are skipped “just this once”.

A script encodes the correct sequence once. Everyone β€” including your CI runner β€” then executes the same path. The sections below move from the simplest wrapper to a full pipeline. If you need a refresher on the commands themselves, read Terraform Core Concepts and Syntax: A Beginner’s Guide first.

Make Terraform non-interactive

Before writing any script, know the flags that stop Terraform from waiting for a human:

Flag or variable Effect When to use
-input=false Never prompt for missing variables; fail instead Every scripted command
-auto-approve Skip the yes/no confirmation on apply or destroy Only when applying a saved, reviewed plan
-out=tfplan Save the plan to a file Always; apply the file, not a fresh plan
-detailed-exitcode Exit 0 = no changes, 1 = error, 2 = changes pending Drift checks and conditional applies
TF_IN_AUTOMATION=1 Suppresses hints aimed at interactive users CI environments
TF_VAR_name Supplies a variable via the environment Passing secrets from CI without files
-lock-timeout=5m Wait for a state lock rather than failing instantly Shared environments with concurrent runs

A production-ready Bash wrapper

This script takes an environment name and an action, selects the right variable and backend files, and refuses to continue if any step fails. Save it as tf.sh and make it executable with chmod +x tf.sh.

#!/usr/bin/env bash
# Usage: ./tf.sh <env> <plan|apply|destroy>
set -euo pipefail

ENV="${1:?environment required (dev|staging|prod)}"
ACTION="${2:?action required (plan|apply|destroy)}"
DIR="$(cd "$(dirname "$0")" && pwd)"
VAR_FILE="$DIR/envs/$ENV.tfvars"
BACKEND_FILE="$DIR/envs/$ENV.backend.hcl"
PLAN_FILE="$DIR/.plans/$ENV.tfplan"

export TF_IN_AUTOMATION=1
mkdir -p "$DIR/.plans"

[[ -f "$VAR_FILE" ]]     || { echo "Missing $VAR_FILE" >&2; exit 1; }
[[ -f "$BACKEND_FILE" ]] || { echo "Missing $BACKEND_FILE" >&2; exit 1; }

terraform -chdir="$DIR" init -input=false -reconfigure \
  -backend-config="$BACKEND_FILE"
terraform -chdir="$DIR" fmt -check -recursive
terraform -chdir="$DIR" validate

case "$ACTION" in
  plan)
    terraform -chdir="$DIR" plan -input=false -lock-timeout=5m \
      -var-file="$VAR_FILE" -out="$PLAN_FILE"
    ;;
  apply)
    [[ -f "$PLAN_FILE" ]] || { echo "Run plan first" >&2; exit 1; }
    terraform -chdir="$DIR" apply -input=false -lock-timeout=5m "$PLAN_FILE"
    rm -f "$PLAN_FILE"
    ;;
  destroy)
    [[ "$ENV" == "prod" ]] && { echo "Refusing to destroy prod from a script" >&2; exit 1; }
    terraform -chdir="$DIR" destroy -input=false -var-file="$VAR_FILE" -auto-approve
    ;;
  *)
    echo "Unknown action: $ACTION" >&2; exit 1 ;;
esac

Things worth noticing:

  • set -euo pipefail stops the script on the first error, on unset variables, and on failures inside pipes. Without it, a failed validate would be followed by an apply.
  • Backend settings live in envs/<env>.backend.hcl and are passed with -backend-config, so one codebase can target different state files per environment.
  • apply only ever consumes a saved plan. Deleting the plan after a successful apply prevents the same plan being applied twice.
  • Production destroy is blocked outright. Deliberate friction around destructive actions is a feature.

Environment-specific values belong in the .tfvars files; see Using Variables and Expressions in Terraform for how to structure them.

A Makefile for discoverable commands

Makefiles are popular because make plan ENV=dev is easy to remember and tab-complete. The Makefile can call the wrapper above or run Terraform directly:

ENV ?= dev
TF  := terraform -chdir=infra
VAR := -var-file=envs/$(ENV).tfvars

.PHONY: init fmt validate plan apply destroy lint

init:
	$(TF) init -input=false -backend-config=envs/$(ENV).backend.hcl

fmt:
	$(TF) fmt -recursive

validate: init
	$(TF) validate

lint: validate
	tflint --chdir=infra
	trivy config infra

plan: lint
	$(TF) plan -input=false $(VAR) -out=.plans/$(ENV).tfplan

apply:
	$(TF) apply -input=false .plans/$(ENV).tfplan

destroy:
	@[ "$(ENV)" != "prod" ] || (echo "refusing to destroy prod"; exit 1)
	$(TF) destroy -input=false $(VAR)

Make’s dependency chain (plan needs lint, which needs validate, which needs init) means a developer cannot skip steps by accident. Remember that Makefile recipes must be indented with a real tab character, not spaces.

From scripts to CI/CD

Once the script exists, a pipeline is just the same script run by a machine on every pull request and merge. In GitHub Actions the pattern is: on pull request, run ./tf.sh staging plan and post the plan as a comment; on merge to main, run plan then apply in a protected environment that requires a reviewer’s approval. GitLab CI, Jenkins and AWS CodePipeline follow identical logic with different YAML. Authenticate the runner with OIDC rather than stored access keys, and store the plan as a pipeline artifact so the apply job uses exactly what was reviewed.

Tools such as Atlantis, HCP Terraform, Spacelift and env0 take this further by running plan on every PR automatically and applying on comment or merge β€” useful once you have more than a handful of root modules.

Seven automation mistakes to avoid

  1. Using -auto-approve on a fresh plan. You approve nothing and apply whatever the world looks like now. Always apply a saved plan file.
  2. No set -e in Bash. A failed validate is followed by a successful-looking apply of broken code.
  3. Hard-coding the environment. Scripts that only know about dev get copied and edited for prod, and the copies drift apart.
  4. Printing secrets. set -x and echo statements leak TF_VAR_db_password into CI logs.
  5. Running concurrent applies against one state. Rely on state locking and serialise pipeline jobs per environment.
  6. Skipping init in the script. Provider or module changes then fail at plan with confusing errors.
  7. Scripting destroy without guard rails. Require an explicit confirmation variable or block it for production entirely.

Frequently asked questions

Should I use Bash, Python or Make to wrap Terraform?

Bash and Make cover 90 percent of needs and are available on every CI runner. Reach for Python (for example with the python-terraform library or subprocess) only when you need complex logic such as generating variable files from an inventory.

Is Terragrunt a replacement for scripts?

Terragrunt is a thin wrapper that handles backend configuration, dependencies between modules and keeping code DRY across environments. It solves many of the same problems as a custom script with less code, and is worth evaluating once you manage several environments.

How do I pass secrets to a scripted run?

Export them as TF_VAR_<name> environment variables from your CI secret store, or fetch them inside Terraform from a secrets manager data source. Never write them into .tfvars files that get committed.

Can a script detect drift automatically?

Yes. Run terraform plan -detailed-exitcode on a schedule; exit code 2 means the live infrastructure differs from code, and your script can send an alert.

Key takeaways

  • Wrap init, fmt, validate, plan and apply in one script so every run follows the same path.
  • Use -input=false, saved plan files and TF_IN_AUTOMATION to make runs non-interactive and reproducible.
  • Parameterise by environment with separate .tfvars and backend files, never by copying scripts.
  • Fail fast with set -euo pipefail and guard destructive actions.
  • The same script becomes your CI/CD job; add OIDC authentication and plan artifacts.

Want to build these pipelines end to end on real cloud accounts? Our DevOps course covers Terraform automation, Bash scripting and CI/CD with live projects, mentor support and placement assistance. For step-by-step demos, subscribe to our YouTube channel.

AG
Written byAnurag Gupta

Part of the Techknowledgehub team of industry mentors, writing practical guides to help you build a job-ready tech career.

More articles by Anurag Gupta β†’
Keep reading

Related articles

Leave a Reply