ITCertFoundryTechnical training system
AUTOMATION

Terraform Code Cheat Sheet

HCL syntax, block types, the core CLI workflow, and the patterns you'll actually reuse across projects.

The core CLI workflow

CommandWhat it doesterraform initDownloads providers/modules and sets up the working directory — run this first in any project, and again after adding a new provider or moduleterraform fmtRewrites .tf files into canonical formatting — safe to run anytime, changes style only, never behaviorterraform validateChecks syntax and internal consistency without touching any real infrastructure or stateterraform planShows exactly what would be created, changed, or destroyed — changes nothing yetterraform applyExecutes the plan and actually creates/changes/destroys real resourcesterraform destroyTears down everything Terraform is tracking in state for this configurationterraform state listLists every resource Terraform currently tracks in stateterraform state show <addr>Shows the full tracked attributes of one specific resourceterraform import <addr> <id>Brings an already-existing, manually created resource under Terraform's management without recreating itterraform taint <addr>Marks a resource to be destroyed and recreated on the next apply, without changing its configurationterraform workspace list / new / selectManages named, isolated state files within one configuration — commonly used for dev/staging/prod separation

The four block types you'll use constantly

resource — declares a real, managed piece of infrastructure. Syntax: resource "<provider_type>" "<local_name>" { ... }.

variable — declares an input, with an optional type and default. Referenced elsewhere as var.<name>.

output — surfaces a value after apply (like a generated IP or ID), useful for chaining into other tooling or just confirming what got created. Referenced by other configurations as module.<name>.<output_name>.

data — reads information about a resource that already exists but that this configuration doesn't manage — a lookup, not a create.

Resource block

resource "aws_instance" "web" {
  ami           = "ami-0123456789abcdef0"
  instance_type = "t3.micro"

  tags = {
    Name = "web-server"
  }
}

Variable block + reference

variable "instance_type" {
  type    = string
  default = "t3.micro"
}

resource "aws_instance" "web" {
  instance_type = var.instance_type
}

Output block

output "instance_public_ip" {
  value = aws_instance.web.public_ip
}

Data source (read-only lookup)

data "aws_ami" "latest_amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}

locals — named expressions, not inputs

Unlike a variable, a local isn't set from outside the configuration — it's a named expression computed inside it, useful for avoiding repetition (like a tag map reused across many resources).

locals {
  common_tags = {
    Project = "itcertfoundry"
    Managed = "terraform"
  }
}

resource "aws_instance" "web" {
  tags = local.common_tags
}

count vs. for_each — creating multiples

count is simplest for N identical copies, but reordering/removing an item from the middle of a count-based list can cause Terraform to destroy and recreate resources you didn't intend to touch, since it tracks by numeric index.

for_each tracks by a stable key instead of position, so removing one item only affects that one resource — the safer default once resources aren't truly interchangeable.

# count: index-based, good for identical copies
resource "aws_instance" "web" {
  count         = 3
  ami           = "ami-0123456789abcdef0"
  instance_type = "t3.micro"
}

# for_each: key-based, good when each instance needs distinct settings
resource "aws_instance" "web" {
  for_each      = toset(["app1", "app2", "app3"])
  ami           = "ami-0123456789abcdef0"
  instance_type = "t3.micro"
  tags          = { Name = each.key }
}

String interpolation

name = "${var.environment}-web-server"
# In modern Terraform, a bare reference doesn't need \${...} wrapping:
instance_type = var.instance_type

Referencing another resource's attributes

This is how Terraform builds an implicit dependency graph — referencing aws_security_group.web_sg.id inside the aws_instance block tells Terraform the security group must be created first, with no manual ordering required.

resource "aws_security_group" "web_sg" {
  name = "web-sg"
}

resource "aws_instance" "web" {
  vpc_security_group_ids = [aws_security_group.web_sg.id]
}

Modules — reusing configuration

A module is just a directory of .tf files referenced from elsewhere — the same resource/variable/output blocks you already know, packaged for reuse across projects instead of copy-pasted.

module "vpc" {
  source = "./modules/vpc"

  cidr_block = "10.0.0.0/16"
}

Common mistakes worth avoiding

Committing a .tf file with a hardcoded secret (access key, password) directly in it — use variables sourced from environment variables or a secrets manager instead, never a literal value in version control.

Running apply without reading the plan output first — plan is the safety checkpoint that catches an unintended destroy/replace before it happens for real.

Forgetting that count-based resources are removed/recreated by INDEX, not identity — switching a list's order can silently destroy and recreate resources that didn't actually change.

Real scenarios you'll actually face

"Someone changed a resource by hand in the cloud console, and now Terraform's plan wants to 'fix' it back." This is state drift — Terraform's state file says one thing, the real infrastructure now says another, and `plan` always trusts state as the source of truth, proposing whatever change makes reality match it again. The honest fix depends on intent: if the manual change should stick, update the .tf file to match it (and possibly `terraform apply` a no-op-producing config, or `terraform import`/`terraform state` commands for more surgical fixes); if it shouldn't have happened, let the next apply revert it. Either way, the console change is the actual root cause worth addressing (why did someone bypass Terraform), not just the drift itself.

"I removed one item from the middle of a count-based list and it destroyed and recreated resources I never touched." count tracks resources by numeric INDEX, not by identity — remove item 2 out of 5, and every item that was previously indexed 3, 4, 5 shifts down one slot, and Terraform sees that as those slots now describing different resources, triggering destroy/recreate on all of them. `for_each` with a stable key (a name, not a position) avoids this entirely, since removing one key only ever affects that one resource — this is exactly why for_each is the safer default once resources aren't truly interchangeable duplicates of each other.

"I need to bring an existing hand-created resource under Terraform's management without destroying and rebuilding it." This is what `terraform import` is for specifically — it links a real, already-existing resource to a resource block in your configuration without ever calling create on it. The part people get wrong: import only populates STATE, it doesn't write the matching .tf configuration for you — you still need a resource block already written (even with placeholder values) for the import target to attach to, or the next plan will immediately want to destroy what you just imported.

OFFLINE OPTION

Want a printable copy?

This complete guide is also available as a professionally formatted PDF.

Download PDF ↓