WebDisk
Public Cloud

Apache CloudStack mit Terraform verwalten: von den API-Schlüsseln bis zum Kubernetes-Cluster

Veröffentlicht:

WebDisk Blog · Kategorie: Public Cloud · Lesezeit: ~26 Minuten

Kurz gefasst:- Apache CloudStack ist ein Open-Source-Orchestrator für IaaS-Clouds – eine einzige API über KVM-Servern, Storage und Netzwerk. Seine Begriffe (Zone, Offering, Netzwerk, VPC, Projekt) lassen sich nahezu 1:1 auf Terraform-Ressourcen abbilden.- Die Provider-Konfiguration besteht aus drei Werten: API-Adresse, Schlüssel und Secret. Der Rest ist gewöhnliches Terraform – plan, apply, der State liegt remote in S3, ohne einen zusätzlichen Dienst für die Sperren.- Wir zeigen ein vollständiges Beispiel: einen Kubernetes-Cluster auf CloudStack, vollständig als Code beschrieben – von der VPC und den ACL-Regeln bis zur fertigen kubeconfig und den per Helm ausgerollten Komponenten. >Sie arbeiten nicht mit dem Terminal? Überspringen Sie die Codeblöcke – die erste Hälfte des Textes beschreibt CloudStack selbst und seine Architektur und liest sich auch ohne sie.

Es ist Montag, und ein Ticket trifft ein: „Wir brauchen dieselbe Umgebung wie die Produktion, nur kleiner.“ In einer von Hand zusammengeklickten Cloud bedeutet diese Bitte zwei Arbeitstage, eine Liste im Notizblock und die stille Gewissheit, dass am Ende doch etwas abweichen wird – weil jemand eine Firewall-Regel vergisst oder ein anderes Disk-Offering wählt. In einer als Code beschriebenen Cloud bedeutet sie: ein Verzeichnis kopieren, vier Zahlen in der Variablendatei ändern und die Pipeline starten.

Dieser Artikel handelt davon, wie Sie zu diesem zweiten Zustand gelangen – auf Apache CloudStack, der Plattform, auf der unsere Public Cloud läuft. Wir beginnen mit einer soliden Beschreibung von CloudStack selbst: was es ist, woraus es besteht und mit welchen Begriffen es arbeitet – denn ohne das ist Terraform nur Syntax. Danach gehen wir zur Provider-Konfiguration über, zu den ersten Ressourcen und zu einem vollständigen, realen Beispiel: einem Kubernetes-Cluster, den wir bei uns komplett aus Terraform heraus aufsetzen. Am Ende folgt eine Liste der Fallstricke, die uns Zeit gekostet haben, damit Sie sie nicht selbst entdecken müssen. Der Text richtet sich an zwei Gruppen: an Administratoren, die CloudStack gerade erst kennenlernen und seine Begriffe verstehen wollen, und an Teams, die Terraform von den Hyperscalern kennen und nach Entsprechungen in der Open-Source-Cloud suchen.

Was ist Apache CloudStack eigentlich?

Apache CloudStack ist eine Open-Source-Plattform zum Aufbau von IaaS-Clouds – Software, die Racks voller Server, Storage-Arrays und Switches in eine Cloud mit Self-Service-Panel und API verwandelt. Der Nutzer klickt auf „Maschine erstellen“, und CloudStack entscheidet, auf welchem physischen Host sie laufen soll, legt ihre Festplatte im Storage an, hängt sie in das richtige Netzwerk ein, weist ihr eine IP-Adresse zu und liefert eine Konsole im Browser zurück. Das ist die Schicht, die bei den Hyperscalern „Control Plane“ heißt – nur dass Sie sie hier herunterladen, lesen und bei sich selbst betreiben können.

Das Projekt hat eine lange Geschichte: Es entstand als kommerzielles Produkt der Firma Cloud.com, ging an Citrix über und wurde 2012 an die Apache Software Foundation übergeben, wo es 2013 zum Top-Level-Projekt aufstieg. Seitdem entwickelt es sich als klassisches ASF-Projekt – mit öffentlicher Mailingliste, Abstimmungen über Releases und der Apache-2.0-Lizenz, ohne „Enterprise-Version“ und ohne die Falle der Doppellizenzierung. Es treibt Public Clouds, Telko-Clouds und private Umgebungen in Unternehmen an, die die Kontrolle über die Virtualisierungsschicht nicht aus der Hand geben wollen.

Es lohnt sich, CloudStack gleich einzuordnen, denn die häufigste Frage lautet: „Und worin unterscheidet es sich von OpenStack?“. Am kürzesten gesagt: in der Philosophie der Komplexität. OpenStack ist eine Sammlung von mehreren Dutzend zusammenwirkenden Projekten (Nova, Neutron, Cinder, Keystone, Glance …), von denen jedes seine eigene Datenbank, seine eigene API und seinen eigenen Lebenszyklus hat – das ergibt enorme Flexibilität auf Kosten eines beträchtlichen Teams, das allein die Plattform selbst betreut. CloudStack ist eine einzige Java-Anwendung (Management Server) und eine einzige MySQL-Datenbank, in der das gesamte Cloud-Modell steckt. Die Installation passt in einige wenige Pakete, ein Update besteht aus dem Austausch des Pakets und einer Schema-Migration der Datenbank. Für diese Einfachheit bezahlt man mit geringerer Modularität – ein einzelnes Subsystem gegen ein eigenes auszutauschen fällt schwerer. Für einen mittelgroßen Betreiber ist das in der Regel ein guter Tausch.

CloudStack virtualisiert nicht selbst – es steuert Hypervisoren (ein Hypervisor ist die Software, die virtuelle Maschinen auf einem physischen Server ausführt). Unterstützt werden KVM (die verbreitetste Wahl und unsere), VMware vSphere, XCP-ng/XenServer sowie einige heute weniger bedeutende. Beim Storage spricht es die Sprache von NFS, iSCSI, lokalen Festplatten und – für uns entscheidend – Ceph RBD.

Hierarchie der Infrastruktur: Zone, Pod, Cluster, Host

Wer mit CloudStack sprechen will, muss dessen Einteilung der Welt übernehmen. Die Schichten reichen von der größten zur kleinsten:

  • Zone (zone) – die größte Einheit, in der Praxis ein Rechenzentrum oder eine Availability Zone. Eine Zone besitzt ihr eigenes öffentliches Netzwerk, ihren eigenen Secondary Storage und ihren eigenen Satz an Systemmaschinen. Das ist das Erste, was Sie angeben, wenn Sie irgendetwas anlegen.
  • Pod – üblicherweise ein Rack oder eine Rack-Reihe, in der Praxis eine Broadcast-Domäne für das Management-Netzwerk.
  • Cluster (cluster) – eine Gruppe von Hosts desselben Hypervisor-Typs, die denselben Primary Storage teilen. Das ist die Grenze, innerhalb derer CloudStack Maschinen ungehindert live migrieren kann.
  • Host – ein einzelner physischer Server mit Hypervisor und CloudStack-Agent.

Dazu kommen zwei Arten von Speicherplatz, die man nicht verwechseln sollte:

  • Primary Storage – hier leben die Festplatten der laufenden Maschinen. Er muss schnell und für die Hosts im Cluster sichtbar sein. Bei uns sind das Ceph-RBD-Pools (ein eigener auf NVMe, ein eigener auf Kapazitätsplatten).
  • Secondary Storage – der Speicher für System-Templates, ISO-Images, Snapshots und exportierte Volumes. Er muss nicht schnell sein, er muss geräumig und in der gesamten Zone erreichbar sein. Klassischerweise NFS.

Systemmaschinen, also eine Cloud, die sich selbst bedient

Eine der markanteren Eigenschaften von CloudStack: Einen Teil der eigenen Funktionalität erbringt es mit virtuellen Maschinen, die es sich selbst erzeugt. Es gibt hier keinen großen, zentralisierten Software-Router und keinen separaten Cluster für Netzwerkdienste – es gibt eine Handvoll kleiner VMs, je eine pro Aufgabe:

  • Virtual Router (r-*-VM) – das Herz des Tenant-Netzwerks, also eines einzelnen, von den übrigen isolierten Kunden oder Projekts. Für jedes isolierte Netzwerk und jede VPC startet CloudStack einen Router, der DHCP, DNS, SNAT (das Ersetzen der Quelladresse, dank dessen Maschinen aus dem privaten Netz über eine gemeinsame öffentliche Adresse ins Internet gelangen), Portweiterleitungen, Firewall, Load Balancer und Site-to-Site-VPN bereitstellt. Er lässt sich in einer redundanten Variante bestellen: Ein Router-Paar teilt sich eine Adresse über das Protokoll VRRP, sodass beim Ausfall des aktiven Routers der zweite sie binnen weniger Sekunden übernimmt.
  • Secondary Storage VM (s-*-VM) – kümmert sich um das Herunterladen und Registrieren von Templates, das Hochladen von ISOs, um Snapshots und das Kopieren von Images zwischen Zonen.
  • Console Proxy (v-*-VM) – tunnelt VNC von den Hosts in den Browser; ihr ist es zu verdanken, dass die Schaltfläche „Konsole“ im Panel funktioniert.

Diese Architektur hat zwei wichtige praktische Konsequenzen. Erstens: Die Netzwerkdienste skalieren mit der Zahl der Netzwerke, denn jedes bekommt seinen eigenen Router – es gibt keinen einzelnen Flaschenhals für die gesamte Cloud. Zweitens, und das ist eine ehrliche Warnung: Der Load Balancer in CloudStack lebt innerhalb des Virtual Routers und ist ein Load Balancer der vierten Schicht (TCP, mit sogenannten Sticky Sessions, also der Bindung eines Clients an einen Server für die Dauer der Sitzung, sowie – seit CloudStack 4.22 – mit SSL-Terminierung direkt auf dem Router; in früheren Releases erforderte SSL-Offloading ein externes Gerät). Das ist kein Gegenstück zum ALB mit Routing nach Pfaden und Headern – einen echten Ingress der siebten Schicht müssen Sie selbst aufsetzen, etwa in Kubernetes. Den Durchsatz eines solchen LB begrenzt der einzelne Router, bei hohem Verkehrsaufkommen plant man das also anders.

Hierarchie der Abrechnung: Domain, Account, Benutzer, Projekt

Parallel zur Infrastruktur führt CloudStack eine zweite Hierarchie – jene, die darüber entscheidet, wer was sieht und wofür bezahlt:

  • Domain – eine baumartige Organisationsstruktur. Die Domain ROOT enthält alles, darunter können Domains von Partnern hängen und darunter Domains von Kunden, jede mit eigenen Limits.
  • Account (account) – Eigentümer der Ressourcen und Grenze der Abrechnung. Ein Account kann mehrere Benutzer haben.
  • Benutzer (user) – eine konkrete Person oder Integration; auf der Ebene des Benutzers werden die API-Schlüssel erzeugt.
  • Projekt (project) – ein Container für Ressourcen, die von mehreren Accounts gemeinsam genutzt werden, mit gemeinsamem Limit und gemeinsamer Rechnung.

Projekte sind für Terraform besonders wichtig, denn fast jede Ressource nimmt den Parameter project entgegen – und ihn wegzulassen bedeutet, dass die Maschine im privaten Bereich des Accounts landet statt im gemeinsamen. Das ist einer der häufigeren Gründe für „Warum sehe ich nicht, was ich gerade erstellt habe?“.

Offerings: die Speisekarte, die nicht der Benutzer zusammenstellt

Hier unterscheidet sich CloudStack deutlich von AWS, und das ist der Unterschied, über den jeder stolpert, der von den Hyperscalern kommt. In CloudStack definieren Sie in Terraform keine Parameter der Maschine – Sie wählen einen Posten von der Speisekarte, die der Betreiber der Cloud zusammengestellt hat. Diese Speisekarte sind die Offerings:

  • Service Offering – das Rechenprofil: Anzahl der vCPUs, Taktfrequenz, RAM, eventuelle IOPS-Limits (Zahl der Plattenoperationen pro Sekunde) und Host-Tags. Es kann starr sein (wie t3.medium) oder customized – dann geben Sie die Parameter beim Erstellen der Maschine selbst an, in den vom Betreiber festgelegten Grenzen.
  • Disk Offering – das Profil des Volumes: Größe (oder „custom“), Storage-Typ, Garantien für IOPS/Durchsatz.
  • Network Offering – das interessanteste von ihnen: Es definiert, welche Dienste das Netzwerk erbringt. Ob es einen eigenen Router mit SNAT besitzt, ob es DHCP und DNS bietet, ob es einen Load Balancer, eine Firewall oder VPN bereitstellt, ob es im redundanten Modus arbeitet. Ein Wechsel des Network Offerings ändert die Fähigkeiten des Netzwerks, nicht nur seine „Größe“.
  • VPC Offering – dasselbe eine Etage höher, für die gesamte private Cloud.

Praktische Schlussfolgerung für Terraform: Im Code verweisen Sie auf die Offerings über den Namen oder die UUID und beschreiben nicht deren Parameter. Ist ein Offering vom Typ „customized“, wandern die konkreten Werte in das Feld details.

Netzwerk: isoliert, geteilt und VPC

Das Netzwerkmodell von CloudStack sieht so aus:

  • Isoliertes Netzwerk (isolated) – ein privates Netzwerk eines einzelnen Accounts oder Projekts, mit eigenem VLAN oder VXLAN (das sind zwei Wege, ein separates Netz auf gemeinsamer Verkabelung abzutrennen; VXLAN ist neuer und hebt das für VLANs typische Limit von rund viertausend Netzen auf) und eigenem Virtual Router. Der Router macht SNAT ins Internet, eingehenden Verkehr lassen Sie über Portweiterleitungen, Static NAT oder LB-Regeln durch. Die Firewall arbeitet auf der öffentlichen Adresse, ist zustandsbehaftet und standardmäßig geschlossen.
  • Geteiltes Netzwerk (shared) – ein gemeinsames Netzwerk für viele Accounts, üblicherweise mit einem per DHCP vergebenen Adresspool. Ohne eigenen Router pro Tenant.
  • VPC – eine private Cloud mit mehreren Tiers (Subnetzen), einem gemeinsamen Router und ACL-Listen zwischen den Tiers. Das Gegenstück zur VPC aus AWS, mit einem wesentlichen Unterschied: Die ACL-Regeln sind in CloudStack zustandsbehaftet, die Antwort auf eine erlaubte Verbindung kommt also ohne eigene Regel zurück; Ingress und Egress werden nummeriert und getrennt konfiguriert. Das ist die Variante, die wir für alles Ernsthaftere verwenden.
  • Sicherheitsgruppen (security groups) – ein Verkehrsfilter, der an die Maschinen selbst geheftet wird und nicht an das Netzwerk. Sie funktionieren in Zonen vom Typ „basic“ und ebenso in Zonen vom Typ „advanced“, sofern der Betreiber sie beim Anlegen der Zone aktiviert hat – dann isolieren sie die Maschinen innerhalb eines einzigen, zonenweit geteilten Netzwerks (unter KVM; unter XenServer/XCP-ng nur bei einem Netzwerk im Bridge-Modus). Mit isolierten Netzwerken oder mit VPCs lassen sie sich nicht kombinieren – dort übernehmen die Firewall auf der öffentlichen Adresse und die ACL-Listen die Rolle des Filters.

API: eine einzige, signiert, asynchron

Die gesamte Cloud stellt eine einzige HTTP-API unter der Adresse /client/api bereit. Die Aufrufe werden mit dem Algorithmus HMAC-SHA1 durch das Paar API-Schlüssel + Secret signiert, und die Antwort erhalten Sie in JSON oder XML. Das Panel besitzt keinerlei „private“ Endpunkte – es klickt genau dieselbe API an wie Sie.

Die zweite Eigenschaft, die man kennen muss, bevor man mit dem Automatisieren beginnt: Die meisten zustandsändernden Operationen sind asynchron. deployVirtualMachine gibt keine fertige Maschine zurück – es gibt die Kennung eines Jobs zurück, dessen Fortschritt Sie über queryAsyncJobResult abfragen. Der Terraform-Provider erledigt das für Sie, aber es erklärt, warum manche Operationen dauern, warum „Eventual Consistency“ auftritt und warum man manchmal ausdrücklich auf die Bereitschaft eines Dienstes warten muss (wir kommen beim Kubernetes-Beispiel darauf zurück).

Für die manuelle Arbeit mit dieser API dient der offizielle Client CloudMonkey (cmk) – hervorragend zum Erkunden und zum Herausziehen der UUIDs, die Sie anschließend in Terraform einfügen:

# Liste der Zonen und Compute-Offerings – nützlich beim Schreiben des ersten Moduls
cmk list zones filter=id,name
cmk list serviceofferings filter=id,name,cpunumber,memory
cmk list networkofferings filter=id,name,state

Warum sollte man CloudStack in Code beschreiben, statt im Panel zu klicken?

Das Panel von CloudStack ist vollständig – man kann darin alles erledigen. Das Problem liegt nicht bei den Möglichkeiten, sondern bei dem, was ein Panel von Natur aus nicht leistet:

  • Wiederholbarkeit. Eine von Hand gebaute Testumgebung, die „genauso ist wie die Produktion“, ist nie wirklich dieselbe. Aus demselben Modul mit einer anderen Variablendatei gebaut – schon.
  • Prüfung der Änderungen vor der Ausführung. terraform plan zeigt die Differenz zwischen dem beschriebenen und dem tatsächlichen Zustand, bevor irgendetwas geschieht. Im Panel ist das Gegenstück dazu die Vorsicht des Administrators.
  • Historie und Verantwortlichkeit. Wer hat die ACL-Regel hinzugefügt und warum? Im Repository ist das ein einziges git blame. Im Panel – bestenfalls ein Eintrag im Ereignisprotokoll ohne Begründung.
  • Drift-Erkennung. Hat jemand „nur kurz“ etwas von Hand geändert? Der nächste plan zeigt es.
  • Wiederherstellung nach einer Katastrophe. Code ist die glaubwürdigste Architekturdokumentation – eine, die mit Sicherheit aktuell ist, weil die Umgebung aus ihr entstanden ist.

Der Preis ist real und man sollte ihn kennen: Code muss gepflegt und der State geschützt werden, und die erste Bereitstellung dauert länger als das Zusammenklicken. Der Ertrag stellt sich bei der zweiten Umgebung und beim ersten Ausfall ein.

Wie richtet man Terraform mit CloudStack ein?

Die Konfiguration umfasst vier Schritte: API-Schlüssel, Provider, die bewusste Wahl seiner Version und die Verlagerung des State auf ein entferntes Backend. Drei davon erledigt man einmal pro Projekt und kann sie danach vergessen. Der vierte – die Wahl des Providers – ist die Entscheidung, auf die man am häufigsten zurückkommt, deshalb widmen wir ihr den meisten Platz.

Schritt 1: API-Schlüssel

Terraform authentifiziert sich mit den Schlüsseln eines CloudStack-Benutzers. Sie erzeugen sie im Panel: Accounts → Konto auswählen → Users → Benutzer auswählen → Generate Keys, anschließend kopieren Sie API Key und Secret Key. Das Secret sehen Sie nur bei der Erzeugung vollständig – danach zeigt es das Panel nur noch maskiert.

Zwei Regeln, an die man sich vom ersten Tag an halten sollte:

  • Ein eigener Benutzer für die Automatisierung. Verwenden Sie keine Schlüssel eines Menschen. Ein technisches Konto lässt sich leichter entziehen, rotieren und im Ereignisprotokoll nachverfolgen.
  • Die geringstmöglichen Berechtigungen. Wenn Terraform ein einziges Projekt verwalten soll, geben Sie ihm nicht die Rolle eines Domain-Administrators.

Schritt 2: Provider und Variablen

Die Konfiguration des Providers besteht aus buchstäblich drei Werten. Entscheidend ist, dass zwei davon niemals ins Repository gelangen:

# providers.tf
terraform {
  required_version = "~> 1.10"

  required_providers {
    cloudstack = {
      source = "cloudstack/cloudstack"
      version = "0.6.0"
    }
  }
}

provider "cloudstack" {
  api_url = var.cloudstack_api_url
  api_key = var.cloudstack_api_key
  secret_key = var.cloudstack_secret_key
}

# variables.tf
variable "cloudstack_api_url" {
  description = "Adres API CloudStacka, np. https://panel.example.com/client/api"
  type = string
}

variable "cloudstack_api_key" {
  description = "Klucz API użytkownika technicznego"
  type = string
  sensitive = true # erscheint nicht in den Logs von plan/apply
}

variable "cloudstack_secret_key" {
  description = "Sekret użytkownika technicznego"
  type = string
  sensitive = true
}

Die Werte übergeben Sie über Umgebungsvariablen – Terraform liest jede Variable mit dem Präfix TF_VAR_:

export TF_VAR_cloudstack_api_url="https://panel.example.com/client/api"
export TF_VAR_cloudstack_api_key="$(pass show cloud/terraform/api-key)"
export TF_VAR_cloudstack_secret_key="$(pass show cloud/terraform/secret-key)"

terraform init
terraform plan

In der Pipeline setzen Sie dieselben drei Werte als maskierte CI/CD-Variablen ein. Eine Datei *.tfvars mit Secrets ist der häufigste Weg, auf dem Schlüssel versehentlich in Git landen – besser, Sie legen sie gar nicht erst an.

Schritt 3: Die Version des Providers ist von Bedeutung

Das ist die Stelle, an der die Dokumentation mitunter optimistisch ist, deshalb sagen wir offen, wie es sich verhält. Der offizielle Provider cloudstack/cloudstack wird im Repository von Apache entwickelt und ist der richtige Ausgangspunkt – allerdings in einem Rhythmus, an den man sich gewöhnen muss: Stabile Releases erscheinen alle anderthalb bis zwei Jahre (0.4.0 – Februar 2022, 0.5.0 – April 2024, 0.6.0 – Oktober 2025). Das letzte stabile Release in der Registry ist 0.6.0 vom 27. Oktober 2025, und der Zweig 0.7.0 ist dort vorerst ausschließlich ein Release Candidate (0.7.0-rc1 vom 19. August 2026, veröffentlicht kurz vor der Entstehung dieses Textes; Stand: August 2026). Durch eine derartige Pause sammelt sich im Hauptzweig des Repositorys ein Vorrat an Ressourcen und Attributen an, die das stabile Release noch nicht enthält. Praktische Schlussfolgerung: Bevor Sie zu dem Schluss kommen, dem Provider fehle etwas, werfen Sie einen Blick in das CHANGELOG – eine Funktion, die es vor einem Jahr noch nicht gab, hat es inzwischen bereits in ein stabiles Release geschafft.

Eine gute Veranschaulichung ist das Attribut cidrlist in der Ressource cloudstack_loadbalancer_rule – die Möglichkeit, per Adressliste einzuschränken, wer die Regel des Load Balancers überhaupt erreichen kann. Im Release 0.5.0 gab es das Attribut noch nicht; in den Hauptzweig kam es Ende August 2025 und im stabilen 0.6.0 ist es enthalten. Die Moral ist einfach und immer dieselbe: Eine Lücke, die Sie vor einem Jahr umgangen haben, ist heute mitunter schon im stabilen Release geschlossen – es lohnt sich nachzusehen, bevor Sie einen Workaround bauen, den Sie anschließend pflegen müssen.

Praktische Empfehlungen:

  • Starten Sie mit dem offiziellen cloudstack/cloudstack aus dem Namensraum von Apache – er ist die Referenz für die Dokumentation und die Beispiele, auch für die in diesem Artikel.
  • Der Provider ist die Abhängigkeit mit den höchsten Berechtigungen. Er ist ein Programm, das Sie auf Ihrem Laptop und auf dem CI-Runner ausführen: mit API-Schlüsseln zur gesamten Cloud, mit den Werten der als sensitive gekennzeichneten Attribute und in derselben Umgebung, in der die Zugangsdaten zum Bucket mit dem State liegen. Behandeln Sie ihn wie jede andere Abhängigkeit mit einer solchen Reichweite: Beziehen Sie ihn aus dem offiziellen Namensraum in der Registry, und falls die Unternehmensrichtlinie es verlangt – bauen Sie ihn selbst aus einer geprüften Quelle und liefern Sie ihn aus einem lokalen Spiegel (filesystem_mirror) aus. Seien Sie sich dabei der Grenze bewusst: .terraform.lock.hcl schützt vor dem stillen Austausch der Binärdatei im Nachhinein, nicht vor Code, der von Anfang an schlecht war.
  • Pinnen Sie die exakte Version (version = "0.6.0", nicht >= 0.6) und committen Sie die Datei .terraform.lock.hcl. Eine Cloud, die sich „von selbst geändert“ hat, weil die Pipeline einen neueren Provider heruntergeladen hat, ist eine ausgesprochen unangenehme Klasse von Ausfällen.
  • OpenTofu funktioniert genauso. Im Jahr 2023 wechselte Terraform von einer Open-Source-Lizenz zur BUSL (Business Source License): Der Code ist weiterhin offen, und die interne Nutzung – auch die kommerzielle – ist kostenlos, formal ist es jedoch keine Open Source mehr, denn Terraform darf nicht als Dienst angeboten werden, der mit den kostenpflichtigen Produkten von HashiCorp konkurriert. Als Antwort entstand ein Fork unter der Linux Foundation; die Provider für CloudStack sind gewöhnliche Plugins und werden von beiden Werkzeugen unterstützt. Wenn die BUSL-Lizenz für Sie ein Problem darstellt – meist deshalb, weil die Unternehmensrichtlinie ausschließlich Open-Source-Lizenzen zulässt –, ist das Ersetzen von terraform durch tofu in den Befehlen in der Regel die gesamte Migration.

Schritt 4: der State – und eine angenehme Überraschung für CloudStack-Betreiber

Terraform hält in der State-Datei ein Abbild dessen fest, was es erstellt hat. Sie lokal aufzubewahren funktioniert genau bis zu dem Moment, in dem ein zweiter Mensch oder eine Pipeline ein apply ausführt – danach beginnt das Drama. Der State wandert also in ein entferntes Backend, und hier ist der Betreiber von CloudStack in einer komfortablen Lage, denn er hat fast immer S3-kompatiblen Storage zur Hand.

Bis vor Kurzem benötigte das Backend s3 zum Sperren des State eine DynamoDB-Tabelle – also einen Dienst, den es außerhalb von AWS schlicht nicht gibt. Seit Terraform 1.10 gibt es eine native Sperre auf Basis bedingter Schreibvorgänge in S3 (use_lockfile = true), und in 1.11 wurde die Variante mit DynamoDB als veraltet gekennzeichnet. Für alle, die auf Ceph RGW, MinIO oder einem anderen S3 aufsetzen, bedeutet das: Ein vollwertiges Backend mit gemeinsamer Sperre umfasst heute gut ein Dutzend Konfigurationszeilen:

# backend.tf
terraform {
  backend "s3" {
    bucket = "terraform-states"
    key = "cloudstack/prod.tfstate"

    endpoints = {
      s3 = "https://s3.example.com"
    }

    region = "us-east-1" # formal erforderlich; für S3 außerhalb von AWS ohne Bedeutung

    # wir deaktivieren die ausschließlich für AWS geltenden Validierungen
    skip_credentials_validation = true
    skip_region_validation = true
    skip_requesting_account_id = true
    skip_s3_checksum = true

    use_lockfile = true # native Sperre in S3, ohne DynamoDB (Terraform >= 1.10)
  }
}

Die Schlüssel zum Bucket übergeben Sie bei der Initialisierung, um sie nicht im Repository abzulegen:

terraform init \
  -backend-config="access_key=$S3_ACCESS_KEY" \
  -backend-config="secret_key=$S3_SECRET_KEY"

Wenn Sie Object Storage bei WebDisk nutzen, haben Sie das Backend für Terraform bereits fertig – es ist derselbe Bucket und es sind dieselben Schlüssel, die Sie für alles andere verwenden.

Achtung – das wird leicht vergessen: Die State-Datei enthält die Werte der Attribute im Klartext, einschließlich derjenigen, die als sensitive gekennzeichnet sind. Behandeln Sie den Bucket mit den State-Dateien wie einen Speicher für Secrets: privat, mit serverseitiger Verschlüsselung, mit Versionierung und mit einem Zugriff, der auf diejenigen beschränkt ist, die ihn wirklich benötigen.

Die erste Ressource: eine virtuelle Maschine

Die kürzeste sinnvolle Konfiguration sieht so aus. Beachten Sie, dass die Parameter der Maschine nicht im Code erfunden werden – sie sind ein Verweis auf ein Offering:

data "cloudstack_zone" "main" {
  filter {
    name = "name"
    value = "^PL-DCO-1$"
  }
}

data "cloudstack_template" "ubuntu" {
  template_filter = "featured"
  filter {
    name = "name"
    value = "^Ubuntu 24\\.04 LTS$"
  }
}

resource "cloudstack_network" "app" {
  name = "app-net"
  cidr = "10.20.0.0/24"
  network_offering = "DefaultIsolatedNetworkOfferingWithSourceNatService"
  zone = data.cloudstack_zone.main.id
}

resource "cloudstack_instance" "web" {
  name = "web-01"
  display_name = "web-01"
  service_offering = "Medium Instance" # Posten von der Speisekarte des Betreibers
  template = data.cloudstack_template.ubuntu.id
  network_id = cloudstack_network.app.id
  zone = data.cloudstack_zone.main.id
  root_disk_size = 40

  # cloud-init: Konfiguration des Systems beim ersten Start
  user_data = base64encode(file("${path.module}/cloud-init/web.yaml"))

  expunge = true # destroy löscht wirklich, statt etwas im Papierkorb zu lassen
}

Einige Dinge, die einen Kommentar verdienen:

  • Die Werte in den Blöcken filter sind reguläre Ausdrücke, keine Namen. Der Provider kompiliert sie als Regex und gleicht sie ohne Verankerung ab, sodass Ubuntu 24.04 LTS auch Ubuntu 24.04 LTS minimal erfasst – und bei mehreren Treffern wählt die Data Source des Templates dasjenige mit dem neuesten Erstellungsdatum aus, nicht zwingend das, das Sie gemeint haben (die Data Source der Zone – schlicht das letzte aus der Liste). Daher \\. und ^…$ oben; bei einer stärker zugemüllten Bibliothek von Templates lohnt es sich, einen zweiten Filter zu ergänzen, z. B. { name = "hypervisor", value = "KVM" }.
  • service_offering über den Namen. Das funktioniert und ist gut lesbar, aber der Name des Offerings kann sich ändern. In Umgebungen, die lange leben sollen, ist es sicherer, die UUID anzugeben.
  • Ein Offering vom Typ „customized“ hat keine fest hinterlegten Parameter – sie werden im Feld details angegeben, z. B. details = { cpuNumber = "4", cpuSpeed = "2000", memory = "8192" }. Ohne das weist CloudStack die Anfrage zurück.
  • user_data ist der Standardkanal für cloud-init. Achten Sie auf das Größenlimit – bei umfangreicheren Konfigurationen hilft base64gzip() statt base64encode().
  • expunge = true bedeutet, dass terraform destroy die Maschine wirklich löscht, statt sie bis zum Ablauf im Zustand „Destroyed“ zu belassen. Für ephemere Umgebungen ist das die richtige Wahl; für die Produktion sollten Sie es sich überlegen.

VPC und Tiers: ACL-Regeln sind keine eigenständigen Ressourcen

Ernsthaftere Umgebungen baut man auf einer VPC auf. Das wirkt ganz natürlich: eine private Cloud, darin die Netze und vor den Netzen – eine ACL-Liste:

resource "cloudstack_vpc" "main" {
  name = "prod-vpc"
  cidr = "10.0.0.0/16"
  vpc_offering = "Redundant VPC offering"
  zone = data.cloudstack_zone.main.id
  project = var.project
}

resource "cloudstack_network_acl" "app" {
  name = "prod-acl"
  description = "ACL dla tieru aplikacyjnego"
  vpc_id = cloudstack_vpc.main.id
  project = var.project
}

resource "cloudstack_network_acl_rule" "app" {
  acl_id = cloudstack_network_acl.app.id
  project = var.project

  # ACHTUNG: Alle Regeln einer ACL leben in EINER Ressource
  rule {
    action = "allow"
    cidr_list = ["0.0.0.0/0"]
    protocol = "all"
    traffic_type = "egress"
  }

  # ein Port oder ein Bereich pro `rule`-Block – 0.6.0 nimmt keine Liste mehr an
  rule {
    action = "allow"
    cidr_list = var.admin_cidrs # Verwaltungsnetze / VPN
    protocol = "tcp"
    port = "22"
    traffic_type = "ingress"
  }

  rule {
    action = "allow"
    cidr_list = var.admin_cidrs
    protocol = "tcp"
    port = "6443"
    traffic_type = "ingress"
  }

  rule {
    action = "allow"
    cidr_list = ["0.0.0.0/0"]
    protocol = "tcp"
    port = "443"
    traffic_type = "ingress"
  }
}

resource "cloudstack_network" "app" {
  name = "prod-app-tier"
  cidr = "10.0.1.0/24"
  network_offering = "Isolated Network for VPC with SNAT"
  vpc_id = cloudstack_vpc.main.id
  acl_id = cloudstack_network_acl.app.id
  zone = data.cloudstack_zone.main.id
  project = var.project
}

Hier lauert die erste echte Falle. Die Ressource cloudstack_network_acl_rule steht nicht für eine einzelne Regel – sie steht für den gesamten Regelsatz einer ACL. Die rule-Blöcke darin sind die vollständige Liste. Fügt jemand von Hand im Panel eine Regel hinzu, entfernt sie das nächste apply, denn Terraform führt den Satz auf den im Code beschriebenen Zustand zurück. Dieses Verhalten ist richtig und gewollt – es überrascht aber, wenn man ein Modell nach dem Muster „eine Ressource, eine Regel“ erwartet.

Und gleich die zweite Sache, die überraschen kann: das Feld ports ist verschwunden. In 0.6.0 ist es als veraltet markiert, und der Provider verweigert schlicht das Anlegen einer neuen Regel, die es verwendet – Sie erhalten den Fehler „The 'ports' field is no longer supported for creating new rules“. Statt einer Liste geben Sie port mit einem einzelnen Port oder einem einzelnen Bereich an ("80" oder "80-90"), zwei Ports sind also einfach zwei rule-Blöcke. In die andere Richtung wirkt das genauso unerbittlich: Im Zweig 0.5.x gibt es das Feld port noch nicht, dort gilt ausschließlich ports. Ein gutes Beispiel dafür, warum es sich lohnt, die genaue Version zu pinnen und .terraform.lock.hcl im Repository zu halten.

Öffentliche Adresse: erst die Reservierung, dann NAT und Load Balancer

Die Reihenfolge ist immer dieselbe:

resource "cloudstack_ipaddress" "public" {
  vpc_id = cloudstack_vpc.main.id
  zone = data.cloudstack_zone.main.id
  project = var.project
}

# Variante 1: einfache Portweiterleitung auf eine Maschine
resource "cloudstack_port_forward" "ssh" {
  ip_address_id = cloudstack_ipaddress.public.id
  project = var.project

  forward {
    protocol = "tcp"
    private_port = 22
    public_port = 2222
    virtual_machine_id = cloudstack_instance.bastion.id
  }
}

# Variante 2: L4 Load Balancer auf dem Virtual Router, mit Liste erlaubter Quellen
resource "cloudstack_loadbalancer_rule" "api" {
  name = "kubernetes-apiserver"
  description = "Dostęp do API Kubernetesa"
  ip_address_id = cloudstack_ipaddress.public.id
  network_id = cloudstack_network.app.id
  algorithm = "roundrobin"
  private_port = 6443
  public_port = 6443
  protocol = "tcp"
  member_ids = cloudstack_instance.controlplane[*].id
  project = var.project

  cidrlist = var.admin_cidrs # wer diese Regel überhaupt erreichen darf
}

Das Attribut cidrlist kam mit Release 0.6.0 – ab dieser Version haben Sie es sofort zur Hand. In älteren Versionen ist die Load-Balancer-Regel offen für die ganze Welt und Sie müssen sie separat mit einer Firewall-Regel schließen.

Ein reales Beispiel: ein vollständig in Code beschriebener Kubernetes-Cluster

Jetzt der interessanteste Teil: wie das in einem Projekt aussieht, das tatsächlich läuft. Bei uns ist der komplette Kubernetes-Cluster auf Talos Linux – einer Distribution ausschließlich für Kubernetes, ohne Shell und ohne SSH, rein deklarativ über eine API konfiguriert – von der leeren CloudStack-Zone bis zum laufenden ArgoCD in Terraform beschrieben.

Bevor wir den Code zeigen – eine ehrliche Antwort auf die Frage, die jeder stellen wird, der CloudStack kennt: ja, CloudStack hat einen eingebauten Kubernetes-Dienst (CloudStack Kubernetes Service), und der offizielle Provider hat dafür die Ressource cloudstack_kubernetes_cluster, die bereits im stabilen Release 0.6.0 enthalten ist. Eine Ressource, und der Cluster steht. Für viele Einsatzzwecke ist das der richtige Weg, und es gibt keinen Grund, ihn zu umgehen.

Wir haben den längeren gewählt, aus drei Gründen. Erstens sind die Kubernetes-Versionen im eingebauten Dienst wieder eine „Speisekarte des Betreibers“ – verfügbar ist das, wofür der Cloud-Administrator ein Image hochgeladen hat; wir wollten die Version des Knotenbetriebssystems und des Clusters über eine einzige URL in der .tfvars-Datei steuern. Zweitens wollten wir eine eigene Zusammensetzung der Systemschicht – Cilium als CNI, Ceph RBD für persistente Volumes, ArgoCD ab dem ersten Durchlauf – und dasselbe Muster auch außerhalb von CloudStack. Drittens liefert Talos einen Knoten ohne Shell und ohne SSH, also eine Konfiguration, die sich nicht „mal eben von Hand korrigieren“ lässt.

Der Preis dieser Entscheidung liegt offen: einige hundert Zeilen Code mehr und die Pflege einer Schicht, die im eingebauten Dienst der Cloud-Betreiber führt – einschließlich der Knoten-Updates. Wenn Sie das nicht brauchen, beginnen Sie mit dem eingebauten Dienst.

Aufbau des Repositorys

├── modules/
│ ├── cloudstack/ # VPC, Netz, ACL, Knoten, LB, Template
│ └── talos/ # Maschinenkonfiguration von Talos + Patches
└── environments/
    ├── dev/
    └── prod/
        ├── backend.tf # State in S3 (Ceph RGW)
        ├── providers.tf # cloudstack, talos, helm, kubectl, http
        ├── main.tf # Verdrahtung des Ganzen
        ├── cloudstack.tfvars # Werte der Umgebung
        └── infra-deployments/
            ├── charts/ # cilium, CSI, CCM, prometheus, argocd
            └── argocd-apps/ # GitOps-Anwendungen

Die Aufteilung ist klassisch und bewährt sich gut: Module beschreiben das „Wie“, die Umgebungsverzeichnisse das „Wie viel und wo“. Der Unterschied zwischen dev und prod ist in der Praxis die .tfvars-Datei – die Werte unten sind Beispiele und zeigen, was Umgebungen überhaupt unterscheidet, und nicht die Größe unserer Cluster:

# environments/prod/cloudstack.tfvars
zone = "PL-DCO-1"
project = "..." # UUID des CloudStack-Projekts

vpc_name = "Prod Redundant VPC"
vpc_cidr = "10.0.0.0/16"
vpc_offering = "Virtual Private Cloud with VR HA"
talos_network_name = "prod-network"
talos_network_cidr = "10.0.1.0/24"
talos_network_offering = "Isolated Network for VPC with SNAT"
talos_image_url = "https://factory.talos.dev/image/<hash>/<wersja>/cloudstack-amd64.raw.gz"

cluster_name = "prod"
controlplane_vip = "10.0.1.2"

In derselben Datei stehen außerdem controlplane_count und worker_count – die Knotenzahlen sind in der Praxis das Einzige, was sich beim Hoch- oder Herunterskalieren einer Umgebung ändert; die konkreten Werte halten wir in den Variablen der jeweiligen Umgebung, nicht im Rumpf des Moduls.

Das System-Template direkt aus einer URL

Das Erste, was Menschen beeindruckt, die das manuelle Hochladen von Images gewohnt sind: CloudStack kann ein Template registrieren, indem es dieses von einer beliebigen HTTP-Adresse herunterlädt, und Terraform kann das steuern. Talos stellt über den Dienst Image Factory fertige Images für CloudStack bereit, damit wird die Betriebssystemversion der Knoten zu … einer Variablen in der Konfigurationsdatei:

resource "cloudstack_template" "talos" {
  name = "Talos-${regex("v[0-9]+\\.[0-9]+\\.[0-9]+", var.talos_image_url)}"
  format = "RAW"
  hypervisor = "KVM"
  os_type = "Other PV Virtio-SCSI (64-bit)"
  url = var.talos_image_url
  zone = var.zone
  project = var.project
}

Ein Upgrade von Talos auf eine neuere Version ist die Änderung einer einzigen URL. Der Template-Name zieht die Versionsnummer (ein Tag der Form vX.Y.Z) mit einem regulären Ausdruck aus der Adresse, sodass eine neue Version ein neues Template anlegt, statt das alte still zu überschreiben.

Knoten: Maschine + Konfiguration in user_data

Talos hat kein SSH – die gesamte Knotenkonfiguration erhält es beim ersten Start in user_data. Das harmoniert sehr gut mit Terraform: Das Modul talos erzeugt die Maschinenkonfigurationen, und das Modul cloudstack injiziert sie in die Instanzen:

resource "cloudstack_instance" "controlplane" {
  count = var.controlplane_count

  name = "${var.cluster_name}-control-${count.index + 1}"
  service_offering = "cs.custom.nvme"
  template = cloudstack_template.talos.id
  network_id = cloudstack_network.talos.id
  zone = var.zone
  project = var.project

  uefi = true
  root_disk_size = 200
  details = { cpuNumber = "2", cpuSpeed = "2000", memory = "16000" }

  user_data = base64gzip(var.talos_controlplane_config[count.index])

  expunge = true
}

base64gzip() statt base64encode() ist keine Zierde – eine Talos-Konfiguration mit Patches kann die Größenbeschränkung von user_data überschreiten.

expunge = true lassen wir hier bewusst stehen, trotz der Warnung aus dem vorigen Abschnitt: Ein Cluster-Knoten ist austauschbar und entsteht aus demselben Code neu, und vor dem Verlust des Clusters schützt eine etcd-Sicherung, nicht der Papierkorb von CloudStack. Bei einer Maschine, die Daten hält, fiele die Wahl umgekehrt aus.

Die Reihenfolge der Ereignisse – wo Terraform Hilfe braucht

Der interessanteste Teil des gesamten Projekts ist die main.tf der Umgebung, denn sie zeigt etwas, das in Tutorials nicht zu sehen ist: Einen Cluster aufzubauen ist eine Sequenz, in der ein Teil der Schritte auf die Realität warten muss und nicht nur auf den Abhängigkeitsgraphen von Terraform.

Bevor wir den Ablauf verfolgen, muss eines ausdrücklich benannt werden: Ab hier arbeiten in einem einzigen Verzeichnis mehrere Provider gleichzeitig. cloudstack erzeugt die Infrastruktur, talos spricht über die Talos-API mit den Knoten (daher die Ressourcen mit dem Präfix talos_), helm und kubectl installieren Komponenten im bereits fertigen Cluster, und http dient einer einzigen Bereitschaftsprüfung. Suchen Sie die Ressourcen talos_* also nicht in der Dokumentation des CloudStack-Providers – dort gibt es sie nicht und wird es sie nicht geben. Terraform fügt das alles in einem Durchlauf zusammen, denn die Ausgabe des einen Providers ist mitunter die Eingabe des nächsten.

Der Ablauf sieht so aus:

  1. Es entsteht die VPC und es wird eine öffentliche IP-Adresse reserviert.
  2. Das Modul talos erzeugt die Maschinenkonfigurationen – und kennt dabei bereits den öffentlichen Endpunkt des Clusters, denn die Adresse steht vor dem Anlegen der Knoten fest.
  3. Das Modul cloudstack erzeugt Template, Netz, ACL, Knoten und LB-Regeln.
  4. talos_machine_configuration_apply sendet die Konfiguration an jeden Knoten.
  5. talos_machine_bootstrap initialisiert etcd – die verteilte Datenbank, in der Kubernetes seinen gesamten Zustand hält – auf dem ersten Control-Plane-Knoten; die übrigen Knoten treten ihr von selbst bei.
  6. Terraform wartet, bis die Kubernetes-API wirklich antwortet.
  7. Erst dann wird die kubeconfig abgeholt und die Komponenten werden mit Helm installiert.

Schritt sechs ist der, der in den meisten Beispielen fehlt. Der Bootstrap endet, bevor kube-apiserver Verkehr annimmt, die nächste Ressource würde also ins Leere greifen. Ein Muster, das man sich merken sollte – das Abfragen des Health-Endpunkts mit Wiederholungen, als ganz normale data source:

data "http" "check_kubeapi" {
  depends_on = [talos_machine_bootstrap.cluster]

  url = "https://${cloudstack_ipaddress.lb.ip_address}:6443/healthz"
  insecure = true # bewusste Abkürzung – das apiserver-Zertifikat signiert die Cluster-CA, keine öffentliche Stelle

  retry {
    attempts = 10
    min_delay_ms = 15000
    max_delay_ms = 15000 # fester Abstand: bis zu 2,5 Minuten Geduld
  }
}

resource "talos_cluster_kubeconfig" "cluster" {
  depends_on = [data.http.check_kubeapi] # hier steckt der ganze Trick
  client_configuration = module.talos.talos_client_configuration
  node = cloudstack_ipaddress.lb.ip_address
}

Diese Lösung ersetzt ein time_sleep mit einem aus der Luft gegriffenen Wert: Sie wartet genau so lange wie nötig, und wenn der Cluster nicht hochkommt, bricht sie das apply mit einem klaren Fehler ab, statt still weiterzulaufen. Ein explizites max_delay_ms ist hier nicht überflüssig: Ohne es nimmt der Provider die voreingestellte Obergrenze von 30 s und einen exponentiellen Backoff, das reale Warten wächst damit auf fast fünf Minuten.

Ein Wort zu insecure = true, damit niemand diese Gewohnheit weiterträgt: Es ist eine Abkürzung, keine Empfehlung. Wir prüfen ausschließlich /healthz und senden dorthin nichts, und das Zertifikat des kube-apiserver ist von der Cluster-CA signiert, die Standardprüfung würde es also ohnehin ablehnen. Schließen lässt sich das aber – der Provider http akzeptiert ca_cert_pem, und die Kubernetes-CA existiert zu diesem Zeitpunkt bereits im Terraform-State, denn sie wird von talos_machine_secrets erzeugt; es genügt, sie als Ausgabe des Moduls bereitzustellen.

Die Kubernetes-Schicht im selben Durchlauf

Nach dem Abholen der kubeconfig konfiguriert dasselbe apply die Provider helm und kubectl mit den Daten des soeben erzeugten Clusters und installiert die Systemschicht:

module "cilium" { source = "./infra-deployments/charts/cilium" }
module "kube-prometheus-stack" { source = "./infra-deployments/charts/kube-prometheus-stack" }
module "cloudstack-kubernetes-provider" { source = "./infra-deployments/charts/cloudstack-kubernetes-provider" /* ... */ }
module "cloudstack-csi" { source = "./infra-deployments/charts/cloudstack-csi" /* ... */ }
module "ceph_csi_rbd" { source = "./infra-deployments/charts/ceph-csi-rbd" /* ... */ }
module "argocd" { source = "./infra-deployments/charts/argocd" }
module "argocd-apps" { source = "./infra-deployments/argocd-apps" }

Drei dieser Komponenten lohnen eine eigene Erläuterung:

  • cilium – das CNI des Clusters, also das Plugin, das für das Netz zwischen den Pods und für die Verkehrsrichtlinien zuständig ist.
  • cloudstack-kubernetes-provider – der Cloud Controller Manager. Dank ihm bestellt ein Service vom Typ LoadBalancer in Kubernetes in CloudStack tatsächlich eine öffentliche Adresse und eine LB-Regel, statt ewig im Zustand Pending zu hängen.
  • cloudstack-csi – der Storage-Treiber. Er erlaubt es, dass ein PersistentVolumeClaim ein echtes CloudStack-Volume erzeugt und es an den richtigen Knoten anbindet.

Die Verantwortungsgrenze ist hier bewusst gezogen: Terraform stellt den Cluster und seine Systemschicht bereit, ab den Anwendungen übernimmt GitOps – ein Modell, in dem der Zustand der Anwendungen in einem Git-Repository beschrieben ist und ein im Cluster laufender Agent (bei uns ArgoCD) ihn laufend mit der Realität vergleicht und Abweichungen beseitigt. Terraform ist hervorragend im Erzeugen von Infrastruktur und schwach als Schleife zur ständigen Rekonziliation von Anwendungen – das Vermischen dieser Rollen endet mit Pipelines, die eine Viertelstunde dauern.

Pipeline: plan automatisch, apply hinter einem manuellen Gate

Das Ganze läuft in GitLab CI in einem Aufbau, den wir für jede produktive Infrastruktur empfehlen:

stages: [validate, plan, apply]

image:
  name: hashicorp/terraform:1.10.3
  entrypoint: [""]

before_script:
  - cd $BASE_PATH
  - terraform init
      -backend-config="access_key=$S3_ACCESS_KEY"
      -backend-config="secret_key=$S3_SECRET_KEY"

validate:
  stage: validate
  script: [terraform validate]

iac-scan: # statische Sicherheitsanalyse des IaC-Codes
  stage: validate
  image:
    name: aquasec/trivy:latest
    entrypoint: [""] # ohne das startet GitLab keine Shell in diesem Image
  before_script: [] # hier gibt es nichts zu initialisieren
  script: [trivy config --exit-code 1 $BASE_PATH]

plan:
  stage: plan
  script:
    - terraform plan -out=tfplan -var-file cloudstack.tfvars
    - terraform show -json tfplan > tfplan.json
  artifacts:
    paths: [$BASE_PATH/tfplan, $BASE_PATH/tfplan.json]
    access: developer # der Plan trägt dieselben Secrets wie der State (GitLab >= 16.7)
    expire_in: 7 days # muss das manuelle Gate vor `apply` überleben

apply:
  stage: apply
  script: [terraform apply tfplan]
  when: manual
  dependencies: [plan]
  manual_confirmation: "Potwierdź, że chcesz wdrożyć te zmiany"

Das Plan-Artefakt ist genauso sensibel wie die State-Datei. Ein gespeicherter tfplan – und erst recht tfplan.json, denn terraform show -json gibt als sensitive markierte Werte im Klartext aus – enthält dieselben Secrets wie der State, samt einer Kopie des States von vor der Änderung. GitLab-Artefakte kann standardmäßig jeder herunterladen, der den Job sieht, und sie liegen wochenlang herum; beschränken Sie also den Zugriff (access: developer) und verkürzen Sie die Aufbewahrung (expire_in) – wobei das Artefakt das manuelle Gate vor apply überleben muss, denn ohne es hat apply nichts auszurollen.

Drei Dinge, die hier die Arbeit machen:

  • apply führt den gespeicherten Plan aus, statt ihn neu zu berechnen – die bloße Übergabe der Plandatei wertet Terraform als Freigabe, -auto-approve ist hier also überflüssig. Sie rollen genau das aus, was jemand gelesen und akzeptiert hat.
  • Ein manuelles Gate mit Bestätigungsfrage. Infrastruktur ist nicht der Ort für ein apply automatisch nach dem Merge.
  • Ein IaC-Sicherheitsscan in der Validierungsphase. trivy config geht die .tf-Dateien durch und fängt typische Sünden ab, bevor sie Realität werden – bei uns am häufigsten in user_data eingefügte Secrets. Zwei Dinge sollte man dazu wissen. Erstens: Wir scannen mit Trivy, weil tfsec darin aufgegangen ist und keine neuen Regeln mehr bekommt. Zweitens, ehrlicherweise: fertige Regeln, die direkt für den CloudStack-Provider geschrieben sind, gibt es nur wenige – der dickere Satz (weltweit offene Ports, deaktivierte Verschlüsselung) betrifft die Provider der Hyperscaler. Ein billiges Gate ist es trotzdem, in demselben Geist, den wir im Text über den Open-Source-Sicherheitsstack beschrieben haben.

Fallstricke, die Zeit kosten

Aus der Praxis gesammelt, in der Reihenfolge ihrer Häufigkeit:

  1. Provider-Version. Funktionen kommen mit Releases, und die erscheinen selten. Bevor Sie zu dem Schluss kommen, „CloudStack kann das nicht“, prüfen Sie, ob es nicht Ihre Provider-Version ist, die es nicht kann – die Cloud-API beherrscht die jeweilige Funktion meist schon lange.
  2. Fest verdrahtete UUIDs. Die Netzwerk-ID aus dem Panel zu kopieren und in den Code einzufügen funktioniert – bis zum ersten Neuaufbau der Umgebung. Alles, was sich per data source finden lässt, finden Sie per data source; den Rest halten Sie in Variablen, nicht im Rumpf des Moduls. Das ist im Übrigen eine klassische Altlast: wir selbst haben genau eine solche Stelle im Code und wissen, dass sie uns irgendwann beißen wird.
  3. Asynchronität der API. depends_on garantiert die Reihenfolge der Ressourcenerstellung, aber nicht die Betriebsbereitschaft des Dienstes. Wo Sie auf die Realität warten (Kubernetes-API, ein Dienst in der Maschine, DNS-Propagierung), setzen Sie eine Prüfung mit Wiederholung ein – wie das data "http" mit dem retry-Block weiter oben.
  4. ACL als eine einzige Ressource. Alle Regeln einer Liste leben in einer einzigen Terraform-Ressource. Eine manuelle Änderung im Panel wird beim nächsten apply zurückgenommen.
  5. Offerings vom Typ „customized“ erfordern das Feld details mit cpuNumber, cpuSpeed und memory. Ohne diese Angaben weist CloudStack die Anfrage zurück, und die Fehlermeldung nennt den Grund nicht immer direkt.
  6. expunge = true macht destroy unumkehrbar – die Maschine lässt sich aus dem Zustand „Destroyed“ nicht mehr zurückholen. Sicher dort, wo die Maschine austauschbar ist (etwa ein Cluster-Knoten), riskant überall dort, wo sie Daten hält.
  7. Der Load Balancer arbeitet auf der vierten Schicht und wohnt im Virtual Router. Planen Sie darauf weder pfadbasiertes Routing noch großen HTTP-Verkehr – dafür ist der Ingress im Cluster oder eine separate Maschine mit Reverse Proxy da.
  8. Secrets im State – und im Plan. API-Schlüssel, Passwörter und Ceph-Schlüssel landen im Klartext im tfstate und zusammen mit ihm im gespeicherten tfplan sowie in tfplan.json. Sichern Sie den Bucket mit den State-Dateien wie einen Passwortspeicher, und behandeln Sie das Plan-Artefakt in der CI genauso: eingeschränkter Zugriff und kurze Aufbewahrungsdauer.
  9. Parallele Pipelines. Ohne State-Sperre können zwei gleichzeitige apply die Umgebung auseinanderlaufen lassen. use_lockfile = true kostet eine einzige Zeile.

CloudStack und Terraform in der WebDisk Cloud

Unsere Public Cloud läuft auf Apache CloudStack mit dem Hypervisor KVM und Storage auf einem Ceph-Cluster – der Primary Storage besteht aus RBD-Pools (getrennt für NVMe, getrennt für Kapazität), und der Object Storage ist RGW mit S3-kompatibler API. Alles, was wir oben beschrieben haben, betrifft also genau die Plattform, auf der Sie Ihre eigene Umgebung betreiben können.

Was das in der Praxis bedeutet:

  • API-Schlüssel erzeugen Sie selbst im Panel – Sie müssen sie nicht bei uns bestellen. Mit demselben Schlüssel bedienen Sie Terraform, CloudMonkey und eigene Skripte.
  • Den Bucket für den Terraform-State können Sie bei uns direkt neben den Maschinen halten – mit nativer S3-Sperre, ohne irgendeinen zusätzlichen Dienst von außen.
  • Es gibt hier keine Zwischenschicht. Sie sprechen mit derselben CloudStack-API, die auch das Panel verwendet – und da es sich um ein Projekt der Apache Software Foundation handelt, ist Ihr Code nicht an einen einzigen Anbieter gebunden. Das ist ein konkreter, messbarer Weg, die Abhängigkeit vom Anbieter zu verringern, über die wir an anderer Stelle ausführlicher geschrieben haben.
  • Der Umfang der Selbstbedienung ist größer, als es scheint: VPC und Tiers, ACL-Listen, öffentliche Adressen, Portweiterleitungen, LB-Regeln, Templates, Snapshots, Volumes – all das sind Terraform-Ressourcen.

Und wenn Sie einen fertigen Cluster mit Betreuung statt eigenem Terraform suchen – das oben beschriebene Projekt ist genau das, was wir im Rahmen des technischen Kubernetes-Supports tun; warum das Aufsetzen eines Clusters die einfachste Etappe ist, haben wir in einem separaten Artikel beschrieben.

Häufige Fragen

Worin unterscheidet sich Apache CloudStack von OpenStack? Vor allem in der Komplexität und im Deployment-Modell. OpenStack ist eine Sammlung von einigen Dutzend zusammenarbeitenden Projekten mit eigenen APIs und eigenen Lebenszyklen – das bietet maximale Flexibilität, erfordert aber ein Team, das die Plattform selbst betreibt. CloudStack ist eine einzige Java-Anwendung mit einer einzigen MySQL-Datenbank, die das gesamte Cloud-Modell umfasst: Installation und Aktualisierung sind deutlich einfacher, um den Preis geringerer Modularität. Beide sind Open-Source-Projekte unter der Apache-2.0-Lizenz, beide haben einen Terraform-Provider.

Gibt es für CloudStack einen offiziellen Terraform-Provider? Ja – cloudstack/cloudstack, entwickelt im Apache-Repository. Man muss allerdings sein Tempo kennen: Das letzte stabile Release in der Registry ist 0.6.0 vom Oktober 2025, und der Zweig 0.7.0 ist dort vorerst nur als Release Candidate vorhanden (0.7.0-rc1, August 2026). Praktische Schlussfolgerung: Pinnen Sie die genaue Version, committen Sie .terraform.lock.hcl und werfen Sie einen Blick in das CHANGELOG – eine Funktion, die vor einem Jahr fehlte, kann inzwischen in ein stabiles Release eingegangen sein.

Woher bekomme ich API-Schlüssel für CloudStack? Aus dem Panel: Accounts → Konto → Users → Benutzer → Generate Keys. Das Secret wird nur bei der Erzeugung vollständig angezeigt. Legen Sie für die Automatisierung einen separaten technischen Benutzer mit den geringstmöglichen Rechten an – er lässt sich leichter rotieren und im Ereignisprotokoll nachverfolgen als Schlüssel, die einer Person zugeordnet sind.

Wo halte ich die Terraform-State-Datei, wenn ich kein AWS habe? In jedem S3-kompatiblen Storage – Ceph RGW, MinIO oder dem Object Storage eines Anbieters. Das s3-Backend unterstützt einen eigenen Endpunkt und kann seit Terraform 1.10 den State nativ sperren, mit bedingten Schreibvorgängen in S3 (use_lockfile = true), ohne DynamoDB-Tabelle. Denken Sie daran, dass der State Secrets im Klartext enthält – sichern Sie den Bucket entsprechend.

Kann ich statt Terraform OpenTofu verwenden? Ja. OpenTofu ist ein Fork von Terraform unter der Linux Foundation, entstanden nach dem Wechsel der Terraform-Lizenz zu BUSL (Business Source License) – die Quellen bleiben offen, formal ist das aber keine Open-Source-Lizenz mehr. Die CloudStack-Provider sind gewöhnliche Plugins und funktionieren in beiden Werkzeugen; die Konfiguration bleibt dieselbe, es ändert sich der Befehl (tofu statt terraform). Wenn die BUSL-Lizenz für Sie ein Problem ist, ist dieser Befehlswechsel üblicherweise schon die ganze Migration.

Wenn CloudStack einen eingebauten Kubernetes-Dienst (CKS) hat – wozu dann einen Cluster mit eigenem Code aufsetzen? Dafür gibt es keinen Grund, wenn der eingebaute Dienst Ihren Anwendungsfall abdeckt – der offizielle Provider hat dafür die Ressource cloudstack_kubernetes_cluster bereits im stabilen Release 0.6.0, und das ist der kürzeste Weg zu einem laufenden Cluster. Einen eigenen Cluster (bei uns auf Talos Linux) wählen Sie dann, wenn Sie Kontrolle über Version und Konfiguration der Knoten brauchen, einen eigenen Satz an CNI- und Storage-Treibern oder dasselbe Muster auf mehreren Plattformen zugleich. Der Preis dafür sind mehr Code und die volle Verantwortung für eine Schicht, die im eingebauten Dienst der Cloud-Betreiber pflegt.

Warum hängt mein Service vom Typ LoadBalancer in Kubernetes auf CloudStack im Zustand Pending? Weil im Cluster die Komponente fehlt, die Adresse und Regel in der Cloud anfordert. Dafür ist cloudstack-kubernetes-provider zuständig, also der Cloud Controller Manager für CloudStack – ohne ihn hat Kubernetes keine Möglichkeit, eine öffentliche IP zu erbitten. Analog dazu ist für Volumes cloudstack-csi zuständig.

Ersetzt der Load Balancer von CloudStack einen Application Load Balancer? Nein, und man sollte ihn auch nicht so betrachten. Der LB in CloudStack läuft im Virtual Router, ist ein Load Balancer der vierten Schicht mit Sticky Sessions und – seit Release 4.22 – mit SSL-Terminierung direkt auf dem Router, und sein Durchsatz wird durch den einzelnen Router begrenzt. Wenn Ihre Cloud auf einem älteren Release läuft, gibt es dort SSL-Offloading auf dem Router schlicht nicht. Für Routing nach Pfaden, Headern oder Hostnamen verwenden Sie den Ingress im Kubernetes-Cluster oder einen eigenen Reverse Proxy – CloudStack liefert ihnen dann nur noch die öffentliche Adresse.

Kommt Terraform mit einer Cloud zurecht, die bereits existiert? Ja, über den Import bestehender Ressourcen in den State (terraform import oder import-Blöcke), aber bleiben Sie realistisch: Bei einer größeren Umgebung ist das eine Arbeit von Tagen, nicht von Stunden, und sie erfordert geduldiges Anpassen des Codes an die Realität, bis plan keine Änderungen mehr vorschlägt. Häufiger lohnt es sich, das Neue in Code zu fassen und das Alte bei Gelegenheit eines Umbaus zu migrieren.

Zusammenfassung

Apache CloudStack hat eine Eigenschaft, die gut mit Infrastructure as Code zusammenspielt: Es ist um eine einzige, konsistente API und ein einziges, gut lesbares Objektmodell herum gebaut. Zone, Offering, Netzwerk, VPC, ACL, Template, Projekt – das sind Begriffe, die das Panel ebenso gut beschreiben wie die .tf-Dateien. Hat man sie einmal verstanden, ist Terraform keine Übersetzung zwischen zwei Welten mehr, sondern einfach eine zweite Art, dasselbe zu sagen.

Drei Dinge lohnt es sich, aus diesem Text mitzunehmen. Erstens: Der Einstieg ist billig – drei Werte in der Provider-Konfiguration, ein S3-Backend, das Sie wahrscheinlich schon haben, und die erste Maschine in gut einem Dutzend Zeilen. Zweitens: Skalierung zahlt sich aus – ein kompletter Kubernetes-Cluster mit Pod-Netzwerk, Storage und GitOps lässt sich in Code beschreiben und mit einem einzigen Befehl wiederherstellen, und der Unterschied zwischen dev und prod schrumpft auf eine Datei mit Variablen. Drittens: Kennen Sie die Grenzen – seltene Provider-Releases, vom Betreiber definierte Offerings, eine asynchrone API und ein Load Balancer der vierten Schicht sind keine Mängel, sondern Eigenschaften der Plattform, die man beim Entwurf der Umgebung berücksichtigen muss.

Wenn Sie bei null anfangen, ist folgende Reihenfolge gut: Erzeugen Sie API-Schlüssel, beschreiben Sie eine Maschine und ein Netzwerk, verlagern Sie den State nach S3, ergänzen Sie eine Pipeline mit manuellem Gate vor apply, und greifen Sie erst danach zu Modulen und Umgebungen. Jeder dieser Schritte ergibt für sich allein Sinn – und keiner verlangt, das umzuschreiben, was bereits funktioniert.

Sie fragen sich, ob sich Ihre Umgebung in Code beschreiben lässt und wo Sie anfangen sollen? Schreiben Sie uns – das WebDisk-Team geht gerne mit Ihnen das erste Modul durch.