Core workflow
terraform init
Download providers and modules and configure the backend. Run it first in any new checkout.
terraform init -upgrade
-upgrade- Allow newer provider and module versions within constraints.
-backend-config=FILE- Supply backend settings per environment.
-reconfigure- Ignore the saved backend configuration.
-migrate-state- Copy state to a newly configured backend.
Gotcha Commit .terraform.lock.hcl so every machine and CI run uses the same provider builds.
terraform fmt -recursive && terraform validate
Normalize formatting and check that the configuration is internally consistent, without calling any API.
terraform fmt -check -recursive && terraform validate
fmt -check- Exit non-zero if files need formatting (for CI).
fmt -recursive- Include subdirectories such as modules.
validate- Syntax, references and types; requires init.
terraform plan -out=<file>
Compute the changes needed and save the exact plan, so apply performs only what you reviewed.
terraform plan -out=tfplan
-out=FILE- Save the plan for apply.
-var-file=FILE- Load variables, for example prod.tfvars.
-target=ADDR- Plan a subset; for emergencies only.
-detailed-exitcode- Exit 0 no changes, 1 error, 2 changes present.
Reading a plan
Read the summary line first, then every replace and destroy.
Plan: 1 to add, 2 to change, 1 to destroy.
Any -/+ on a database, disk or load balancer deserves a second pair of eyes.
- + create
- A new resource.
- ~ update in-place
- Changed attributes; no replacement.
- -/+ replace
- Destroyed and recreated because a ForceNew attribute changed.
- +/- create before destroy
- Replacement with lifecycle create_before_destroy.
- - destroy
- Deleted.
- <= read
- A data source read during apply.
- # forces replacement
- The attribute that caused a replace.
terraform apply <planfile>
Apply a saved plan exactly as reviewed. Without a plan file, apply plans again and asks for confirmation.
terraform apply tfplan
PLANFILE- Apply without re-planning and without a prompt.
-auto-approve- Skip confirmation; only in pipelines that apply a reviewed plan.
-parallelism=N- Concurrent operations (default 10); lower it when APIs rate-limit.
Gotcha A saved plan goes stale if state changes. Terraform refuses it then; plan again rather than forcing.
terraform plan -refresh-only
Detect drift: show what changed in real infrastructure outside Terraform, without proposing changes.
-refresh-only- Compare state with reality only.
apply -refresh-only- Accept the drift into state.
Gotcha Accepting drift into state does not fix the configuration. Update the code too, or the next plan reverts the manual change.
terraform apply -replace=<addr>
Force one resource to be destroyed and recreated, for example a VM with a broken disk. Replaces the old taint command.
terraform apply -replace=aws_instance.web[1]
-replace=ADDR- Plan a replacement for that resource only.
terraform destroy
Delete everything the configuration manages. Review the plan it prints as carefully as any apply.
terraform plan -destroy -out=destroy.tfplan
plan -destroy- Preview and save a destroy plan.
-target=ADDR- Destroy only a subset.
Gotcha Guard critical resources with lifecycle { prevent_destroy = true } so a destroy plan errors out instead.
terraform output
Print root module outputs, for example an endpoint or an IP, for scripts and humans.
terraform output -raw lb_dns_name
-raw NAME- One string without quotes.
-json- Every output as JSON; includes sensitive values.
terraform console
Evaluate expressions against the current state and variables: test a function or a for expression.
echo 'cidrsubnet("10.0.0.0/16", 8, 3)' | terraform console
"10.0.3.0/24"
State surgery
terraform state list
Every resource address Terraform tracks.
terraform state list | grep aws_security_group
ADDR-PREFIX- Filter, for example module.network.
-id=ID- Find the address that holds a cloud ID.
terraform state show <addr>
All recorded attributes of one resource.
terraform state show aws_db_instance.main
moved block
Rename or move a resource in code without destroying and recreating it. Reviewable, unlike state mv.
moved {
from = aws_instance.web
to = module.web.aws_instance.this
}
from / to- Old and new resource addresses.
state mv- The imperative equivalent; changes state immediately and leaves no trace in code.
Gotcha Without moved (or state mv), a refactor into a module plans a destroy and create of everything.
import block
Adopt an existing resource into state, and let plan generate the matching configuration.
import {
to = aws_s3_bucket.logs
id = "acme-prod-logs"
}
# terraform plan -generate-config-out=generated.tf
to- Resource address to import into.
id- Provider-specific ID of the existing resource.
-generate-config-out- Write HCL for imported resources (Terraform 1.5+).
terraform import ADDR ID- The older one-off CLI form.
terraform state rm <addr>
Stop managing a resource without deleting it in the cloud.
terraform state rm aws_s3_bucket.legacy
-dry-run- Show what would be removed.
removed block- Terraform 1.7+ declarative alternative with lifecycle { destroy = false }.
terraform state pull > backup.tfstate
Save a copy of the current remote state before any state surgery.
terraform state pull > state-$(date +%F-%H%M).tfstate
state push- Upload a state file; dangerous, last resort.
Gotcha State files contain secrets in plain text. Store backups as carefully as the remote state itself.
terraform force-unlock <lock-id>
Release a state lock left behind by a crashed or cancelled run.
terraform force-unlock 6f1e2d3c-4b5a-6978-8f90-a1b2c3d4e5f6
LOCK_ID- Printed in the 'Error acquiring the state lock' message.
-force- Skip the confirmation prompt.
Gotcha Make sure no other apply is actually running. Unlocking under a live run lets two writers corrupt state.
Remote state backend
Encrypted, versioned, locked remote state in S3.
terraform {
required_version = ">= 1.10"
backend "s3" {
bucket = "acme-terraform-state"
key = "prod/network/terraform.tfstate"
region = "eu-west-1"
encrypt = true
use_lockfile = true
}
}
key- One state file per environment and stack keeps the blast radius small.
use_lockfile- Native S3 locking (Terraform 1.10+); older setups use dynamodb_table.
bucket versioning- Enable it so a bad write can be rolled back.
Safety & debugging
lifecycle block
Protect or shape how a resource changes.
resource "aws_db_instance" "main" {
# ...
lifecycle {
prevent_destroy = true
create_before_destroy = false
ignore_changes = [tags["LastBackup"]]
}
}
prevent_destroy- Any plan that would destroy the resource fails.
create_before_destroy- Build the replacement before removing the old one.
ignore_changes- Stop fighting attributes that something else manages.
-var / -var-file / TF_VAR_
Where variable values come from, lowest to highest precedence.
TF_VAR_region=eu-west-1 terraform plan -var-file=prod.tfvars -var 'replicas=3'
1. default- The default in the variable block.
2. TF_VAR_name- Environment variables.
3. terraform.tfvars, *.auto.tfvars- Loaded automatically, auto files in lexical order.
4. -var-file / -var- Command line, in the order given; the last one wins.
terraform workspace select <name>
Switch between separate state files for the same configuration.
terraform workspace list
new NAME- Create a workspace.
show- Print the current workspace.
terraform.workspace- The name, usable inside configuration.
Gotcha Workspaces share one backend and credentials. For truly separate prod and staging, prefer separate directories or state keys.
TF_LOG=DEBUG terraform plan
Log provider API calls and internal decisions when a plan or apply misbehaves.
TF_LOG=DEBUG TF_LOG_PATH=tf-debug.log terraform plan
TF_LOG- TRACE, DEBUG, INFO, WARN or ERROR.
TF_LOG_PATH- Write the log to a file.
TF_LOG_PROVIDER- Provider logs only.
Gotcha Debug logs can include credentials and secrets. Scrub them before attaching to a ticket.
terraform providers lock -platform=<os_arch>
Record provider checksums for every platform your team and CI use.
terraform providers lock -platform=linux_amd64 -platform=darwin_arm64
-platform- Add hashes for another OS and architecture.
terraform providers- Show which providers each module requires.