Terraform Cheatsheet

Terraform CheatsheetAlle Kapitel

Terraform Cheatsheet im Volltext

Stand Terraform 1.17 · Edition 2026.11

Cheatsheet im Volltext

Derselbe Inhalt wie im PDF, zum Mitlesen, Durchsuchen und direkten Kopieren der HCL-Snippets. Stand: Terraform v1.17 (Edition 2026.11).

HCL-Kern

Bausteine

resource: verwaltetes Objekt (Create/Update/Delete durch Terraform). data: Read-only-Lookup bestehender Objekte. variable: Eingabe pro Root-/Child-Modul. locals: benannte Zwischenwerte, DRY für Ausdrücke. output: Rückgabewerte, Schnittstelle zwischen Modulen und nach außen.

Typen, Validation, sensitive

Typen explizit setzen: string, number, bool, list(...), map(...), object({...}), fängt Fehler beim Plan statt beim Apply.

validation-Block prüft Eingaben mit condition + error_message.

sensitive = true maskiert Werte im CLI-Output, entfernt sie aber nicht aus State oder Plan-Datei.

variable "env" {
  type = string
  validation {
    condition     = contains(["dev","prod"], var.env)
    error_message = "env muss dev oder prod sein."
  }
}
locals {
  name_prefix = "app-${var.env}"
}
output "lb_dns" {
  value       = aws_lb.this.dns_name
  description = "Public LB-Endpoint"
}

Provider & Versionen

Version-Pinning

required_version pinnt die CLI, required_providers pinnt Provider (source + version). Pessimistic-Operator ~> lässt nur die letzte angegebene Stelle steigen: ~> 6.0 = 6.x, ~> 6.0.1 = 6.0.x.

.terraform.lock.hcl einchecken, exakte Provider-Versionen + Hashes für alle Umgebungen. terraform init -upgrade ist eine bewusste Aktion, kein Default.

Provider-Alias

Mehrere Accounts/Rollen: gleicher Provider mit alias, Zuordnung per provider = am Resource- bzw. providers =-Map am Modul-Block.

Nur andere Region im selben Account: AWS-Provider 6.0+ nimmt region an den meisten Ressourcen direkt, ohne Alias.

terraform {
  required_version = ">= 1.16.7, < 2.0"
  required_providers {
    aws = { source = "hashicorp/aws",
            version = "~> 6.0" }
  }
}
provider "aws" { region = "eu-central-1" }
provider "aws" {
  alias  = "dr"
  region = "eu-west-1"
  assume_role { role_arn = var.dr_role_arn }
}
resource "aws_s3_bucket" "replica" {
  provider = aws.dr
  bucket   = "${local.name_prefix}-replica"
}

State

Remote Backend, Locking

State gehört in ein Remote Backend (S3, azurerm, gcs, …): gemeinsamer Stand + Locking gegen parallele Applies.

S3: natives Locking via use_lockfile = true (seit 1.10, GA in 1.11); die DynamoDB-Argumente sind seit 1.11 deprecated.

terraform {
  backend "s3" {
    bucket       = "tf-state-prod"
    key          = "network/terraform.tfstate"
    region       = "eu-central-1"
    use_lockfile = true   # natives S3-Locking
    encrypt      = true
  }
}

State ist sensibel

Der State enthält Resource-Attribute im Klartext, auch Passwörter und Keys. Zugriff wie auf ein Secret beschränken, Verschlüsselung at rest aktivieren.

Werte aus dem State heraushalten: ephemeral Values/Resources (1.10+) und write-only Attribute (1.11+) werden nicht persistiert.

State-Kommandos, Drift

state list / state show: Inventar inspizieren. state mv / state rm: imperative Chirurgie, für geplantes Refactoring besser moved/removed-Blöcke.

Drift: plan -refresh-only zeigt Abweichungen der Realität vom State, apply -refresh-only übernimmt sie in den State (ohne Infrastruktur-Änderung).

plan -minimal-refresh (1.17+) refresht nur Ressourcen mit geplanter Änderung; übrige Drift bleibt unsichtbar. Nicht bei Provider-Upgrades nutzen.

terraform state list
terraform state show aws_s3_bucket.logs
terraform state mv aws_iam_role.a aws_iam_role.b
terraform state rm aws_iam_role.orphan
terraform plan -refresh-only   # Drift-Report

Module

Struktur

Standard-Layout: main.tf, variables.tf, outputs.tf (+ README). Kleine, fachlich geschnittene Module; Komposition flach im Root-Modul. Tiefe Modul-in-Modul-Ketten machen Plans träge und Schnittstellen unklar.

Quellen, Versionierung

Registry-Quelle <ns>/<name>/<provider> + version (nur Registry-Quellen unterstützen version).

Git-Quelle über ?ref= auf Tag/Commit pinnen, nie auf einen beweglichen Branch.

Seit 1.15 dürfen source/version Variablen und Locals enthalten; die Variable braucht const = true (zur init-Zeit bekannt, nicht sensitive/ephemeral). Seit 1.17 gilt das auch in required_providers.

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "6.7.3"
  name    = local.name_prefix
  cidr    = "10.20.0.0/16"
}
module "baseline" {
  source = "git::https://git.example.com/baseline.git?ref=v2.3.1"
}

Meta-Argumente

for_each, count, depends_on

for_each über Map/Set: stabile Keys als Adresse, Einträge löschen verschiebt keine Nachbarn.

count: nur für identische Kopien oder das An/Aus-Muster (count = var.enabled ? 1 : 0). Bei benannten Objekten drohen Index-Shifts.

depends_on: nur wenn die implizite Abhängigkeit über Referenzen nicht ausreicht (z.B. IAM-Policy vor Nutzung).

lifecycle

create_before_destroy: Ersatz zuerst hochziehen. prevent_destroy: Plan schlägt fehl, statt z.B. eine Datenbank zu löschen.

ignore_changes: einzelne Attribute extern verwalten lassen, chirurgisch einsetzen. replace_triggered_by: Ersatz an fremde Änderung koppeln.

destroy = false (1.16+): Delete wird zu Forget (bei Replace, entfallenden count/for_each-Instanzen, destroy); das Objekt bleibt unverwaltet stehen. Wer den ganzen Block löscht, braucht weiter removed.

resource "aws_iam_user" "team" {
  for_each = toset(["ci", "audit", "ops"])
  name     = "svc-${each.key}"
}
resource "aws_db_instance" "main" {
  # ...
  manage_master_user_password = true # Secrets Manager
  lifecycle {
    prevent_destroy = true
  }
}

dynamic blocks

Wiederholte Nested Blocks

dynamic "<block>" + for_each + content erzeugt wiederholte verschachtelte Blöcke; Iterator heißt wie das Block-Label. Sparsam einsetzen: verschachtelte dynamics sind schwer lesbar, oft ist die ausgeschriebene Liste klarer. Bietet der Provider eine eigene Ressource je Eintrag (z.B. aws_vpc_security_group_ingress_rule), dort for_each statt dynamic.

resource "aws_s3_bucket_lifecycle_configuration" "logs" {
  bucket = aws_s3_bucket.logs.id
  dynamic "rule" {
    for_each = var.expire_days   # z.B. {tmp=7, audit=365}
    content {
      id     = rule.key
      status = "Enabled"
      filter { prefix = "${rule.key}/" }
      expiration { days = rule.value }
    }
  }
}

Plan / Apply / Destroy

Kern-Workflow

fmt -check + validate als schnelles Gate, dann plan -out=tfplan und apply tfplan, es wird exakt der reviewte Plan angewandt.

plan -detailed-exitcode: 0 = keine Änderung, 1 = Fehler, 2 = Änderungen; Exit 2 im CI-Step explizit auswerten, sonst bricht der Step unter set -e ab. -replace=<addr> erzwingt gezielte Neuerstellung. destroy nur für ganze (Wegwerf-)Umgebungen.

-policies=<dir> (GA seit 1.17): fehlt das tfpolicy-plugin, meldet Terraform einen Fehler, plan und apply laufen aber ohne Policy-Prüfung weiter.

-target ist ein Notfall-Werkzeug

-target schneidet den Abhängigkeitsgraphen ab, das Ergebnis entspricht nicht mehr der Konfiguration. Nur zur Incident-Reparatur, danach sofort ein voller Plan ohne -target, bis er leer ist.

terraform fmt -check -recursive
terraform validate
terraform plan -out=tfplan -detailed-exitcode
terraform apply tfplan
terraform apply -replace='aws_instance.worker[2]'

Import & Refactoring

import-Block (1.5+)

Bestehende Infrastruktur deklarativ adoptieren: planbar, reviewbar, im normalen Plan/Apply-Zyklus. -generate-config-out=<neue Datei> (experimentell, nur Root-Modul, ohne count/for_each) erzeugt HCL für importierte Objekte ohne Config. for_each am import-Block seit 1.7, import-Blöcke in Child-Modulen seit 1.16.

Statt id geht seit 1.12 identity = {...} (strukturierte Resource Identity, sofern der Provider sie liefert; schließt id aus).

terraform query (1.14+): list-Blöcke in *.tfquery.hcl finden bestehende Objekte, -generate-config-out liefert Config + import-Blöcke.

moved / removed

moved (1.1+): Adresse geändert (Rename, Modul-Umzug), Terraform migriert den State beim Plan, kein state mv.

removed (1.7+): Objekt aus Config + State entfernen, ohne es zu zerstören (destroy = false).

import {
  to = aws_s3_bucket.logs
  id = "legacy-logs-bucket"
}
moved {
  from = aws_instance.app
  to   = module.compute.aws_instance.app
}
removed {
  from = aws_s3_bucket.archiv
  lifecycle { destroy = false }
}

Workspaces & Tests

Workspaces und ihre Grenzen

Ein Workspace = eigener State bei gleicher Config und gleichem Backend. Gut für kurzlebige Varianten (Feature-Test, Review-Umgebung).

Grenze: Prod/Non-Prod-Trennung, gleiche Credentials, gleicher State-Bucket, terraform.workspace-Verzweigungen im Code. Dafür getrennte Root-Module + Backends verwenden.

terraform workspace new review-142
terraform workspace select review-142
terraform workspace list
# im Code: name = "app-${terraform.workspace}"

terraform test (1.6+)

Tests in *.tftest.hcl: run-Blöcke führen command = plan (legt nichts an, braucht ohne Mock aber Provider-Zugang) oder apply aus, Default ist apply, und prüfen assert-Bedingungen. Seit 1.7: mock_provider + Overrides, Provider-Antworten simulieren statt echte Infrastruktur anzulegen.

# tests/naming.tftest.hcl
run "prod_prefix" {
  command = plan
  variables { env = "prod" }
  assert {
    condition     = local.name_prefix == "app-prod"
    error_message = "Prefix-Schema verletzt"
  }
}

CI/CD-Patterns

Plan-Artefakt, Approval

Build-Job: plan -out=tfplan, Plan-Artefakt + lesbaren show-Output an den Review hängen. Nach Approval wendet der Apply-Job dasselbe Artefakt an, kein zweiter Plan zwischen Review und Apply, keine unbemerkte Differenz.

tfplan enthält sensitive Werte im Klartext; das Artefakt wie den State schützen.

Pipeline-Hygiene

init -input=false (keine interaktiven Prompts), fmt -check + validate als frühes Gate, Lock-Datei committed; init -lockfile=readonly schreibt sie nie um, neue oder entfernte Provider brechen ab.

State-Zugriff nur für die Pipeline-Identität; kurzlebige Cloud-Credentials statt statischer Keys.

terraform init -input=false -lockfile=readonly
terraform plan -input=false -out=tfplan
terraform show -no-color tfplan > plan.txt  # Review
# nach Approval:
terraform apply -input=false tfplan

Version & Lizenz

1.x-Release-Linie, BUSL

Aktuelle stabile Linie: 1.17 (zuletzt 1.17.1 vom 28.10.2026), auf 1.16 zuletzt 1.16.7 vom 22.10.2026.

Sicherheit: 1.17.x und 1.16.7 sind mit Go 1.26.10 gebaut und verwerfen TLS-Ketten über die kompromittierten eMudhra-Intermediates (CVE-2026-111943), etwa bei init gegen Registry, Mirror oder Backend. 1.15.9 (Go 1.25.10) hat den Fix nicht. Provider sind eigene Binaries mit eigener Toolchain.

Untergrenze daher 1.16.7 oder 1.17.x. Bis 1.16.2 gab es außerdem Fehler bei destroy = false mit create_before_destroy und bei import-Blöcken.

Seit 1.6 steht Terraform unter der Business Source License 1.1 (Change License MPL 2.0, je Version vier Jahre nach Veröffentlichung); die Einschränkung zielt auf konkurrierende Angebote, interne Nutzung im eigenen Unternehmen zählt laut Lizenz nicht dazu. Versionen bis 1.5.x bleiben MPL 2.0. OpenTofu ist ein MPL-2.0-lizenzierter Fork des letzten MPL-Stands (letztes MPL-Release: 1.5.7).

Anti-Patterns

Was du nicht tun solltest

-target als Routine: zerschnittener Graph, Code und Realität divergieren, nur im Notfall.

State von Hand editieren: JSON-Chirurgie korrumpiert Serial/Lineage, state-Kommandos oder moved/removed nutzen.

count für benannte Objekte: Löschen in der Mitte verschiebt Indizes, Nachbarn werden zerstört/neu erzeugt. for_each nehmen.

Provider/Module unpinned: nächstes init zieht ein Major-Update in den Apply. Provider pinnen + Lock-Datei committen; die Lock-Datei deckt nur Provider ab, Modulversionen löst jedes frische init neu auf, also exakt pinnen (version bzw. ?ref=-Tag).

Secrets als Klartext-Variable: landen in State und Plan-Datei, ephemeral/write-only nutzen, State-Zugriff beschränken.

Workspaces als Prod-Trennung: ein Backend, eine Credential-Grenze für alles, getrennte Root-Module + Backends.

Apply ohne Plan-Artefakt in CI: zwischen Review und Apply entsteht ein anderer Plan, immer apply tfplan.

Monolith-Root-Modul: ein State für alles = maximaler Blast-Radius und träge Plans, nach Lebenszyklus/Team schneiden.

ignore_changes als Dauerpflaster: verdeckt Drift und echte Ownership-Fragen, klären, wer das Attribut verwaltet.