Terraform Language Reference

36 of the words and commands you'll meet writing Terraform — what each one means, an HCL or CLI example, and the gotcha you'll wish someone had told you.

Last reviewed:

Sections

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.

Meta-arguments

count

Creates N copies of a resource. Each instance is addressed by index — resource[0], resource[1].

resource "aws_instance" "web" {
  count         = 3
  instance_type = "t3.micro"
}

💡 Avoid count for lists that reorder — removing the middle item re-creates everything after it. Prefer for_each.

for_each

Creates one instance per element of a map or set. Instances are keyed by name, so adding/removing is stable.

resource "aws_iam_user" "u" {
  for_each = toset(["alice", "bob"])
  name     = each.value
}

depends_on

Forces an explicit ordering when Terraform can’t infer the dependency from references alone.

resource "aws_instance" "web" {
  depends_on = [aws_iam_role_policy.app]
}

lifecycle

Tunes how Terraform creates, updates and deletes a resource — create_before_destroy, prevent_destroy, ignore_changes.

lifecycle {
  create_before_destroy = true
  prevent_destroy       = true
  ignore_changes        = [tags]
}

💡 prevent_destroy makes apply error rather than delete the resource — good for databases.

provider alias

Lets you configure the same provider more than once (e.g. two AWS regions) and pick which one a resource uses.

provider "aws" {
  alias  = "us"
  region = "us-east-1"
}
resource "aws_acm_certificate" "cdn" {
  provider = aws.us
}

workspace

Named state instances within one backend, letting you reuse a config for dev/staging/prod with separate state.

terraform workspace new staging
terraform workspace select staging

💡 Reference the current one with terraform.workspace. Many teams prefer separate directories or backends instead.

English phrases engineers use

  • "Run a plan first — I want to see the diff before we apply."
  • "That field shows drift; someone changed it in the console."
  • "Wrap it in a module so we can reuse it across environments."
  • "Use for_each, not count — otherwise removing one item re-creates the rest."
  • "The state is locked — there's an apply still running in CI."
  • "Set prevent_destroy on the database so apply can't nuke it."
  • "Pin the provider version in required_providers and commit the lock file."