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é | Pulumi | Terraform/OpenTofu | AWS CDK | Crossplane | CloudFormation |
|---|---|---|---|---|---|
| Langage | TS/Python/Go/C#/Java | HCL | TS/Python/Java/Go | YAML/CRDs | JSON/YAML |
| Support IDE | Complet (types + autocomplete) | Partiel (plugin HCL) | Complet | Minimal | Minimal |
| Tests | Frameworks standards | Terratest (Go) | Frameworks standards | Aucun natif | Aucun natif |
| Gestion d’état | Cloud/S3/GCS/Azure/local | Cloud/S3/GCS/Azure/local | Stacks CloudFormation | Kubernetes etcd | CloudFormation |
| Multi-cloud | Oui (programme unique) | Oui (plusieurs fournisseurs) | Centré sur AWS | Oui | AWS uniquement |
| Paquets réutilisables | npm/PyPI/Maven/NuGet | Terraform Registry | npm/PyPI/Maven/NuGet | Helm/OCI | Stacks imbriqués |
| Courbe d’apprentissage | Faible (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
| Backend | Commande | Idéal pour |
|---|---|---|
| Pulumi Cloud | pulumi login | Équipes, chiffrement des secrets, journal d’audit intégré |
| AWS S3 | pulumi login s3://bucket/chemin | Équipes natives AWS |
| Azure Blob | pulumi login azblob://conteneur/chemin | Équipes natives Azure |
| Google Cloud Storage | pulumi login gs://bucket/chemin | Équipes natives GCP |
| Fichier local | pulumi login --local | Tests 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
ComponentResourcepermet 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/actionsdans GitHub Actions ou équivalent dans GitLab CI