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 :

  1. Faire un POC sur 1 feature simple
  2. Mesurer : vitesse dev, bugs, satisfaction équipe
  3. Décider avec de la vraie data

Et vous, quel outil utilisez-vous ? Retours ?