Zum Inhalt springen

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 plan und tofu apply anlegen, ändern, ersetzen und löschen
  • Erklären, warum der State geheime Werte enthält und nicht in Git gehört

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.

Der Speicher eines Knotens Ein Balken zeigt die 8 GB eines Knotens. 0,8 GB bleiben dem Host, die übrigen 7,2 GB meldet OpenNebula für VMs. Davon reserviert die k3s-VM 6 GB, auf einem Knoten der Server, auf dem anderen der Agent. Frei bleiben 1,2 GB. Achte darauf, dass daneben nur noch eine VM mit 1 GB Platz hat, weil OpenNebula den ganzen Speicher jeder VM reserviert. KNOTEN 1 ODER KNOTEN 2 8 GB Arbeitsspeicher Host 0,8 GB k3s-VM 6 GB, k3s-server oder k3s-agent frei 1,2 GB 7,2 GB für VMs, ALLOCATED_MEM in onehost list

Darum räumst du als Erstes auf. Die Gruppen, Quotas, Preise und Vorlagen aus Teil 3 bleiben bestehen, du entfernst nur die laufenden VMs.

Terminal-Fenster
oneflow list
oneflow delete <id-des-dienstes>
onevm list
for id in $(onevm list -l ID --no-header); do onevm terminate "$id"; done
onehost list

Hast 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.

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.

install-tools.sh
#!/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.sh
set -euo pipefail
K8S_MINOR=${K8S_MINOR:-v1.36}
apt-get update
apt-get install -y apt-transport-https ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
# OpenTofu
curl -fsSL https://get.opentofu.org/opentofu.gpg -o /etc/apt/keyrings/opentofu.gpg
curl -fsSL https://packages.opentofu.org/opentofu/tofu/gpgkey | gpg --dearmor --yes -o /etc/apt/keyrings/opentofu-repo.gpg
chmod a+r /etc/apt/keyrings/opentofu.gpg /etc/apt/keyrings/opentofu-repo.gpg
cat > /etc/apt/sources.list.d/opentofu.list <<LIST
deb [signed-by=/etc/apt/keyrings/opentofu.gpg,/etc/apt/keyrings/opentofu-repo.gpg] https://packages.opentofu.org/opentofu/tofu/any/ any main
LIST
# kubectl, the minor version follows k3s, kubectl may differ from the cluster by one minor version
curl -fsSL "https://pkgs.k8s.io/core:/stable:/$K8S_MINOR/deb/Release.key" | gpg --dearmor --yes -o /etc/apt/keyrings/kubernetes.gpg
echo "deb [signed-by=/etc/apt/keyrings/kubernetes.gpg] https://pkgs.k8s.io/core:/stable:/$K8S_MINOR/deb/ /" > /etc/apt/sources.list.d/kubernetes.list
# Helm
curl -fsSL https://packages.buildkite.com/helm-linux/helm-debian/gpgkey | gpg --dearmor --yes -o /etc/apt/keyrings/helm.gpg
echo "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 update
apt-get install -y tofu kubectl helm
tofu version
kubectl version --client
helm version
Terminal-Fenster
sudo ~/cc/platform/install-tools.sh

Im 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.

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.

Terminal-Fenster
cp -r ~/cc/platform/repo ~/platform
cd ~/platform/tofu
ls -a

In tofu/ liegt die Konfiguration, aufgeteilt in Dateien. OpenTofu liest alle Dateien mit der Endung .tf eines Ordners zusammen, die Aufteilung dient nur der Übersicht.

DateiInhalt
versions.tfdie Version von OpenTofu und die Provider
variables.tfdie Variablen mit Beschreibung und Standardwert
main.tfProvider, Data Sources und Ressourcen
outputs.tfWerte, die OpenTofu nach dem Apply ausgibt
k3s.sh.tftpldas Startskript der VMs, Thema von Kapitel 4.3
.terraform.lock.hcldie genauen Versionen und Prüfsummen der 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.

versions.tf
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.

Terminal-Fenster
export OPENNEBULA_USERNAME=oneadmin
export OPENNEBULA_PASSWORD=$(cut -d: -f2- ~/.one/one_auth)
main.tf
# 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_PASSWORD
provider "opennebula" {
endpoint = var.endpoint
}
# The agent joins the server with this token, it lives only in the state
resource "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"
}
}
BlockBedeutung
providerwie OpenTofu OpenNebula erreicht
resource "random_password"ein zufälliges Token, mit dem sich die Knoten des Clusters verbinden
localsWerte, die im Code mehrfach vorkommen, hier die zwei VMs mit Adresse und Rolle
dataetwas, 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.

Terminal-Fenster
./make-tfvars.sh > terraform.tfvars
cat terraform.tfvars

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.

Terminal-Fenster
tofu init
tofu plan
tofu apply

Der 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"
}
Terminal-Fenster
tofu state list
tofu state show 'opennebula_virtual_machine.k3s["k3s-server"]'
tofu output

tofu 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.

Benennst du eine VM von Hand um, entsteht Drift. Im Lab sah der nächste Plan so aus.

Terminal-Fenster
onevm rename k3s-agent handarbeit
tofu 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.

Terminal-Fenster
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.