pulumi-best-practices

作者: pulumi

编写可靠、可维护的Pulumi基础设施代码的全面最佳实践。避免在apply()回调中创建资源;直接将Output对象作为输入传递,以保留依赖跟踪和预览可见性。使用ComponentResource类将相关资源分组为可复用的逻辑单元,并通过parent: this建立正确的父子层级。从一开始就使用--secret标志或config.requireSecret()加密机密,防止状态文件中泄露凭据...

npx skills add https://github.com/pulumi/agent-skills --skill pulumi-best-practices

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() callbacks
  • Resource 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 inputs
  • await on output values outside of apply
  • String 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:ComponentName
  • Call registerOutputs() at the end of the constructor
  • Expose outputs as class properties for consumers
  • Accept ComponentResourceOptions to 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 resources
  • Resources 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-1234567890

Right:

# Encrypted from the start
pulumi config set --secret databasePassword hunter2
pulumi config set --secret apiKey sk-1234567890

In 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:

  1. Add alias during refactor
  2. Run pulumi up on all stacks
  3. Remove 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 --yes interactively without reviewing changes
  • No 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 --yes

Right:

# Always preview first
pulumi preview

# Review the output, then deploy
pulumi up

What 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 replace operations (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: production

PR 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

PracticeKey SignalFix
No resources in applynew Resource() inside .apply()Move resource outside, pass Output directly
Pass outputs directlyExtracted values used as inputsUse Output objects, pulumi.interpolate
Use componentsFlat structure, repeated patternsCreate ComponentResource classes
Set parent: thisComponent children at root levelPass { parent: this } to all child resources
Secrets from day onePlaintext passwords/keys in configUse --secret flag, ESC
Aliases when refactoringDelete+create in previewAdd alias with old name/parent
Preview before deploypulumi up --yesAlways 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 do CLI, 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 skill pulumi-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.

来自 pulumi 的更多技能

package-usage
pulumi
追踪Pulumi组织中哪些堆栈使用了特定包及其版本。用于跨堆栈审计,识别过时或未维护的…
official
pulumi-automation-api
pulumi
跨多个堆栈和应用程序对Pulumi基础设施操作进行编程化编排。支持本地源(现有Pulumi项目)和内联源(嵌入式程序)架构,实现从简单到复杂多堆栈场景的灵活部署模式。处理具有依赖顺序的多堆栈编排、并行独立部署以及跨堆栈输出传递,以实现协调的基础设施配置。提供编程化...
official
pulumi-component
pulumi
可复用的基础设施组件,支持多语言、提供合理默认值并采用组合模式。需满足四个核心要素:继承ComponentResource、接收标准参数、为所有子资源设置parent: this、在构造函数末尾调用registerOutputs()。Args接口必须使用Input<T>包装器,避免联合类型和函数,保持结构扁平以支持多语言SDK生成。仅将必要输出暴露为公共属性;隐藏...
official
pulumi-debug-failed-operation
pulumi
调试失败的Pulumi更新或预览:读取Pulumi已记录的错误,找出原因并修复。当用户要求…时加载此技能。
official
pulumi-esc
pulumi
集中式机密、配置和动态凭据管理,适用于Pulumi基础设施和应用程序。支持通过导入和分层进行环境组合,包含环境变量、pulumiConfig和文件的保留键。通过OIDC为AWS、Azure和GCP生成短期凭据;与AWS Secrets Manager、Azure Key Vault、HashiCorp Vault和1Password集成。核心CLI命令包括pulumi env init、pulumi env edit、pulumi env open(显示...
official
pulumi-neo-handoff
pulumi
将当前线程单向移交给新的 Pulumi Neo 任务。当用户明确要求移交、发送、转移或继续当前…时使用。
official
pulumi-overview
pulumi
将此技能用于任何创建、修改、检查或销毁云基础设施或SaaS配置的任务,从一次性CLI操作到完整的…
official
pulumi-terraform-to-pulumi
pulumi
将Terraform/OpenTofu项目迁移到Pulumi,包括将HCL源代码转换和/或将Terraform状态导入到Pulumi堆栈中。当用户…
official