DS DevShelfHub Projects · AI tools
Cheatsheets / Terraform
Cheatsheet · Dev tooling

Terraform Cheatsheet: Providers, State and Modules Reference

By DevShelfHub

Providers, resources, variables, modules, state, backends, workspaces, CLI — the daily IaC surface.

106 items 8 min Providers State Modules

Start hereQuick start · 6 you’ll reach for daily

Initterraform init
Planterraform plan -out=tfplan
Applyterraform apply tfplan
Formatterraform fmt -recursive
Inspectterraform state list
Destroyterraform destroy

Target versions · paceVersions

Targets: terraform ≥ 1.7 opentofu ≥ 1.7 aws provider ≥ 5

Snippets target HCL2 with Terraform 1.7+. OpenTofu is a drop-in CLI (tofu) and an open-source fork of Terraform under the Linux Foundation. Pin both required_version and provider versions in every config — no drift between dev and prod.

install · daily-loopSetup

bash
# Install
brew tap hashicorp/tap && brew install hashicorp/tap/terraform
# Alternative: open-source fork
brew install opentofu                   # tofu is a drop-in for `terraform`

# New project
mkdir infra && cd infra
terraform fmt -recursive
terraform init                          # downloads providers + initialises backend
terraform validate
terraform plan -out=tfplan
terraform apply tfplan
terraform destroy

# Inspect state
terraform state list
terraform state show aws_s3_bucket.assets
terraform output -json

# Auth (cloud)
export AWS_PROFILE=prod
export ARM_USE_CLI=true
gcloud auth application-default login

the languageHCL essentials

resource "type" "name" { … }Declares a resource of a provider type.
data "type" "name" { … }Read-only lookup.
variable "x" { type = string default = … }Input variable.
output "x" { value = … }Output value.
locals { name = "app-${var.env}" }Module-local computed values.
module "x" { source = "./modules/x" }Composition. Use a registry source for shared modules.
terraform { … }Top-level settings: version, providers, backend.
type constraints: string, number, bool, list, set, map, object, tuple, anySpell out variable shapes.
${expr} interpolationIn strings: "name-${var.env}". Outside strings, drop the wrapper.
heredoc: <<-EOT … EOTMulti-line strings.

where resources come fromProviders

required_providers { aws = { source, version } }Pin source + version constraint.
version = "~> 5.60"Pessimistic constraint: 5.60.x.
version = ">= 5, < 6"Range constraint.
provider "aws" { region = … }Provider config block.
provider "aws" { alias = "us_west" region = "us-west-2" }Named alias for multi-region.
resource "…" { provider = aws.us_west }Point a resource at an aliased provider.
default_tags { tags = { … } }Apply to every taggable resource.
.terraform.lock.hclLock file. Commit it. Pins exact provider versions.
terraform providers / providers lock -platform=…Inspect / cross-platform lock entries.
javascript
terraform {
  required_version = ">= 1.7"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.60"
    }
    random = { source = "hashicorp/random", version = "~> 3.6" }
  }

  backend "s3" {
    bucket         = "acme-tf-state"
    key            = "prod/app.tfstate"
    region         = "us-east-1"
    dynamodb_table = "tf-locks"
    encrypt        = true
  }
}

provider "aws" {
  region = "us-east-1"
  default_tags {
    tags = {
      Owner    = "platform"
      Env      = "prod"
      ManagedBy = "terraform"
    }
  }
}

# Pinned provider versions + remote, locked, encrypted state.
# `terraform init -upgrade` updates within the version constraint.

declare & lookupResources & data sources

resource "aws_s3_bucket" "b" { bucket = "…" }Most basic resource block.
aws_s3_bucket.b.arnReference an attribute of another resource.
data "aws_caller_identity" "me" {}Read identity / metadata.
data.aws_caller_identity.me.account_idReference a data source.
count = 3Make N copies. count.index available inside.
for_each = toset(["a", "b"])Preferred for sets / maps. each.key, each.value.
depends_on = [aws_iam_role.r]Explicit ordering when Terraform can’t infer it.
lifecycle { create_before_destroy = true }Avoid downtime on resources with unique names.
lifecycle { prevent_destroy = true }Hard guard against deletion.
lifecycle { ignore_changes = [tags] }Stop fighting external mutations.
moved { from = … to = … }Refactor without state surgery.
import { id = "…" to = … }Declarative import (1.5+). Replaces terraform import.

inputsVariables

variable "x" { type = string default = … }Default makes it optional.
variable "x" { sensitive = true }Hides value from CLI / plan output.
validation { condition = … error_message = … }Reject bad input at plan time.
type = object({ a = string, b = number })Structured input.
type = map(object({ … }))Common pattern: keyed config blocks.
terraform.tfvars / *.auto.tfvarsAuto-loaded values per env.
-var "x=y" / -var-file=prod.tfvarsCLI overrides.
TF_VAR_name=valueEnv-var fallback.
nullable = falseReject explicit null assignment.

expose valuesOutputs

output "x" { value = … }Most basic.
output "x" { sensitive = true value = … }Mask in plan / output.
description = "…"Shows up in docs / Terraform Cloud UI.
precondition / postconditionCross-resource assertions.
terraform output -json xRead at the CLI for piping into shell.
module.x.output_nameModule outputs are referenced via the module name.
depends_on (outputs)Force-order an output. Rare; mostly for cross-module dependencies.

reusable bundlesModules

module "x" { source = "./modules/x" }Local path source.
source = "terraform-aws-modules/vpc/aws" version = "~> 5"Public registry source.
source = "git::ssh://git@github.com/org/repo.git//path?ref=v1.2.0"Git source. Pin to a tag.
source = "app.terraform.io/org/vpc/aws"Private registry (Terraform Cloud / Enterprise).
for_each on a moduleMap over instances. Each gets its own state.
providers = { aws = aws.alt }Pass aliased providers into a module.
Module shape: main.tf, variables.tf, outputs.tf, README.mdThe conventional layout.
terraform-docs (CLI)Auto-generate module READMEs from HCL.
javascript
# modules/s3-bucket/main.tf
variable "name"        { type = string }
variable "force_destroy" {
  type    = bool
  default = false
}

resource "aws_s3_bucket" "this" {
  bucket        = var.name
  force_destroy = var.force_destroy
}

resource "aws_s3_bucket_public_access_block" "this" {
  bucket                  = aws_s3_bucket.this.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

output "arn"    { value = aws_s3_bucket.this.arn }
output "bucket" { value = aws_s3_bucket.this.id }

# envs/prod/main.tf — using the module
module "assets" {
  source  = "../../modules/s3-bucket"
  name    = "acme-assets-prod"
}

output "assets_bucket" {
  value = module.assets.bucket
}

the source of truthState

terraform.tfstate (local)Dev only. Never commit to git.
terraform state listAll addresses in state.
terraform state show <addr>Full attributes of one resource.
terraform state mv old newRename without re-creating.
terraform state rm <addr>Forget a resource. Real one is untouched.
terraform refreshResync state with reality. Use sparingly.
terraform taint / -replace=<addr>taint is legacy; prefer apply -replace.
Sensitive values in stateAlways there. Encrypt the backend.
terraform state pull / pushFor surgical edits. Be careful.

where state livesBackends

backend "s3" { bucket, key, region, dynamodb_table }Preferred AWS-native: S3 for state, DynamoDB for locking.
backend "gcs" { bucket, prefix }GCS for state. Object versioning gives history.
backend "azurerm" { resource_group_name, storage_account_name }Azure Storage for state.
backend "remote" { hostname, organization, workspaces }Terraform Cloud / Enterprise.
encrypt = trueAlways on remote backends.
terraform init -migrate-stateMove from one backend to another.
terraform init -reconfigureForce re-init without migrating.
Per-env state fileskey = "prod/app.tfstate" vs staging/app.tfstate.

built-in expressionsFunctions

format("%s-%d", name, n) / formatlistPrintf-style string formatting.
join(",", list) / split(",", str)List/string round-trip.
lookup(map, key, default)Safe map access with fallback.
try(maybe, fallback)Catch expression errors.
coalesce(a, b, c)First non-null / non-empty.
merge(map_a, map_b)Right-wins map merge.
for x in list : exprList comprehension: [for x in list : x.id].
{for k, v in map : k => v.id}Map comprehension.
cidrsubnet, cidrhost, cidrnetmaskNetwork math without leaving Terraform.
file("./path") / templatefile("./tpl", vars)Read / render local files.
jsonencode / jsondecode / yamlencode / yamldecodeCross-format conversion.

multi-env stateWorkspaces

terraform workspace new prodCreate + switch. Default workspace is default.
terraform workspace list / showInspect.
terraform workspace select stagingSwitch.
terraform.workspace (HCL)Read in expressions.
When NOT to useFor prod vs staging with different settings — prefer separate directories + per-env tfvars.
When TO useThrowaway feature branches sharing the same config.
TFC / TFE workspaces are differentTop-level units there; not the same as the CLI concept.

daily commandsCLI

terraform init / -upgrade / -reconfigureSet up / refresh providers + backend.
terraform plan -out=tfplanPlan + saved plan file. Apply only what you reviewed.
terraform plan -refresh=falseSkip the refresh step. Faster, slightly stale.
terraform plan -target=aws_s3_bucket.bNarrow plan. Use sparingly — lies about other drift.
terraform apply -auto-approveCI-friendly. Pair with a saved plan.
terraform apply -replace=<addr>Force one resource to be recreated.
terraform destroy -target=…Targeted destroy. Same caveats as targeted plan.
terraform fmt -recursive / validateFormat + static-check. Run in pre-commit.
terraform graph | dot -Tsvg > g.svgRender the dependency graph.
terraform consoleREPL for expressions against current state.

provider + for_each + outputsEnd-to-end · Encrypted S3 buckets

A KMS key + a map of S3 buckets each with server-side encryption, tagged via provider defaults, names keyed by env. The pattern most teams converge on.

javascript
# main.tf — VPC + S3 bucket + KMS key, with for_each + tags.
terraform {
  required_version = ">= 1.7"
  required_providers { aws = { source = "hashicorp/aws", version = "~> 5.60" } }
}

provider "aws" { region = "us-east-1" }

variable "env"      { type = string  default = "dev" }
variable "buckets"  {
  type    = map(string)
  default = { assets = "private", logs = "private" }
}

locals { name_prefix = "acme-${var.env}" }

resource "aws_kms_key" "primary" {
  description             = "${local.name_prefix} primary key"
  deletion_window_in_days = 30
  enable_key_rotation     = true
}

resource "aws_s3_bucket" "this" {
  for_each       = var.buckets
  bucket         = "${local.name_prefix}-${each.key}"
  force_destroy  = var.env != "prod"
}

resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
  for_each = aws_s3_bucket.this
  bucket   = each.value.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.primary.arn
    }
  }
}

output "buckets" {
  value = { for k, b in aws_s3_bucket.this : k => b.id }
}

Best practiceGood to know

Save the plan, apply the plan. terraform plan -out=tfplan → review → apply tfplan. Avoids the “plan moved while I was reviewing” race.
Use for_each, not count, when you can. Removing an item from the middle of a count list shifts every later resource’s address — destroying and re-creating things you didn’t mean to touch.
Pin everything. required_version, every provider version, every module version. Commit .terraform.lock.hcl. Floating versions = surprise rollouts.

Common trapsWatch out for

State has secrets, even if your config doesn’t. Random passwords, generated tokens, RDS connection strings — all in state. Encrypt the backend; lock down read access; rotate exposed credentials immediately.
-target lies about the rest of the world. Useful as an emergency tool, dangerous as a habit. Drift accumulates outside the targeted set; your next full plan will rediscover surprises.
External changes get reverted on apply. Manual fixes in the console get overwritten next apply. Either import the change back into HCL, or use lifecycle.ignore_changes.

Go deeperSee also

Terraform FAQ

What is Terraform used for?

Terraform is an open-source infrastructure-as-code (IaC) tool by HashiCorp. You declare cloud resources (VMs, databases, networks, DNS, etc.) in HCL configuration files, and Terraform plans and applies the changes across any provider — AWS, GCP, Azure, Kubernetes, and hundreds more. It keeps a state file to track real-world infrastructure and produce diffs.

What is Terraform state and why does it matter?

State is a JSON file (terraform.tfstate) that maps your configuration to real infrastructure. Terraform uses it to compute diffs, prevent duplicate resource creation, and track dependencies. For team use, always store state in a remote backend (S3, GCS, Terraform Cloud, or Azure Blob) with locking enabled so concurrent runs don't corrupt it.

What is the difference between terraform plan and terraform apply?

terraform plan performs a dry run — it shows what changes Terraform would make (create, update, destroy) without modifying anything. terraform apply executes those changes. Always review the plan output before applying, especially for destroy actions. Use terraform plan -out=tfplan then terraform apply tfplan to guarantee the applied plan matches what you reviewed.

What are Terraform modules and when should I use them?

A module is a reusable collection of resources grouped in a directory. Use modules to encapsulate repeatable patterns — a VPC, a Kubernetes node pool, a serverless function — so you can instantiate them with different variables instead of copying code. The Terraform Registry hosts community modules; for teams, use private module registries or monorepo modules.

What is the difference between Terraform and OpenTofu?

OpenTofu is a community fork of Terraform 1.5 created after HashiCorp changed the Terraform licence to BUSL (non-OSI) in August 2023. OpenTofu remains MPL-licensed (fully open source) and is maintained by the Linux Foundation. The two are largely compatible at the HCL level, though they are diverging with new features.

Is Terraform free?

The Terraform CLI is free under the BUSL licence for most use cases (commercial use is restricted for managed service providers). OpenTofu is MPL-2.0 and fully free. Terraform Cloud has a free tier for individuals; team and business plans are paid. For most individual or team IaC use, the free CLI plus a remote state backend is sufficient.