Dies ist eine alte Version des Dokuments!
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_filesläuft früh,runcmdspät. Eine Datei, die einem erst später angelegten Benutzer gehören soll, brauchtdefer: 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. bootcmdläuft bei jedem Start und sehr früh;runcmdnur 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: Im Lab legt Ansible die Datei /health bereits im Image an. Eine neue Instanz meldet sich dadurch gesund, sobald nginx läuft – möglicherweise bevor cloud-init fertig ist. Besser: Das Image enthält keinen Health-Endpunkt, cloud-init legt ihn als letzten Schritt an. Dafür im Ansible-Playbook den Task „Health-Endpoint“ entfernen und im Packer-Test die Datei für die Dauer des Tests selbst anlegen und danach wieder löschen.
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
