Terraform Core Workflow & Commands
The complete Terraform workflow — Write, Plan, Apply, and Destroy — with what each step does internally, essential commands, the production CI/CD pattern using plan files, importing existing infrastructure, and verbose logging.
Terraform Core Workflow & Commands
The Core Workflow: Write → Plan → Apply
flowchart LR
Write["✍️ Write\n.tf config files"]
-->Init["terraform init\ndownload providers\n& modules"]
Init
-->Fmt["terraform fmt\nformat HCL"]
Fmt
-->Validate["terraform validate\ncheck syntax"]
Validate
-->Plan["terraform plan\ncompare config vs state\nshow diff"]
Plan
-->Apply["terraform apply\ncall provider APIs\nupdate state"]
Apply
-->|"repeat"| Write
Apply
-->|"teardown"| Destroy["terraform destroy\ndelete all resources\nclears state"]
What happens at each step
| Step | Command | What Terraform does internally |
|---|---|---|
| Init | terraform init |
Downloads provider plugins to .terraform/ — reads required_providers in config. Downloads modules. Creates or updates .terraform.lock.hcl. |
| Format | terraform fmt |
Rewrites .tf files to the canonical HCL style (indentation, alignment). Idempotent. |
| Validate | terraform validate |
Parses HCL and checks types, required attributes, and expression validity. Does NOT call any provider APIs — no credentials needed. |
| Plan | terraform plan |
Reads state, calls provider's Read for each existing resource, compares desired config to actual state, outputs the diff. No changes made. |
| Apply | terraform apply |
Runs plan again (or executes a saved plan file), then calls provider APIs to create/update/delete. Updates state file after each resource. |
| Destroy | terraform destroy |
Equivalent to terraform apply -destroy — creates a plan to delete everything in state, then applies it. |
Initialize
# Standard init — downloads providers and modules
terraform init
# Upgrade providers and modules to newest allowed versions
terraform init -upgrade
# Don't download plugins (use cached)
terraform init -get-plugins=false
# Don't verify plugin signatures (not recommended)
terraform init -verify-plugins=false
terraform init must be re-run after:
- Adding a new provider or module
- Changing a provider version constraint
- Changing the backend configuration
Format and Validate
# Reformat all .tf files to canonical HCL style
terraform fmt
# Show what would change without rewriting
terraform fmt -diff
# Check formatting (exit 1 if not formatted) — useful in CI
terraform fmt -check -recursive
# Validate syntax and type correctness
terraform validate
# Validate output as JSON (for tool integration)
terraform validate -json
# Validate without connecting to backend
terraform validate -backend=false
Plan
# Show what would change
terraform plan
# Save plan to a file (use in CI — prevents race conditions)
terraform plan -out=tfplan
# Destroy plan
terraform plan -destroy
# Target a specific resource
terraform plan -target=aws_instance.web
# Override a variable
terraform plan -var="instance_type=t3.large"
# Use a variable file
terraform plan -var-file=prod.tfvars
# Skip refreshing real-world resource state (faster, but may miss drift)
terraform plan -refresh=false
Apply
# Interactive apply (shows plan + confirmation prompt)
terraform apply
# Apply without confirmation prompt (use in CI after reviewing plan)
terraform apply --auto-approve
# Apply a saved plan file — executes exactly what was planned, no re-plan
terraform apply tfplan
# Target a specific resource only
terraform apply -target=aws_instance.web
# Limit parallel resource operations (default: 10)
terraform apply --parallelism=5
# Pass variable on command line
terraform apply -var="region=us-west-2"
# Apply without refreshing state (skip read of real-world resources)
terraform apply -refresh=false
Destroy
# Destroy all managed infrastructure
terraform destroy
# Without confirmation prompt
terraform destroy --auto-approve
# Preview what would be destroyed
terraform plan -destroy
# Destroy a specific resource only
terraform destroy -target=aws_instance.web
Production CI/CD Pattern: Plan File
In a CI/CD pipeline (GitHub Actions, GitLab CI, Jenkins), always use a saved plan file to prevent the plan from changing between review and apply:
# CI Step 1 — PR pipeline: plan and save
terraform plan -out=tfplan
# CI Step 2 — human reviews the plan output
# (GitHub Actions: annotate PR with plan output)
# CI Step 3 — merge to main: apply exactly what was planned
terraform apply tfplan
Why this matters: without a plan file, terraform apply runs a new plan before applying. Between your PR review and the merge, someone else might have applied a change — the new plan could differ from what you reviewed. The plan file makes apply deterministic.
# Example: GitHub Actions workflow
jobs:
plan:
steps:
- run: terraform plan -out=tfplan -no-color 2>&1 | tee plan.txt
- name: Comment plan on PR
uses: actions/github-script@v6
with:
script: |
const plan = require('fs').readFileSync('plan.txt', 'utf8')
github.rest.issues.createComment({ body: `\`\`\`\n${plan}\n\`\`\`` })
apply:
needs: plan
if: github.ref == 'refs/heads/main'
steps:
- run: terraform apply -auto-approve tfplan
Import Existing Infrastructure
Use terraform import when infrastructure already exists in the cloud and you want to bring it under Terraform management.
# Import an EC2 instance (AWS resource ID = instance ID)
terraform import aws_instance.web i-1234567890abcdef0
# Import an S3 bucket
terraform import aws_s3_bucket.assets my-bucket-name
# Import a resource into a module
terraform import module.networking.aws_vpc.main vpc-0abc123def456
# Import using for_each key
terraform import 'aws_instance.web["production"]' i-1234567890abcdef0
Terraform 1.5+ declarative import block (preferred over the CLI command):
# Write the resource block first
resource "aws_instance" "web" {
# ... configuration
}
# Add an import block — Terraform generates the config on plan/apply
import {
to = aws_instance.web
id = "i-1234567890abcdef0"
}
Run terraform plan -generate-config-out=generated.tf to have Terraform write the resource config for you based on the real resource's attributes.
Outputs
# List all output values
terraform output
# Get a specific output
terraform output instance_public_ip
# Output in JSON format (for scripting)
terraform output -json
# Output a specific value in raw format (no quotes)
terraform output -raw instance_public_ip
State Commands
# List all resources in state
terraform state list
# Show details of a specific resource
terraform state show aws_instance.web
# Move/rename a resource in state (no API calls)
terraform state mv aws_instance.old aws_instance.new
# Move resource into a module
terraform state mv aws_instance.web module.compute.aws_instance.web
# Remove resource from state (resource stays in cloud, Terraform forgets it)
terraform state rm aws_instance.web
# Pull remote state to stdout
terraform state pull
# Manually unlock state (if a crash left it locked)
terraform force-unlock LOCK_ID
# Refresh state from real-world resources
terraform apply -refresh-only
Verbose Logging
Enable when debugging provider issues, unexpected plan differences, or authentication problems:
# Log levels: TRACE, DEBUG, INFO, WARN, ERROR
export TF_LOG=DEBUG
terraform apply
# Log only Terraform core (not providers)
export TF_LOG_CORE=TRACE
# Log only provider calls
export TF_LOG_PROVIDER=DEBUG
# Write logs to a file
export TF_LOG_PATH=./terraform.log
# Disable logging
export TF_LOG=off
TRACE is the most verbose — it logs every API call the provider makes. Use DEBUG for most debugging; TRACE when debugging provider auth or HTTP-level issues.
Workspace Commands
terraform workspace new staging # create new workspace
terraform workspace select staging # switch to workspace
terraform workspace list # list all workspaces
terraform workspace show # current workspace name
terraform workspace delete staging # delete workspace (must be empty)
Related Posts
- IaC Concepts & TF Overview — why IaC exists, declarative vs imperative, and Terraform's core value proposition before diving into the workflow
- TF State & Backend Management — the state file that
planreads andapplyupdates; remote backends and state locking - Terraform Basics: Providers, Resources, and Initialization — provider plugin architecture, resource blocks, and data sources that the workflow operates on
- TF Cloud Capabilities & Workflow — how Terraform Cloud runs the same workflow remotely with VCS integration and Sentinel policy checks
