Lange Zeit habe ich meine Proxmox-VMs von Hand gebaut. Ich bin durch einen Installer geklickt, habe meinen User angelegt, einen SSH-Key eingefügt, den Guest Agent installiert und bei der nächsten VM die Hälfte davon wieder vergessen. Sie liefen. Sie waren nur nicht reproduzierbar, und ich könnte dir nicht sagen, worin sich zwei Maschinen unterscheiden, die ich im Abstand eines Monats gebaut habe.
Ein Cloud-Init-Template löst das größtenteils. Dieser Beitrag zeigt, wie ich eines aus einem offiziellen Cloud-Image baue, und zwar mit der qm-CLI, sodass jeder Schritt als Skript in Git liegen kann. Ich betreibe dieses Setup noch nicht lange, nimm es also als funktionierende Methode. Die Stellen, bei denen ich unsicher bin, markiere ich.
Wenn du noch keinen Proxmox-Host zum Ausprobieren hast, deckt das Lab „Install Proxmox VE“ die Basisinstallation ab.
Du übst darin, Proxmox VE auf zwei gespiegelten SSDs mit ZFS zu installieren und dem Host eine feste Adresse zu geben.
Warum ein Cloud-Image und keine geklonte VM
Die gängige Alternative ist, eine VM von Hand zu installieren, aufzuräumen und in ein Template umzuwandeln. Das funktioniert, aber das Template trägt jeden Zustand mit sich, den du hinterlassen hast: Host-Keys, Machine-ID, Shell-History, ein verirrtes Paket. Das zu bereinigen ist eine eigene Checkliste, die ich lieber nicht pflegen möchte.
Ein offizielles Cloud-Image startet sauber. Es erwartet, dass Cloud-Init es beim ersten Boot konfiguriert: User, SSH-Keys, Netzwerk, Hostname. Proxmox kann diese Werte über ein virtuelles Cloud-Init-Laufwerk bereitstellen. Das Template bleibt generisch, und jeder Klon bekommt seine Identität erst bei der Erstellung.
Der Nachteil ist, dass du auf den GUI-Installer verzichtest und vorab wissen musst, was du willst. Für mich ist das ein Vorteil. Die Liste dessen, was ich will, ist genau die Dokumentation, die ich schon ewig schreiben wollte.
Das Template bauen
Ich verwende hier das Cloud-Image von Ubuntu. Jede Distribution, die ein Cloud-Image liefert, funktioniert genauso. Führe die Befehle auf dem Proxmox-Node aus. VM-ID, Storage-Name und Bridge sind meine, ersetze sie durch deine.
cd /var/lib/vz/template/iso
wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
qm create 9000 --name ubuntu-2404-ci \
--memory 2048 --cores 2 \
--net0 virtio,bridge=vmbr0 \
--ostype l26 --agent enabled=1
qm importdisk 9000 noble-server-cloudimg-amd64.img local-zfs
qm set 9000 --scsihw virtio-scsi-pci --scsi0 local-zfs:vm-9000-disk-0
qm set 9000 --ide2 local-zfs:cloudinit
qm set 9000 --boot order=scsi0
qm set 9000 --serial0 socket --vga serial0
Ein paar Anmerkungen, wozu die einzelnen Teile da sind:
importdiskmacht aus dem heruntergeladenen Image eine VM-Disk auf deinem Storage. Neuere Proxmox-Versionen haben außerdemqm disk import. Prüfe, was deine Version unterstützt.- Das
cloudinit-Laufwerk aufide2ist der Ort, an dem Proxmox die Einstellungen für User, Key und Netzwerk ablegt, die Cloud-Init beim Booten liest. - Die serielle Konsole ist da, weil viele Cloud-Images eine erwarten. Ohne sie habe ich erlebt, dass die Web-Konsole nichts Brauchbares anzeigt. Ich bin nicht sicher, ob sie bei jedem Image nötig ist.
Konvertiere es noch nicht. Kümmere dich zuerst um den Guest Agent.
Das Guest-Agent-Problem
Das Cloud-Image bringt qemu-guest-agent nicht mit. Ohne ihn kann Proxmox die IP-Adresse der VM nicht sehen, und sauberes Herunterfahren sowie Quiescing bei Backups sind weniger zuverlässig. Ich könnte ihn von Hand installieren und das Ergebnis konvertieren, aber das bringt das Problem des handgebauten Templates zurück.
Stattdessen verwende ich ein Vendor-Data-Snippet, das Cloud-Init beim ersten Boot anwendet:
#cloud-config
package_update: true
packages:
- qemu-guest-agent
runcmd:
- systemctl enable --now qemu-guest-agent
Speichere es als /var/lib/vz/snippets/vendor.yaml. Snippets brauchen einen Storage vom Typ Verzeichnis, bei dem der Inhaltstyp Snippets aktiviert ist. Der Standard-Storage local kann das, aber ZFS-basierter Storage wie local-zfs kann keine Snippets aufnehmen. Danach hängst du es an:
qm set 9000 --cicustom "vendor=local:snippets/vendor.yaml"
qm template 9000
Eine Einschränkung für Cluster: Diese Snippet-Datei liegt auf dem lokalen Storage eines einzelnen Nodes. Landet ein Klon auf einem Node, der sie nicht hat, startet der Klon nicht. Wie ich das löse, habe ich noch nicht entschieden. Ich sehe zwei Möglichkeiten: die Datei aus demselben Skript auf jeden Node kopieren oder die Snippets auf Shared Storage legen. Kopieren ist einfacher, also tendiere ich dazu.
Klonen mit Einstellungen
Das Template enthält, was allen gemeinsam ist. Jeder Klon ergänzt, was für ihn spezifisch ist:
qm clone 9000 120 --name devvm --full
qm set 120 --ciuser priya --sshkeys ~/.ssh/lab_keys.pub
qm set 120 --ipconfig0 ip=dhcp
qm resize 120 scsi0 32G
qm start 120
Das Flag --sshkeys nimmt eine Datei mit öffentlichen Schlüsseln. Nur öffentliche Schlüssel kommen auf die VM. Ich setze gar kein Passwort, also gibt es nichts, was leaken oder rotiert werden müsste. Wenn du eine statische Adresse willst, erledigt das --ipconfig0 ip=10.0.20.50/24,gw=10.0.20.1. Ich bevorzuge eine DHCP-Reservierung, weil die Adressierung dann an einer Stelle bleibt.
Nach ein bis zwei Minuten sollte qm guest cmd 120 network-get-interfaces Daten zurückgeben, sobald der Agent läuft. Bei einem Timeout läuft der Agent wahrscheinlich noch nicht, oder das Vendor-Snippet wurde nicht angewendet.
Was ich prüfe, bevor ich dem Ganzen traue
Ein Template, das bootet, ist nicht dasselbe wie ein Template, das korrekt ist. Das ist mein kurzer Check nach jedem Rebuild:
- Per SSH mit dem Key einloggen und bestätigen, dass der Passwort-Login deaktiviert ist.
- Prüfen, dass der Hostname dem VM-Namen entspricht und nicht dem des Templates.
cloud-init status --longauf Fehler prüfen.- Prüfen, dass der Guest Agent läuft und Proxmox die IP anzeigt.
- Sicherstellen, dass zwei Klone nicht dieselbe Machine-ID haben:
cat /etc/machine-idauf beiden.
Der letzte Punkt ist der Grund, warum ich mich für ein Cloud-Image entschieden habe. Es ist auch der, den ich am wenigsten auf die harte Tour herausfinden möchte.
Ab in Git damit
Der ganze Sinn ist, dass nichts davon in meinem Kopf lebt. Die Build-Befehle stehen in einem Shell-Skript, das Vendor-Snippet liegt daneben, und beides ist committed. Das Template neu zu bauen heißt dann: VM 9000 löschen, Skript ausführen. In Ansible habe ich das noch nicht eingebunden. Das ist der nächste Schritt, und ich erwarte, dass er lästiger wird als die Shell-Variante, weil die Proxmox-Module API-Zugangsdaten brauchen, die verwaltet werden müssen. Mich würde interessieren, ob sich das für einen Workflow mit jeweils einem einzelnen Node lohnt.
Wobei ich mir unsicher bin
- Image-Updates. Das
current-Image wird upstream ersetzt. Ich möchte es pinnen oder zumindest eine Prüfsumme festhalten, damit ein Rebuild wirklich derselbe Build ist. Wie, habe ich noch nicht entschieden. - Template-Drift. Klone werden nicht aktualisiert, wenn sich das Template ändert. Ich behandle VMs als Wegwerfobjekte, mit Daten auf separatem Storage, sodass ein Rebuild günstiger ist, als eine Template-Linie zu patchen. Ich bin nicht sicher, ob das für jede VM gilt, die ich betreibe.
- Verhalten im Cluster. Ich habe das bisher nur auf jeweils einem Node getestet.
Wie handhabt ihr das? Baut ihr Templates mit der qm-CLI, Terraform, Packer oder etwas anderem? Und habt ihr eure Basis-Images gepinnt, oder lasst ihr sie einfach mitlaufen und geht mit den Überraschungen um?
Have a question about this guide?
Create a free account to ask the community and join the discussion. Members who run the real thing answer every day.
