$npx -y skills add pulumi/agent-skills --skill pulumi-best-practicesLoad when the user is writing, reviewing, or debugging Pulumi TypeScript/Python programs; asks about Output<T> or apply() usage; wants to create ComponentResource classes; needs to refactor resources without destroying them (aliases); is setting up secrets or config; or is config
| 1 | # Pulumi Best Practices |
| 2 | |
| 3 | ## When to Use This Skill |
| 4 | |
| 5 | Invoke this skill when: |
| 6 | |
| 7 | - Writing new Pulumi programs or components |
| 8 | - Reviewing Pulumi code for correctness |
| 9 | - Refactoring existing Pulumi infrastructure |
| 10 | - Debugging resource dependency issues |
| 11 | - Setting up configuration and secrets |
| 12 | |
| 13 | ## Practices |
| 14 | |
| 15 | ### 1. Never Create Resources Inside `apply()` |
| 16 | |
| 17 | **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. |
| 18 | |
| 19 | **Detection signals**: |
| 20 | |
| 21 | - `new aws.` or other resource constructors inside `.apply()` callbacks |
| 22 | - Resource creation inside `pulumi.all([...]).apply()` |
| 23 | - Dynamic resource counts determined at runtime inside apply |
| 24 | |
| 25 | **Wrong**: |
| 26 | |
| 27 | ```typescript |
| 28 | const bucket = new aws.s3.Bucket("bucket"); |
| 29 | |
| 30 | bucket.id.apply(bucketId => { |
| 31 | // WRONG: This resource won't appear in preview |
| 32 | new aws.s3.BucketObject("object", { |
| 33 | bucket: bucketId, |
| 34 | content: "hello", |
| 35 | }); |
| 36 | }); |
| 37 | ``` |
| 38 | |
| 39 | **Right**: |
| 40 | |
| 41 | ```typescript |
| 42 | const bucket = new aws.s3.Bucket("bucket"); |
| 43 | |
| 44 | // Pass the output directly - Pulumi handles the dependency |
| 45 | const object = new aws.s3.BucketObject("object", { |
| 46 | bucket: bucket.id, // Output<string> works here |
| 47 | content: "hello", |
| 48 | }); |
| 49 | ``` |
| 50 | |
| 51 | **When apply is appropriate**: |
| 52 | |
| 53 | - Transforming output values for use in tags, names, or computed strings |
| 54 | - Logging or debugging (not resource creation) |
| 55 | - Conditional logic that affects resource properties, not resource existence |
| 56 | |
| 57 | **Reference**: https://www.pulumi.com/docs/concepts/inputs-outputs/ |
| 58 | |
| 59 | --- |
| 60 | |
| 61 | ### 2. Pass Outputs Directly as Inputs |
| 62 | |
| 63 | **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. |
| 64 | |
| 65 | **Detection signals**: |
| 66 | |
| 67 | - Variables extracted from `.apply()` used later as resource inputs |
| 68 | - `await` on output values outside of apply |
| 69 | - String concatenation with outputs instead of `pulumi.interpolate` |
| 70 | |
| 71 | **Wrong**: |
| 72 | |
| 73 | ```typescript |
| 74 | const vpc = new aws.ec2.Vpc("vpc", { cidrBlock: "10.0.0.0/16" }); |
| 75 | |
| 76 | // WRONG: Extracting the value breaks the dependency chain |
| 77 | let vpcId: string; |
| 78 | vpc.id.apply(id => { vpcId = id; }); |
| 79 | |
| 80 | const subnet = new aws.ec2.Subnet("subnet", { |
| 81 | vpcId: vpcId, // May be undefined, no tracked dependency |
| 82 | cidrBlock: "10.0.1.0/24", |
| 83 | }); |
| 84 | ``` |
| 85 | |
| 86 | **Right**: |
| 87 | |
| 88 | ```typescript |
| 89 | const vpc = new aws.ec2.Vpc("vpc", { cidrBlock: "10.0.0.0/16" }); |
| 90 | |
| 91 | const subnet = new aws.ec2.Subnet("subnet", { |
| 92 | vpcId: vpc.id, // Pass the Output directly |
| 93 | cidrBlock: "10.0.1.0/24", |
| 94 | }); |
| 95 | ``` |
| 96 | |
| 97 | **For string interpolation**: |
| 98 | |
| 99 | ```typescript |
| 100 | // WRONG |
| 101 | const name = bucket.id.apply(id => `prefix-${id}-suffix`); |
| 102 | |
| 103 | // RIGHT - use pulumi.interpolate for template literals |
| 104 | const name = pulumi.interpolate`prefix-${bucket.id}-suffix`; |
| 105 | |
| 106 | // RIGHT - use pulumi.concat for simple concatenation |
| 107 | const name = pulumi.concat("prefix-", bucket.id, "-suffix"); |
| 108 | ``` |
| 109 | |
| 110 | **Reference**: https://www.pulumi.com/docs/concepts/inputs-outputs/ |
| 111 | |
| 112 | --- |
| 113 | |
| 114 | ### 3. Use Components for Related Resources |
| 115 | |
| 116 | **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. |
| 117 | |
| 118 | **Detection signals**: |
| 119 | |
| 120 | - Multiple related resources created at top level without grouping |
| 121 | - Repeated resource patterns across stacks that should be abstracted |
| 122 | - Hard to understand resource relationships from the Pulumi console |
| 123 | |
| 124 | **Wrong**: |
| 125 | |
| 126 | ```typescript |
| 127 | // Flat structure - no logical grouping, hard to reuse |
| 128 | const bucket = new aws.s3.Bucket("app-bucket"); |
| 129 | const bucketPolicy = new aws.s3.BucketPolicy("app-bucket-policy", { |
| 130 | bucket: bucket.id, |
| 131 | policy: policyDoc, |
| 132 | }); |
| 133 | const originAccessIdentity = new aws.cloudfront.OriginAccessIdentity("app-oai"); |
| 134 | const distribution = new aws.cloudfront.Distribution("app-cdn", { /* ... */ }); |
| 135 | ``` |
| 136 | |
| 137 | **Right**: |
| 138 | |
| 139 | ```typescript |
| 140 | interface StaticSiteArgs { |
| 141 | domain: string; |
| 142 | content: pulumi.asset.AssetArchive; |
| 143 | } |
| 144 | |
| 145 | class StaticSite extends pulumi.ComponentResource { |
| 146 | public readonly url: pulumi.Output<string>; |
| 147 | |
| 148 | constructor(name: string, args: StaticSiteArgs, opts?: pulumi.ComponentResourceOptions) { |
| 149 | super("myorg:components:StaticSite", name, args, opts); |
| 150 | |
| 151 | // |