CLI #

Terraform is operated entirely through the command line. There’s no official GUI, no local dashboard — all interaction with Terraform happens through CLI commands. This isn’t a weakness — it’s a strength. The CLI allows every Terraform operation to be automated, scripted, and run in CI/CD pipelines. Understanding Terraform’s CLI commands — not just how to run them, but also how to read their output and when to use specific flags — is a foundational skill you’ll use every day.

The Core Workflow #

These four commands form Terraform’s main working cycle. Almost everything you do in Terraform revolves around them.

flowchart LR
    INIT["terraform init\nDownload providers\nSet up backend"] --> PLAN["terraform plan\nPreview changes\nReview the diff"]
    PLAN --> APPLY["terraform apply\nExecute changes\nCreate/Update/Delete"]
    APPLY --> DESTROY["terraform destroy\nRemove all resources\n(teardown)"]

    PLAN -.->|"Something wrong?"| PLAN
    APPLY -.->|"New changes"| PLAN

    style INIT fill:#e3f2fd,stroke:#1565c0
    style PLAN fill:#fff3e0,stroke:#e65100
    style APPLY fill:#e8f5e9,stroke:#2e7d32
    style DESTROY fill:#ffebee,stroke:#c62828
CommandFunctionWhen to useFrequency
terraform initDownload providers, set up backendFirst time / after changing providersOnce per project
terraform planPreview changesBefore every applyVery often
terraform applyExecute changesAfter reviewing the planOften
terraform destroyRemove all resourcesEnvironment teardownRarely

terraform init #

terraform init is the first command you must run in a new Terraform directory, and every time there are changes to the provider or backend configuration. It does three things: downloads provider plugins, initializes the backend for state, and downloads any required modules.

# Basic initialization
terraform init

# Expected output:
# Initializing the backend...
# Initializing provider plugins...
# - Finding hashicorp/aws versions matching "~> 5.0"...
# - Installing hashicorp/aws v5.31.0...
# - Finding cloudflare/cloudflare versions matching "~> 4.0"...
# - Installing cloudflare/cloudflare v4.22.0...
# Terraform has been successfully initialized!

Important Flags #

# Update providers to the latest version satisfying the constraint
terraform init -upgrade
# Use this when you want the newest provider versions

# Initialize without backend setup (for testing or migration)
terraform init -backend=false

# Force a backend reconfigure (when changing backends or moving state)
terraform init -reconfigure

# Migrate state from an old backend to a new one
terraform init -migrate-state

# Only download modules, skip backend and provider setup
terraform init -get-plugins=false

After init, Terraform creates a .terraform/ directory containing the downloaded provider binaries. This directory must not be committed to Git — it holds large binaries that differ per OS. However, the .terraform.lock.hcl file must be committed — it guarantees everyone uses the same provider versions.

# After init, the directory will contain:
# .
# ├── .terraform/
# │   ├── providers/           # Provider binaries (DON'T commit)
# │   │   └── registry.terraform.io/
# │   └── modules/             # Downloaded modules
# ├── .terraform.lock.hcl      # Lock file (MUST commit)
# ├── main.tf
# └── variables.tf

terraform plan #

terraform plan is the command you’ll use most often. It reads the configuration, compares it with the state, and shows what would change without executing anything. It’s Terraform’s main safety net.

# Basic plan
terraform plan

# Save the plan to a file (for deterministic applies)
terraform plan -out=tfplan
terraform apply tfplan  # Apply exactly the saved plan

# Override variables during plan
terraform plan -var="environment=production"
terraform plan -var-file="prod.tfvars"

# Plan only specific resources (useful for debugging)
terraform plan -target=aws_instance.web

# Skip state refresh (faster, but less accurate)
terraform plan -refresh=false

Reading Plan Output #

Every symbol in the plan output has a specific meaning. Being able to read plan output quickly is a crucial skill.

flowchart TD
    A["Plan Symbols"] --> B["+ create\nA new resource will be created"]
    A --> C["- destroy\nThe resource will be deleted"]
    A --> D["~ update in-place\nThe resource is modified without replacement"]
    A --> E["-/+ replace\nThe resource is deleted then recreated"]
    A --> F["<= read\nThe data source will be read"]

    E --> G["⚠️ BE CAREFUL\nCan cause downtime\nfor stateful resources"]

    style B fill:#e8f5e9,stroke:#2e7d32
    style C fill:#ffebee,stroke:#c62828
    style D fill:#fff3e0,stroke:#e65100
    style E fill:#fff3e0,stroke:#e65100
    style F fill:#e3f2fd,stroke:#1565c0
    style G fill:#ffebee,stroke:#c62828
# Example plan output with various symbols:

# + create (a new resource will be created)
+ resource "aws_instance" "web" {
    + ami           = "ami-0abcdef1234567890"
    + instance_type = "t3.micro"
    + id            = (known after apply)
  }

# - destroy (the resource will be deleted)
- resource "aws_instance" "old" {
    - id = "i-0abcdef1234567890"
  }

# ~ update in-place (the resource is modified without replacement)
~ resource "aws_instance" "web" {
    ~ instance_type = "t3.micro" -> "t3.small"
      id            = "i-0abcdef1234567890"
  }

# -/+ destroy and recreate (a change that requires replacement)
-/+ resource "aws_instance" "web" {
    ~ ami = "ami-old" -> "ami-new"  # forces replacement
    - id  = "i-0abcdef1234567890"
    + id  = (known after apply)
  }

Notice the -/+ symbol — it means the resource will be deleted first, then recreated. If that resource is a production server, that means downtime. Always scrutinize the plan output carefully before typing yes.

terraform apply #

terraform apply executes the planned changes. By default, it re-runs the plan and asks for confirmation before making changes.

# Apply with interactive confirmation (default)
terraform apply
# Terraform will perform the following actions:
#   ... (plan output) ...
# Do you want to perform these actions?
#   Terraform will perform the actions described above.
#   Only 'yes' will be accepted to approve.
#   Enter a value: yes

# Apply without confirmation (for CI/CD)
terraform apply -auto-approve

# Apply from a saved plan (safest for production)
terraform plan -out=tfplan
terraform apply tfplan  # Exactly the saved plan, no confirmation prompt

Important Flags #

# Override variables during apply
terraform apply -var="instance_type=t3.small"
terraform apply -var-file="prod.tfvars"

# Apply only specific resources (for targeted fixes)
terraform apply -target=aws_instance.web

# Limit the number of concurrent operations (default: 10)
terraform apply -parallelism=5
# Useful when the provider API has rate limits

# Refresh state before applying
terraform apply -refresh=true  # Default
terraform apply -refresh=false # Skip refresh (faster)
Use -auto-approve only in CI/CD pipelines that already have a prior review process (like PR approval before merging to main). Running -auto-approve directly in a production terminal is an unnecessary risk — one typo in the configuration could delete critical resources.

-target — Use with Caution #

-target lets you apply only specific resources. It’s useful for debugging or targeted fixes, but not for routine use.

# Useful for targeted fixes
terraform apply -target=aws_instance.web

# ANTI-PATTERN: Using -target routinely
# - Makes state out of sync with the configuration
# - Untargeted resources can become inconsistent
# - Better to use -target only for debugging

State Inspection Commands #

These commands help you understand the current state and what Terraform is managing.

# List all resources in state
terraform state list
# aws_instance.web
# aws_security_group.web
# aws_subnet.public[0]
# aws_subnet.public[1]
# aws_vpc.main

# View details of one resource
terraform state show aws_instance.web
# resource "aws_instance" "web" {
#     ami                     = "ami-0abcdef1234567890"
#     instance_type           = "t3.micro"
#     public_ip               = "54.123.45.67"
#     ...
# }

# Refresh state from the actual cloud condition
terraform refresh
# Re-reads all resources from the provider APIs
# Useful if there were manual changes outside Terraform

# Show output values
terraform output
terraform output -raw instance_public_ip  # Specific output, no quotes
terraform output -json                     # JSON format
CommandFunctionWhen to use
state listList resourcesAudit, check what’s managed
state show <addr>Resource attribute detailsDebugging, checking values
state rm <addr>Remove from stateUnmanage a resource without destroying it
state mv <src> <dst>Rename/move in stateRefactoring
state pullDownload stateBackup, debugging
refreshSync state with the cloudAfter manual changes

Formatting and Validation Commands #

These commands keep configuration quality high and are very important to integrate into your daily workflow.

# Format configuration files consistently
terraform fmt                 # Format all .tf files in the current directory
terraform fmt -recursive      # Include subdirectories
terraform fmt -check          # Check without changing (exit code 1 if formatting is needed)
terraform fmt -diff           # Show the formatting diff

# Validate configuration syntax
terraform validate
# Success! The configuration is valid.
# Validation doesn't connect to providers — it only checks HCL syntax
# RECOMMENDATION: Run before every commit
terraform fmt -check && terraform validate

# In CI/CD pipelines:
terraform fmt -check -recursive  # Fail if formatting is inconsistent
terraform validate               # Fail if there are syntax errors

Debugging Commands #

When things aren’t working as expected, these commands are very helpful.

# Enable detailed logging
TF_LOG=TRACE terraform apply   # Most verbose
TF_LOG=DEBUG terraform apply   # Very detailed
TF_LOG=INFO terraform apply    # General information
TF_LOG=WARN terraform apply    # Only warnings
TF_LOG=ERROR terraform apply   # Only errors

# Save logs to a file (useful for very long logs)
TF_LOG=DEBUG TF_LOG_PATH=terraform.log terraform apply
# Logs go to terraform.log, the terminal stays clean

# View the dependency graph (DOT format for Graphviz)
terraform graph
terraform graph | dot -Tpng > graph.png  # Visualize as an image
terraform graph -type=plan               # Graph of the plan, not the config

Complete Reference Table #

Here’s a summary of all the commands covered, categorized by function.

CategoryCommandFunction
Init & SetupinitDownload providers, set up backend
init -upgradeUpdate providers to the latest version
init -reconfigureReconfigure the backend
Plan & ApplyplanPreview changes
plan -out=tfplanSave the plan to a file
applyExecute changes
apply tfplanApply from a saved plan
apply -auto-approveApply without confirmation
destroyRemove all resources
Statestate listView resources in state
state showResource details
state rmRemove from state
state mvRename/move in state
state pull/pushDownload/upload state
refreshSync state with the cloud
QualityfmtFormat the configuration
validateValidate syntax
graphDependency graph
DebugTF_LOG=DEBUGEnable verbose logging
providersView the providers in use
versionCheck the Terraform version

Summary #

  • Core workflow: initplanapply — run them in this order every time you work with new or changed configurations.
  • Always read the plan output before apply — understand the + (create), - (destroy), ~ (update), and -/+ (replace) symbols.
  • -/+ means replace — the resource is deleted and recreated, which can cause downtime for stateful resources like databases.
  • Save plans with -out=tfplan for production — make sure what you apply is exactly what was reviewed.
  • terraform fmt and terraform validate are good habits before committing — integrate them into the CI pipeline.
  • -target is for debugging, not routine use — overuse can make state inconsistent.
  • TF_LOG=DEBUG is your best friend when something isn’t working as expected.

← Previous: Installation   Next: Directory Structure →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact