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

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

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.

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

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

| 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 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.