Core blocks
provider
A plugin that lets Terraform talk to a platform (AWS, GCP, Cloudflare, GitHub). It exposes the resources and data sources you can manage.
provider "aws" {
region = "eu-central-1"
}
💡 Providers are downloaded during terraform init and pinned in .terraform.lock.hcl.
resource
A single piece of infrastructure Terraform creates and manages — a VM, a bucket, a DNS record. The verb of Terraform.
resource "aws_s3_bucket" "logs" {
bucket = "my-app-logs"
}
💡 Addressed as <type>.<name>, e.g. aws_s3_bucket.logs.
data source
Reads existing information from a provider without creating anything — look up an AMI, an existing VPC, your account ID.
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
}
module
A reusable package of resources called from another configuration. The unit of composition and reuse in Terraform.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
cidr = "10.0.0.0/16"
}
💡 The top-level configuration is itself the "root module".
variable
A typed input to a configuration or module. Set via -var, *.tfvars files, env vars (TF_VAR_*), or defaults.
variable "instance_count" {
type = number
default = 2
}
output
A value a module exposes to its caller (or prints after apply) — an IP address, a bucket name, a connection string.
output "bucket_arn" {
value = aws_s3_bucket.logs.arn
}
💡 Mark sensitive = true to keep it out of CLI output.
locals
Named local values to avoid repetition — like constants computed once and referenced as local.<name>.
locals {
common_tags = {
Project = "lingo"
Env = var.env
}
}
terraform block
Settings for Terraform itself — required version, required providers, and the backend configuration.
terraform {
required_version = ">= 1.6"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
HCL syntax
block
The fundamental HCL container: a type, optional labels, and a body in braces. resource, variable, module are all blocks.
block_type "label_one" "label_two" {
argument = value
}
attribute
A name = value assignment inside a block. Also called an argument.
instance_type = "t3.micro"
expression
Anything that produces a value — a literal, a reference, a function call, or arithmetic.
max(var.min_size, length(var.azs))
interpolation
Embedding an expression inside a string with ${...}. In HCL2 you rarely need it for whole values — bare references work.
name = "web-${var.env}-${count.index}"
heredoc
A multi-line string literal. Use <<-EOT (with the dash) to strip leading indentation.
user_data = <<-EOT
#!/bin/bash
echo hello
EOT
ternary
The conditional expression: condition ? true_value : false_value. The closest HCL has to an if statement.
instance_type = var.env == "prod" ? "m5.large" : "t3.micro"
for expression
Transforms one collection into another. Square brackets build a list (tuple); braces build a map (object).
upper_names = [for n in var.names : upper(n)]
by_id = {for u in var.users : u.id => u.name}
Lifecycle commands
init
Initialises a working directory — downloads providers and modules, configures the backend. Run it first, and after changing providers.
terraform init -upgrade
plan
Shows what Terraform would change without doing it. The "+" is create, "-" is destroy, "~" is update in place.
terraform plan -out=tfplan
💡 Save the plan with -out and apply exactly that plan to avoid surprises between plan and apply.
apply
Actually makes the changes to reach the desired state. Asks for confirmation unless you pass -auto-approve.
terraform apply tfplan
destroy
Removes everything Terraform manages in this state. The same as a plan where every resource is being deleted.
terraform destroy -target=aws_s3_bucket.logs
import
Brings an existing real-world resource under Terraform management by writing it into state.
terraform import aws_s3_bucket.logs my-app-logs
💡 Modern Terraform can also do this declaratively with an import {} block.
taint
Marks a resource for recreation on the next apply. Largely replaced by terraform apply -replace.
terraform apply -replace="aws_instance.web"
refresh
Updates state to match real infrastructure without changing anything. Now folded into plan/apply by default.
terraform plan -refresh-only
validate
Checks that the configuration is syntactically valid and internally consistent — no API calls, no state needed.
terraform validate
fmt
Rewrites files to the canonical HCL style (indentation, alignment). Run it in CI with -check to enforce formatting.
terraform fmt -recursive -check
State
state file
Terraform’s record of the real resources it manages and how they map to your config. Source of truth for what exists.
terraform state list
💡 Treat it as sensitive — it can contain passwords and keys in plaintext.
terraform.tfstate
The default local state file. A JSON document; never edit it by hand — use the state subcommands.
cat terraform.tfstate | jq .resources
remote backend
Stores state somewhere shared (S3, Terraform Cloud, GCS) so a team isn’t passing a local file around.
terraform {
backend "s3" {
bucket = "tf-state"
key = "prod/app.tfstate"
region = "eu-central-1"
}
}
state lock
A mutex that stops two people running apply at once and corrupting state. The S3 backend uses DynamoDB (or native locking) for this.
terraform force-unlock <LOCK_ID>
💡 Only force-unlock when you are certain no other run is in progress.
drift
When real infrastructure no longer matches state — someone changed it in the console. A plan reveals drift as proposed changes.
terraform plan -refresh-only
state mv / rm
Surgically move a resource’s address (after a refactor) or remove it from state without destroying the real resource.
terraform state mv aws_s3_bucket.old aws_s3_bucket.new
terraform state rm aws_s3_bucket.logs
💡 For refactors, a moved {} block in config is preferred over manual state mv.