| FOTO | AUTO | EDV | AUDIO |

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 apply sperrt 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, nicht azurerm_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 description und type.

Azure-Besonderheiten

  • azurerm und azapi: Der Provider azurerm bildet Azure-Ressourcen mit handgepflegten Schemas ab. Neue oder Preview-Funktionen fehlen dort manchmal. Der Provider azapi spricht 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
  • Lebenszyklus – Blue/Green mit for_each, Autoscale mit ignore_changes
  • Packer – das zweite HashiCorp-Werkzeug im Lab