TL;DR — Résumé Rapide

Pulumi permet de définir l'infra cloud avec TypeScript, Python, Go, C# et Java. Guide complet sur les stacks, l'état, les tests et l'intégration CI/CD.

Pulumi est une plateforme d’infrastructure as code qui permet de définir, déployer et gérer des ressources cloud avec de vrais langages de programmation — TypeScript, Python, Go, C#, Java — plutôt qu’avec des langages dédiés comme HCL ou YAML. Si vous avez déjà buté sur les limites des boucles Terraform, lutté contre la verbosité JSON de CloudFormation, ou souhaité tester votre infrastructure avec le même framework de tests que votre code applicatif, Pulumi résout exactement ces problèmes. Ce guide couvre tout, de l’installation et des concepts fondamentaux aux tests, à la gestion d’état, à l’intégration CI/CD et à un exemple pratique multi-cloud.

Prérequis

  • Maîtrise d’au moins un de ces langages : TypeScript/JavaScript, Python, Go, C# ou Java
  • Un compte AWS, Azure ou GCP (le niveau gratuit suffit pour les exemples)
  • Node.js 18+ installé (pour les exemples TypeScript/JavaScript)
  • Compréhension basique des concepts cloud (VPC, buckets de stockage, VMs)
  • Compte Pulumi Cloud (niveau gratuit) ou un backend d’état alternatif

Pourquoi Pulumi : Vrais Langages vs. HCL et YAML

La différence fondamentale entre Pulumi et des outils comme Terraform ou CloudFormation est que les programmes Pulumi sont écrits en langages de programmation généralistes. Cela libère des capacités difficiles ou impossibles avec des outils basés sur DSL.

Support IDE complet — Votre éditeur fournit l’autocomplétion pour chaque propriété de ressource, la documentation inline et les erreurs de type avant tout déploiement. Mal orthographier le nom d’une propriété de bucket S3 est une erreur à la compilation, pas un échec à l’exécution après 30 secondes d’appel API.

Flux de contrôle standard — Les boucles, conditionnelles, fonctions et classes fonctionnent comme attendu. Créer 10 subnets est une boucle for, pas le méta-argument count de Terraform ni les contorsions de for_each.

Composants réutilisables comme paquets — Vous pouvez publier des composants d’infrastructure sur npm, PyPI ou Maven. Une équipe qui construit un module VPC conforme le publie comme paquet ; d’autres équipes l’ajoutent comme dépendance.

Tests avec des frameworks standards — Les tests unitaires utilisent Jest, pytest ou le package testing de Go. Les tests de propriété utilisent Policy as Code. Les tests d’intégration déploient de vraie infrastructure et la valident de bout en bout.

FonctionnalitéPulumiTerraform/OpenTofuAWS CDKCrossplaneCloudFormation
LangageTS/Python/Go/C#/JavaHCLTS/Python/Java/GoYAML/CRDsJSON/YAML
Support IDEComplet (types + autocomplete)Partiel (plugin HCL)CompletMinimalMinimal
TestsFrameworks standardsTerratest (Go)Frameworks standardsAucun natifAucun natif
Gestion d’étatCloud/S3/GCS/Azure/localCloud/S3/GCS/Azure/localStacks CloudFormationKubernetes etcdCloudFormation
Multi-cloudOui (programme unique)Oui (plusieurs fournisseurs)Centré sur AWSOuiAWS uniquement
Paquets réutilisablesnpm/PyPI/Maven/NuGetTerraform Registrynpm/PyPI/Maven/NuGetHelm/OCIStacks imbriqués
Courbe d’apprentissageFaible (langage existant)Moyenne (apprendre HCL)Faible (langage existant)Élevée (CRDs Kubernetes)Élevée (JSON/YAML verbeux)

Architecture : Fonctionnement de Pulumi

Language host — Un processus exécutant le runtime du langage choisi (Node.js, Python, JVM, etc.) qui exécute votre programme et appelle le SDK Pulumi pour enregistrer les ressources.

Moteur Pulumi — Le processus d’orchestration central qui reçoit les demandes d’enregistrement de ressources, calcule le graphe de dépendances, compare l’état désiré avec le dernier état connu et génère un plan.

Fournisseurs de ressources — Des plugins qui traduisent les déclarations de ressources Pulumi en appels API (AWS, Azure, GCP, Kubernetes, etc.).

Backend d’état — Stocke le dernier état connu de votre stack. Le moteur fait un diff de la nouvelle sortie du programme contre cet état pour déterminer quoi créer, mettre à jour ou supprimer.

Moteur de déploiement — Exécute le plan en respectant le graphe de dépendances, effectuant les opérations de ressources indépendantes en parallèle.

Installation

# Linux/macOS via script d'installation
curl -fsSL https://get.pulumi.com | sh

# macOS via Homebrew
brew install pulumi

# Windows via Chocolatey
choco install pulumi

# Vérifier
pulumi version

Connexion au backend d’état :

# Pulumi Cloud (par défaut, niveau gratuit disponible)
pulumi login

# Backend S3
pulumi login s3://votre-bucket-etat/pulumi

# Système de fichiers local (déconseillé pour les équipes)
pulumi login --local

Structure du Projet

pulumi new aws-typescript        # TypeScript + AWS
pulumi new aws-python            # Python + AWS
pulumi new azure-go              # Go + Azure

Un projet TypeScript a cette structure :

mon-infra/
  Pulumi.yaml          # Métadonnées du projet
  Pulumi.dev.yaml      # Configuration du stack "dev"
  package.json         # Dépendances npm
  index.ts             # Point d'entrée

Concepts Fondamentaux

Ressources

import * as aws from "@pulumi/aws";

const bucket = new aws.s3.Bucket("mon-bucket", {
    acl: "private",
    tags: { Environnement: "production" },
    versioning: { enabled: true },
});

export const nomBucket = bucket.bucket;
export const arnBucket = bucket.arn;

Entrées et Sorties

Les propriétés de ressources sont des Output<T> — des valeurs asynchrones qui se résolvent après la création de la ressource :

// pulumi.interpolate — pour les templates de chaînes
const urlBucket = pulumi.interpolate`https://${bucket.bucket}.s3.amazonaws.com`;

// Output.apply — pour les transformations
const nomMajuscule = bucket.bucket.apply(name => name.toUpperCase());

// Output.all — quand plusieurs sorties sont nécessaires ensemble
const combine = pulumi.all([bucket.bucket, bucket.arn]).apply(([nom, arn]) => ({
    nom, arn, url: `https://${nom}.s3.amazonaws.com`,
}));

Références de Stack

const stackReseau = new pulumi.StackReference("org/network/prod");
const vpcId = stackReseau.getOutput("vpcId");
const subnetPrivesIds = stackReseau.getOutput("privateSubnetIds");

ComponentResource

class BucketS3Securise extends pulumi.ComponentResource {
    public readonly bucket: aws.s3.Bucket;

    constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
        super("monorg:storage:BucketS3Securise", 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({ nomBucket: this.bucket.bucket });
    }
}

Infrastructure en Python

import pulumi
import pulumi_aws as aws

vpc = aws.ec2.Vpc("vpc-principal",
    cidr_block="10.0.0.0/16",
    enable_dns_hostnames=True,
    tags={"Environnement": pulumi.get_stack()},
)

zones_disponibilite = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
subnets = [
    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(zones_disponibilite)
]

pulumi.export("vpc_id", vpc.id)

Backends d’État

BackendCommandeIdéal pour
Pulumi Cloudpulumi loginÉquipes, chiffrement des secrets, journal d’audit intégré
AWS S3pulumi login s3://bucket/cheminÉquipes natives AWS
Azure Blobpulumi login azblob://conteneur/cheminÉquipes natives Azure
Google Cloud Storagepulumi login gs://bucket/cheminÉquipes natives GCP
Fichier localpulumi login --localTests individuels seulement

Stacks et Environnements

pulumi stack init dev
pulumi stack init staging
pulumi stack init prod

pulumi stack select prod
pulumi config set aws:region eu-west-1
pulumi config set --secret motDePasseDB "valeur-tres-secrete"

Tests

Tests Unitaires avec 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 avec CrossGuard

new PolicyPack("conformite-aws", {
    policies: [
        {
            name: "s3-pas-de-lecture-publique",
            description: "Les buckets S3 ne doivent pas autoriser la lecture publique",
            enforcementLevel: "mandatory",
            validateResource: validateResourceOfType(aws.s3.Bucket, (bucket, args, reportViolation) => {
                if (bucket.acl === "public-read") {
                    reportViolation("Le bucket S3 ne doit pas être lisible publiquement.");
                }
            }),
        },
    ],
});

Intégration CI/CD

GitHub Actions

name: Déploiement Pulumi
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 provisionnerEnvironnement(tenantId: string) {
    const stack = await LocalWorkspace.createOrSelectStack({
        stackName: `tenant-${tenantId}`,
        projectName: "saas-infra",
        program: async () => {
            const bucket = new aws.s3.Bucket(`${tenantId}-donnees`);
            return { nomBucket: bucket.bucket };
        },
    });

    await stack.setConfig("aws:region", { value: "eu-west-1" });
    const resultat = await stack.up({ onOutput: console.log });
    return resultat.outputs;
}

Exemple Multi-Cloud Pratique

import * as aws from "@pulumi/aws";
import * as cloudflare from "@pulumi/cloudflare";

const config = new pulumi.Config();
const domaine = config.require("domainName");
const cfZoneId = config.requireSecret("cloudflareZoneId");

const lb = new aws.lb.LoadBalancer("lb-web", {
    internal: false,
    loadBalancerType: "application",
    subnets: subnetsPublicsIds,
});

const enregistrementDns = new cloudflare.Record("dns-web", {
    zoneId: cfZoneId,
    name: domaine,
    type: "CNAME",
    value: lb.dnsName,
    proxied: true,
});

export const urlApp = pulumi.interpolate`https://${domaine}`;

Pièges et Cas Particuliers

Sorties dans un contexte de chaîne — N’utilisez jamais const url = "https://" + bucket.bucket. Utilisez pulumi.interpolate. L’opérateur + convertit une Output en [object Object].

Suppression de sorties de stack — Supprimer un export ne supprime pas la ressource ; il supprime seulement la valeur de sortie. Pour supprimer la ressource, retirez-la du code du programme.

pulumi refresh vs pulumi up — Utilisez pulumi refresh pour synchroniser l’état avec l’état réel du cloud après des changements manuels. Utilisez pulumi up pour appliquer les changements du programme.

Nommage des ressources — Pulumi ajoute un suffixe aléatoire aux noms de ressources par défaut (ex. mon-bucket-a3f8c2b). Utilisez la propriété name explicitement si vous avez besoin d’un nom fixe.

Récapitulatif

  • Pulumi utilise de vrais langages de programmation (TypeScript, Python, Go, C#, Java) au lieu de HCL ou YAML, offrant un support IDE complet et des tests standards
  • Le moteur calcule un graphe de dépendances et applique les changements en parallèle, comparant contre l’état stocké dans Pulumi Cloud, S3, Azure Blob ou GCS
  • Les stacks isolent les environnements dev/staging/prod ; chaque stack a sa propre configuration et son propre état
  • ComponentResource permet des abstractions d’infrastructure réutilisables publiables comme paquets npm ou PyPI
  • Les tests unitaires utilisent des mocks ; les tests d’intégration utilisent l’Automation API ; la conformité utilise les politiques CrossGuard
  • L’Automation API intègre Pulumi dans le code applicatif pour les portails en libre-service et les environnements éphémères
  • L’intégration CI/CD est une seule étape pulumi/actions dans GitHub Actions ou équivalent dans GitLab CI

Articles Connexes