DevOps & IaC15 min

Terraform — Commands & HCL Cheat Sheet

Complete Terraform reference — CLI commands, HCL syntax, variables, outputs, modules, state management, workspaces, and provider patterns.

Core CLI Workflow

# Initialize (download providers + modules)
terraform init

# Re-initialize with backend config
terraform init -backend-config="bucket=my-tfstate"

# Upgrade providers
terraform init -upgrade

# Format code
terraform fmt
terraform fmt -recursive          # All .tf files in subdirs
terraform fmt -check              # Exit 1 if not formatted (CI)

# Validate syntax / config
terraform validate

# Plan (preview changes)
terraform plan
terraform plan -out=tfplan         # Save plan to file
terraform plan -var="env=prod"
terraform plan -var-file="prod.tfvars"
terraform plan -target=aws_instance.web  # Plan specific resource

# Apply
terraform apply
terraform apply tfplan              # Apply saved plan (no prompt)
terraform apply -auto-approve       # Skip confirmation
terraform apply -var="env=prod"
terraform apply -target=aws_instance.web

# Destroy
terraform destroy
terraform destroy -auto-approve
terraform destroy -target=aws_instance.web  # Destroy specific resource

State Management

# Show state
terraform show                     # All resources in state
terraform state list               # List resource addresses
terraform state show aws_instance.web   # One resource in detail

# Move resource in state (rename without destroy/recreate)
terraform state mv aws_instance.old aws_instance.new
terraform state mv 'module.vpc.aws_vpc.main' 'aws_vpc.main'

# Import existing resource into state
terraform import aws_instance.web i-0123456789abcdef0
terraform import 'azurerm_resource_group.main' '/subscriptions/xxx/resourceGroups/myRG'

# Remove resource from state (stop managing without destroying)
terraform state rm aws_instance.web

# Pull / push remote state
terraform state pull > state.json
terraform state push state.json

# Refresh state (sync with real infra)
terraform refresh

Variables

# variables.tf
variable "region" {
  type        = string
  description = "AWS region"
  default     = "us-east-1"
}

variable "instance_count" {
  type    = number
  default = 2
}

variable "environment" {
  type = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Must be dev, staging, or prod."
  }
}

variable "tags" {
  type    = map(string)
  default = {
    owner = "ops"
    env   = "dev"
  }
}

variable "db_password" {
  type      = string
  sensitive = true   # never shown in plan/apply output
}

Passing Variable Values

# CLI
terraform apply -var="region=eu-west-1" -var="instance_count=3"

# .tfvars file
terraform apply -var-file="prod.tfvars"

# Automatic: terraform.tfvars or *.auto.tfvars are loaded automatically

# Environment variables (TF_VAR_ prefix)
export TF_VAR_region=eu-west-1
export TF_VAR_db_password=supersecret

Outputs

# outputs.tf
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    # hidden in CLI output
}
terraform output                   # Show all outputs
terraform output instance_ip       # Specific output
terraform output -json             # JSON format
terraform output -raw instance_ip  # Raw value (no quotes)

Resources & Data Sources

# Resource
resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type

  tags = merge(var.tags, {
    Name = "web-server"
  })

  lifecycle {
    create_before_destroy = true
    prevent_destroy       = true      # error if you try to terraform destroy this
    ignore_changes        = [tags]    # don't update when tags change externally
  }
}

# Data source (read-only, no resource created)
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]      # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

# Reference data source
resource "aws_instance" "web" {
  ami = data.aws_ami.ubuntu.id
}

Locals

locals {
  common_tags = {
    project     = var.project
    environment = var.environment
    managed_by  = "terraform"
  }

  name_prefix = "${var.project}-${var.environment}"
  is_prod     = var.environment == "prod"
}

# Use locals
resource "aws_instance" "web" {
  tags = local.common_tags
}

Modules

# Call a module
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.5.0"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
}

# Reference module output
resource "aws_instance" "web" {
  subnet_id = module.vpc.public_subnets[0]
}
# Download modules (runs automatically on init)
terraform get
terraform get -update

Loops & Conditionals

# count (simple repeat)
resource "aws_instance" "web" {
  count         = 3
  ami           = "ami-123"
  instance_type = "t3.micro"
  tags = { Name = "web-${count.index}" }
}

# for_each (map/set — preferred, stable IDs)
resource "aws_iam_user" "team" {
  for_each = toset(["alice", "bob", "charlie"])
  name     = each.key
}

resource "aws_s3_bucket" "envs" {
  for_each = {
    dev  = "us-east-1"
    prod = "eu-west-1"
  }
  bucket = "myapp-${each.key}"
  provider = aws.${each.value}     # not real syntax, illustrative
}

# Conditional resource (count trick)
resource "aws_route53_record" "www" {
  count = var.create_dns ? 1 : 0
  # ...
}

# Ternary
instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"

Workspaces

terraform workspace list              # List workspaces
terraform workspace show              # Current workspace
terraform workspace new staging       # Create + switch
terraform workspace select prod       # Switch
terraform workspace delete staging    # Delete

# Use workspace name in config
resource "aws_s3_bucket" "state" {
  bucket = "myapp-tfstate-${terraform.workspace}"
}

Backends (Remote State)

# S3 backend
terraform {
  backend "s3" {
    bucket         = "my-tfstate-bucket"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "tf-state-lock"
    encrypt        = true
  }
}

# Azure blob backend
terraform {
  backend "azurerm" {
    resource_group_name  = "tfstate-rg"
    storage_account_name = "tfstateacct"
    container_name       = "tfstate"
    key                  = "prod.terraform.tfstate"
  }
}

Built-in Functions

# String
lower("HELLO")                    # "hello"
upper("hello")                    # "HELLO"
format("%s-%d", "app", 1)         # "app-1"
join("-", ["a", "b", "c"])        # "a-b-c"
split(",", "a,b,c")               # ["a","b","c"]
replace("hello world", " ", "-")  # "hello-world"
trimspace("  hi  ")               # "hi"
substr("hello", 0, 3)             # "hel"

# Collection
length(var.list)
toset(["a","b","a"])               # {"a","b"} — deduplicated
tolist(var.some_set)
merge(map1, map2)                  # merge two maps
flatten([["a","b"],["c"]])         # ["a","b","c"]
distinct(["a","a","b"])            # ["a","b"]
keys(var.map)
values(var.map)
lookup(var.map, "key", "default")

# Type
tostring(42)
tonumber("42")
tobool("true")

# Encoding
jsonencode({key = "value"})         # "{\"key\":\"value\"}"
jsondecode("{\"key\":\"value\"}")
base64encode("hello")
base64decode("aGVsbG8=")

# Filesystem (only at plan time)
file("./config.yaml")
templatefile("./user-data.sh.tpl", { env = var.environment })

Key Best Practices

PracticeDetail
Remote stateAlways use S3/Azure Blob + locking (DynamoDB/Blob lease)
State lockingPrevent concurrent applies from corrupting state
Workspaces vs dirsUse dirs for strong isolation; workspaces for small variations
-targetUse sparingly; can cause drift in full plan
prevent_destroySet on databases and critical resources
Sensitive outputsMark passwords/keys as sensitive = true
Version constraintsPin provider versions: version = "~> 5.0"
terraform.tfvarsAuto-loaded; don't commit secrets to git