Das Wissensportal für IT-Professionals. Entdecke die Tiefe und Breite unseres IT-Contents in exklusiven Themenchannels und Magazinmarken.

SIGS DATACOM GmbH

Lindlaustraße 2c, 53842 Troisdorf

Tel: +49 (0)2241/2341-100

kundenservice@sigs-datacom.de

Next Generation: Infrastructure as Code mit Pulumi und Java

Infrastructure as Code (IaC) ist ein wesentlicher Bestandteil der modernen Cloud-nativen Anwendungsentwicklung. Während Terraform eines der populärsten Werkzeuge in diesem Bereich ist, gibt es mittlerweile Alternativen, die eine nahtlose Integration für Entwickler bieten. In diesem Artikel werfen wir einen Blick auf Pulumi als eine neue Generation von IaC-Werkzeugen, die es Entwicklern ermöglicht, mit einer echten Programmiersprache zu arbeiten.

Author Image
Mario-Leander Reimer

Managing Director und CTO


  • 22.11.2024
  • Lesezeit: 15 Minuten
  • 298 Views

Mit der zunehmenden Popularität der Cloud-nativen Anwendungsentwicklung mit Frameworks wie Spring Boot oder Quarkus hat auch die Verwaltung der Cloud-Infrastruktur an Bedeutung gewonnen. Infrastructure as Code (IaC) hat sich hier als wichtiger Ansatz etabliert. Die Vorteile liegen auf der Hand: Alle Änderungen sind versioniert und nachvollziehbar, Deployment-Prozesse können automatisiert werden. Dies reduziert menschliche Fehler und erhöht die Effizienz im Umgang mit komplexen Infrastruktur-Setups.

Leider steigt damit auch die kognitive Belastung der Entwickler, da sie nicht mehr nur Anwendungen entwickeln, sondern auch die zugrunde liegende Infrastruktur programmatisch aufbauen müssen. Werkzeuge wie Terraform haben sich hier als weit verbreitete Lösung etabliert. Terraform verwendet HCL, eine deklarative Sprache, die jedoch nicht immer intuitiv ist und eine zusätzliche Lernkurve für Entwickler darstellt.

Hinzu kommt, dass das Testen von Terraform-Konfigurationen komplex ist und oft nur durch aufwendige integrative Tests zuverlässig durchgeführt werden kann. Darüber hinaus werden weitere Werkzeuge für die Entwicklung, Qualitätssicherung und Automatisierung benötigt, was die Werkzeugkette zusätzlich verkompliziert. Mit der jüngsten Fragmentierung des Terraform-Ökosystems durch das OpenTofu-Projekt ist die Situation noch undurchsichtiger geworden.

Warum Pulumi?

Pulumi ist ein modernes Framework für IaC, das viele der Herausforderungen herkömmlicher Tools wie Terraform adressiert. Der wesentliche Unterschied: Mit Pulumi können Entwickler ihre Cloud-Infrastruktur in etablierten Programmiersprachen wie TypeScript, Python, C# oder Go definieren. Pulumi ist kein wirklich neues Framework, es wurde bereits 2018 veröffentlicht. Es steht unter der Open-Source-Lizenz Apache v2, was es für den Einsatz in kommerziellen Projekten attraktiv macht.

Sein Hauptzweck ist es, Cloud-native Infrastruktur für verschiedene Anbieter wie AWS, Google Cloud und Microsoft Azure über eine einheitliche API zu verwalten. Darüber hinaus unterstützt Pulumi nicht nur Public-Cloud-Anbieter, sondern auch andere Technologien und SaaS-Dienste aus dem Cloud-nativen Ökosystem wie Docker, Kubernetes, GitHub und viele weitere. In der Pulumi Package Registry sind derzeit 182 weitere sogenannte Provider registriert, von denen jedoch nicht alle bereits ein passendes Java SDK anbieten.

Der imperative Programmieransatz ermöglicht die Verwendung komplexer logischer Bedingungen, Schleifen und gängiger Abstraktionen. Der Infrastruktur-Code kann besser strukturiert werden, was die Wiederverwendbarkeit und Wartbarkeit fördert. Auch die dynamische Generierung von Infrastruktur, auf Basis einer Datenbank oder CMDB, ist denkbar und möglich.

Diese Flexibilität reduziert die kognitive Last, da Entwickler nun in ihrer gewohnten Umgebung und Sprache arbeiten können, anstatt sich in domänenspezifische Sprachen wie HCL einarbeiten zu müssen. Auch die verwendeten Werkzeuge, wie IDEs, Linter, Testframeworks und CI/CD-Tools, sowie etablierte Best Practices bleiben die gleichen wie bei der Softwareentwicklung, was den Lernaufwand und den Werkzeug-Overhead signifikant minimiert. Das Infrastrukturmanagement wird so zu einem natürlichen Bestandteil des Entwicklungsprozesses – statt einer separaten Disziplin, die zusätzliche Tools und Fähigkeiten erfordert.

Pulumi trifft Java

Obwohl Pulumi bereits seit einigen Jahren mit Sprachen wie Type-Script und Python gut etabliert ist, ist der Java-Support eine noch relativ neue Entwicklung und befindet sich derzeit im Public Preview. Dieser Schritt ist besonders für Java-Entwickler von Interesse, da sie nun Cloud-native Infrastruktur direkt in ihrer vertrauten Sprache definieren können. Pulumi unterstützt dabei Java ab Version 11 und lässt sich problemlos in die Build-Tools Maven und Gradle integrieren.

Um mit Pulumi und Java zu starten, ist die Installation der Pulumi-CLI der erste Schritt. Die Schnittstelle über die Kommandozeile ist plattformunabhängig und kann auf allen gängigen Betriebssystemen über die Befehle aus Listing 1 sehr einfach installiert werden.

# Für MacOS (Homebrew)
brew install pulumi

# Für Linux
curl -fsSL https://get.pulumi.com | sh

# Für Windows (PowerShell)
iex ((New-Object System.Net.WebClient).DownloadString(
 'https://get.pulumi.com/install.ps1'))
Listing 1: Installation der Pulumi-CLI

Sobald die CLI installiert ist, kann ein neues Pulumi-Projekt in Java erstellt und initialisiert werden. Der Befehl pulumi new javaerstellt ein Grundgerüst für ein Pulumi-Projekt in Java, inklusive der notwendigen Maven-Konfigurationsdateien. Die Verzeichnisstruktur sieht anschließend wie folgt aus:

├── Pulumi.yaml
├── src
│      └── main
│              └── java
│                      └── myproject
│                            └── App.java
└── pom.xml (für Maven)

Der Befehl pulumi new java-gradle erzeugt alternativ ein Grundgerüst mit Gradle als Build-Tool. Beide Java-Templates sind jedoch Cloud-agnostisch und es müssen zusätzliche Java-Module für den jeweiligen Cloud-Anbieter der Wahl als weitere Dependencies installiert werden. Aber auch hierfür stehen bereits zusätzliche Templates zur Verfügung, die diese Aufgabe übernehmen:

  • pulumi new aws-java: erstellt ein AWS Java-Starterprojekt.
  • pulumi new azure-java: erstellt ein Azure Java-Starterprojekt.
  • pulumi new gcp-java: erstellt ein GCP-Java-Starterprojekt.

In der generierten Datei App.java können nun die benötigten Cloud-Ressourcen definiert und angelegt werden. Listing 2 zeigt ein einfaches Beispiel für die Bereitstellung eines Google Cloud Storage Bucket zusammen mit einer README-Datei zur Dokumentation in der Pulumi Cloud.

import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.gcp.storage.Bucket;
import com.pulumi.gcp.storage.BucketArgs;

public class App {
 public static void main(String[] args) {
  Pulumi.run(ctx -> {
  // Create a GCP resource (Storage Bucket)
  var bucket = new Bucket(
   "java-pulumi-bucket", BucketArgs.builder()
   .location("EU")
   .build());
  ctx.export("bucketName", bucket.url());

 try {
  // Read the README file and export it as an output
  var readme = Files.readString(Paths.get(
   "./Pulumi.README.md"));
  ctx.export("readme", Output.of(readme));
 } catch (IOException e) {
   throw new RuntimeException(e);
 }
});
 }
}
Listing 2: Beispiel für die Bereitstellung eines Google Cloud Storage Bucket

Um die Infrastruktur anschließend bereitzustellen, wird wiederum die Pulumi CLI verwendet. Mit dem Befehl pulumi up werden die im Stack definierten Ressourcen erzeugt beziehungsweise aktualisiert, falls diese bereits vorhanden sind. Mit dem Befehl pulumi preview können die aktuellen Änderungen angezeigt werden, ohne sie zu übernehmen. Der Befehl pulumi stack ermittelt den aktuellen Status und zeigt diesen an. Um die definierten Infrastruktur-Ressourcen zu löschen, wird der Befehl pulumi destroyverwendet.

Pulumi AI: Der Copilot für Infrastrukturentwicklung

Pulumi bietet mit Pulumi AI eine innovative Unterstützung für die schnelle Erstellung und Anpassung von Pulumi-Stacks. Das Tool fungiert als eine Art „Copilot“, der automatisch Infrastruktur-Code in der gewünschten Programmiersprache generiert – sei es Java, TypeScript, Python oder andere. Entwickler können Pulumi AI direkt über die Pulumi CLI nutzen, um den Code für Infrastrukturkomponenten über natürlichsprachliche Befehle zu definieren. Um Pulumi AI mit Java zu nutzen, ist ein einfacher CLI-Befehl ausreichend. Beispielsweise könnte folgende Anfrage an Pulumi AI gestellt werden:

pulumi ai web --language=java
"Create a Kubernetes cluster in GCP with 3 nodes"

Pulumi AI generiert dann einen Vorschlag, der sich an den Vorgaben orientiert und diesen in der aktuell gewählten Sprache – in diesem Fall Java – erstellt. Der Code kann dann aus dem Browser in die IDE übernommen und in das Projekt integriert werden. Auch ist es möglich, über die CLI ein komplett neues Projekt mithilfe von KI zu generieren.

Diese Funktionalität scheint sich jedoch noch in einem frühen Entwicklungsstadium zu befinden. Der generierte Code ist derzeit fast nie korrekt und erfordert eine umfangreiche manuelle Nachbearbeitung. Disclaimer: Das gleiche gilt für andere populäre Copiloten wie Google Gemini oder GitHub Copilot.

Insgesamt ist Pulumi AI aber sicherlich ein spannendes Werkzeug, das mit jeder Weiterentwicklung präziser werden wird und das Potenzial hat, die Produktivität der Entwickler zu steigern.

Schritt für Schritt: Aufbau einer MicroserviceInfrastruktur in der Google Cloud

In diesem Abschnitt wird nun exemplarisch eine komplette Infrastruktur für eine Microservice-Applikation in der Google Cloud Platform (GCP) aufgebaut. Für den Betrieb der Applikation werden eine Artifact Registry zur Ablage des Docker-Images, ein Kubernetes-Cluster zur Ausführung und eine PostgreSQL-Datenbank zur Datenhaltung benötigt. Schließlich wird der Microservice auf Kubernetes deployt. Der gesamte Aufbau erfolgt mit Pulumi und Java. Das komplette Projekt steht auf GitHub zur Verfügung.

1. Initialisieren des Pulumi-Projekts
Zu Beginn wird ein neues Projekt initialisiert:pulumi new gcp-java -n microservice-infrastruktur. Damit wird ein neues GCP-Pulumi-Projekt erstellt und die benötigte Verzeichnisstruktur angelegt.

2. Erstellen einer Artifact Registry für das Docker-Image
Als Nächstes wird eine GCP Artifact Registry für die Ablage der Docker-Images des Microservice eingerichtet. Listing 3 erstellt eine Artifact Registry in GCP in der Region europe-west1 (Belgien), die als Docker-Repository verwendet wird. Die Id der Registry wird exportiert, um später die URL als Referenz für das Docker-Image zu verwenden.

// Create a GCP Artifact Registry repository for Docker images
var repository = new Repository("microservice-repo",
 RepositoryArgs.builder()
 .repositoryId("microservice-repo")
 // Updated location to Europe (Belgium)
 .location("europe-west1") 
 // Docker image repository
 .format("Docker") 
 .description("Docker repository for microservice")
 .dockerConfig(RepositoryDockerConfigArgs.builder()
    // Immutable tags enabled
    .immutableTags(true) 
    .build())
 .build());

// Export the repository ID
ctx.export("repositoryUrl", repository.id());
Listing 3: Erstellen einer Artifact Registry

Theoretisch kann das Docker-Image auch mithilfe von Pulumi gebaut und sofort gepusht werden. Hierfür kommt das Docker Build Pulumi Package zum Einsatz. Um das Java-API von diesem Package verwenden zu können, muss die Dependency aus Listing 4 in der pom.xml hinzugefügt werden.

<dependency>
 <groupId>com.pulumi</groupId>
 <artifactId>dockerbuild</artifactId>
 <version>(,1.0.0]</version>
</dependency>
Listing 4: Neue Dependency in der pom.xml

Allerdings scheint sich das Package noch in einem sehr frühen Stadium zu befinden, was sich definitiv auch in der Versionsnummer 0.0.1-alpha.2 widerspiegelt. Die Dokumentation ist spärlich und auch das Java-Beispiel auf der Package-Seite funktioniert so nicht. Lediglich die Beispiele im GitHub-Repository des Pakets geben Hinweise auf mögliche Anwendungsfälle. In der Realität würde dieser Schritt wahrscheinlich bereits in einem Schritt der CI-Pipeline erfolgen, zum Beispiel über die Docker Build-Push Git-Hub Action.

3. Kubernetes-Cluster in der GCP erstellen
In diesem Schritt wird ein Kubernetes-Cluster in der Google Cloud bereitgestellt. Kubernetes dient als Plattform zur Verwaltung und Skalierung von Container-Anwendungen, was insbesondere für Microservice-Architekturen von Vorteil ist. In Pulumi kann der Cluster über die Java-API des GCP Packages mit wenigen Zeilen Code aufgesetzt werden.

In dem Codebeispiel in Listing 5 wird ein Google Kubernetes Autopilot Cluster in der Region europe-west1 in der neuesten Version (derzeit 1.31) erstellt und gestartet.

//create a GKE autopilot cluster
var cluster = new Cluster(
   "pulumi-java-auto-cluster", ClusterArgs.builder()
 .deletionProtection(false)
 .location("europe-west1")
 .enableAutopilot(true)
 // we could also use semantic versioning here, like 1.30
 .nodeVersion("latest")
 .minMasterVersion("latest")
 .build());
Listing 5: Kubernetes-Cluster in der GCP erstellen

Autopilot ist eine Managed Kubernetes Option, die in der Google Cloud angeboten wird. Im Gegensatz zum traditionellen Google Kubernetes Engine (GKE) Cluster, bei dem der Benutzer für die Konfiguration, Skalierung und Wartung der Knoten (Nodes) verantwortlich ist, übernimmt Autopilot diese Aufgaben automatisch. Autopilot optimiert die Verwaltung der Infrastruktur auf Basis der betriebenen Workloads und bietet eine „Serverless“-ähnliche Erfahrung für Kubernetes.

Natürlich bietet die Java-API auch die Möglichkeit, einen selbstverwalteten GKE-Cluster bereitzustellen. Dies erfordert jedoch deutlich mehr Code, um zum Beispiel die Cluster-Node-Pools mit ihren Konfigurationen und Autoscaling-Verhalten zu definieren oder weitere Add-ons wie HPA oder Load Balancing zu konfigurieren. Im GitHub-Repository zu diesem Artikel findet sich ein ausführliches Beispiel für einen Managed GKE Cluster.

Allerdings ist hier Vorsicht geboten:

Nicht alle Konfigurationen, die über die API definiert werden können, können später auch tatsächlich erstellt werden! Aber erst bei der Ausführung von pulumi up erhält der Entwickler dann eine entsprechende Fehlermeldung. Diese ist manchmal nicht ganz einfach zu interpretieren, da sie Details über das zugrunde liegende Google Cloud Go SDK enthält. Sowohl ein Blick in die Dokumentation des Go SDK als auch die Pulumi-Beispiele der anderen Sprachen sind hier zu empfehlen.

4. PostgreSQL-Datenbank in der GCP erstellen
Als Nächstes wird die SQL-Datenbank erstellt, welche später vom Microservice benutzt werden soll. Auch hier stellt das Pulumi GCP Package ein entsprechendes Java-API bereit.

Der Code aus Listing 6 erzeugt eine PostgreSQL-Instanz der Version 14 in der GCP mit einer db-f1-micro-Konfiguration.

var postgresInstance = new DatabaseInstance(
 "pulumi-java-postgres-db",
 DatabaseInstanceArgs.builder()
  .region(region)
  .databaseVersion("POSTGRES_14")
  .settings(DatabaseInstanceSettingsArgs.builder()
   .tier("db-f1-micro")
   .build())
 .build());
Listing 6: SQL-Datenbank erstellt

Auch hier ist wieder etwas GCP-Know-how beziehungsweise das Studium der JavaDocs nötig: Postgres wird nur auf Shared-Core-Maschinentypen oder benutzerdefinierten Typen unterstützt. Eine falsche Konfiguration führt auch hier zu einem Fehler beim Deployment.

5. Deployment des Microservice auf Kubernetes
Nachdem der Kubernetes-Cluster, die PostgreSQL-Datenbank und die Artifact Registry bereitgestellt worden sind, wird in diesem Schritt nun der Microservice auf Kubernetes bereitgestellt. Dazu werden ein dedizierter Namespace, ein Deployment und eine Service Resource Definition benötigt. Das Kubernetes Pulumi Package [1] stellt APIs und Abstraktionen für alle gängigen Kubernetes-Ressourcen bereit. Um das Package nutzen zu können, muss die Dependency aus Listing 7 in der pom.xml hinzugefügt werden.

<dependency>
 <groupId>com.pulumi</groupId>
 <artifactId>kubernetes</artifactId>
 <version>(,4.18.2]</version>
</dependency>
Listing 7: Weitere Dependency in der pom.xml

Um mit einem Kubernetes-Cluster über das Java-API interagieren zu können, muss zunächst eine Provider-Instanz mit der Kubeconfig-Definition des Clusters erzeugt werden. Anschließend können die jeweiligen Kubernetes Custom Resources, wie ein Namespace, wie gewohnt über das Fluent API definiert werden (s. Listing 8).

// initialize Kubernetes provider with kubeconfig
var provider = new Provider("gke-provider", ProviderArgs.builder()
 .kubeconfig(kubeconfig)
 .build());

// create the microservice Kubernetes namespace
var appLabels = Map.of("app", "microservice");
var namespace = new Namespace("microservice", NamespaceArgs.builder()
 .metadata(ObjectMetaArgs.builder()
   .name("microservice")
  .labels(appLabels)
  .build())
 .build(),
 CustomResourceOptions.builder().provider(provider).build());
Listing 8: Deployment des Microservice auf Kubernetes

Soweit so gut und einfach? Eher nicht. Denn auf den zweiten Blick zeigt der imperative Programmieransatz an diesem Beispiel auch seine Schwächen. Die Fluent API mit ihren Klassen und Abstraktionen ist lediglich ein 1:1-Abbild der Kubernetes-Ressourcen und deren Strukturdefinitionen. YAML als deklarative Syntax ist hier klar im Vorteil, der entsprechende Java-Code wirkt dagegen extrem schwerfällig und aufgebläht.

Auch hier bietet das Pulumi Kubernetes SDK eine Lösung: YAML-Manifest-Definitionen können über die Klasse ConfigFileeingelesen und angewendet werden. Alternativ wird auch Kustomize unterstützt, um eine einmal erstellte Basis-YAML-Datei mehrfach zu verwenden und in verschiedenen Umgebungen (z. B. DEV, QA, PROD) anzupassen. Anstatt für jede Umgebung separate YAML-Dateien zu erstellen und zu pflegen, können mit Kustomize sogenannte „Overlays“ definiert werden, die spezifische Konfigurationen für verschiedene Umgebungen hinzufügen oder überschreiben.

Der Code aus Listing 9 kombiniert die Stärken beider Ansätze und erscheint deutlich schlanker.

// create the Kubernetes resources from a manifest file
var manifest = new ConfigFile("manifest", ConfigFileArgs.builder()
 .file("./src/main/k8s/microservice.yaml")
 .build(),
ComponentResourceOptions.builder().provider(provider).build());

// create the Kubernetes resources from a Kustomize directory
var kustomize = new Directory("kustomize", DirectoryArgs.builder()
 .directory("./src/main/k8s")
 .build(),
ComponentResourceOptions.builder().provider(provider).build());
Listing 9: YAML und „Overlays“

In der Praxis wird dieser Schritt jedoch eher über das YAML-Manifest und ein GitOps-Tool der Wahl wie Flux oder ArgoCD erfolgen.

Unittests für Pulumi Stacks

Testgetriebene Entwicklung hat sich in der Softwareentwicklung als bewährte Praxis etabliert, die sich auch auf die Infrastrukturentwicklung mit Pulumi übertragen lässt. Durch das Schreiben von Tests können Fehler frühzeitig entdeckt und ungewollte Nebeneffekte vermieden werden.

Pulumi bietet native Unterstützung für Unittests, um die Struktur und Konfiguration von Stacks zu überprüfen. Beispielsweise kann überprüft werden, ob ein Kubernetes-Cluster die richtige Anzahl an Knoten hat oder ob ein Google Cloud Storage Bucket in der richtigen Region angelegt wurde.

In dem Beispiel aus Listing 10 wird ein einfacher Unittest durchgeführt, der die Existenz und die grundlegende Konfiguration eines Google Storage Bucket überprüft.

var postgresInstance = new DatabaseInstance(
 "pulumi-java-postgres-db",
 DatabaseInstanceArgs.builder()
  .region(region)
  .databaseVersion("POSTGRES_14")
  .settings(DatabaseInstanceSettingsArgs.builder()
   .tier("db-f1-micro")
   .build())
 .build());
Listing 10: Einfacher Unittest

Das Pulumi Test Framework verwendet sogenannte Mocks, um die tatsächliche Bereitstellung der Ressourcen zu simulieren und Tests schneller durchzuführen, ohne die Ressourcen in der Cloud zu erstellen.

Fazit

Pulumi eröffnet spannende Möglichkeiten für die Infrastrukturentwicklung und bringt das Konzept „Infrastructure as Code“ nahtlos in die Java-Welt. Besonders der imperative Ansatz macht die Arbeit mit Pulumi angenehm – Infrastruktur kann direkt in einer vertrauten Sprache definiert werden und ermöglicht so dynamische Konfigurationen, die mit rein deklarativen Werkzeugen nur schwer umsetzbar wären. Allerdings ist das Java SDK noch jung, speziell was den Support in Pulumi Packages betrifft. Einige Funktionen sind noch nicht vollständig ausgereift beziehungsweise dokumentiert und gelegentlich stößt man auf kleinere Herausforderungen, die sich mit zunehmender Reife des SDK wohl verbessern sollten.

Pulumi AI stellt eine interessante Erweiterung dar, die Entwicklern als „Copilot“ für eine schnelle Codegenerierung dient. Auch wenn die generierten Ergebnisse in Java derzeit noch manuell nachbearbeitet werden müssen, bleibt Pulumi AI eine vielversprechende Technologie, die das Potenzial hat, Arbeitsabläufe deutlich zu beschleunigen.

Trotz aller Vorteile imperativer Infrastruktur-Definitionen hat der deklarative Ansatz nach wie vor seine Berechtigung: In vielen Fällen bleibt er unübertroffen in Bezug auf Lesbarkeit und Einfachheit, was bei großen oder stark standardisierten Infrastrukturen ein entscheidender Faktor sein kann. Pulumi kombiniert die besten Aspekte beider Welten und bietet eine flexible, moderne Alternative zu klassischen Infrastruktur-Werkzeugen wie Terraform.


Mario-Leander Reimer ist Sprecher auf der OOP 2025: Turbocharging AI Innovation: How AI Platforms Enable The Bulletproof Deployment of GenAI Use Cases am 4.2.2025, von 10:45 – 12:15 Uhr.

-> zur Anmeldung


Online-Ressource

Kubernetes Package, www.pulumi.com/registry/packages/kubernetes

. . .

Author Image

Mario-Leander Reimer

Managing Director und CTO
Zu Inhalten

Mario-Leander Reimer ist passionierter Softwareentwickler und -architekt, stolzer Vater und #CloudNativeNerd. Er ist Managing Director und CTO bei der QAware GmbH und beschäftigt sich intensiv mit den Innovationen und Technologien rund um den Cloud Native Stack und deren Einsatzmöglichkeiten im Unternehmensumfeld. Außerdem unterrichtet er Softwarequalitätssicherung an der TH Rosenheim.


Artikel teilen