Terraform Cheatsheet
Terraform is the most widely used infrastructure-as-code tool in cloud teams. This cheatsheet covers the commands and patterns you will use daily.
Core CLI Commands#
| Command | What it does |
|---|---|
terraform init | Initialises the working directory, downloads providers and modules |
terraform plan | Shows what changes Terraform will make (dry run) |
terraform apply | Applies the planned changes to your infrastructure |
terraform destroy | Destroys all resources managed by the current state |
terraform validate | Checks syntax and internal consistency of config files |
terraform fmt | Reformats .tf files to the canonical HCL style |
terraform show | Prints the current state or a saved plan file |
terraform output | Prints the values of declared output blocks |
terraform state list | Lists all resources tracked in state |
terraform state show <addr> | Shows detailed attributes of one resource in state |
terraform state rm <addr> | Removes a resource from state without destroying it |
terraform import <addr> <id> | Imports an existing cloud resource into Terraform state |
terraform workspace list | Lists all workspaces |
terraform workspace new <name> | Creates and switches to a new workspace |
terraform workspace select <name> | Switches to an existing workspace |
terraform taint <addr> | Marks a resource for forced recreation on next apply (deprecated in 1.x, use -replace) |
terraform apply -replace=<addr> | Forces recreation of a specific resource |
terraform refresh | Updates state to match real-world infrastructure (use carefully) |
Pass -auto-approve to apply or destroy to skip the confirmation prompt in CI pipelines.
HCL Syntax#
Resource block#
resource "aws_s3_bucket" "my_bucket" {
bucket = "my-unique-bucket-name"
tags = {
Environment = "prod"
Team = "platform"
}
}
Data source block#
Data sources read existing infrastructure without managing it.
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-*-22.04-amd64-server-*"]
}
}
Reference it with data.aws_ami.ubuntu.id.
Variable block#
variable "instance_type" {
type = string
description = "EC2 instance type"
default = "t3.micro"
}
Pass values with -var="instance_type=t3.small" or a terraform.tfvars file.
Output block#
output "bucket_arn" {
value = aws_s3_bucket.my_bucket.arn
description = "ARN of the S3 bucket"
}
Locals block#
Use locals to avoid repeating expressions.
locals {
name_prefix = "${var.project}-${var.environment}"
common_tags = {
Project = var.project
Environment = var.environment
ManagedBy = "terraform"
}
}
Provider and Backend Configuration#
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "my-tfstate-bucket"
key = "prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
provider "aws" {
region = var.aws_region
}
Always pin provider versions with ~> (allow patch and minor, not major) to avoid surprise breaking changes.
Module Pattern#
Calling a module#
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.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"]
}
Referencing module outputs#
resource "aws_instance" "app" {
subnet_id = module.vpc.private_subnets[0]
}
Lifecycle Rules#
Add a lifecycle block inside any resource to control how Terraform handles changes.
resource "aws_db_instance" "main" {
# ... other config ...
lifecycle {
create_before_destroy = true # create new resource before destroying old one
prevent_destroy = true # block terraform destroy for this resource
ignore_changes = [tags] # don't detect drift on these attributes
}
}
prevent_destroy = true is useful for databases and other stateful resources. It will cause terraform destroy to error rather than deleting the resource.
For Expressions and Dynamic Blocks#
For expression (list)#
locals {
upper_names = [for name in var.names : upper(name)]
}
For expression (map)#
locals {
instance_ids = { for k, v in aws_instance.servers : k => v.id }
}
Dynamic block#
Use dynamic when you need to generate repeated nested blocks from a list.
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"]
}
}
}
State Management#
Remote backend options#
| Backend | Provider | Notes |
|---|---|---|
| S3 + DynamoDB | AWS | Most common for AWS teams; DynamoDB handles locking |
| GCS | GCP | Native GCS locking built in |
| azurerm | Azure | Blob storage with lease-based locking |
| Terraform Cloud / HCP | HashiCorp | Managed state, runs, and secrets |
State locking prevents two people from running apply simultaneously and corrupting state. Always use a locking backend in shared environments.
Never edit .tfstate files by hand. Use terraform state subcommands instead.
Common Mistakes#
Committing .tfstate to Git. State files can contain secrets (database passwords, private keys). Use a remote backend and add *.tfstate and *.tfstate.backup to .gitignore.
Not pinning provider versions. A major provider version bump can break your config without warning. Pin with version = "~> 5.0".
Using terraform taint carelessly. Tainting a database will destroy it and create a new one. Understand the resource’s create_before_destroy behaviour first.
One giant root module. Split large configurations into modules or separate state files per environment to reduce blast radius.
Running apply without reviewing plan output. Always read the plan. The +, -, and ~ symbols tell you what will be created, destroyed, or updated in-place.
Quick Decision Guide#
| Situation | Pattern to use |
|---|---|
| Repeated resource patterns | Extract a module |
| Separate dev/staging/prod | Separate state files or workspaces |
| Sharing outputs between state files | terraform_remote_state data source |
| Existing resource not in state | terraform import |
| Drift on an attribute you don’t control | ignore_changes in lifecycle |
| Protect a critical resource from deletion | prevent_destroy = true |
| Force a resource to be replaced | terraform apply -replace=<address> |