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
| Practice | Detail |
|---|---|
| Remote state | Always use S3/Azure Blob + locking (DynamoDB/Blob lease) |
| State locking | Prevent concurrent applies from corrupting state |
| Workspaces vs dirs | Use dirs for strong isolation; workspaces for small variations |
-target | Use sparingly; can cause drift in full plan |
prevent_destroy | Set on databases and critical resources |
| Sensitive outputs | Mark passwords/keys as sensitive = true |
| Version constraints | Pin provider versions: version = "~> 5.0" |
terraform.tfvars | Auto-loaded; don't commit secrets to git |
