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#

CommandWhat it does
terraform initInitialises the working directory, downloads providers and modules
terraform planShows what changes Terraform will make (dry run)
terraform applyApplies the planned changes to your infrastructure
terraform destroyDestroys all resources managed by the current state
terraform validateChecks syntax and internal consistency of config files
terraform fmtReformats .tf files to the canonical HCL style
terraform showPrints the current state or a saved plan file
terraform outputPrints the values of declared output blocks
terraform state listLists 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 listLists 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 refreshUpdates 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#

BackendProviderNotes
S3 + DynamoDBAWSMost common for AWS teams; DynamoDB handles locking
GCSGCPNative GCS locking built in
azurermAzureBlob storage with lease-based locking
Terraform Cloud / HCPHashiCorpManaged 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#

SituationPattern to use
Repeated resource patternsExtract a module
Separate dev/staging/prodSeparate state files or workspaces
Sharing outputs between state filesterraform_remote_state data source
Existing resource not in stateterraform import
Drift on an attribute you don’t controlignore_changes in lifecycle
Protect a critical resource from deletionprevent_destroy = true
Force a resource to be replacedterraform apply -replace=<address>