MeshWorld India Logo MeshWorld.
Cheatsheet Terraform DevOps Infrastructure IaC Cloud Developer Tools 9 min read

Terraform Cheat Sheet: IaC Commands, HCL & State

Cobie
By Cobie
| Updated: May 20, 2026
Terraform Cheat Sheet: IaC Commands, HCL & State
TL;DR
  • Workflow: terraform initterraform planterraform applyterraform destroy
  • State is the source of truth — never edit terraform.tfstate by hand
  • terraform plan -out=tfplan then terraform apply tfplan = safe, reproducible deploys
  • OpenTofu is the CNCF fork of Terraform (BSL license change in Aug 2023) — commands are identical
  • Use terraform workspace for per-environment isolation, or separate state backends for production

Quick reference tables

Core commands

CommandWhat it does
terraform initInitialize working dir, download providers
terraform init -upgradeUpgrade providers to latest allowed versions
terraform validateCheck HCL syntax and configuration
terraform fmtFormat all .tf files in current directory
terraform fmt -recursiveFormat all files in all subdirectories
terraform planPreview changes without applying
terraform plan -out=tfplanSave plan to a file
terraform applyApply changes (prompts for confirmation)
terraform apply -auto-approveApply without prompt (CI use)
terraform apply tfplanApply a saved plan file
terraform destroyDestroy all managed infrastructure
terraform destroy -auto-approveDestroy without prompt
terraform outputShow output values
terraform output -jsonOutput values as JSON
terraform showShow current state or a plan file
terraform refreshSync state with real infrastructure
terraform graphGenerate dependency graph (Graphviz DOT)

State commands

CommandWhat it does
terraform state listList all resources in state
terraform state show aws_instance.webShow details of a specific resource
terraform state mv old.address new.addressMove/rename resource in state
terraform state rm aws_instance.webRemove resource from state (without destroying)
terraform state pullDownload and print remote state
terraform state pushUpload local state to remote backend
terraform import aws_instance.web i-abc123Import existing resource into state

Workspace commands

CommandWhat it does
terraform workspace listList all workspaces
terraform workspace new stagingCreate a new workspace
terraform workspace select stagingSwitch to a workspace
terraform workspace showShow current workspace name
terraform workspace delete stagingDelete a workspace

HCL building blocks

Resource

The main building block — declare an infrastructure object:

hcl
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:

hcl
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:

hcl
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:

hcl
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:

hcl
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:

hcl
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

hcl
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

plaintext
modules/
└── web-server/
    ├── main.tf       # Resources
    ├── variables.tf  # Input variables
    ├── outputs.tf    # Output values
    └── versions.tf   # Provider constraints

Calling a local module:

hcl
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):

plaintext
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:

hcl
environment    = "prod"
instance_count = 3
allowed_cidrs  = ["0.0.0.0/0"]

Common patterns

Count — create N copies of a resource

hcl
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

hcl
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

hcl
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

hcl
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

OpenTofu — Drop-in Terraform Replacement

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.

TerraformOpenTofuNotes
terraform inittofu initSame behavior
terraform plantofu planSame behavior
terraform applytofu applySame behavior
terraform.tfstateterraform.tfstateCompatible state format
registry.terraform.ioregistry.opentofu.orgSame providers available
HCL versionHCL version + new featuresOTF adds templatestring, encryption, etc.

Install OpenTofu:

bash
# macOS
brew install opentofu

# Linux
curl --proto '=https' --tlsv1.2 -fsSL https://get.opentofu.org/install-opentofu.sh | sh

Safe deployment workflow

plaintext
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:

bash
# 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

ProblemFix
Error: No valid credential sources foundSet AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY or use instance profile
State lock timeoutCheck DynamoDB lock table; run terraform force-unlock LOCK_ID carefully
Resource driftRun terraform refresh or add ignore_changes lifecycle rule
Error: CycleYou have a circular dependency — use depends_on explicitly or refactor
terraform import failsThe resource must be empty in state first; terraform state rm if needed
Plan is always differentA provider is normalizing attributes differently — use ignore_changes for volatile fields
Sensitive value shown in outputAdd sensitive = true to the output block

Summary

  • plan -out + apply tfplan is 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.