pulumi-best-practices

Par pulumi · agent-skills

Charger lorsque l'utilisateur écrit, révise ou débogue des programmes Pulumi TypeScript/Python ; pose des questions sur l'utilisation de `Output<T>` ou `apply()` ; souhaite créer des classes `ComponentResource` ; doit refactoriser des ressources sans les détruire (aliases) ; configure des secrets ou la config ; ou met en place un workflow CI `pulumi preview`/`up`. Charger également pour les questions sur l'ordre de dépendance des ressources, les relations parent/enfant entre ressources, ou `pulumi.interpolate`.

npx skills add https://github.com/pulumi/agent-skills --skill pulumi-best-practices

Bonnes pratiques Pulumi

Quand utiliser cette compétence

Invoquez cette compétence quand :

  • Vous écrivez de nouveaux programmes ou composants Pulumi
  • Vous vérifiez le code Pulumi pour sa correction
  • Vous refactorisez une infrastructure Pulumi existante
  • Vous déboguez des problèmes de dépendances entre ressources
  • Vous configurez des configurations et des secrets

Pratiques

1. Ne jamais créer de ressources à l'intérieur d'apply()

Pourquoi : Les ressources créées à l'intérieur d'apply() n'apparaissent pas dans pulumi preview, rendant les changements imprévisibles. Pulumi ne peut pas suivre correctement les dépendances, ce qui entraîne des conditions de concurrence et des échecs de déploiement.

Signaux de détection :

  • Constructeurs new aws. ou autres constructeurs de ressources à l'intérieur des callbacks .apply()
  • Création de ressources à l'intérieur de pulumi.all([...]).apply()
  • Nombres de ressources dynamiques déterminés au runtime à l'intérieur d'apply

Mauvais :

const bucket = new aws.s3.Bucket("bucket");

bucket.id.apply(bucketId => {
    // MAUVAIS : Cette ressource n'apparaîtra pas dans preview
    new aws.s3.BucketObject("object", {
        bucket: bucketId,
        content: "hello",
    });
});

Bon :

const bucket = new aws.s3.Bucket("bucket");

// Passez la sortie directement - Pulumi gère la dépendance
const object = new aws.s3.BucketObject("object", {
    bucket: bucket.id,  // Output<string> fonctionne ici
    content: "hello",
});

Quand apply est approprié :

  • Transformer les valeurs de sortie pour utilisation dans les tags, noms ou chaînes calculées
  • Journalisation ou débogage (pas création de ressources)
  • Logique conditionnelle qui affecte les propriétés des ressources, pas l'existence des ressources

Référence : https://www.pulumi.com/docs/concepts/inputs-outputs/


2. Passer les outputs directement en tant qu'inputs

Pourquoi : Pulumi construit un graphe acyclique orienté (DAG) basé sur les relations input/output. Passer les outputs directement garantit un ordre de création correct. Déballer manuellement les valeurs casse la chaîne de dépendances, entraînant un déploiement des ressources dans le mauvais ordre ou une référence à des valeurs qui n'existent pas encore.

Signaux de détection :

  • Variables extraites d'.apply() utilisées ultérieurement comme inputs de ressources
  • await sur les valeurs de sortie en dehors d'apply
  • Concaténation de chaînes avec des outputs au lieu d'utiliser pulumi.interpolate

Mauvais :

const vpc = new aws.ec2.Vpc("vpc", { cidrBlock: "10.0.0.0/16" });

// MAUVAIS : Extraire la valeur casse la chaîne de dépendances
let vpcId: string;
vpc.id.apply(id => { vpcId = id; });

const subnet = new aws.ec2.Subnet("subnet", {
    vpcId: vpcId,  // Peut être undefined, aucune dépendance suivie
    cidrBlock: "10.0.1.0/24",
});

Bon :

const vpc = new aws.ec2.Vpc("vpc", { cidrBlock: "10.0.0.0/16" });

const subnet = new aws.ec2.Subnet("subnet", {
    vpcId: vpc.id,  // Passez l'Output directement
    cidrBlock: "10.0.1.0/24",
});

Pour l'interpolation de chaînes :

// MAUVAIS
const name = bucket.id.apply(id => `prefix-${id}-suffix`);

// BON - utilisez pulumi.interpolate pour les template literals
const name = pulumi.interpolate`prefix-${bucket.id}-suffix`;

// BON - utilisez pulumi.concat pour la concaténation simple
const name = pulumi.concat("prefix-", bucket.id, "-suffix");

Référence : https://www.pulumi.com/docs/concepts/inputs-outputs/


3. Utiliser des composants pour les ressources liées

Pourquoi : Les classes ComponentResource regroupent les ressources liées en unités logiques réutilisables. Sans composants, votre graphe de ressources est plat, ce qui rend difficile la compréhension des ressources qui vont ensemble, la réutilisation de motifs entre les stacks ou le raisonnement sur votre infrastructure à un niveau plus élevé.

Signaux de détection :

  • Plusieurs ressources liées créées au niveau supérieur sans regroupement
  • Motifs de ressources répétés entre les stacks qui devraient être abstraits
  • Difficile de comprendre les relations entre ressources depuis la console Pulumi

Mauvais :

// Structure plate - pas de regroupement logique, difficile à réutiliser
const bucket = new aws.s3.Bucket("app-bucket");
const bucketPolicy = new aws.s3.BucketPolicy("app-bucket-policy", {
    bucket: bucket.id,
    policy: policyDoc,
});
const originAccessIdentity = new aws.cloudfront.OriginAccessIdentity("app-oai");
const distribution = new aws.cloudfront.Distribution("app-cdn", { /* ... */ });

Bon :

interface StaticSiteArgs {
    domain: string;
    content: pulumi.asset.AssetArchive;
}

class StaticSite extends pulumi.ComponentResource {
    public readonly url: pulumi.Output<string>;

    constructor(name: string, args: StaticSiteArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:components:StaticSite", name, args, opts);

        // Ressources créées ici - voir pratique 4 pour la configuration du parent
        const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });
        // ...

        this.url = distribution.domainName;
        this.registerOutputs({ url: this.url });
    }
}

// Réutilisable entre les stacks
const site = new StaticSite("marketing", {
    domain: "marketing.example.com",
    content: new pulumi.asset.FileArchive("./dist"),
});

Bonnes pratiques des composants :

  • Utilisez un motif URN cohérent : organization:module:ComponentName
  • Appelez registerOutputs() à la fin du constructeur
  • Exposez les outputs comme propriétés de classe pour les consommateurs
  • Acceptez ComponentResourceOptions pour permettre aux appelants de définir les providers, aliases, etc.

Pour des conseils approfondis sur la création de composants (conception des args, support multi-langage, tests, distribution), utilisez la compétence pulumi-component.

Référence : https://www.pulumi.com/docs/concepts/resources/components/


4. Toujours définir parent: this dans les composants

Pourquoi : Quand vous créez des ressources à l'intérieur d'une ComponentResource sans définir parent: this, ces ressources apparaissent au niveau racine de l'état de votre stack. Cela casse la hiérarchie logique, rend la console Pulumi difficile à naviguer et peut causer des problèmes avec les aliases et la refactorisation. La relation de parent est ce qui fait que le composant regroupe réellement ses enfants.

Signaux de détection :

  • Classes ComponentResource qui ne passent pas { parent: this } aux ressources enfants
  • Ressources à l'intérieur d'un composant apparaissant au niveau racine dans la console
  • Comportement inattendu lors de l'ajout d'aliases aux composants

Mauvais :

class MyComponent extends pulumi.ComponentResource {
    constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:components:MyComponent", name, {}, opts);

        // MAUVAIS : Pas de parent défini - ce bucket apparaît au niveau racine
        const bucket = new aws.s3.Bucket(`${name}-bucket`);
    }
}

Bon :

class MyComponent extends pulumi.ComponentResource {
    constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:components:MyComponent", name, {}, opts);

        // BON : Parent établit la hiérarchie
        const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, {
            parent: this
        });

        const policy = new aws.s3.BucketPolicy(`${name}-policy`, {
            bucket: bucket.id,
            policy: policyDoc,
        }, {
            parent: this
        });
    }
}

Ce que parent: this procure :

  • Les ressources apparaissent imbriquées sous le composant dans la console Pulumi
  • Supprimer le composant supprime tous les enfants
  • Les aliases sur le composant s'appliquent automatiquement aux enfants
  • Propriété clairement définie dans les fichiers d'état

Référence : https://www.pulumi.com/docs/concepts/resources/components/


5. Chiffrer les secrets dès le départ

Pourquoi : Les secrets marqués avec --secret sont chiffrés dans les fichiers d'état, masqués dans la sortie CLI et suivis lors des transformations. Commencer avec une configuration en texte clair et convertir plus tard nécessite une rotation des credentials, une mise à jour des références et un audit des valeurs fuitées dans les logs et l'historique d'état.

Signaux de détection :

  • Mots de passe, clés API, tokens stockés en tant que configuration en texte clair
  • Chaînes de connexion avec credentials intégrées
  • Clés privées ou certificats en texte clair

Mauvais :

# Texte clair - sera visible dans l'état et les logs
pulumi config set databasePassword hunter2
pulumi config set apiKey sk-1234567890

Bon :

# Chiffré dès le départ
pulumi config set --secret databasePassword hunter2
pulumi config set --secret apiKey sk-1234567890

Dans le code :

const config = new pulumi.Config();

// Cela récupère un secret - la valeur reste chiffrée
const dbPassword = config.requireSecret("databasePassword");

// Créer des outputs à partir de secrets préserve le secret
const connectionString = pulumi.interpolate`postgres://user:${dbPassword}@host/db`;
// connectionString est aussi un Output secret

// Marquer explicitement les valeurs comme secret
const computed = pulumi.secret(someValue);

Utilisez Pulumi ESC pour les secrets centralisés :

# Pulumi.yaml
environment:
  - production-secrets  # Tirez de l'environnement ESC
# ESC gère les secrets de manière centralisée entre les stacks
esc env set production-secrets db.password --secret "hunter2"

Ce qui qualifie un secret :

  • Mots de passe et phrases secrètes
  • Clés API et tokens
  • Clés privées et certificats
  • Chaînes de connexion avec credentials
  • Secrets client OAuth
  • Clés de chiffrement

Références :


6. Utiliser les aliases lors de la refactorisation

Pourquoi : Renommer des ressources, les déplacer dans des composants ou changer les parents entraîne Pulumi à les voir comme de nouvelles ressources. Sans aliases, la refactorisation détruit et recrée les ressources, ce qui peut potentiellement causer des interruptions ou une perte de données. Les aliases préservent l'identité des ressources lors des refactorisations.

Signaux de détection :

  • Renommage de ressource sans alias
  • Déplacement de ressource dans ou hors d'une ComponentResource
  • Changement du parent d'une ressource
  • Preview affiche delete+create quand un update était prévu

Mauvais :

// Avant : ressource nommée "my-bucket"
const bucket = new aws.s3.Bucket("my-bucket");

// Après : renommé sans alias - DÉTRUIT LE BUCKET
const bucket = new aws.s3.Bucket("application-bucket");

Bon :

// Après : renommé avec alias - préserve le bucket existant
const bucket = new aws.s3.Bucket("application-bucket", {}, {
    aliases: [{ name: "my-bucket" }],
});

Déplacer dans un composant :

// Avant : ressource au niveau supérieur
const bucket = new aws.s3.Bucket("my-bucket");

// Après : à l'intérieur d'un composant - nécessite un alias avec ancien parent
class MyComponent extends pulumi.ComponentResource {
    constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:components:MyComponent", name, {}, opts);

        const bucket = new aws.s3.Bucket("bucket", {}, {
            parent: this,
            aliases: [{
                name: "my-bucket",
                parent: pulumi.rootStackResource,  // Était à la racine
            }],
        });
    }
}

Types d'aliases :

// Simple changement de nom
aliases: [{ name: "old-name" }]

// Changement de parent
aliases: [{ name: "resource-name", parent: oldParent }]

// URN complet (quand vous connaissez exactement l'URN précédent)
aliases: ["urn:pulumi:stack::project::aws:s3/bucket:Bucket::old-name"]

Cycle de vie :

  1. Ajouter l'alias lors de la refactorisation
  2. Exécuter pulumi up sur tous les stacks
  3. Supprimer l'alias après la mise à jour de tous les stacks (optionnel, mais garde le code propre)

Référence : https://www.pulumi.com/docs/iac/concepts/resources/options/aliases/


7. Prévisualiser avant chaque déploiement

Pourquoi : pulumi preview montre exactement ce qui sera créé, modifié ou détruit. Les surprises en production proviennent du fait de sauter la prévisualisation. Une ressource affichant « replace » alors que vous attendiez « update » signifie une destruction et recréation imminentes.

Signaux de détection :

  • Exécuter pulumi up --yes interactivement sans revoir les changements
  • Aucune étape de prévisualisation nulle part dans le workflow CI/CD pour un changement donné
  • Sortie de prévisualisation non examinée avant la fusion ou l'approbation du déploiement

Mauvais :

# Déployer les yeux fermés
pulumi up --yes

Bon :

# Toujours prévisualiser d'abord
pulumi preview

# Revoir la sortie, puis déployer
pulumi up

Ce qu'il faut chercher dans la prévisualisation :

  • + create - Une nouvelle ressource sera créée
  • ~ update - Une ressource existante sera modifiée sur place
  • - delete - Une ressource sera détruite
  • +-replace - Une ressource sera détruite et recréée (risque d'interruption)
  • ~+-replace - Une ressource sera mise à jour, puis remplacée

Signaux d'alerte :

  • Opérations replace inattendues (vérifiez les changements de propriétés immuables)
  • Ressources supprimées qui ne devraient pas l'être
  • Plus de changements que prévus à partir de votre diff de code

Intégration CI/CD :

# Exemple GitHub Actions
jobs:
  preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Pulumi Preview
        uses: pulumi/actions@v5
        with:
          command: preview
          stack-name: production
        env:
          PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}

  deploy:
    needs: preview
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Pulumi Up
        uses: pulumi/actions@v5
        with:
          command: up
          stack-name: production

Workflow PR :

  • Exécuter la prévisualisation sur chaque PR
  • Poster la sortie de prévisualisation comme commentaire PR
  • Exiger l'examen de la prévisualisation avant la fusion
  • Déployer uniquement lors de la fusion vers main

Références :


Référence rapide

Pratique Signal clé Correction
Pas de ressources dans apply new Resource() à l'intérieur d'.apply() Déplacer la ressource à l'extérieur, passer l'Output directement
Passer les outputs directement Valeurs extraites utilisées comme inputs Utiliser les objets Output, pulumi.interpolate
Utiliser les composants Structure plate, motifs répétés Créer les classes ComponentResource
Définir parent: this Enfants du composant au niveau racine Passer { parent: this } à toutes les ressources enfants
Secrets dès le départ Mots de passe/clés en texte clair dans la config Utiliser le drapeau --secret, ESC
Aliases lors de la refactorisation Delete+create dans la prévisualisation Ajouter un alias avec ancien nom/parent
Prévisualiser avant déploiement pulumi up --yes Toujours exécuter pulumi preview d'abord

Checklist de validation

Lors de la révision du code Pulumi, vérifiez :

  • [ ] Aucun constructeur de ressource à l'intérieur des callbacks apply()
  • [ ] Les outputs passés directement aux ressources dépendantes
  • [ ] Les ressources liées regroupées dans les classes ComponentResource
  • [ ] Les ressources enfants ont { parent: this }
  • [ ] Les valeurs sensibles utilisent config.requireSecret() ou --secret
  • [ ] Les ressources refactorisées ont des aliases préservant l'identité
  • [ ] Le processus de déploiement inclut l'étape de prévisualisation

Compétences liées

  • pulumi-component : Guide approfondi pour l'authoring des classes ComponentResource, conception des interfaces d'args, support multi-langage, tests et distribution. Utilisez la compétence pulumi-component.
  • pulumi-automation-api : Orchestration programmatique de plusieurs stacks. Utilisez la compétence pulumi-automation-api.
  • pulumi-esc : Gestion centralisée des secrets et de la configuration. Utilisez la compétence pulumi-esc.

Skills similaires