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.