TL;DR — Kurzzusammenfassung
Pulumi ermöglicht Cloud-Infrastruktur mit TypeScript, Python, Go, C# und Java. Vollständiger Leitfaden zu Stacks, State, Tests und CI/CD-Integration.
Pulumi ist eine Infrastructure-as-Code-Plattform, mit der du Cloud-Ressourcen in echten Programmiersprachen — TypeScript, Python, Go, C#, Java — definieren, bereitstellen und verwalten kannst, anstatt domänenspezifische Sprachen wie HCL oder YAML zu verwenden. Wer jemals an die Grenzen von Terraform-Schleifen gestoßen ist, mit der JSON-Ausführlichkeit von CloudFormation gekämpft hat oder sich gewünscht hat, Infrastruktur mit demselben Test-Framework wie Anwendungscode zu testen, dem löst Pulumi genau diese Probleme. Dieser Leitfaden behandelt alles von der Installation und den Kernkonzepten bis hin zu Tests, State-Management, CI/CD-Integration und einem praktischen Multi-Cloud-Beispiel.
Voraussetzungen
- Kenntnisse in mindestens einer dieser Sprachen: TypeScript/JavaScript, Python, Go, C# oder Java
- Ein Account bei AWS, Azure oder GCP (Free Tier reicht für die Beispiele)
- Node.js 18+ installiert (für TypeScript/JavaScript-Beispiele)
- Grundlegendes Verständnis von Cloud-Konzepten (VPCs, Storage-Buckets, VMs)
- Pulumi Cloud-Account (Free Tier) oder ein alternatives State-Backend
Warum Pulumi: Echte Sprachen vs. HCL und YAML
Der grundlegende Unterschied zwischen Pulumi und Tools wie Terraform oder CloudFormation ist, dass Pulumi-Programme in universellen Programmiersprachen geschrieben werden. Das ermöglicht Fähigkeiten, die bei DSL-basierten Tools schwierig oder unmöglich sind.
Vollständige IDE-Unterstützung — Dein Editor bietet Autovervollständigung für jede Ressourcen-Eigenschaft, Inline-Dokumentation und Typfehler, bevor du eine Bereitstellung startest. Ein Tippfehler im Namen einer S3-Bucket-Eigenschaft ist ein Kompilierzeitfehler, keine Laufzeitausnahme nach einem 30-Sekunden-API-Aufruf.
Standard-Kontrollfluss — Schleifen, Bedingungen, Funktionen und Klassen funktionieren wie erwartet. 10 Subnetze zu erstellen ist eine for-Schleife, nicht Terraforms count-Meta-Argument oder for_each-Akrobatik.
Wiederverwendbare Komponenten als Pakete — Infrastruktur-Komponenten können auf npm, PyPI oder Maven veröffentlicht werden. Ein Team, das ein konformes VPC-Modul baut, veröffentlicht es als Paket; andere Teams fügen es als Abhängigkeit hinzu.
Tests mit Standard-Frameworks — Unit-Tests verwenden Jest, pytest oder Gos testing-Paket. Property-Tests nutzen Policy as Code. Integrationstests stellen echte Infrastruktur bereit und validieren sie End-to-End.
| Merkmal | Pulumi | Terraform/OpenTofu | AWS CDK | Crossplane | CloudFormation |
|---|---|---|---|---|---|
| Sprache | TS/Python/Go/C#/Java | HCL | TS/Python/Java/Go | YAML/CRDs | JSON/YAML |
| IDE-Support | Vollständig (Typen + Autocomplete) | Teilweise (HCL-Plugin) | Vollständig | Minimal | Minimal |
| Tests | Standard-Frameworks | Terratest (Go) | Standard-Frameworks | Kein nativer Support | Kein nativer Support |
| State-Management | Cloud/S3/GCS/Azure/lokal | Cloud/S3/GCS/Azure/lokal | CloudFormation-Stacks | Kubernetes etcd | CloudFormation |
| Multi-Cloud | Ja (einzelnes Programm) | Ja (mehrere Provider) | AWS-zentriert | Ja | Nur AWS |
| Wiederverwendbare Pakete | npm/PyPI/Maven/NuGet | Terraform Registry | npm/PyPI/Maven/NuGet | Helm/OCI | Verschachtelte Stacks |
| Lernkurve | Niedrig (vorhandene Sprache) | Mittel (HCL lernen) | Niedrig (vorhandene Sprache) | Hoch (Kubernetes CRDs) | Hoch (ausführliches JSON/YAML) |
Architektur: Wie Pulumi funktioniert
Language Host — Ein Prozess, der die gewählte Sprachlaufzeit (Node.js, Python, JVM usw.) ausführt und das Pulumi SDK aufruft, um Ressourcen zu registrieren.
Pulumi-Engine — Der zentrale Orchestrierungsprozess, der Ressourcenregistrierungsanfragen empfängt, den Abhängigkeitsgraphen berechnet, den gewünschten State mit dem letzten bekannten State vergleicht und einen Plan erstellt.
Ressource-Provider — Plugins, die Pulumi-Ressourcendeklarationen in API-Aufrufe übersetzen (AWS, Azure, GCP, Kubernetes usw.).
State-Backend — Speichert den letzten bekannten State deines Stacks. Die Engine vergleicht die neue Programmausgabe mit diesem State, um zu ermitteln, was erstellt, aktualisiert oder gelöscht werden muss.
Deployment-Engine — Führt den Plan gemäß dem Abhängigkeitsgraphen aus, wobei unabhängige Ressourcenoperationen parallel ausgeführt werden.
Installation
# Linux/macOS per Installationsskript
curl -fsSL https://get.pulumi.com | sh
# macOS per Homebrew
brew install pulumi
# Windows per Chocolatey
choco install pulumi
# Prüfen
pulumi version
Am State-Backend anmelden:
# Pulumi Cloud (Standard, Free Tier verfügbar)
pulumi login
# S3-Backend
pulumi login s3://dein-state-bucket/pulumi
# Lokales Dateisystem (nicht für Teams empfohlen)
pulumi login --local
Projektstruktur
pulumi new aws-typescript # TypeScript + AWS
pulumi new aws-python # Python + AWS
pulumi new azure-go # Go + Azure
Ein TypeScript-Projekt hat diese Struktur:
meine-infra/
Pulumi.yaml # Projekt-Metadaten
Pulumi.dev.yaml # Stack-Konfiguration für den "dev"-Stack
package.json # npm-Abhängigkeiten
index.ts # Einstiegspunkt
Kernkonzepte
Ressourcen
import * as aws from "@pulumi/aws";
const bucket = new aws.s3.Bucket("mein-bucket", {
acl: "private",
tags: { Umgebung: "produktion" },
versioning: { enabled: true },
});
export const bucketName = bucket.bucket;
export const bucketArn = bucket.arn;
Inputs und Outputs
Ressourcen-Eigenschaften sind Output<T> — asynchrone Werte, die sich nach der Ressourcenerstellung auflösen:
// pulumi.interpolate — für String-Templates
const bucketUrl = pulumi.interpolate`https://${bucket.bucket}.s3.amazonaws.com`;
// Output.apply — für Transformationen
const grossBucketName = bucket.bucket.apply(name => name.toUpperCase());
// Output.all — wenn mehrere Outputs zusammen benötigt werden
const kombiniert = pulumi.all([bucket.bucket, bucket.arn]).apply(([name, arn]) => ({
name, arn, url: `https://${name}.s3.amazonaws.com`,
}));
Stack-Referenzen
const netzwerkStack = new pulumi.StackReference("org/network/prod");
const vpcId = netzwerkStack.getOutput("vpcId");
const privateSubnetIds = netzwerkStack.getOutput("privateSubnetIds");
ComponentResource
class SichererS3Bucket extends pulumi.ComponentResource {
public readonly bucket: aws.s3.Bucket;
constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
super("meinorg:storage:SichererS3Bucket", name, {}, opts);
this.bucket = new aws.s3.Bucket(`${name}-bucket`, {
acl: "private",
versioning: { enabled: true },
}, { parent: this });
new aws.s3.BucketPublicAccessBlock(`${name}-block`, {
bucket: this.bucket.id,
blockPublicAcls: true,
blockPublicPolicy: true,
}, { parent: this });
this.registerOutputs({ bucketName: this.bucket.bucket });
}
}
Infrastruktur in Python
import pulumi
import pulumi_aws as aws
vpc = aws.ec2.Vpc("haupt-vpc",
cidr_block="10.0.0.0/16",
enable_dns_hostnames=True,
tags={"Umgebung": pulumi.get_stack()},
)
verfuegbarkeitszonen = ["eu-central-1a", "eu-central-1b", "eu-central-1c"]
subnetze = [
aws.ec2.Subnet(f"subnet-{i}",
vpc_id=vpc.id,
cidr_block=f"10.0.{i}.0/24",
availability_zone=az,
)
for i, az in enumerate(verfuegbarkeitszonen)
]
pulumi.export("vpc_id", vpc.id)
State-Backends
| Backend | Befehl | Ideal für |
|---|---|---|
| Pulumi Cloud | pulumi login | Teams, Secrets-Verschlüsselung, integriertes Audit-Log |
| AWS S3 | pulumi login s3://bucket/pfad | AWS-native Teams |
| Azure Blob | pulumi login azblob://container/pfad | Azure-native Teams |
| Google Cloud Storage | pulumi login gs://bucket/pfad | GCP-native Teams |
| Lokale Datei | pulumi login --local | Nur Einzeltests |
Stacks und Umgebungen
pulumi stack init dev
pulumi stack init staging
pulumi stack init prod
pulumi stack select prod
pulumi config set aws:region eu-central-1
pulumi config set --secret datenbankPasswort "sehr-geheimes-passwort"
Tests
Unit-Tests mit Mocks
pulumi.runtime.setMocks({
newResource: (args: pulumi.runtime.MockResourceArgs) => {
return { id: `${args.name}-id`, state: args.inputs };
},
call: (args: pulumi.runtime.MockCallArgs) => args.inputs,
});
Policy as Code mit CrossGuard
new PolicyPack("aws-compliance", {
policies: [
{
name: "s3-kein-public-read",
description: "S3-Buckets dürfen keinen öffentlichen Lesezugriff erlauben",
enforcementLevel: "mandatory",
validateResource: validateResourceOfType(aws.s3.Bucket, (bucket, args, reportViolation) => {
if (bucket.acl === "public-read") {
reportViolation("S3-Bucket darf nicht öffentlich lesbar sein.");
}
}),
},
],
});
CI/CD-Integration
GitHub Actions
name: Pulumi Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci
- uses: pulumi/actions@v5
with:
command: up
stack-name: prod
work-dir: ./infra
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Automation API
import { LocalWorkspace } from "@pulumi/pulumi/automation";
async function umgebungBereitstellen(tenantId: string) {
const stack = await LocalWorkspace.createOrSelectStack({
stackName: `tenant-${tenantId}`,
projectName: "saas-infra",
program: async () => {
const bucket = new aws.s3.Bucket(`${tenantId}-daten`);
return { bucketName: bucket.bucket };
},
});
await stack.setConfig("aws:region", { value: "eu-central-1" });
const ergebnis = await stack.up({ onOutput: console.log });
return ergebnis.outputs;
}
Praktisches Multi-Cloud-Beispiel
import * as aws from "@pulumi/aws";
import * as cloudflare from "@pulumi/cloudflare";
const config = new pulumi.Config();
const domain = config.require("domainName");
const cfZoneId = config.requireSecret("cloudflareZoneId");
const lb = new aws.lb.LoadBalancer("web-lb", {
internal: false,
loadBalancerType: "application",
subnets: oeffentlicheSubnetIds,
});
const dnsEintrag = new cloudflare.Record("web-dns", {
zoneId: cfZoneId,
name: domain,
type: "CNAME",
value: lb.dnsName,
proxied: true,
});
export const appUrl = pulumi.interpolate`https://${domain}`;
Fallstricke und Sonderfälle
Outputs im String-Kontext — Verwende niemals const url = "https://" + bucket.bucket. Nutze stattdessen pulumi.interpolate. Der +-Operator konvertiert ein Output in [object Object].
Stack-Outputs löschen — Das Entfernen eines export löscht die Ressource nicht; es entfernt nur den Ausgabewert. Um die Ressource zu löschen, entferne sie aus dem Programmcode.
pulumi refresh vs. pulumi up — Verwende pulumi refresh, um den State nach manuellen Änderungen mit dem tatsächlichen Cloud-State zu synchronisieren. Verwende pulumi up, um Programmänderungen anzuwenden.
Ressourcen-Benennung — Pulumi fügt Ressourcenamen standardmäßig ein zufälliges Suffix hinzu (z.B. mein-bucket-a3f8c2b). Verwende die name-Eigenschaft explizit, wenn du einen festen Namen benötigst.
Zusammenfassung
- Pulumi verwendet echte Programmiersprachen (TypeScript, Python, Go, C#, Java) statt HCL oder YAML und bietet vollständigen IDE-Support und Standard-Tests
- Die Engine berechnet einen Abhängigkeitsgraphen und wendet Änderungen parallel an, verglichen mit dem in Pulumi Cloud, S3, Azure Blob oder GCS gespeicherten State
- Stacks isolieren dev/staging/prod-Umgebungen; jeder Stack hat seine eigene Konfiguration und seinen eigenen State
ComponentResourceermöglicht wiederverwendbare Infrastruktur-Abstraktionen, die als npm- oder PyPI-Pakete veröffentlicht werden können- Unit-Tests verwenden Mocks; Integrationstests nutzen die Automation API; Compliance-Regeln verwenden CrossGuard-Richtlinien
- Die Automation API bettet Pulumi in Anwendungscode ein für Self-Service-Portale und ephemere Umgebungen
- CI/CD-Integration ist ein einzelner
pulumi/actions-Schritt in GitHub Actions oder äquivalent in GitLab CI