Back to blog
February 2026 6 min read

Terraform 101: Plan Before You Break Production

A practical primer on Terraform — installation, the init/plan/apply workflow, variable types, tainting resources, targeted destroys, and setting up AWS credentials for your first provider.

Terraform IaC AWS DevOps Infrastructure

Terraform's entire value proposition is one sentence: describe the infrastructure you want, and let the tool figure out how to get there safely. The workflow that makes that safe is initplanapply — and understanding exactly what each step does (and doesn't do) is what separates confident infrastructure changes from anxious ones.


Installing Terraform on Linux (RHEL/CentOS)

sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum -y install terraform

For other distros and OSes, HashiCorp's official install page has the exact steps.

Terraform configuration can be written in two formats: native .tf (HCL syntax) or .tf.json (pure JSON). Both are fully supported and interchangeable — HCL is what you'll see in almost every real-world codebase since it's far more readable, but .tf.json exists for cases where configuration is generated programmatically.

hello.tf (HCL):

output "hello" {
  value = "Hello World"
}

hello.tf.json (equivalent in JSON):

{
  "output": {
    "hello1": {
      "value": "Hello world"
    }
  }
}

The Core Workflow: init → plan → apply

terraform init

Initializes the working directory: downloads the provider plugins your configuration references and sets up the backend used to store state.

terraform init

Run it when: you first clone a repo with Terraform configs, whenever you add or change a provider, or after modifying backend configuration.

terraform plan

Compares your current state against the configuration and produces an execution plan — exactly what will be created, changed, or destroyed. It makes no real changes.

terraform plan

Run it: always, before apply. This is your review step — the whole point of Terraform's safety model is that nothing touches real infrastructure until you've seen the diff.

terraform apply

Executes the plan: creates, updates, or destroys resources as needed, and updates the state file to match.

terraform apply

Run it: only after you've reviewed the plan output and are confident in it.

| Command | Purpose | When | Effect on infra | |---|---|---|---| | terraform init | Sets up providers/backend | First-time setup, provider changes | None — just prepares the environment | | terraform plan | Previews changes | Before every apply | None — read-only diff | | terraform apply | Executes changes | After reviewing the plan | Creates/modifies/destroys real resources |


Passing Variables

Inline, per-run

terraform plan -var "username=Mubin"

From a .tfvars file

Terraform automatically loads terraform.tfvars if present with just a plain terraform plan. To use a differently named file (e.g. per environment):

terraform plan -var-file="dev.tfvars"

From the environment

Prefix any variable name with TF_VAR_ and export it — Terraform picks it up automatically:

export TF_VAR_username=mubin

Variable Types

Terraform's type system (full reference: HashiCorp's expression types docs):

  • string — text, e.g. "hello"
  • number — whole or fractional, e.g. 15 or 6.283185
  • booltrue or false, used in conditionals
  • list (or tuple) — an ordered sequence, e.g. ["us-west-1a", "us-west-1c"], indexed from 0
  • set — a collection of unique values with no ordering or secondary identifiers
  • map (or object) — named key/value pairs, e.g. { name = "Mabel", age = 52 }

And one special non-type:

  • null — represents deliberate absence. Setting an argument to null behaves as if it were omitted entirely: Terraform falls back to a default if one exists, or errors if the argument is required. Most useful in conditional expressions to dynamically drop an argument when a condition isn't met.

Everyday Commands

List configured providers

terraform providers

Skip the interactive confirmation

terraform apply --auto-approve

Tear everything down

terraform destroy
# or, skipping confirmation:
terraform destroy --auto-approve

Destroy a single resource out of many

terraform destroy --target <resource_type>.<resource_name> --auto-approve

Example:

terraform destroy --target github_repository.terraform-test2-repository --auto-approve

Validate configuration syntax

terraform validate

Print a single output value

terraform output repo_id

repo_id here refers to whatever name you gave that value in an output block — it's not a magic keyword.

Inspect variable values interactively

terraform console

Drops you into a REPL where you can evaluate expressions and variable references against the current state — useful for sanity-checking an interpolation before you commit to it in actual config.

Auto-format your .tf files

terraform fmt

Forcing Resource Replacement: taint

Sometimes a resource is technically "fine" according to its state, but you know it's actually broken or in a bad configuration that an in-place update won't fix. taint marks it for destroy-and-recreate on the next apply.

terraform taint <resource_type>.<resource_name>

Example:

terraform taint aws_security_group.allow_tls

To undo:

terraform untaint aws_security_group.allow_tls

taint/untaint are considered legacy — HashiCorp now recommends the -replace flag on apply instead, since it keeps the intent visible in the plan output rather than silently mutating state ahead of time:

terraform apply -replace=aws_security_group.allow_tls

Setting Up AWS as a Provider

Finding an AMI

Ubuntu 24.04 AMI (as of writing): ami-0ad21ae1d0696ad58 — always re-verify the current AMI ID for your target region, since these change per region and per release.

Creating Credentials for Terraform

  1. In IAM, create an admin group with the IAMFullAccess (or broader, as needed) policy attached.
  2. Create a dedicated terraform IAM user and add it to that group — never use root account keys.
  3. Generate an access key and secret key for the terraform user, scoped for CLI/programmatic access.

Treat these keys like any other production secret — inject them via environment variables or a secrets manager (Vault, AWS SSM Parameter Store) rather than hardcoding them into .tf files or committing them to a repo.


Summary

| Task | Command | |---|---|

  • Initialize | terraform init |
  • Preview changes | terraform plan |
  • Apply changes | terraform apply |
  • Skip confirmation | terraform apply --auto-approve |
  • Destroy everything | terraform destroy --auto-approve |
  • Destroy one resource | terraform destroy --target <type>.<name> |
  • Force replacement | terraform apply -replace=<resource_address> |
  • Validate syntax | terraform validate |
  • Format code | terraform fmt |
  • Inspect a value | terraform console |

Golden rule: plan is not optional and not a formality — it's the only thing standing between "reviewed change" and "surprise production incident." Never pipe straight to apply --auto-approve on anything you haven't already planned and read.