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 awaitsur 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
ComponentResourceOptionspour 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 :
- Ajouter l'alias lors de la refactorisation
- Exécuter
pulumi upsur tous les stacks - 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 --yesinteractivement 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
replaceinattendues (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 :
- https://www.pulumi.com/docs/cli/commands/pulumi_preview/
- https://www.pulumi.com/docs/iac/packages-and-automation/continuous-delivery/github-actions/
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.