- Workflow:
terraform init→terraform plan→terraform apply→terraform destroy - State is the source of truth — never edit
terraform.tfstateby hand terraform plan -out=tfplanthenterraform apply tfplan= safe, reproducible deploys- OpenTofu is the CNCF fork of Terraform (BSL license change in Aug 2023) — commands are identical
- Use
terraform workspacefor per-environment isolation, or separate state backends for production
Quick reference tables
Core commands
| Command | What it does |
|---|---|
terraform init | Initialize working dir, download providers |
terraform init -upgrade | Upgrade providers to latest allowed versions |
terraform validate | Check HCL syntax and configuration |
terraform fmt | Format all .tf files in current directory |
terraform fmt -recursive | Format all files in all subdirectories |
terraform plan | Preview changes without applying |
terraform plan -out=tfplan | Save plan to a file |
terraform apply | Apply changes (prompts for confirmation) |
terraform apply -auto-approve | Apply without prompt (CI use) |
terraform apply tfplan | Apply a saved plan file |
terraform destroy | Destroy all managed infrastructure |
terraform destroy -auto-approve | Destroy without prompt |
terraform output | Show output values |
terraform output -json | Output values as JSON |
terraform show | Show current state or a plan file |
terraform refresh | Sync state with real infrastructure |
terraform graph | Generate dependency graph (Graphviz DOT) |
State commands
| Command | What it does |
|---|---|
terraform state list | List all resources in state |
terraform state show aws_instance.web | Show details of a specific resource |
terraform state mv old.address new.address | Move/rename resource in state |
terraform state rm aws_instance.web | Remove resource from state (without destroying) |
terraform state pull | Download and print remote state |
terraform state push | Upload local state to remote backend |
terraform import aws_instance.web i-abc123 | Import existing resource into state |
Workspace commands
| Command | What it does |
|---|---|
terraform workspace list | List all workspaces |
terraform workspace new staging | Create a new workspace |
terraform workspace select staging | Switch to a workspace |
terraform workspace show | Show current workspace name |
terraform workspace delete staging | Delete a workspace |
HCL building blocks
Resource
The main building block — declare an infrastructure object:
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "web-server"
Environment = var.environment
}
} Variable
Inputs to your module or root configuration:
variable "environment" {
type = string
description = "Deployment environment (dev, staging, prod)"
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Must be dev, staging, or prod."
}
}
variable "instance_count" {
type = number
default = 1
}
variable "allowed_cidrs" {
type = list(string)
default = ["10.0.0.0/8"]
}
variable "tags" {
type = map(string)
default = {}
} Locals
Computed values, not exposed as inputs:
locals {
name_prefix = "${var.environment}-${var.project}"
common_tags = merge(var.tags, {
Environment = var.environment
ManagedBy = "terraform"
})
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
tags = local.common_tags
} Output
Values exported from a module or the root:
output "instance_ip" {
value = aws_instance.web.public_ip
description = "Public IP of the web server"
}
output "db_endpoint" {
value = aws_db_instance.main.endpoint
sensitive = true # Won't print in logs, still accessible
} Data source
Read existing infrastructure without managing it:
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
} Provider
Tell Terraform which cloud or service to talk to:
terraform {
required_version = ">= 1.7"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
backend "s3" {
bucket = "my-tf-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "tf-state-lock"
encrypt = true
}
}
provider "aws" {
region = "us-east-1"
default_tags {
tags = { Project = "myapp" }
}
} Modules
Calling a module
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
}
# Use module outputs
resource "aws_instance" "web" {
subnet_id = module.vpc.public_subnets[0]
} Local module structure
modules/
└── web-server/
├── main.tf # Resources
├── variables.tf # Input variables
├── outputs.tf # Output values
└── versions.tf # Provider constraints Calling a local module:
module "web" {
source = "./modules/web-server"
environment = var.environment
instance_type = "t3.small"
} Variable input methods
Values are resolved in this priority order (highest wins):
1. -var flag: terraform apply -var="environment=prod"
2. -var-file flag: terraform apply -var-file="prod.tfvars"
3. *.auto.tfvars files: automatically loaded
4. terraform.tfvars: automatically loaded
5. TF_VAR_ env vars: TF_VAR_environment=prod terraform apply
6. Default value: defined in variable block
7. Interactive prompt: if none of the above Example prod.tfvars:
environment = "prod"
instance_count = 3
allowed_cidrs = ["0.0.0.0/0"] Common patterns
Count — create N copies of a resource
resource "aws_instance" "web" {
count = var.instance_count
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
tags = {
Name = "web-${count.index}"
}
}
# Reference: aws_instance.web[0].public_ip For each — create resources from a map or set
variable "buckets" {
default = {
assets = "us-east-1"
backups = "us-west-2"
}
}
resource "aws_s3_bucket" "this" {
for_each = var.buckets
bucket = "myapp-${each.key}"
provider = aws.${each.value} # Dynamic provider alias
}
# Reference: aws_s3_bucket.this["assets"].bucket Dynamic blocks
resource "aws_security_group" "web" {
name = "web-sg"
dynamic "ingress" {
for_each = var.allowed_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
} Lifecycle rules
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
lifecycle {
create_before_destroy = true # Zero-downtime replacement
prevent_destroy = true # Block terraform destroy
ignore_changes = [tags] # Ignore changes to tags in state
}
} Terraform vs OpenTofu
HashiCorp changed Terraform from MPL to Business Source License (BSL) in August 2023. OpenTofu is the CNCF-backed community fork under MPL 2.0. Commands are 100% compatible — replace terraform with tofu.
| Terraform | OpenTofu | Notes |
|---|---|---|
terraform init | tofu init | Same behavior |
terraform plan | tofu plan | Same behavior |
terraform apply | tofu apply | Same behavior |
terraform.tfstate | terraform.tfstate | Compatible state format |
| registry.terraform.io | registry.opentofu.org | Same providers available |
| HCL version | HCL version + new features | OTF adds templatestring, encryption, etc. |
Install OpenTofu:
# macOS
brew install opentofu
# Linux
curl --proto '=https' --tlsv1.2 -fsSL https://get.opentofu.org/install-opentofu.sh | sh Safe deployment workflow
1. terraform init # Download providers and modules
2. terraform validate # Catch syntax errors early
3. terraform fmt -check # Fail if formatting is off (CI)
4. terraform plan -out=tfplan # Preview changes, save plan
5. Review the plan! # Check what will be created/destroyed
6. terraform apply tfplan # Apply exactly what was previewed For CI/CD pipelines:
# CI: validate and plan
terraform init -input=false
terraform validate
terraform plan -input=false -out=tfplan
# CD: apply (separate job, after approval)
terraform apply -input=false tfplan Common gotchas
| Problem | Fix |
|---|---|
Error: No valid credential sources found | Set AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY or use instance profile |
| State lock timeout | Check DynamoDB lock table; run terraform force-unlock LOCK_ID carefully |
| Resource drift | Run terraform refresh or add ignore_changes lifecycle rule |
Error: Cycle | You have a circular dependency — use depends_on explicitly or refactor |
terraform import fails | The resource must be empty in state first; terraform state rm if needed |
| Plan is always different | A provider is normalizing attributes differently — use ignore_changes for volatile fields |
| Sensitive value shown in output | Add sensitive = true to the output block |
Summary
plan -out+apply tfplanis the only production-safe deploy pattern- State is sacred — use remote backends (S3 + DynamoDB, Terraform Cloud, GCS) from day one
- Locals,
for_each, and dynamic blocks eliminate repetition without reaching for a module - OpenTofu is a drop-in replacement if BSL licensing is a concern
- Keep modules small and focused — one module per logical infrastructure unit
FAQ
What is the difference between terraform refresh and terraform plan?
refresh updates the state file to match real-world infrastructure without planning changes. plan implicitly refreshes state and then computes a diff. In Terraform 1.x, explicit refresh is rarely needed — plan handles it.
How do I manage multiple environments (dev/staging/prod)? Two approaches: (1) Workspaces — simple but share the same codebase and one state backend. (2) Separate directories per environment with a shared modules library — more isolation, recommended for production.
Can I refactor resources without destroying them?
Yes. Use terraform state mv to rename resources in state, then update the HCL to match. Terraform will see no diff and not destroy/recreate.
What is a remote backend and why do I need one?
By default, state is stored in terraform.tfstate locally. A remote backend (S3, GCS, Terraform Cloud) lets teams share state, enables state locking (prevents concurrent applies), and keeps secrets out of your local machine.
Is Terraform still relevant with Pulumi and CDK? Terraform/OpenTofu remains the most widely used IaC tool (per Stack Overflow 2025 survey). Pulumi and CDK let you use real programming languages but have steeper learning curves and smaller ecosystems. For multi-cloud IaC, Terraform’s provider ecosystem is unmatched.
What to read next
- Docker Cheat Sheet — containerize the apps you provision with Terraform
- Kubernetes Cheat Sheet — deploy to Kubernetes clusters managed by Terraform
- GitHub Actions Cheat Sheet — run Terraform in CI/CD pipelines
Related Articles
Deepen your understanding with these curated continuations.
Kubernetes Cheat Sheet: Every kubectl Command You Actually Need
Complete Kubernetes reference with copy-paste kubectl commands for pods, deployments, services, namespaces, config, scaling, logs, exec, and troubleshooting.
mise Cheat Sheet: Unified Runtime & Tool Manager (2026)
Complete mise reference — install and switch Node, Python, Rust, Go versions, manage tools, activate via shell, Docker, and CI/CD with working commands.
GitHub Actions Cheat Sheet: Workflows, Jobs & Recipes
Complete GitHub Actions reference — triggers, jobs, secrets, matrix builds, caching, artifacts, and real-world recipes for CI/CD pipelines in 2026.