Loading...
Loading...
Best practices for writing reliable Pulumi programs. Covers Output handling, resource dependencies, component structure, secrets management, safe refactoring with aliases, and deployment workflows.
npx skill4agent add pulumi/agent-skills pulumi-best-practicesapply()apply()pulumi previewnew aws..apply()pulumi.all([...]).apply()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",
});
});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",
});.apply()awaitpulumi.interpolateconst 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",
});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",
});// 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");// 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", { /* ... */ });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"),
});organization:module:ComponentNameregisterOutputs()ComponentResourceOptionspulumi-componentparent: thisparent: this{ parent: this }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`);
}
}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
});
}
}--secret# Plaintext - will be visible in state and logs
pulumi config set databasePassword hunter2
pulumi config set apiKey sk-1234567890# Encrypted from the start
pulumi config set --secret databasePassword hunter2
pulumi config set --secret apiKey sk-1234567890const 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);# Pulumi.yaml
environment:
- production-secrets # Pull from ESC environment# ESC manages secrets centrally across stacks
esc env set production-secrets db.password --secret "hunter2"// 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");// After: renamed with alias - preserves the existing bucket
const bucket = new aws.s3.Bucket("application-bucket", {}, {
aliases: [{ name: "my-bucket" }],
});// 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
}],
});
}
}// 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"]pulumi uppulumi previewpulumi up --yes# Deploying blind
pulumi up --yes# Always preview first
pulumi preview
# Review the output, then deploy
pulumi up+ create~ update- delete+-replace~+-replacereplace# 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: production| Practice | Key Signal | Fix |
|---|---|---|
| No resources in apply | | Move resource outside, pass Output directly |
| Pass outputs directly | Extracted values used as inputs | Use Output objects, |
| Use components | Flat structure, repeated patterns | Create ComponentResource classes |
| Set parent: this | Component children at root level | Pass |
| Secrets from day one | Plaintext passwords/keys in config | Use |
| Aliases when refactoring | Delete+create in preview | Add alias with old name/parent |
| Preview before deploy | | Always run |
apply(){ parent: this }config.requireSecret()--secretpulumi-componentpulumi-automation-apipulumi-esc