OpenTofu mit OpenNebula
Auftrag
«Fangt mit den zwei Maschinen für die Container an», schreibt Simone Wyss. «Wenn ihr sie in zehn Minuten weggeworfen und wieder aufgebaut habt, glaube ich euch das mit dem Code.»
Lernziele
Nach diesem Kapitel kannst du:
- Begründen, warum die VMs aus Teil 2 und 3 den Platz für den Cluster freigeben müssen
- OpenTofu mit dem Provider für OpenNebula einrichten
- Provider, Data Sources, Variablen, Ressourcen und Outputs in einer Konfiguration erkennen
- VMs mit
tofu planundtofu applyanlegen, ändern, ersetzen und löschen - Erklären, warum der State geheime Werte enthält und nicht in Git gehört
Platz auf den Knoten
Abschnitt betitelt „Platz auf den Knoten“Der Kubernetes-Cluster von Teil 4 läuft auf zwei VMs mit je 6 GB Arbeitsspeicher, auf jedem Knoten eine. So viel braucht er spätestens in Teil 5 mit Prometheus und Loki. Ein lokales Sprachmodell aus den Zusatzaufträgen braucht zusätzlich Platz. Jeder Knoten hat 8 GB, davon meldet OpenNebula 7,2 GB für VMs. onehost list zeigt das in der Spalte ALLOCATED_MEM.
OpenNebula reserviert für jede VM den ganzen Speicher aus ihrer Vorlage auf ihrem Knoten, auch wenn die VM ihn gerade nicht braucht. Neben dem Cluster bleiben auf jedem Knoten 1,2 GB frei, genug für eine einzige VM mit 1 GB. Die drei Webserver aus Teil 2, die VMs der Praxis und der Dienst der Treuhand aus Teil 3 belegen zusammen 6 GB. Mit ihnen startet der Cluster nicht, OpenNebula fände keinen Knoten mit 6 GB frei.
Darum räumst du als Erstes auf. Die Gruppen, Quotas, Preise und Vorlagen aus Teil 3 bleiben bestehen, du entfernst nur die laufenden VMs.
oneflow listoneflow delete <id-des-dienstes>onevm listfor id in $(onevm list -l ID --no-header); do onevm terminate "$id"; doneonehost listHast du im Zusatzkapitel 3.5 den Dienst der Treuhand gestartet, entfernst du ihn zuerst, sonst startet OneFlow seine VM gleich wieder. Danach beendet die Schleife jede VM, die onevm list zeigt, egal wie du sie in den Teilen 2 und 3 genannt hast. oneadmin darf die VMs aller Mitglieder beenden. Am Ende zeigt onehost list auf beiden Knoten 0K / 7.2G.
Werkzeuge
Abschnitt betitelt „Werkzeuge“Auf dem Front-end brauchst du drei Werkzeuge. OpenTofu legt die VMs an und kubectl spricht mit Kubernetes. Helm installiert Pakete in den Cluster. Das Skript holt alle drei aus den offiziellen Paketquellen, damit apt upgrade sie später aktualisiert. Die Anleitungen zur Installation von OpenTofu 1.13, kubectl 1.36 und Helm beschreiben diese Paketquellen.
#!/bin/bash# Installs OpenTofu, kubectl and Helm on the front-end from their official package repositories, the step of chapter 4.2.## sudo ~/cc/platform/install-tools.shset -euo pipefail
K8S_MINOR=${K8S_MINOR:-v1.36}
apt-get updateapt-get install -y apt-transport-https ca-certificates curl gnupginstall -m 0755 -d /etc/apt/keyrings
# OpenTofucurl -fsSL https://get.opentofu.org/opentofu.gpg -o /etc/apt/keyrings/opentofu.gpgcurl -fsSL https://packages.opentofu.org/opentofu/tofu/gpgkey | gpg --dearmor --yes -o /etc/apt/keyrings/opentofu-repo.gpgchmod a+r /etc/apt/keyrings/opentofu.gpg /etc/apt/keyrings/opentofu-repo.gpgcat > /etc/apt/sources.list.d/opentofu.list <<LISTdeb [signed-by=/etc/apt/keyrings/opentofu.gpg,/etc/apt/keyrings/opentofu-repo.gpg] https://packages.opentofu.org/opentofu/tofu/any/ any mainLIST
# kubectl, the minor version follows k3s, kubectl may differ from the cluster by one minor versioncurl -fsSL "https://pkgs.k8s.io/core:/stable:/$K8S_MINOR/deb/Release.key" | gpg --dearmor --yes -o /etc/apt/keyrings/kubernetes.gpgecho "deb [signed-by=/etc/apt/keyrings/kubernetes.gpg] https://pkgs.k8s.io/core:/stable:/$K8S_MINOR/deb/ /" > /etc/apt/sources.list.d/kubernetes.list
# Helmcurl -fsSL https://packages.buildkite.com/helm-linux/helm-debian/gpgkey | gpg --dearmor --yes -o /etc/apt/keyrings/helm.gpgecho "deb [signed-by=/etc/apt/keyrings/helm.gpg] https://packages.buildkite.com/helm-linux/helm-debian/any/ any main" > /etc/apt/sources.list.d/helm.list
apt-get updateapt-get install -y tofu kubectl helmtofu versionkubectl version --clienthelm versionsudo ~/cc/platform/install-tools.shIm Lab waren das OpenTofu 1.13.2, kubectl 1.36.6 und Helm 4.3.0. OpenTofu ist ein Fork von Terraform unter der Linux Foundation mit der freien Lizenz MPL 2.0. Die Sprache und die meisten Provider sind dieselben. Wer Terraform kennt, kann OpenTofu lesen.
Das Plattform-Repo
Abschnitt betitelt „Das Plattform-Repo“Der Code der Plattform liegt in einem eigenen Repo. Die Vorlage dafür ist platform/repo im Kursrepo. Du kopierst sie nach ~/platform und machst daraus in Kapitel 4.4 ein Git-Repo.
cp -r ~/cc/platform/repo ~/platformcd ~/platform/tofuls -aIn tofu/ liegt die Konfiguration, aufgeteilt in Dateien. OpenTofu liest alle Dateien mit der Endung .tf eines Ordners zusammen, die Aufteilung dient nur der Übersicht.
| Datei | Inhalt |
|---|---|
versions.tf | die Version von OpenTofu und die Provider |
variables.tf | die Variablen mit Beschreibung und Standardwert |
main.tf | Provider, Data Sources und Ressourcen |
outputs.tf | Werte, die OpenTofu nach dem Apply ausgibt |
k3s.sh.tftpl | das Startskript der VMs, Thema von Kapitel 4.3 |
.terraform.lock.hcl | die genauen Versionen und Prüfsummen der Provider |
Provider
Abschnitt betitelt „Provider“Ein Provider übersetzt Blöcke im Code in Aufrufe einer API. Der Provider opennebula/opennebula spricht mit der XML-RPC-Schnittstelle von OpenNebula auf Port 2633, dieselbe, die auch die CLI und Sunstone nutzen. random erzeugt zufällige Werte.
terraform { required_version = ">= 1.10"
required_providers { opennebula = { source = "opennebula/opennebula" version = "~> 1.5" } random = { source = "hashicorp/random" version = "~> 3.7" } }}~> 1.5 heisst 1.5 oder neuer, aber nicht 2.0. Welche Version tofu init tatsächlich holt, hält es in .terraform.lock.hcl fest. Diese Datei gehört in Git, damit alle mit denselben Providern arbeiten. Benutzer und Passwort von OpenNebula stehen nicht im Code. Der Provider liest sie aus Umgebungsvariablen, die du aus deiner Datei ~/.one/one_auth aus Kapitel 2.2 setzt. Alle Einstellungen und Ressourcen nennt die Dokumentation des Providers in der Version 1.5.0, die .terraform.lock.hcl festhält.
export OPENNEBULA_USERNAME=oneadminexport OPENNEBULA_PASSWORD=$(cut -d: -f2- ~/.one/one_auth)Die Konfiguration
Abschnitt betitelt „Die Konfiguration“# Two VMs in cc-net with k3s, server and agent, the code of chapter 4.2 and 4.3
# User and password come from OPENNEBULA_USERNAME and OPENNEBULA_PASSWORDprovider "opennebula" { endpoint = var.endpoint}
# The agent joins the server with this token, it lives only in the stateresource "random_password" "k3s_token" { length = 48 special = false}
locals { nodes = { "k3s-server" = { ip = var.server_ip, role = "server" } "k3s-agent" = { ip = var.agent_ip, role = "agent" } }}
data "opennebula_virtual_network" "cc_net" { name = "cc-net"}
data "opennebula_image" "ubuntu" { name = "ubuntu-26.04"}
resource "opennebula_virtual_machine" "k3s" { for_each = local.nodes
name = each.key cpu = 1 vcpu = 4 memory = var.memory
# Without the real CPU of the node the guest lacks AVX2, which slows down the local model of part 5 cpumodel { model = "host-passthrough" }
context = { NETWORK = "YES" SET_HOSTNAME = "$NAME" SSH_PUBLIC_KEY = "$USER[SSH_PUBLIC_KEY]" START_SCRIPT_BASE64 = base64encode(templatefile("${path.module}/k3s.sh.tftpl", { role = each.value.role ip = each.value.ip server_ip = var.server_ip token = random_password.k3s_token.result k3s_version = var.k3s_version })) }
# OpenNebula sets the machine type q35 itself, without it here every plan would show a change os { arch = "x86_64" boot = "disk0" machine = "q35" }
disk { image_id = data.opennebula_image.ubuntu.id size = var.disk_size target = "vda" }
nic { network_id = data.opennebula_virtual_network.cc_net.id ip = each.value.ip model = "virtio" }
graphics { type = "VNC" listen = "0.0.0.0" }}| Block | Bedeutung |
|---|---|
provider | wie OpenTofu OpenNebula erreicht |
resource "random_password" | ein zufälliges Token, mit dem sich die Knoten des Clusters verbinden |
locals | Werte, die im Code mehrfach vorkommen, hier die zwei VMs mit Adresse und Rolle |
data | etwas, das schon existiert und nur gelesen wird, hier das Netz cc-net und das Image ubuntu-26.04 aus Teil 2 |
resource "opennebula_virtual_machine" | die VMs, dank for_each eine pro Eintrag in local.nodes |
Eine Data Source verbindet den Code mit dem, was du von Hand gebaut hast, ohne es zu übernehmen. OpenTofu liest die Nummer von cc-net, legt das Netz aber nie an und löscht es nie.
Die Adressen der VMs kommen aus deinen persönlichen Parametern. In /etc/cc-lab.env heissen sie CC_K3S_SERVER_IP und CC_K3S_AGENT_IP. Ein kleines Skript schreibt sie in terraform.tfvars, die Datei, aus der OpenTofu Variablen liest.
./make-tfvars.sh > terraform.tfvarscat terraform.tfvarsInit, Plan und Apply
Abschnitt betitelt „Init, Plan und Apply“tofu init lädt die Provider in den Ordner .terraform. Ohne diesen Schritt brechen plan und apply mit «Inconsistent dependency lock file» ab, auch in jeder neuen Kopie des Repos. Die Optionen von tofu init beschreibt die Referenz des Befehls in der Dokumentation von OpenTofu 1.13.
tofu inittofu plantofu applyDer Plan zeigt drei Ressourcen mit +, das Token und zwei VMs. Beim Token steht result = (sensitive value). OpenTofu blendet geheime Werte in der Ausgabe aus, speichert sie aber im State. Nach yes legt OpenTofu die VMs an, im Lab in zehn Sekunden, je eine auf einem Knoten. Am Ende stehen die Outputs. Die Adressen unten stammen aus dem Lab des Kurses, bei dir stehen die Adressen deines Netzes cc-net mit den Endungen .110 und .111.
Outputs:
api = "https://172.28.217.110:6443"ips = { "k3s-agent" = "172.28.217.111" "k3s-server" = "172.28.217.110"}State lesen
Abschnitt betitelt „State lesen“tofu state listtofu state show 'opennebula_virtual_machine.k3s["k3s-server"]'tofu outputtofu state list zeigt die drei verwalteten Ressourcen und die zwei Data Sources, die OpenTofu nur liest. tofu state show listet alle Attribute einer VM so auf, wie OpenNebula sie meldet. Der State liegt in terraform.tfstate. Öffnest du die Datei, findest du das Token im Klartext. Die Datei .gitignore im Repo schliesst darum den State und den Ordner .terraform aus.
Ändern, ersetzen und neu bauen
Abschnitt betitelt „Ändern, ersetzen und neu bauen“Benennst du eine VM von Hand um, entsteht Drift. Im Lab sah der nächste Plan so aus.
onevm rename k3s-agent handarbeittofu plan # opennebula_virtual_machine.k3s["k3s-agent"] will be updated in-place ~ name = "handarbeit" -> "k3s-agent"Plan: 0 to add, 1 to change, 0 to destroy.tofu apply benennt sie zurück. Manche Änderungen lassen sich nicht an einer laufenden VM machen. Dann ersetzt OpenTofu sie und zeigt -/+. Mit -replace erzwingst du das Ersetzen einer einzelnen VM, etwa wenn ihr Startskript schiefgegangen ist.
tofu apply -replace='opennebula_virtual_machine.k3s["k3s-agent"]'Im Lab dauerte das knapp zwei Minuten, weil OpenNebula die alte VM zuerst herunterfährt. War der alte Agent dem Cluster schon beigetreten, lehnt der Server den neuen ab. k3s merkt sich für jeden Namen eines Nodes ein Passwort, das die neue VM nicht kennt. Der neue Agent meldet dann «Node password rejected, duplicate hostname» und bleibt NotReady. Lösche in diesem Fall den alten Node mit kubectl delete node k3s-agent, sobald kubectl aus Kapitel 4.3 läuft. Im Lab trat der neue Agent danach innert Sekunden bei.
Ob der Code alles beschreibt, zeigt erst ein ganzer Neubau. tofu destroy löscht alles, was im State steht, tofu apply baut es wieder auf. Für den leeren Cluster von Teil 4 dauerte das im Lab etwa zwei Minuten. Die Basis verlangt nur die VMs aus OpenTofu und einen Plan ohne Änderungen. Den Neubau der ganzen Plattform planst und misst du im Zusatzauftrag A in Kapitel 4.6. Wie das Startskript Kubernetes installiert, zeigt Kapitel 4.3.
Aufgabe Lab
Räume die Knoten auf, installiere die Werkzeuge und lege die beiden VMs mit OpenTofu an. Zeige mit tofu plan, dass danach keine Änderungen offen sind.
Lösung
Nach dem Aufräumen zeigt onehost list auf beiden Knoten 0K / 7.2G. tofu apply legt k3s-server und k3s-agent mit den Adressen aus deinen Parametern an, onevm list zeigt beide mit 6G auf verschiedenen Knoten. Ein zweiter tofu plan meldet «No changes. Your infrastructure matches the configuration.»
Aufgabe Drift
Erzeuge eine Drift, indem du eine der beiden VMs in Sunstone oder mit der CLI änderst. Zeige sie mit tofu plan und behebe sie. Welche Änderung von Hand würde OpenTofu nicht bemerken?
Lösung
Ein neuer Name, wie oben, erscheint als ~ name im Plan, tofu apply macht ihn rückgängig. OpenTofu vergleicht aber nur die Attribute, die der Provider kennt. Änderungen in der VM selbst bleiben unsichtbar, etwa ein per SSH installiertes Paket oder eine gelöschte Datei. Für den Inhalt der VMs sorgt das Startskript. Im Cluster übernimmt diese Aufgabe ab Kapitel 4.4 Argo CD.
Aufgabe Variable
Die Treuhand fragt, ob der Cluster mit weniger Speicher auskäme, damit daneben wieder eine VM für sie Platz hat. Wo änderst du das und welche Folgen hat es?
Lösung
In variables.tf steht die Variable memory mit dem Wert 6144. In terraform.tfvars setzt du etwa memory = 5120, dann zeigt der Plan eine Änderung an beiden VMs. Jeder Knoten hätte danach 2,2 GB frei, genug für zwei kleine VMs. In Teil 5 brauchen Prometheus, Loki und das lokale Modell aber diesen Speicher. Im Test lag der Server mit allem bei 3,4 GB, das Modell brauchte auf dem Agent 2,7 GB. Eine spätere Messung über eine Stunde mit Anfragen ergab für das Modell bis 3,3 GiB, wie Kapitel 6.4 zeigt.
Texte und Abbildungen stehen unter CC BY-SA 4.0 und der Code unter der MIT-Lizenz. Die Quellen liegen auf GitHub.