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
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_typeReferencing 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.
Want a printable copy?
This complete guide is also available as a professionally formatted PDF.
Download PDF ↓