| FOTO | AUTO | EDV | AUDIO |

cloud-init: der erste Atemzug einer Instanz

cloud-init ist der Standard für die Initialisierung von Linux-Instanzen in der Cloud. Es läuft beim ersten Start, liest die Metadaten der Plattform und die mitgegebene Konfiguration und macht aus einem generischen Image eine konkrete Instanz: Hostname, Benutzer, SSH-Schlüssel, Netzwerk, instanzspezifische Dateien. Im IaC-Lab schreibt cloud-init die Startseite mit Hostname und Image-Version.

Zurück zur IaC-Übersicht

Image oder cloud-init?

Die wichtigste Designfrage. Faustregel: Alles, was für alle Instanzen gleich ist, gehört ins Image. cloud-init erledigt nur, was pro Instanz oder pro Umgebung verschieden ist.

Aufgabe Image (Packer) cloud-init Begründung
Pakete installieren ✔ Langsam, abhängig von Paketquellen, nicht reproduzierbar beim Start
Härtung, Agenten, Zertifikatskette ✔ Einmal geprüft, in jeder Instanz identisch
Dienste aktivieren ✔ Teil des getesteten Softwarestands
Hostname, SSH-Schlüssel ✔ Pro Instanz verschieden, liefert die Plattform
Umgebung (dev/test/prod), Endpunkte ✔ Ein Image für alle Umgebungen
Secrets (✔) Nur als Abruf zur Laufzeit, nie als Inhalt (siehe unten)
Bereitschaft signalisieren ✔ Erst wenn der Bootstrap fertig ist

Jede Sekunde in cloud-init verzögert die Bereitstellung. Beim Autoscaling heißt das: Die zusätzliche Kapazität kommt später, genau wenn sie gebraucht wird. Ein schlanker Bootstrap ist deshalb auch eine Performance-Frage.

Wie cloud-init abläuft

cloud-init arbeitet in Stufen, die an den Bootvorgang gekoppelt sind:

 Boot
  │
  ├─ Generator    Soll cloud-init überhaupt laufen?
  ├─ Local        Datenquelle finden (auf Azure: IMDS), Netzwerk konfigurieren
  ├─ Network      Konfiguration lesen, Basis-Module: Hostname, Benutzer, write_files, SSH
  ├─ Config       Weitere Module: z. B. Zeitzone, NTP, Pakete
  └─ Final        Spät im Boot: Paketinstallation, runcmd, Skripte, final_message
                  ↓
               cloud-init status: done

Wichtig für die Praxis:

  • Reihenfolge der Module ist fest, nicht die Reihenfolge in der YAML-Datei. write_files läuft früh, runcmd spät. Eine Datei, die einem erst später angelegten Benutzer gehören soll, braucht defer: true.
  • Häufigkeit: Die meisten Module laufen einmal pro Instanz. cloud-init erkennt eine neue Instanz an ihrer ID. Deshalb muss das Image generalisiert sein (cloud-init clean), sonst hält cloud-init die neue Instanz für einen Neustart der Build-VM und tut nichts.
  • bootcmd läuft bei jedem Start und sehr früh; runcmd nur beim ersten Start.

Formate

Erste Zeile Format
#cloud-config Deklarative YAML-Konfiguration – der Normalfall
## template: jinja Jinja-Vorlage mit Zugriff auf Instanzdaten; danach folgt #cloud-config
#!/bin/bash Shell-Skript, wird in der Final-Stufe einmal ausgeführt
MIME-Multipart Kombination mehrerer Teile, z. B. Konfiguration plus Skript

Deklarativ vor Skript: Was sich mit Modulen wie write_files, users oder timezone ausdrücken lässt, wird nicht per Shell-Befehl gelöst – aus denselben Gründen wie bei Terraform und Ansible (siehe Prinzipien).

Das Lab-Beispiel, verbessert

Im Lab ermittelt ein Shell-Befehl den Hostnamen. Mit einer Jinja-Vorlage stellt cloud-init die Instanzdaten direkt bereit. Außerdem gibt es eine inhaltliche Verbesserung: Die Instanz meldet sich erst dann beim Load Balancer gesund, wenn der Bootstrap vollständig ist.

cloud-init.yaml
## template: jinja
#cloud-config

write_files:
  - path: /var/www/html/index.html
    permissions: "0644"
    content: |
      <!doctype html>
      <html><body style="font-family:sans-serif">
        <h1>IaC-Lab (${environment})</h1>
        <p>Instanz: {{ v1.local_hostname }}</p>
        <p>Region: {{ v1.region }}</p>
      </body></html>

runcmd:
  - [sh, -c, 'echo "<p>Image-Version: $(cat /etc/image-release)</p>" >> /var/www/html/index.html']
  # Letzter Schritt: erst jetzt bekommt die Instanz Verkehr
  - [sh, -c, 'echo ok > /var/www/html/health']
 
final_message: "Bootstrap abgeschlossen nach $UPTIME Sekunden"

Hier greifen zwei Vorlagen-Ebenen ineinander:

Syntax Ausgewertet von Zeitpunkt Beispiel
${…} Terraform templatefile() beim terraform plan ${environment}
iac cloud-init (Jinja) beim Start der Instanz v1.local_hostname
$(…) Shell bei Ausführung von runcmd $(cat /etc/image-release)

Health erst nach dem Bootstrap: Enthielte das Image bereits die Datei /health, würde sich eine neue Instanz gesund melden, sobald nginx läuft – möglicherweise bevor cloud-init fertig ist. Deshalb enthält das Image im Lab keinen Health-Endpunkt; cloud-init legt ihn als letzten Schritt an. Der Packer-Test erzeugt die Datei nur für die Dauer des Tests und entfernt sie danach wieder.

Welche Instanzdaten verfügbar sind, zeigt auf einer laufenden Instanz cloud-init query –all. Die v1-Schlüssel sind plattformübergreifend einheitlich; nicht jede Plattform füllt jeden Schlüssel.

Terraform-Integration

Für eine einzelne Konfigurationsdatei genügt templatefile() wie im Lab. Für mehrteilige Konfigurationen gibt es den Provider hashicorp/cloudinit:

data "cloudinit_config" "web" {
  gzip          = true
  base64_encode = true
 
  part {
    content_type = "text/cloud-config"
    content = templatefile("${path.module}/cloud-init.yaml", {
      environment = var.environment
    })
  }
 
  part {
    content_type = "text/x-shellscript"
    content      = file("${path.module}/scripts/register.sh")
  }
}
 
resource "azurerm_linux_virtual_machine_scale_set" "web" {
  # ...
  custom_data = data.cloudinit_config.web.rendered
}

gzip hilft, unter der Größengrenze für Custom Data zu bleiben. Wird die Konfiguration trotzdem groß, ist das ein Warnzeichen: Dann gehört vermutlich zu viel in den Bootstrap, was ins Image gehört.

Azure-Besonderheiten

  • custom_data ist der Weg für cloud-init. Azure bietet zusätzlich ein Feld user_data; das wird von cloud-init nicht zur Provisionierung verarbeitet, sondern ist für Anwendungen über den Instance Metadata Service abrufbar.
  • Änderungen an custom_data betreffen beim Scale Set das Modell. Bestehende Instanzen übernehmen sie erst beim Austausch – im Lab also über das Rolling Upgrade. Das ist konsequentes Nutzvieh-Verhalten: Neue Konfiguration bedeutet neue Instanzen.
  • Netzwerk konfiguriert die Azure-Datenquelle selbst. Eigene Netzwerkkonfiguration in cloud-init ist auf Azure fast nie nötig und eher eine Fehlerquelle.
  • Azure Linux Agent und cloud-init arbeiten zusammen: In aktuellen Marketplace-Images übernimmt cloud-init die Provisionierung, der Agent Erweiterungen und Health Reporting.

Secrets: abrufen statt mitgeben

Custom Data ist kein sicherer Ort für Passwörter oder Schlüssel. Der Inhalt liegt im Modell der VM bzw. des Scale Sets und ist auf der Instanz lesbar. Stattdessen holt die Instanz ihre Geheimnisse zur Laufzeit selbst aus dem Key Vault – mit ihrer eigenen Identität, ohne gespeichertes Passwort.

Voraussetzungen in Terraform:

resource "azurerm_linux_virtual_machine_scale_set" "web" {
  # ...
  identity {
    type = "SystemAssigned"
  }
}
 
resource "azurerm_role_assignment" "web_kv" {
  scope                = azurerm_key_vault.app.id
  role_definition_name = "Key Vault Secrets User"
  principal_id         = azurerm_linux_virtual_machine_scale_set.web.identity[0].principal_id
}

Abruf beim Start (als Skript im Image, aufgerufen aus runcmd oder besser von einem systemd-Dienst der Anwendung):

#!/usr/bin/env bash
# /usr/local/sbin/fetch-secret <vault-name> <secret-name> <ziel-datei>
set -euo pipefail
VAULT="$1"; SECRET="$2"; TARGET="$3"
 
TOKEN=$(curl -sf -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net" \
  | jq -r .access_token)
 
curl -sf -H "Authorization: Bearer $TOKEN" \
  "https://${VAULT}.vault.azure.net/secrets/${SECRET}?api-version=7.4" \
  | jq -r .value > "$TARGET"
 
chmod 600 "$TARGET"

Im cloud-init steht dann nur der Name des Vaults und des Secrets – beides keine Geheimnisse:

runcmd:
  - [/usr/local/sbin/fetch-secret, "kv-iaclab-${environment}", "db-password", "/etc/webapp/db-password"]

Noch besser: Die Anwendung ruft das Secret selbst bei Bedarf ab und legt es gar nicht erst als Datei ab. Das Skript ist der pragmatische Weg für Anwendungen, die eine Datei erwarten.

Testen vor dem Rollout

Schema prüfen: cloud-init validiert die Konfiguration gegen sein Schema und meldet Tippfehler in Modulnamen oder falsche Typen.

cloud-init schema --config-file cloud-init.yaml --annotate

Bei Terraform-Vorlagen vorher die ${…}-Platzhalter ersetzen, etwa mit terraform console und templatefile(). In der Pipeline passt die Prüfung in die Stufe validate.

Lokal ausprobieren: Statt jede Änderung in Azure zu testen, startet eine lokale VM mit derselben Konfiguration in Sekunden – etwa mit Multipass oder LXD:

multipass launch 24.04 --name ci-test --cloud-init cloud-init-rendered.yaml
multipass exec ci-test -- cloud-init status --wait --long
multipass exec ci-test -- cat /var/www/html/index.html
multipass delete --purge ci-test

Plattformspezifische Instanzdaten (z. B. v1.region) sind lokal anders oder leer – für die Logik der Konfiguration genügt das.

Fehlersuche

Befehl / Datei Zeigt
cloud-init status –long Fertig? Mit Fehlern? Welche Datenquelle?
/var/log/cloud-init-output.log Ausgabe aller Befehle (runcmd, Skripte) – der erste Blick
/var/log/cloud-init.log Ausführliches Protokoll jedes Moduls
cloud-init query –all Alle Instanzdaten, die für Jinja verfügbar sind
cloud-init analyze blame Welche Module wie lange gedauert haben
cloud-init collect-logs Alle Protokolle als Archiv, z. B. für ein Ticket

Auf Azure ohne SSH-Zugang – der Normalfall im Lab – gibt es zwei Wege, ohne sich anzumelden:

# Protokoll per Run Command abrufen
az vmss run-command invoke -g rg-iaclab-web -n vmss-iaclab-web --instance-id 0 \
  --command-id RunShellScript --scripts "tail -n 50 /var/log/cloud-init-output.log"
 
# Boot-Diagnose: Konsolenausgabe des Starts (Boot-Diagnose muss aktiviert sein)
az vmss get-instance-view -g rg-iaclab-web -n vmss-iaclab-web --instance-id 0

Und dann? Der Fehler wird im Code behoben, nicht auf der Instanz. Eine Instanz mit fehlgeschlagenem Bootstrap wird ersetzt. Weil der Health-Endpunkt erst am Ende von cloud-init entsteht, bekommt sie ohnehin keinen Verkehr – und Automatic Repair tauscht sie nach der Grace Period aus.

Anti-Muster

Anti-Muster Problem Besser
packages: mit langer Liste Langsamer Start, Ausfall wenn Paketquelle nicht erreichbar Pakete ins Image
package_upgrade: true Jede Instanz hat einen anderen Patchstand Patches über neue Image-Version
Passwörter in custom_data Im Modell und auf der Instanz lesbar Abruf aus dem Key Vault
Lange Shell-Skripte in runcmd Schwer testbar, fehleranfällig Skript ins Image, aus runcmd aufrufen
Health-Endpunkt im Image Verkehr vor Abschluss des Bootstraps Endpunkt als letzter cloud-init-Schritt
Konfiguration per ansible-pull beim Start Instanz hängt von Git und Netz ab, Zustand je nach Startzeit Ansible beim Image-Build

ansible-pull beim Start ist nicht grundsätzlich falsch – es ist ein legitimes Pull-Modell für veränderliche Infrastruktur (siehe GitOps). Für Nutzvieh, das schnell und identisch starten soll, ist das fertige Image der bessere Weg.

Weiterführend

  • Packer – was vor cloud-init ins Image kommt
  • IaC-Lab – cloud-init im Zusammenspiel mit Scale Set und Load Balancer
  • Lebenszyklus – Bootstrap als Teil des Lebens einer Instanz
  • Prinzipien – deklarativ, idempotent, unveränderlich