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.

MerkmalPulumiTerraform/OpenTofuAWS CDKCrossplaneCloudFormation
SpracheTS/Python/Go/C#/JavaHCLTS/Python/Java/GoYAML/CRDsJSON/YAML
IDE-SupportVollständig (Typen + Autocomplete)Teilweise (HCL-Plugin)VollständigMinimalMinimal
TestsStandard-FrameworksTerratest (Go)Standard-FrameworksKein nativer SupportKein nativer Support
State-ManagementCloud/S3/GCS/Azure/lokalCloud/S3/GCS/Azure/lokalCloudFormation-StacksKubernetes etcdCloudFormation
Multi-CloudJa (einzelnes Programm)Ja (mehrere Provider)AWS-zentriertJaNur AWS
Wiederverwendbare Paketenpm/PyPI/Maven/NuGetTerraform Registrynpm/PyPI/Maven/NuGetHelm/OCIVerschachtelte Stacks
LernkurveNiedrig (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

BackendBefehlIdeal für
Pulumi Cloudpulumi loginTeams, Secrets-Verschlüsselung, integriertes Audit-Log
AWS S3pulumi login s3://bucket/pfadAWS-native Teams
Azure Blobpulumi login azblob://container/pfadAzure-native Teams
Google Cloud Storagepulumi login gs://bucket/pfadGCP-native Teams
Lokale Dateipulumi login --localNur 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
  • ComponentResource ermö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

Verwandte Artikel