Start / Cheat Sheets

Cheat Sheet

Terraform und OpenTofu: Kern — Cheat Sheet

Stand: · Terraform und OpenTofu in der Praxis

TerraformOpenTofuHCLInfrastructure as Code

Der Spickzettel zur ersten Hälfte des Seminars Terraform und OpenTofu in der Praxis: Plan lesen, Projekt aufbauen, Typen, Ressourcen, Module und State. Er bringt nichts davon bei, dafür sind die Module da. Secrets, Umgebungen, Import, Tests, Pipelines, Betrieb und Migration stehen auf dem zweiten Blatt: Terraform und OpenTofu: Teambetrieb.

Maßgeblich für Argumente, Flags und Versionsstände sind die Terraform-Dokumentation und die OpenTofu-Dokumentation. Dieses Blatt trifft eine Auswahl. Wo die beiden Werkzeuge auseinandergehen, steht Nur OpenTofu dabei; die Befehle sind als terraform geschrieben, bei OpenTofu heißt es tofu.

Plan-Symbole: was mit einer Ressource passiert

ZeichenAktion
+erstellen
~an Ort und Stelle ändern
-/+ersetzen: entfernen, dann neu erstellen
-entfernen

Ob geändert oder ersetzt wird, legt der Provider je Argument fest. Im Plan steht es als # forces replacement.

# docker_container.web must be replaced
-/+ resource "docker_container" "web" {
      name = "saatplan-web"
    ~ ports {
        ~ external = 8008 -> 8080 # forces replacement
          internal = 80
      }
  }
Plan: 1 to add, 0 to change, 1 to destroy.

Vor jedem apply: Was wird ersetzt, was entfernt, und ist das gewollt? Der State ordnet jeder Ressource genau ein reales Objekt zu; fehlt er, will das Werkzeug alles neu anlegen.

Module: Infrastructure as Code einordnen · CLI-Workflow und Projektstruktur

Terraform oder OpenTofu

TerraformOpenTofu
HerkunftHashiCorp, seit 2025 Teil von IBMFork von Terraform 1.5 aus dem Jahr 2023
LizenzBusiness Source License 1.1Mozilla Public License 2.0
Trägerein HerstellerLinux Foundation, CNCF-Sandbox-Projekt
Kommandoterraformtofu
Registryregistry.terraform.ioregistry.opentofu.org
Stand 09/20261.16, Version 1.17 in der Beta1.13

Interne Nutzung erlauben beide, die BSL schließt nur konkurrierende Angebote aus.

Die Unterschiede aus diesem Teil des Seminars (— heißt: dort nicht vorhanden):

ThemaTerraformOpenTofu
Konfigurationsdateien.tfzusätzlich .tofu, die Vorrang haben, etwa versions.tofu
CLI-Konfiguration~/.terraformrc~/.tofurc
Language Serverterraform-lstofu-ls
Lock File für alle Plattformenterraform providers lock -platform=…ab 1.12 schreibt tofu init die Prüfsummen selbst
Plan lesbar und als JSONshow und show -json getrenntab 1.12 -json-into=DATEI in einem Lauf
Ressourcen auslassen—-exclude
Modulquelle OCI-Registry—oci://…?tag=…
Variablen in source/versionab 1.15 mit const = trueerkennt Konstanten meist selbst
pg-Backend: Tabellen- und Indexnamefest vorgegebentable_name, index_name frei wählbar
Verschlüsselung von State und Plan—im Werkzeug
for_each für Provider-Konfigurationen—ja
Plattform und SupportStacks und HCP Terraform, HerstellervertragCommunity und Dienstleister

Modul: Infrastructure as Code einordnen

Werkzeuge abgrenzen

WerkzeugZuständig für
Terraform / OpenTofuLebenszyklus von Infrastruktur-Objekten
Packervorgefertigte Maschinen- und Container-Images
AnsibleKonfiguration bestehender Systeme
cloud-initEinrichtung beim ersten Start einer VM
Image-BuildInhalt und Abhängigkeiten der Anwendung
Kubernetes-WerkzeugeWorkloads im Cluster, oft per GitOps

Die Grenze folgt dem Lebenszyklus: Was gemeinsam entsteht und vergeht, gehört zu einem Werkzeug.

Modul: Infrastructure as Code einordnen

Projektdateien und was in Git gehört

DateiInhaltIn Git?
versions.tfrequired_version, required_providersja
providers.tfalle provider-Blöckeja
main.tfRessourcen und Data Sourcesja
variables.tfalle Variablen, alphabetischja
locals.tflokale Werteja
outputs.tfalle Outputs, alphabetischja
.terraform.lock.hclgewählte Provider-Versionen, Prüfsummenja
.terraform/heruntergeladene Provider und Modulenein
terraform.tfstatelokaler State, kann Geheimnisse enthaltennein
*.tfplangespeicherte Plänenein

Alle .tf-Dateien eines Verzeichnisses bilden zusammen eine Konfiguration, die Aufteilung ist Konvention. Jede Variable bekommt type und description.

terraform {
  required_version = ">= 1.16.0"
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 4.6"
    }
  }
}

Ohne Internet: plugin_cache_dir und filesystem_mirror im Block provider_installation der CLI-Konfiguration; den Spiegel füllt terraform providers mirror.

Module: CLI-Workflow und Projektstruktur · HCL, Datentypen und robuste Konfigurationen

CLI-Zyklus: acht Befehle

BefehlWas er tutZielsystem?
initlädt Provider, richtet das Backend ein, schreibt das Lock Filenein
fmtbringt .tf-Dateien ins kanonische Format, mit -check nur prüfennein
validateprüft Syntax und innere Stimmigkeit, ohne Variablen und Statenein
planliest den Ist-Zustand und berechnet die Änderungenlesend
applyführt die Änderungen nach Bestätigung ausja
showzeigt den State oder einen gespeicherten Plannein
outputgibt die Output-Werte aus dem State ausnein
destroyplant und entfernt alle verwalteten Objekteja

fmt arbeitet nur im aktuellen Verzeichnis, für Unterordner braucht es -recursive.

terraform plan -out=saatplan.tfplan
terraform show saatplan.tfplan
terraform show -json saatplan.tfplan > plan.json
terraform apply saatplan.tfplan

apply mit Plandatei fragt nicht mehr nach. Die Datei enthält sensible Werte im Klartext.

Exit-Code bei plan -detailed-exitcodeBedeutung
0Erfolg, keine Änderungen
1Fehler
2Erfolg, Änderungen stehen aus, also Plan zur Freigabe vorlegen

Ohne -detailed-exitcode endet plan auch bei ausstehenden Änderungen mit 0.

Modul: CLI-Workflow und Projektstruktur

HCL: resource oder data, Variablen, Locals, Outputs

resourcedata
Wirkunglegt an, ändert und löscht ein Objektliest ein vorhandenes Objekt nur aus
Adressedocker_network.saatplandata.docker_network.saatplan
Lebenszyklusgehört zur Konfigurationliefert Werte, ändert nie etwas
variable "umgebung" {
  type        = string
  description = "Zielumgebung, test oder prod"
}
locals {
  praefix = "saatplan-${var.umgebung}"
  restart = var.umgebung == "prod" ? "always" : "no"
  api_env = [for k, v in var.api.env : "${k}=${v}"]
}
output "web_name" {
  description = "Name des Web-Containers"
  value       = docker_container.web.name
}

Gelesen wird local.praefix, nicht locals. Ein Argument mit null verhält sich, als fehlte es. JSON und YAML baut man mit jsonencode und yamlencode, nicht per Template.

Modul: HCL, Datentypen und robuste Konfigurationen

Typen, optionale Attribute und Validierung

FamilieTypenWofür
primitivstring, number, boolUmgebung, Port, Schalter
Sammlunglist, set, mapStandorte, Umgebungsvariablen
strukturiertobject, tupleEinstellungen eines Containers
Platzhalteranynur für wirklich dynamische Daten

Die Doku ist deutlich: any nie verwenden, nur um sich die Typangabe zu sparen.

variable "api" {
  type = object({
    image   = string
    port    = number
    restart = optional(string, "unless-stopped")
    env     = optional(map(string), {})
  })
  description = "Einstellungen des API-Containers"
}
variable "api_port" {
  type        = number
  description = "Externer Port der Saatplan-API"
  validation {
    condition     = var.api_port >= 1024 && var.api_port <= 65535
    error_message = "Port 1024 bis 65535 erwartet."
  }
}

Fehlt ein optionales Attribut oder ist es null, gilt der Standardwert. Mehrere validation-Blöcke je Variable sind erlaubt; seit Terraform 1.9 und OpenTofu 1.9 dürfen sie auch andere Variablen einbeziehen.

Modul: HCL, Datentypen und robuste Konfigurationen

count, for_each und depends_on

count über eine Listefor_each mit Schlüssel
Adressenapi[0], api[1], api[2]api["erlenbruch"] und so weiter
Ein Eintrag fällt wegIndizes rücken nach, Objekte werden ersetztnur der eine Schlüssel verschwindet
resource "docker_container" "api" {
  for_each = toset(var.standorte)   # list(string)
  name     = "saatplan-api-${each.key}"
  image    = docker_image.api.image_id
  env      = ["STANDORT=${each.key}"]
}

for_each braucht eine Map oder ein Set aus Strings mit bekannten Schlüsseln, nicht „known after apply“.

Abhängigkeiten entstehen über Verweise, nicht über depends_on. Das kostet: mehr unbekannte Werte, mehr Ersetzungen, Data Sources erst beim apply gelesen. Und Reihenfolge ist nicht Bereitschaft.

Modul: Ressourcen modellieren und Änderungen steuern

Lifecycle-Regeln

Regelsenkt das RisikoKehrseite
create_before_destroyAusfall beim Ersetzenalt und neu existieren gleichzeitig, feste Namen kollidieren
prevent_destroyversehentliches Löschenwirkt nicht mehr, sobald der Block aus der Konfiguration fliegt
ignore_changesDauerdiff durch Änderungen von außengilt nur für Updates, beim Anlegen zählt der Wert
replace_triggered_byveraltete Objekte nach einer Änderungnur Ressourcen-Verweise, Variablen über terraform_data

Werte im lifecycle-Block müssen Literale sein, weil sie vor allen anderen Ausdrücken ausgewertet werden.

Modul: Ressourcen modellieren und Änderungen steuern

Precondition, Postcondition und Check

Mittelgeprüftbei Fehlschlag
validationan der Variablen, vor dem Plankein Plan
preconditionvor der Änderung am Objektdie geplante Änderung unterbleibt
postconditionnach Anlegen oder Lesenstoppt Folgeschritte, macht nichts rückgängig
checkam Ende von plan und applynur eine Warnung, der Lauf geht weiter
lifecycle {
  postcondition {
    condition     = length(self.ports) == 0
    error_message = "db veröffentlicht einen Port."
  }
}

In einer postcondition steht self für das Objekt selbst.

Modul: Ressourcen modellieren und Änderungen steuern

Gezielt eingreifen: -replace, -target, -exclude

MittelwofürPreis
-replaceein Objekt ist beschädigt, die Konfiguration stimmtkeiner, der Plan zeigt den Austausch offen
-targetFehler beheben, Grenzen umgehender Rest bleibt ungeplant, Drift fällt nicht auf
-excludeNur OpenTofu: alles außer einer Adressedieselbe Vorsicht wie bei -target
Provisionerletzter Auswegihr Verhalten lässt sich nicht im Plan abbilden
terraform plan -out=saatplan.tfplan -replace='docker_container.api["sonnhalde"]'
terraform plan -target=docker_network.saatplan
tofu plan -exclude='docker_container.api["muehlwiese"]'

Statt Provisionern: upload-Block, Software ins Image, healthcheck und wait. Von count auf for_each zieht je Instanz ein moved-Block um.

Modul: Ressourcen modellieren und Änderungen steuern

Module: Schnittstelle und Provider

BestandteilInhalt
README.mdZweck des Moduls, Annahmen, Beispielaufruf
main.tf, variables.tf, outputs.tfempfohlene Mindeststruktur, auch wenn eine Datei leer bleibt
descriptionein bis zwei Sätze an jeder Variable und jedem Output
examples/Aufrufe mit der Adresse, die ein externer Aufrufer nutzen würde
deprecatedWarnung bei Variablen und Outputs, die entfallen sollen (Terraform ab 1.15, OpenTofu ebenso)
CHANGELOG.mdUpgrade-Hinweise je Version, Konvention

Ein Output ist ein Versprechen an alle Aufrufer; was nicht hinausgeht, darf sich ändern.

provider "docker" {
  alias = "test"
  host  = "ssh://deploy@docker-test.lindenhof.example"
}
module "saatplan_test" {
  source    = "./modules/saatplan-app"
  providers = { docker = docker.test }
}

Das Child Module deklariert nur required_providers mit source und Untergrenze (>= 4.6), keinen provider-Block: Sonst verträgt es kein count, for_each oder depends_on, und beim Entfernen fehlt der Provider zum Löschen. Ohne source nimmt das Werkzeug hashicorp/docker an.

Modul: Wiederverwendbare Module entwickeln

Modulquellen und Versionsbindung

QuelleBeispiel für sourceVersion
lokaler Pfad./modules/saatplan-appwie der Aufrufer
Git mit Taggit::https://…/saatplan-app.git?ref=v1.2.0über ref
Unterordnergit::…/saatplan-infra.git//modules/saatplan-app?ref=v1.2.0über ref
Registrylindenhof/saatplan-app/dockerArgument version
OCI-Registryoci://registry.lindenhof.example/saatplan-app?tag=v1.2.0Nur OpenTofu
ModuleProvider
Version im module-Block oder per refVersion in required_providers
keine Einträge in .terraform.lock.hclAuswahl samt Prüfsummen im Lock File
fremde Module exakt pinnenin wiederverwendbaren Modulen nur >=
neue Version erst nach init aktivUpgrade bewusst mit init -upgrade

version wirkt nur bei Registry-Quellen. Für die Registry heißt das Repository terraform-PROVIDER-NAME, Tags folgen Semantic Versioning (v1.0.4).

Modul: Wiederverwendbare Module entwickeln

State-Kommandos

KommandoZweck und Vorsicht
state list, state showlesen, ändern nichts
state pullaktuellen Stand als JSON ausgeben, etwa für eine Sicherung
state pushStand ins Backend schreiben, mit Prüfung von lineage und serial
state mvInstanz umadressieren, besser ein moved-Block im Code
state rmBindung lösen, das Objekt läuft weiter, besser ein removed-Block
state replace-providerProvider-Quelle im State austauschen
force-unlock LOCK_IDfremden Lock lösen, nur wenn sicher kein Lauf mehr schreibt

Ändernde state-Kommandos schreiben immer ein Backup. Werkzeuge lesen den State über show -json und output -json, das Format kann sich ändern. terraform_remote_state liest nur Root-Outputs, braucht aber Lesezugriff auf den ganzen State samt sensibler Werte.

Modul: State, Backends und Zusammenarbeit

Remote Backend pg, Locking und Migration

MerkmalVerhalten
VoraussetzungPostgreSQL ab Version 10, die Datenbank muss vor init existieren
AblageTabelle states im Schema terraform_remote_state oder schema_name
Workspaceseine Zeile je Workspace-Name, ohne Workspaces heißt sie default
LockingAdvisory Locks, gelöst mit Ende der Sitzung oder Verbindung
force-unlocknicht unterstützt, offene Locks zeigt die Systemsicht pg_locks
Rechteeigenes Schema und Schema public, dort liegt die Sequenz für die IDs
Nur OpenTofutable_name und index_name frei wählbar, Lock beim Anlegen je Schema

Jedes Root Module braucht ein eigenes Schema. Zugangsdaten über die Umgebung (PG_CONN_STR, PGUSER, PGPASSWORD): Werte aus Code oder -backend-config landen in .terraform und in Plandateien.

terraform plan -lock-timeout=2m      # warten statt abbrechen
terraform init -migrate-state        # lokalen State ins Backend übernehmen

-reconfigure statt -migrate-state lässt den alten State zurück. Das pg-Backend führt keine Historie; gesichert wird per pg_dump.

Modul: State, Backends und Zusammenarbeit

Typische Fallen

  • State und Plandatei im Commit. Beide tragen sensible Werte im Klartext ins Repository, plan.json ebenso.
  • State gelöscht, weil die Container ja laufen. Der nächste Plan will alles neu anlegen. Von Hand editieren ist genauso falsch, dafür gibt es die state-Kommandos.
  • Veralteter Plan. Hat sich der State seit dem Planen geändert, lehnt apply die Plandatei ab.
  • Bequemer default. Ein Standardwert für Werte, die je Umgebung anders sind, verdeckt die vergessene Angabe.
  • Validierung prüft nur die Form. Ob ein Port auf dem Host frei ist, weiß sie nicht. Unbekannte Werte verschieben die Prüfung bis zum apply.
  • create_before_destroy mit festem Namen. Docker vergibt Containernamen nur einmal, das Ersetzen scheitert.
  • ignore_changes = all. Friert die Ressource ein, auch gegen gewollte Änderungen.
  • prevent_destroy überall. Blockiert auch gewollte Umbauten. Es gehört an Daten, die nicht neu entstehen können.
  • replace_triggered_by auf ein wechselndes Attribut. Ersetzt das Objekt bei jedem Lauf.
  • destroy löscht auch das Volume. Mit allen Daten darin, in prod nie beiläufig. Verwaltet wird das Volume, nicht die Daten darin.
  • Ports aus Gewohnheit. Ein „nur zum Testen“ veröffentlichter Datenbankport ist danach von außen erreichbar.
  • Modul reicht alles durch. Jedes Ressourcenargument als Variable heißt, das Modul verbirgt nichts mehr. Ganze Ressourcenobjekte als Output machen interne Attribute zum Vertrag.
  • Variable ohne type. Sie nimmt jeden Wert an und scheitert erst tief im Plan.
  • Umbenennen ohne deprecated. Aufrufer brechen ohne Vorwarnung.
  • source zeigt auf einen Branch. Derselbe Aufruf liefert morgen anderen Code. Ein Breaking Change ohne neue Hauptversion wirkt genauso.
  • Sicherung im selben Volume. Sie fällt mit dem State aus.
  • Alter Stand mit -force zurückgespielt. Die Container sehen inzwischen anders aus.
  • Gemischte OpenTofu-Versionen am pg-Backend. Ab OpenTofu 1.10 sperrt das Anlegen anders, also nie parallel auf eine Datenbank.

Dieses Thema als Schulung für Ihr Team

Dieser Beitrag erklärt das Thema. Damit Ihr Team es danach auch anwendet, gibt es Terraform und OpenTofu in der Praxis als Schulung — an Ihrem eigenen Code, mit den Fragen, die ein Text nicht beantwortet. Sie wählen die Module, wir bauen daraus ein Programm.

5 Tage·ab 900 EUR netto pro Tag (bis 3 Teilnehmende) ·Termin nach Vereinbarung

Als Team-Schulung anfragenZum Seminar Terraform und OpenTofu in der Praxis →