Pulumi Best Practices
PulumiOfficialPublished by Pulumi
- Skill
- Guidelines
- Cloud and DevOps
- Apache-2.0
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a comprehensive set of architectural and operational guidelines for Pulumi developers. It focuses on maintaining a clean, predictable resource graph by enforcing correct input/output handling, logical resource grouping through components, and safe refactoring techniques. It also covers critical operational habits such as secret management and mandatory preview workflows to prevent accidental infrastructure destruction.
What it does
- Explains how to correctly handle Pulumi Output objects and avoid resource creation inside apply blocks.
- Provides patterns for grouping related resources into reusable ComponentResource classes.
- Details the use of aliases to prevent resource destruction during refactoring or renaming.
- Outlines secure practices for managing secrets and configuration from the start of a project.
- Defines the mandatory workflow of reviewing previews before executing infrastructure updates.
- Offers a validation checklist for code reviews to ensure infrastructure stability.
When to use it
- When writing new Pulumi programs or designing custom components.
- During code reviews to ensure infrastructure correctness and maintainability.
- When refactoring existing infrastructure to avoid unintended resource replacement.
- When debugging dependency order issues or unexpected deployment behaviors.
- When setting up CI/CD pipelines for infrastructure deployment.
Advantages
- Prevents common pitfalls like race conditions and accidental resource deletion.
- Improves code reusability and organization through logical component structures.
- Ensures infrastructure state remains consistent and predictable across updates.
- Reduces security risks by promoting secret encryption from day one.
- Provides clear, actionable patterns for complex infrastructure tasks.
Usage examples
- “How do I pass a bucket ID to a policy without using apply?”
- The agent explains that you should pass the Output object directly to the resource constructor, allowing Pulumi to manage the dependency graph automatically.
- “I need to rename my S3 bucket but I am afraid it will delete my data.”
- The agent guides you to use the aliases option in the resource constructor to preserve the resource identity during the rename.
- “How should I structure my code for a reusable web server component?”
- The agent provides a template for creating a class that extends pulumi.ComponentResource, including proper parent assignment and output registration.
- “Why is my Pulumi preview showing that my resources will be replaced?”
- The agent helps you identify if you are changing immutable properties or if you need to apply aliases to maintain resource continuity.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/pulumi/pulumi-best-practices/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip pulumi-best-practices.zip -d .agents/skills
Documentation by Pulumi
Pulumi Best Practices
When to Use This Skill
Invoke this skill when:
Writing new Pulumi programs or components
Reviewing Pulumi code for correctness
Refactoring existing Pulumi infrastructure
Debugging resource dependency issues
Setting up configuration and secrets
Practices
1. Never Create Resources Inside apply()
Why: Resources created inside apply() don't appear in pulumi preview, making changes unpredictable. Pulumi cannot properly track dependencies, leading to race conditions and deployment failures.
Detection signals:
new aws.or other resource constructors inside.apply()callbacksResource creation inside
pulumi.all([...]).apply()Dynamic resource counts determined at runtime inside apply
Wrong:
const bucket = new aws.s3.Bucket("bucket");
bucket.id.apply(bucketId => {
// WRONG: This resource won't appear in preview
new aws.s3.BucketObject("object", {
bucket: bucketId,
content: "hello",
});
});Right:
const bucket = new aws.s3.Bucket("bucket");
// Pass the output directly - Pulumi handles the dependency
const object = new aws.s3.BucketObject("object", {
bucket: bucket.id, // Output<string> works here
content: "hello",
});When apply is appropriate:
Transforming output values for use in tags, names, or computed strings
Logging or debugging (not resource creation)
Conditional logic that affects resource properties, not resource existence
Reference: https://www.pulumi.com/docs/concepts/inputs-outputs/
2. Pass Outputs Directly as Inputs
Why: Pulumi builds a directed acyclic graph (DAG) based on input/output relationships. Passing outputs directly ensures correct creation order. Unwrapping values manually breaks the dependency chain, causing resources to deploy in wrong order or reference values that don't exist yet.
Detection signals:
Variables extracted from
.apply()used later as resource inputsawaiton output values outside of applyString concatenation with outputs instead of
pulumi.interpolate
Wrong:
const vpc = new aws.ec2.Vpc("vpc", { cidrBlock: "10.0.0.0/16" });
// WRONG: Extracting the value breaks the dependency chain
let vpcId: string;
vpc.id.apply(id => { vpcId = id; });
const subnet = new aws.ec2.Subnet("subnet", {
vpcId: vpcId, // May be undefined, no tracked dependency
cidrBlock: "10.0.1.0/24",
});Right:
const vpc = new aws.ec2.Vpc("vpc", { cidrBlock: "10.0.0.0/16" });
const subnet = new aws.ec2.Subnet("subnet", {
vpcId: vpc.id, // Pass the Output directly
cidrBlock: "10.0.1.0/24",
});For string interpolation:
// WRONG
const name = bucket.id.apply(id => `prefix-${id}-suffix`);
// RIGHT - use pulumi.interpolate for template literals
const name = pulumi.interpolate`prefix-${bucket.id}-suffix`;
// RIGHT - use pulumi.concat for simple concatenation
const name = pulumi.concat("prefix-", bucket.id, "-suffix");Reference: https://www.pulumi.com/docs/concepts/inputs-outputs/
3. Use Components for Related Resources
Why: ComponentResource classes group related resources into reusable, logical units. Without components, your resource graph is flat, making it hard to understand which resources belong together, reuse patterns across stacks, or reason about your infrastructure at a higher level.
Detection signals:
Multiple related resources created at top level without grouping
Repeated resource patterns across stacks that should be abstracted
Hard to understand resource relationships from the Pulumi console
Wrong:
// Flat structure - no logical grouping, hard to reuse
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", { /* ... */ });Right:
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);
// Resources created here - see practice 4 for parent setup
const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });
// ...
this.url = distribution.domainName;
this.registerOutputs({ url: this.url });
}
}
// Reusable across stacks
const site = new StaticSite("marketing", {
domain: "marketing.example.com",
content: new pulumi.asset.FileArchive("./dist"),
});Component best practices:
Use a consistent type URN pattern:
organization:module:ComponentNameCall
registerOutputs()at the end of the constructorExpose outputs as class properties for consumers
Accept
ComponentResourceOptionsto allow callers to set providers, aliases, etc.
For in-depth component authoring guidance (args design, multi-language support, testing, distribution), use skill pulumi-component.
Reference: https://www.pulumi.com/docs/concepts/resources/components/
4. Always Set parent: this in Components
Why: When you create resources inside a ComponentResource without setting parent: this, those resources appear at the root level of your stack's state. This breaks the logical hierarchy, makes the Pulumi console hard to navigate, and can cause issues with aliases and refactoring. The parent relationship is what makes the component actually group its children.
Detection signals:
ComponentResource classes that don't pass
{ parent: this }to child resourcesResources inside a component appearing at root level in the console
Unexpected behavior when adding aliases to components
Wrong:
class MyComponent extends pulumi.ComponentResource {
constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
super("myorg:components:MyComponent", name, {}, opts);
// WRONG: No parent set - this bucket appears at root level
const bucket = new aws.s3.Bucket(`${name}-bucket`);
}
}Right:
class MyComponent extends pulumi.ComponentResource {
constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
super("myorg:components:MyComponent", name, {}, opts);
// RIGHT: Parent establishes hierarchy
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
});
}
}What parent: this provides:
Resources appear nested under the component in Pulumi console
Deleting the component deletes all children
Aliases on the component automatically apply to children
Clear ownership in state files
Reference: https://www.pulumi.com/docs/concepts/resources/components/
5. Encrypt Secrets from Day One
Why: Secrets marked with --secret are encrypted in state files, masked in CLI output, and tracked through transformations. Starting with plaintext config and converting later requires credential rotation, reference updates, and audit of leaked values in logs and state history.
Detection signals:
Passwords, API keys, tokens stored as plain config
Connection strings with embedded credentials
Private keys or certificates in plaintext
Wrong:
# Plaintext - will be visible in state and logs
pulumi config set databasePassword hunter2
pulumi config set apiKey sk-1234567890Right:
# Encrypted from the start
pulumi config set --secret databasePassword hunter2
pulumi config set --secret apiKey sk-1234567890In code:
const config = new pulumi.Config();
// This retrieves a secret - the value stays encrypted
const dbPassword = config.requireSecret("databasePassword");
// Creating outputs from secrets preserves secrecy
const connectionString = pulumi.interpolate`postgres://user:${dbPassword}@host/db`;
// connectionString is also a secret Output
// Explicitly mark values as secret
const computed = pulumi.secret(someValue);Use Pulumi ESC for centralized secrets:
# Pulumi.yaml
environment:
- production-secrets # Pull from ESC environment# ESC manages secrets centrally across stacks
esc env set production-secrets db.password --secret "hunter2"What qualifies as a secret:
Passwords and passphrases
API keys and tokens
Private keys and certificates
Connection strings with credentials
OAuth client secrets
Encryption keys
References:
6. Use Aliases When Refactoring
Why: Renaming resources, moving them into components, or changing parents causes Pulumi to see them as new resources. Without aliases, refactoring destroys and recreates resources, potentially causing downtime or data loss. Aliases preserve resource identity through refactors.
Detection signals:
Resource rename without alias
Moving resource into or out of a ComponentResource
Changing the parent of a resource
Preview shows delete+create when update was intended
Wrong:
// Before: resource named "my-bucket"
const bucket = new aws.s3.Bucket("my-bucket");
// After: renamed without alias - DESTROYS THE BUCKET
const bucket = new aws.s3.Bucket("application-bucket");Right:
// After: renamed with alias - preserves the existing bucket
const bucket = new aws.s3.Bucket("application-bucket", {}, {
aliases: [{ name: "my-bucket" }],
});Moving into a component:
// Before: top-level resource
const bucket = new aws.s3.Bucket("my-bucket");
// After: inside a component - needs alias with old 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, // Was at root
}],
});
}
}Alias types:
// Simple name change
aliases: [{ name: "old-name" }]
// Parent change
aliases: [{ name: "resource-name", parent: oldParent }]
// Full URN (when you know the exact previous URN)
aliases: ["urn:pulumi:stack::project::aws:s3/bucket:Bucket::old-name"]Lifecycle:
Add alias during refactor
Run
pulumi upon all stacksRemove alias after all stacks updated (optional, but keeps code clean)
Reference: https://www.pulumi.com/docs/iac/concepts/resources/options/aliases/
7. Preview Before Every Deployment
Why: pulumi preview shows exactly what will be created, updated, or destroyed. Surprises in production come from skipping preview. A resource showing "replace" when you expected "update" means imminent destruction and recreation.
Detection signals:
Running
pulumi up --yesinteractively without reviewing changesNo preview step anywhere in the CI/CD workflow for a given change
Preview output not reviewed before merge or deployment approval
Wrong:
# Deploying blind
pulumi up --yesRight:
# Always preview first
pulumi preview
# Review the output, then deploy
pulumi upWhat to look for in preview:
+ create- New resource will be created~ update- Existing resource will be modified in place- delete- Resource will be destroyed+-replace- Resource will be destroyed and recreated (potential downtime)~+-replace- Resource will be updated, then replaced
Warning signs:
Unexpected
replaceoperations (check for immutable property changes)Resources being deleted that shouldn't be
More changes than expected from your code diff
CI/CD integration:
# GitHub Actions example
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: productionPR workflow:
Run preview on every PR
Post preview output as PR comment
Require preview review before merge
Deploy only on merge to main
References:
Quick Reference
| Practice | Key Signal | Fix |
|---|---|---|
| No resources in apply | new Resource() inside .apply() | Move resource outside, pass Output directly |
| Pass outputs directly | Extracted values used as inputs | Use Output objects, pulumi.interpolate |
| Use components | Flat structure, repeated patterns | Create ComponentResource classes |
| Set parent: this | Component children at root level | Pass { parent: this } to all child resources |
| Secrets from day one | Plaintext passwords/keys in config | Use --secret flag, ESC |
| Aliases when refactoring | Delete+create in preview | Add alias with old name/parent |
| Preview before deploy | pulumi up --yes | Always run pulumi preview first |
Validation Checklist
When reviewing Pulumi code, verify:
[ ] No resource constructors inside
apply()callbacks[ ] Outputs passed directly to dependent resources
[ ] Related resources grouped in ComponentResource classes
[ ] Child resources have
{ parent: this }[ ] Sensitive values use
config.requireSecret()or--secret[ ] Refactored resources have aliases preserving identity
[ ] Deployment process includes preview step
Related Skills
pulumi-overview: Entry-point skill that orients an agent across the three Pulumi surfaces (
pulumi doCLI, IaC projects, and Pulumi Cloud) and routes to specialized skills. Load it first when the task begins with general infrastructure phrasing or spans multiple Pulumi surfaces. Use skillpulumi-overview.pulumi-component: Deep guide to authoring ComponentResource classes, designing args interfaces, multi-language support, testing, and distribution. Use skill
pulumi-component.pulumi-automation-api: Programmatic orchestration of multiple stacks. Use skill
pulumi-automation-api.pulumi-esc: Centralized secrets and configuration management. Use skill
pulumi-esc.
Files
- ATTRIBUTION.mdAdded by AutoGineer873 B
- LICENSELicence file added by AutoGineer11.1 KiB
- SKILL.md15.4 KiB
- agents/openai.yaml218 B
- use_cases.yaml380 B
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs an API key
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Pulumi
- Publisher
- Pulumi (Official)
- Source
- pulumi/agent-skills · pulumi/skills/pulumi-best-practices · commit 82f222a
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Pulumi or Pulumi.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download pulumi-best-practices.zip