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://mp-staging.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
LICENSE
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
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.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download pulumi-best-practices.zip