Terraform und OpenTofu
Terraform beschreibt Infrastruktur deklarativ in der Sprache HCL (HashiCorp Configuration Language) und bringt sie über Provider in beliebige Plattformen: Azure, AWS, VMware, DNS-Dienste, GitLab und viele mehr. Diese Seite vertieft die Konzepte, die im IaC-Lab bereits praktisch vorkommen.
Zurück zur IaC-Übersicht · Grundbegriffe unter Prinzipien
Arbeitsablauf
terraform init Provider und Module laden, Backend verbinden
│
terraform plan Soll (Code) mit Ist (State + Realität) vergleichen
│ → Liste der Änderungen: + anlegen, ~ ändern, - löschen, -/+ ersetzen
│
terraform apply Änderungen ausführen, State aktualisieren
│
terraform destroy Alles Verwaltete wieder entfernen
Im Plan ist das Symbol -/+ besonders wichtig: Die Ressource kann nicht verändert werden und wird gelöscht und neu angelegt. Bei einer VM oder Datenbank bedeutet das Datenverlust – genau deshalb stuft die Change-Klassifizierung jedes Ersetzen als Normal Change ein.
Die Bausteine
| Block | Zweck | Beispiel im Lab |
|---|---|---|
terraform | Terraform-Version, Provider-Versionen, Backend | required_providers { azurerm … } |
provider | Verbindung zur Plattform | provider „azurerm“ { features {} } |
resource | Etwas, das Terraform anlegt und verwaltet | azurerm_linux_virtual_machine_scale_set |
data | Etwas, das nur gelesen wird | data „azurerm_subnet“ „app“ |
variable | Eingabewert | var.image_version |
locals | Berechnete Hilfswerte | local.tags |
output | Ausgabewert, z. B. für andere Schichten | output „url“ |
module | Wiederverwendbares Paket aus mehreren Ressourcen | – (siehe unten) |
Referenzen erzeugen Abhängigkeiten. Wenn das Scale Set azurerm_lb_backend_address_pool.web.id verwendet, weiß Terraform, dass der Pool zuerst existieren muss. Daraus baut Terraform einen Abhängigkeitsgraphen und arbeitet unabhängige Ressourcen parallel ab. depends_on ist nur nötig, wenn eine Abhängigkeit nicht aus Referenzen hervorgeht – im Lab etwa zwischen Scale Set und Load-Balancer-Regel.
terraform graph | dot -Tsvg > graph.svg # Abhängigkeitsgraph visualisieren (Graphviz)
Mehrere gleichartige Ressourcen
# for_each mit Map – stabil, jede Instanz hat einen sprechenden Schlüssel
resource "azurerm_resource_group" "env" {
for_each = toset(["dev", "test", "prod"])
name = "rg-webshop-${each.key}"
location = "germanywestcentral"
}
# Adressen: azurerm_resource_group.env["dev"], …["test"], …["prod"]
count erzeugt nummerierte Instanzen ([0], [1], …). Wird ein Element aus der Mitte entfernt, verschieben sich alle folgenden Nummern – Terraform will dann mehrere Ressourcen ersetzen. Faustregel: count nur für „an/aus“ (count = var.enabled ? 1 : 0), sonst for_each.
State: das Gedächtnis
Terraform merkt sich im State, welche reale Ressource zu welchem Block im Code gehört. Ohne State wüsste Terraform nicht, dass azurerm_resource_group.base die Ressourcengruppe mit einer bestimmten Azure-ID ist.
- Remote Backend: Der State liegt zentral (im Lab: Azure Storage), damit alle – vor allem die Pipeline – denselben Stand sehen.
- Sperre: Während
applysperrt Terraform den State (bei Azure über einen Blob-Lease). Zwei gleichzeitige Applies sind damit ausgeschlossen. - Sensibel: Der State enthält alle Attribute der Ressourcen, auch Passwörter und Schlüssel, die Terraform erzeugt hat. Er gehört nie ins Git-Repository.
- Nie von Hand editieren. Für Korrekturen gibt es Befehle und – besser – Blöcke im Code (nächster Abschnitt).
terraform state list # alle verwalteten Ressourcen terraform state show azurerm_lb.web # Attribute einer Ressource terraform plan -refresh-only # nur Drift anzeigen, nichts planen
Refactoring ohne Neubau
Code wird umgebaut: Ressourcen werden umbenannt, in Module verschoben, aus der Verwaltung entlassen oder bestehende Ressourcen übernommen. Früher ging das nur mit terraform state mv/rm/import auf der Kommandozeile – unsichtbar für Review und Pipeline. Heute gibt es dafür Blöcke im Code, die im Merge Request sichtbar sind und im Plan geprüft werden.
moved: umbenennen und verschieben
Beispiel: Umbau des Labs von einem Scale Set auf Blue/Green. Ohne moved würde Terraform das bestehende Scale Set löschen und ein neues [„blue“] anlegen.
moved {
from = azurerm_linux_virtual_machine_scale_set.web
to = azurerm_linux_virtual_machine_scale_set.web["blue"]
}
moved {
from = azurerm_lb_backend_address_pool.web
to = azurerm_lb_backend_address_pool.web["blue"]
}
Der Plan zeigt dann has moved to statt -/+. Nach erfolgreichem Apply können die moved-Blöcke entfernt werden – oder bleiben als Dokumentation stehen, falls andere Umgebungen den Umbau noch nicht durchlaufen haben.
import: bestehende Ressourcen übernehmen
Der Normalfall in der Praxis: Es gibt bereits Infrastruktur, die von Hand angelegt wurde. Sie wird Schritt für Schritt unter Terraform-Verwaltung gestellt („Start where you are“).
import {
to = azurerm_resource_group.legacy
id = "/subscriptions/<subscription-id>/resourceGroups/rg-legacy-app"
}
terraform plan -generate-config-out=generated.tf
Terraform liest die Ressource aus Azure und schreibt passenden HCL-Code in generated.tf. Der generierte Code ist ein Startpunkt: aufräumen, Werte durch Variablen ersetzen, Tags ergänzen. Ziel ist ein Plan, der keine Änderungen mehr zeigt – dann beschreibt der Code exakt die Realität.
removed: aus der Verwaltung entlassen
removed {
from = azurerm_resource_group.legacy
lifecycle {
destroy = false # Ressource bleibt in Azure bestehen
}
}
Nützlich, wenn eine Ressource an eine andere Schicht oder ein anderes Team übergeben wird.
Lifecycle-Steuerung
| Einstellung | Wirkung | Typischer Einsatz |
|---|---|---|
prevent_destroy | Plan bricht ab, wenn die Ressource gelöscht würde | Datenbanken, Storage mit Daten, State-Backend |
create_before_destroy | Beim Ersetzen erst neu anlegen, dann alt löschen | Zertifikate, Ressourcen ohne Ausfallzeit |
ignore_changes | Änderungen an bestimmten Attributen ignorieren | Werte, die bewusst extern gesteuert werden (Autoscale) |
replace_triggered_by | Ersetzen, wenn sich eine andere Ressource ändert | VM neu bauen, wenn sich ihr cloud-init ändert |
precondition/postcondition | Annahmen prüfen, verständliche Fehlermeldung | Gültige Kombinationen von Variablen |
variable "environment" {
type = string
validation {
condition = contains(["dev", "test", "prod"], var.environment)
error_message = "environment muss dev, test oder prod sein."
}
}
Validierungen an Variablen sind die erste und billigste Verteidigungslinie – sie greifen, bevor überhaupt etwas geplant wird.
Module
Ein Modul ist ein Verzeichnis mit Terraform-Code, das mit Eingaben (Variablen) aufgerufen wird und Ausgaben (Outputs) liefert. Module machen Muster wiederverwendbar – etwa „Webserver-Herde mit Load Balancer“ für mehrere Services.
modules/
└── web-herd/
├── main.tf # LB, Probe, Regeln, Scale Set
├── variables.tf # name, image_id, instances, subnet_id, tags
├── outputs.tf # public_ip, vmss_id
└── README.md # Zweck, Beispielaufruf
module "webshop" {
source = "git::https://gitlab.com/<gruppe>/terraform-modules.git//web-herd?ref=v1.2.0"
name = "webshop"
image_id = data.azurerm_shared_image_version.web.id
instances = 3
subnet_id = data.azurerm_subnet.app.id
tags = local.tags
}
- Versionieren: Module immer über einen Git-Tag (
?ref=v1.2.0) einbinden, nie über einen Branch. Sonst ändert sich die Infrastruktur, ohne dass sich der aufrufende Code geändert hat. - Nicht zu früh: Ein Modul lohnt sich, wenn ein Muster wiederholt vorkommt (siehe Einfach vor clever).
- Fertige Module: Für Azure pflegt Microsoft die Azure Verified Modules (AVM) – gute Referenz, auch wenn man sie nicht direkt einsetzt.
Umgebungen: dev, test, prod
| Ansatz | Aufbau | Vorteil | Nachteil |
|---|---|---|---|
| Workspaces | Ein Code-Verzeichnis, mehrere States | Wenig Code | Umgebung nicht im Code sichtbar, Verwechslungsgefahr |
| Verzeichnis pro Umgebung | envs/dev, envs/prod, gemeinsame Module | Explizit, getrennte Pipelines und Rechte möglich | Etwas Wiederholung |
| Variablendatei pro Umgebung | Ein Verzeichnis, dev.tfvars, prod.tfvars | Einfach | Getrennte States nur über Backend-Konfiguration |
Für produktive Umgebungen ist ein Verzeichnis pro Umgebung mit gemeinsamen Modulen am robustesten: Jede Umgebung hat eigenen State, eigene Identitäten und kann in einer eigenen Subscription liegen. Ein Fehler in dev erreicht prod nicht.
envs/
├── dev/
│ ├── main.tf # ruft modules/web-herd auf
│ ├── backend.hcl # key = "webshop/dev.tfstate"
│ └── terraform.tfvars # instances = 1, vm_size = "Standard_B2s"
└── prod/
├── main.tf
├── backend.hcl # key = "webshop/prod.tfstate", eigene Subscription
└── terraform.tfvars # instances = 6, vm_size = "Standard_D4s_v5"
Versionen festschreiben
- versions.tf
terraform { required_version = ">= 1.7, < 2.0" required_providers { azurerm = { source = "hashicorp/azurerm" version = "~> 4.0" # 4.x erlaubt, 5.0 nicht } } }
Die Datei .terraform.lock.hcl hält die exakt verwendete Provider-Version samt Prüfsummen fest und gehört ins Repository. Damit sie auf allen Plattformen passt:
terraform providers lock -platform=linux_amd64 -platform=darwin_arm64 -platform=windows_amd64
Provider-Updates sind eigene, bewusste Merge Requests: terraform init -upgrade, Plan prüfen, Changelog lesen.
Dateiaufbau und Konventionen
| Datei | Inhalt |
|---|---|
versions.tf | terraform-Block mit Versionen und Backend |
providers.tf | Provider-Konfiguration |
variables.tf | Alle Variablen mit Beschreibung und Typ |
main.tf | Ressourcen (bei Bedarf thematisch aufgeteilt: network.tf, compute.tf) |
outputs.tf | Alle Outputs |
terraform.tfvars | Werte der Umgebung (keine Secrets) |
- Ressourcennamen in Terraform (die zweite Bezeichnung) beschreiben die Rolle, nicht den Typ:
azurerm_subnet.app, nichtazurerm_subnet.subnet1. - Azure-Namen folgen einem festen Schema, z. B.
<typkürzel>-<service>-<umgebung>– orientiert an den Microsoft-Empfehlungen zu Namenskonventionen (rg-,vnet-,vmss-,lb-). - Jede Variable bekommt
descriptionundtype.
Azure-Besonderheiten
- azurerm und azapi: Der Provider
azurermbildet Azure-Ressourcen mit handgepflegten Schemas ab. Neue oder Preview-Funktionen fehlen dort manchmal. Der Providerazapispricht die Azure-REST-API direkt an und deckt jede Ressource ab dem ersten Tag ab – als Ergänzung für Lücken, nicht als Ersatz. - Subscription angeben: Seit azurerm 4.0 muss die Subscription explizit gesetzt sein (im Provider oder per
ARM_SUBSCRIPTION_ID). - Anmeldung: Lokal über
az login, in der Pipeline über OIDC (siehe Pipeline). Client Secrets sind nicht nötig. - Bicep als Alternative: Microsofts eigene IaC-Sprache für Azure. Kein State nötig (Azure selbst ist die Quelle), aber auf Azure beschränkt. Terraform lohnt sich, sobald mehr als eine Plattform im Spiel ist – etwa Azure plus DNS, GitLab oder VMware.
Terraform und OpenTofu
HashiCorp hat im August 2023 die Lizenz von Terraform von der Open-Source-Lizenz MPL 2.0 auf die Business Source License (BSL 1.1) umgestellt. Die Nutzung für eigene Infrastruktur ist weiterhin frei; eingeschränkt ist das Anbieten konkurrierender Produkte. Als Reaktion entstand OpenTofu, ein Fork unter dem Dach der Linux Foundation, der unter MPL 2.0 bleibt. HashiCorp gehört inzwischen zu IBM.
| Aspekt | Terraform | OpenTofu |
|---|---|---|
| Lizenz | BSL 1.1 | MPL 2.0 (Open Source) |
| Befehl | terraform | tofu |
| Kompatibilität | – | Weitgehend kompatibel, gleiche Provider |
| Eigene Funktionen | z. B. Integration mit HCP Terraform | z. B. clientseitige Verschlüsselung des States |
| Registry | registry.terraform.io | registry.opentofu.org |
Der gesamte Code in diesem Bereich läuft mit beiden Werkzeugen. Für einen Wechsel genügt es meist, terraform durch tofu zu ersetzen. Da beide Projekte sich eigenständig weiterentwickeln, lohnt vor einer Grundsatzentscheidung ein Blick auf den aktuellen Funktionsstand und die Lizenzsituation.
Befehle für den Alltag
| Befehl | Zweck |
|---|---|
terraform fmt -recursive | Code einheitlich formatieren |
terraform validate | Syntax und Typen prüfen |
terraform plan -out=tfplan | Plan erstellen und speichern |
terraform show tfplan | Gespeicherten Plan anzeigen |
terraform apply tfplan | Genau diesen Plan ausführen |
terraform plan -refresh-only | Nur Drift anzeigen |
terraform plan -replace=<adresse> | Gezieltes Ersetzen einer Ressource erzwingen |
terraform output -raw url | Einzelnen Output lesen |
terraform console | Ausdrücke interaktiv testen |
terraform test | Tests ausführen (siehe Prinzipien) |
TF_LOG=DEBUG terraform plan | Ausführliche Protokollierung bei Fehlersuche |
terraform console ist besonders hilfreich beim Entwickeln komplexer Ausdrücke:
$ terraform console
> { for k, v in { blue = 3, green = 0 } : k => v if v > 0 }
{
"blue" = 3
}
Weiterführend
- IaC-Lab – Terraform in einem durchgängigen Beispiel
- IaC-Pipeline – Terraform automatisiert und abgesichert
- Packer – das zweite HashiCorp-Werkzeug im Lab
