Terraform domine le marché IaC depuis des années. Pulumi arrive avec la promesse d’utiliser de vrais langages de programmation. Après avoir utilisé les deux en production, voici mon retour sans bullshit.
TL;DR : Le verdict
Utilisez Terraform si :
- Équipe majoritairement Ops/infra
- Multi-cloud complexe
- Écosystème de modules mature requis
- Compétences HCL déjà présentes
Utilisez Pulumi si :
- Équipe développeurs forte
- Infra complexe avec logique métier
- Besoin de tests unitaires poussés
- TypeScript/Python/Go déjà mastered
Terraform : Le standard de facto
Forces
1. Écosystème mature
# 4000+ providers officiels
provider "aws" { }
provider "kubernetes" { }
provider "datadog" { }
# ... tout est supporté
2. State management robuste
terraform {
backend "s3" {
bucket = "terraform-state"
key = "prod/terraform.tfstate"
dynamodb_table = "terraform-locks"
}
}
3. Import de ressources existantes
# Import ressource cloud dans Terraform
terraform import aws_instance.web i-1234567890abcdef0
Faiblesses
1. HCL est limité
# Pas de boucles complexes
# Pas de conditions avancées
# Pas de fonctions customs
# Exemple douloureux : dupliquer de la config
resource "aws_instance" "app_1" {
ami = "ami-123"
instance_type = "t3.micro"
# 50 lignes de config...
}
resource "aws_instance" "app_2" {
ami = "ami-123"
instance_type = "t3.micro"
# 50 lignes identiques...
}
Solution : Modules, mais ça complexifie.
2. Tests difficiles
# Tests Terraform = lancer vraiment
terraform plan
terraform apply
# Pas de tests unitaires réels
3. Erreurs cryptiques
Error: Error creating DB Instance: InvalidParameterCombination:
Cannot specify both DBParameterGroupName and AllocatedStorage
Bonne chance pour comprendre…
Pulumi : Le challenger
Forces
1. Vrais langages de programmation
// TypeScript avec IDE autocompletion !
import * as aws from "@pulumi/aws";
const instances = ["web", "api", "worker"].map(name =>
new aws.ec2.Instance(`${name}-instance`, {
ami: "ami-123",
instanceType: "t3.micro",
tags: { Name: name }
})
);
// Boucles, conditions, fonctions : tout est possible
2. Tests unitaires réels
// tests/infra.test.ts
import { describe, it } from '@jest/globals';
describe("Infrastructure", () => {
it("should create S3 bucket with encryption", async () => {
const infra = await createInfra();
expect(infra.bucket.serverSideEncryptionConfiguration).toBeDefined();
expect(infra.bucket.versioning.enabled).toBe(true);
});
});
3. Partage de logique avec l’app
// shared/config.ts
export const regions = ["eu-west-1", "us-east-1"];
// infra/index.ts
import { regions } from '../shared/config';
regions.forEach(region => {
new aws.Provider(region, { region });
// Déployer dans chaque région
});
// app/config.ts
import { regions } from '../shared/config';
// Même config dans l'app !
Faiblesses
1. Écosystème moins mature
Certains providers manquent ou sont incomplets.
2. Debugging complexe
// Erreur dans le code TS
// + erreur dans l'API cloud
// + erreur dans Pulumi
// = 3 stack traces imbriquées
Error:
at Promise.all.pulumi/pulumi/...
at aws.s3.Bucket.pulumi/aws...
at AWS SDK InvalidBucketName...
3. Coût runtime plus élevé
Terraform plan : 5s
Pulumi preview : 15s
// Node.js startup + SDK overhead
Comparatif technique détaillé
Syntaxe et DX
Terraform :
variable "instance_count" {
type = number
}
resource "aws_instance" "app" {
count = var.instance_count
ami = "ami-123"
instance_type = "t3.micro"
tags = {
Name = "app-${count.index}"
}
}
Pulumi :
const instanceCount = config.requireNumber("instance_count");
const instances = Array.from({ length: instanceCount }, (_, i) =>
new aws.ec2.Instance(`app-${i}`, {
ami: "ami-123",
instanceType: "t3.micro",
tags: { Name: `app-${i}` }
})
);
Gagnant : Pulumi (IDE support, type safety)
Gestion de secrets
Terraform :
# Secrets en plain text dans .tfvars 😱
# Ou avec Vault provider (complexe)
variable "db_password" {
type = string
sensitive = true
}
Pulumi :
// Secrets encryptés natifs
import { Config } from "@pulumi/pulumi";
const config = new Config();
const dbPassword = config.requireSecret("dbPassword");
// Encrypted dans state
Gagnant : Pulumi
Multi-cloud
Terraform :
# Excellent support multi-cloud
provider "aws" { }
provider "gcp" { }
provider "azure" { }
# Même syntaxe, providers différents
Pulumi :
import * as aws from "@pulumi/aws";
import * as gcp from "@pulumi/gcp";
import * as azure from "@pulumi/azure";
// APIs différentes par provider
Gagnant : Terraform (meilleure cohérence)
Performance
Benchmark : Déployer 50 resources
Terraform :
plan : 4.2s
apply : 38s
Pulumi :
preview : 11.5s
up : 42s
Gagnant : Terraform (plus rapide)
Collaboration équipe
Terraform :
- HCL : Facile à lire même sans coder
- Modules partagés (Terraform Registry)
- Standards établis
Pulumi :
- Requiert compétences dev
- Réutilisation de packages npm/pypi
- Moins de standards
Gagnant : Terraform (pour équipes mixtes)
Patterns avancés
Pattern 1 : Abstraction complexe (Pulumi brille)
// Créer une abstraction high-level
class WebApp {
constructor(name: string, config: WebAppConfig) {
// Load balancer
this.lb = new aws.lb.LoadBalancer(`${name}-lb`, {
internal: false,
loadBalancerType: "application",
});
// Auto Scaling Group
this.asg = new aws.autoscaling.Group(`${name}-asg`, {
minSize: config.minInstances,
maxSize: config.maxInstances,
launchTemplate: {
id: this.launchTemplate.id,
version: "$Latest"
}
});
// CloudWatch Alarms
if (config.enableAlerts) {
this.createAlerts();
}
// RDS Database
if (config.database) {
this.db = new aws.rds.Instance(`${name}-db`, {
engine: config.database.engine,
instanceClass: config.database.instanceClass,
});
}
}
private createAlerts() {
// Logique complexe d'alerting
}
}
// Utilisation simple
const app = new WebApp("production", {
minInstances: 2,
maxInstances: 10,
enableAlerts: true,
database: {
engine: "postgres",
instanceClass: "db.t3.micro"
}
});
En Terraform : Possible avec modules, mais moins ergonomique.
Pattern 2 : Infra dynamique basée sur API
// Pulumi : Fetch de data externe facile
const zones = await fetchAvailabilityZones();
zones.forEach(zone => {
new aws.ec2.Subnet(`subnet-${zone}`, {
availabilityZone: zone,
cidrBlock: calculateCIDR(zone)
});
});
En Terraform : Utiliser data sources, moins flexible.
Pattern 3 : Policy as Code
Pulumi :
// Policy TypeScript
import { PolicyPack, validateResourceOfType } from "@pulumi/policy";
new PolicyPack("aws-security", {
policies: [{
name: "s3-no-public-read",
description: "S3 buckets must not allow public read access",
enforcementLevel: "mandatory",
validateResource: validateResourceOfType(aws.s3.Bucket, (bucket, args, reportViolation) => {
if (bucket.acl === "public-read") {
reportViolation("S3 bucket must not allow public read");
}
})
}]
});
Terraform :
# Sentinel (HashiCorp payant)
# Ou OPA (plus complexe)
Gagnant : Pulumi (intégré)
Migration Terraform → Pulumi
Outil : pulumi import
# Import automatique depuis Terraform
pulumi import aws:ec2/instance:Instance web i-1234567890abcdef0
Migration progressive
// 1. Importer state Terraform
import * as terraform from "@pulumi/terraform";
const tfState = new terraform.state.RemoteStateReference("existing", {
backendType: "s3",
bucket: "terraform-state",
key: "prod/terraform.tfstate"
});
// 2. Référencer ressources Terraform
const existingVpc = tfState.getOutput("vpc_id");
// 3. Créer nouvelles ressources Pulumi
new aws.ec2.Subnet("new-subnet", {
vpcId: existingVpc,
cidrBlock: "10.0.3.0/24"
});
Cas d’usage réels
Cas 1 : Startup tech (50 devs)
Choix : Pulumi TypeScript
Raison :
- Équipe 100% dev
- Infra complexe avec logique métier
- Besoin de tests unitaires
- TypeScript déjà utilisé partout
Résultat :
- Onboarding dev : 1 jour (vs 1 semaine Terraform)
- Tests infra : 85% coverage
- Bugs infra : -60%
Cas 2 : Grande entreprise (200+ ingénieurs)
Choix : Terraform
Raison :
- Équipe infra dédiée
- Multi-cloud complexe (AWS + Azure + GCP)
- Besoin écosystème mature
- Standards déjà établis
Résultat :
- Modules réutilisables sur 40+ projets
- Gouvernance centralisée
- Coût maîtrisé
Cas 3 : SaaS B2B (infra par client)
Choix : Pulumi
Raison :
- Infra multi-tenant complexe
- Logique métier dans l’infra
- Besoin de générer config dynamiquement
// Créer infra pour chaque client
clients.forEach(client => {
new CustomerInfra(client.id, {
tier: client.subscriptionTier,
region: client.preferredRegion,
features: client.enabledFeatures
});
});
Coût total de possession (TCO)
Terraform
Coûts :
- Terraform Cloud (teamwork) : 20$/user/mois
- HashiCorp Sentinel (policies) : Enterprise only
- Formation équipe : 2-3 jours
Total/an (équipe 10) : ~$2,400 + $6,000 formation
Pulumi
Coûts :
- Pulumi Cloud (free pour OSS)
- Pulumi Team : 50$/user/mois
- Formation : 0 (si équipe dev)
Total/an (équipe 10) : $6,000
Gagnant : Terraform (moins cher pour grandes équipes)
Recommandations finales
Checklist de décision
Choisissez Terraform si :
- Équipe infra/ops dominante
- Multi-cloud stratégique
- Besoin écosystème mature immédiat
- Budget formation limité
Choisissez Pulumi si :
- Équipe développeurs forte
- Infra avec logique complexe
- Tests unitaires critiques
- Lang programmation déjà maîtrisé
Approche hybride possible
┌─────────────────┐
│ Core Infra │ ← Terraform (VPC, IAM, etc.)
└────────┬────────┘
│
┌────────▼────────┐
│ App Infra │ ← Pulumi (App-specific)
└─────────────────┘
Avantages :
- Stabilité Terraform pour base
- Flexibilité Pulumi pour apps
Conclusion
Il n’y a pas de mauvais choix.
Terraform et Pulumi sont tous deux excellents pour faire de l’IaC en 2025.
Le vrai critère : votre équipe.
- Équipe infra/ops → Terraform
- Équipe dev → Pulumi
- Équipe mixte → Terraform (plus facile pour tout le monde)
Mon conseil :
- Faire un POC sur 1 feature simple
- Mesurer : vitesse dev, bugs, satisfaction équipe
- Décider avec de la vraie data
Et vous, quel outil utilisez-vous ? Retours ?