Skip to main content

Terraform CLI — init, plan, apply, destroy, and import Explained

· 8 min read
Goel Academy
DevOps & Cloud Learning Hub

You know terraform apply creates resources. But do you know how to import existing infrastructure into state? Or how to target a single resource during a risky deployment? Or how to use the interactive console to test expressions before putting them in code? The Terraform CLI has more power than most people ever use. Let us fix that.

Complete CLI Reference

Here is every command you will use regularly, organized by workflow phase:

CommandPurpose
terraform initInitialize working directory, download providers
terraform validateCheck configuration syntax and consistency
terraform fmtFormat files to canonical style
terraform planPreview changes without applying
terraform applyApply changes to infrastructure
terraform destroyDestroy all managed infrastructure
terraform importBring existing resources under Terraform management
terraform outputDisplay output values
terraform consoleInteractive expression evaluator
terraform stateAdvanced state management
terraform workspaceManage multiple state environments
terraform graphGenerate dependency graph
terraform providersShow required providers
terraform versionShow Terraform version

terraform init

This is always the first command you run. It downloads providers, initializes the backend, and sets up the working directory.

# Basic initialization
terraform init

# Upgrade providers to latest allowed version
terraform init -upgrade

# Reconfigure backend (e.g., switching from local to S3)
terraform init -reconfigure

# Pass backend config dynamically (great for CI/CD)
terraform init \
-backend-config="bucket=my-state-bucket" \
-backend-config="key=prod/terraform.tfstate" \
-backend-config="region=us-east-1" \
-backend-config="dynamodb_table=terraform-locks"

# Initialize without downloading providers (offline mode)
terraform init -plugin-dir=/path/to/local/providers

The -backend-config flag is essential for CI/CD pipelines where you do not want backend details hardcoded in your Terraform files.

terraform plan

The plan shows you what Terraform will do without actually doing it. Always review the plan before applying.

# Basic plan
terraform plan

# Save the plan to a file (apply this exact plan later)
terraform plan -out=tfplan

# Plan for a specific resource only
terraform plan -target=aws_instance.web

# Pass variables inline
terraform plan -var="environment=staging" -var="instance_type=t3.small"

# Use a specific variables file
terraform plan -var-file="environments/prod.tfvars"

# Plan a destroy operation (see what would be deleted)
terraform plan -destroy

Saving the plan with -out is a best practice for CI/CD. It guarantees that apply executes exactly what was reviewed — no surprises from infrastructure changes between plan and apply.

# CI/CD pattern: plan, review, then apply the exact plan
terraform plan -out=tfplan
# ... review or approval step ...
terraform apply tfplan

terraform apply

Apply executes the changes. By default, it runs a plan first and asks for confirmation.

# main.tf — example configuration
resource "aws_s3_bucket" "logs" {
bucket = "my-app-logs-${var.environment}"
tags = {
Environment = var.environment
ManagedBy = "terraform"
}
}
# Interactive apply (shows plan, asks yes/no)
terraform apply

# Apply a saved plan file (no confirmation prompt)
terraform apply tfplan

# Skip the confirmation prompt (CI/CD only — never in production manually)
terraform apply -auto-approve

# Apply only a specific resource
terraform apply -target=aws_s3_bucket.logs

# Replace a specific resource (force destroy + recreate)
terraform apply -replace="aws_instance.web"

# Apply with inline variables
terraform apply -var="environment=production"

# Set parallelism (default is 10 concurrent operations)
terraform apply -parallelism=20

The -target flag is for surgical operations. It tells Terraform to only plan and apply changes for the specified resource and its dependencies. Use it when you need to fix one resource without touching anything else.

terraform destroy

Destroy removes everything managed by your Terraform configuration. Use with extreme caution.

# Destroy everything (shows plan, asks confirmation)
terraform destroy

# Destroy a specific resource only
terraform destroy -target=aws_instance.web

# Skip confirmation (CI/CD tear-down of ephemeral environments)
terraform destroy -auto-approve

# Destroy with variables (needed if your config requires them)
terraform destroy -var-file="environments/dev.tfvars"

A safer pattern for ephemeral environments:

# Create a dev environment
terraform workspace select dev
terraform apply -var-file="environments/dev.tfvars" -auto-approve

# Tear it down when done
terraform destroy -var-file="environments/dev.tfvars" -auto-approve

terraform import

You have a running EC2 instance that was created manually in the console. You want Terraform to manage it going forward. This is what import is for.

Step 1: Write the resource block in your configuration:

# main.tf — Write the config to match the existing resource
resource "aws_instance" "legacy_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
subnet_id = "subnet-0abc123"

tags = {
Name = "legacy-server"
}
}

Step 2: Import the existing resource into state:

# terraform import <resource_address> <cloud_resource_id>
terraform import aws_instance.legacy_server i-0abc123def456789

# More import examples:
terraform import aws_s3_bucket.data my-existing-bucket
terraform import aws_security_group.web sg-0abc123def456789
terraform import aws_iam_role.app my-app-role
terraform import aws_db_instance.main my-database-identifier

Step 3: Run plan and fix any attribute differences:

terraform plan
# Review the output — adjust your .tf file until plan shows no changes

The goal is to get terraform plan to show "No changes." That means your configuration matches the real resource perfectly.

terraform fmt

Formats your Terraform files to the canonical style. Run this before every commit.

# Format all .tf files in the current directory
terraform fmt

# Format recursively (all subdirectories)
terraform fmt -recursive

# Check formatting without modifying files (useful in CI)
terraform fmt -check

# Show the diff of what would change
terraform fmt -diff

# Format and show which files changed
terraform fmt -write=true

A common CI check:

# Fail the pipeline if files are not formatted
terraform fmt -check -recursive || {
echo "Terraform files are not formatted. Run 'terraform fmt -recursive'."
exit 1
}

terraform validate

Validates the configuration syntax without accessing any remote state or cloud APIs. Much faster than plan for catching errors.

# Validate the configuration
terraform validate

# Output in JSON format (for CI parsing)
terraform validate -json
Success! The configuration is valid.

Note: validate requires init to have been run first (it needs provider schemas).

terraform output

Displays the output values from your state.

# Show all outputs
terraform output

# Show a specific output
terraform output instance_public_ip

# Output in JSON format (for scripting)
terraform output -json

# Get a raw value (no quotes, useful for piping)
terraform output -raw instance_public_ip

# Use output in a shell script
INSTANCE_IP=$(terraform output -raw instance_public_ip)
ssh ec2-user@$INSTANCE_IP

terraform console

The interactive console is an underrated tool. It lets you test expressions, functions, and variable references without modifying any files.

$ terraform console

> var.environment
"production"

> var.availability_zones
["us-east-1a", "us-east-1b", "us-east-1c"]

> length(var.availability_zones)
3

> cidrsubnet("10.0.0.0/16", 8, 1)
"10.0.1.0/24"

> formatdate("YYYY-MM-DD", timestamp())
"2025-04-19"

> jsonencode({"name" = "test", "enabled" = true})
"{\"enabled\":true,\"name\":\"test\"}"

> exit

Use this to experiment with cidrsubnet, format, regex, and other functions before putting them into your code.

terraform workspace

Workspaces let you maintain multiple state files for the same configuration. Think of them as lightweight environments.

# List workspaces
terraform workspace list
# * default
# dev
# staging

# Create a new workspace
terraform workspace new production

# Switch to a workspace
terraform workspace select dev

# Show current workspace
terraform workspace show

# Delete a workspace
terraform workspace delete staging

Use terraform.workspace in your configuration to differentiate environments:

resource "aws_instance" "app" {
ami = var.ami_id
instance_type = terraform.workspace == "production" ? "t3.large" : "t3.micro"

tags = {
Name = "app-${terraform.workspace}"
Environment = terraform.workspace
}
}

terraform state Commands

For direct state manipulation — use with caution.

# List all resources in state
terraform state list

# Show a specific resource's attributes
terraform state show aws_instance.web

# Move/rename a resource in state
terraform state mv aws_instance.old_name aws_instance.new_name

# Remove a resource from state (Terraform "forgets" it)
terraform state rm aws_instance.decommissioned

# Pull remote state to local file
terraform state pull > state-backup.json

# Push local state to remote backend
terraform state push state-backup.json

Environment Variables

Terraform reads several environment variables that control its behavior:

VariablePurposeExample
TF_VAR_<name>Set input variable valuesTF_VAR_region=us-west-2
TF_LOGEnable debug loggingTF_LOG=DEBUG
TF_LOG_PATHWrite logs to a fileTF_LOG_PATH=terraform.log
TF_INPUTDisable interactive promptsTF_INPUT=false
TF_CLI_ARGSDefault CLI argumentsTF_CLI_ARGS="-no-color"
TF_DATA_DIROverride .terraform directoryTF_DATA_DIR=/tmp/tf-data
# Debug a failing plan
TF_LOG=DEBUG terraform plan 2>debug.log

# Set variables via environment (CI/CD pattern)
export TF_VAR_environment="production"
export TF_VAR_instance_type="t3.large"
terraform apply -auto-approve

Wrapping Up

The Terraform CLI is deceptively deep. Most people stick to init, plan, apply, and destroy. But commands like import (bringing existing resources under management), console (testing expressions interactively), and state mv (refactoring without downtime) are the tools that separate beginners from practitioners.

In the next post, we will explore Terraform expressions — conditionals, loops, dynamic blocks, and all the language features that make HCL more powerful than it looks at first glance.