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'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 init → plan → apply — 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.15or6.283185bool—trueorfalse, used in conditionalslist(ortuple) — an ordered sequence, e.g.["us-west-1a", "us-west-1c"], indexed from0set— a collection of unique values with no ordering or secondary identifiersmap(orobject) — named key/value pairs, e.g.{ name = "Mabel", age = 52 }
And one special non-type:
null— represents deliberate absence. Setting an argument tonullbehaves 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_idhere refers to whatever name you gave that value in anoutputblock — 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
- In IAM, create an
admingroup with theIAMFullAccess(or broader, as needed) policy attached. - Create a dedicated
terraformIAM user and add it to that group — never use root account keys. - Generate an access key and secret key for the
terraformuser, 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
.tffiles 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.