mcpField
P
pulumi-component

pulumi-component

github.com/pulumi云与 DevOps781.5k 次安装3 周前更新
代码仓库

关于 pulumi-component

为 Pulumi ComponentResource 编写提供完整指导,面向构建和维护基础设施组件的开发者。内容涵盖组件结构、参数与输出设计,以及多语言打包。

主要功能

  1. 01组件结构与生命周期
  2. 02参数接口设计
  3. 03输出与资源组合
  4. 04多语言组件打包

适用场景

  1. 01创建可复用的基础设施组件
  2. 02将内联资源重构为组件
  3. 03为多语言使用者打包组件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/pulumi/agent-skills --skill pulumi-component

安装后在 Claude Code 中输入/pulumi-component确认已加载。

附带文件agents/openai.yamluse_cases.yaml

详细介绍

编写 Pulumi 组件

ComponentResource 将相关基础设施资源组合成一个可复用的逻辑单元。组件让基础设施更易于理解、复用和维护。在 pulumi preview/pulumi up 的输出和 Pulumi Cloud 控制台中,组件会显示为一个节点,其子节点嵌套在下方。

本技能涵盖组件编写的完整生命周期。如需了解通用的 Pulumi 编码模式(Output 处理、密钥、别名、预览工作流),请改用 pulumi-best-practices 技能。

何时使用此技能

在以下情况下调用此技能:

  • 创建新的 ComponentResource 类
  • 设计组件的 args 接口
  • 让组件可供多种 Pulumi 语言使用
  • 发布或分发组件包
  • 将内联资源重构为可复用组件
  • 调试组件行为(缺少输出、创建卡住、子资源层级错误)

组件结构

每个组件都包含四个必需元素:

  1. 扩展 ComponentResource,并使用类型 URN 调用 super()
  2. 接受标准参数:name、args 和 ComponentResourceOptions
  3. 为所有子资源设置 parent: this
  4. 在构造函数末尾调用 registerOutputs()
TypeScript
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

interface StaticSiteArgs {
    indexDocument?: pulumi.Input<string>;
    errorDocument?: pulumi.Input<string>;
}

class StaticSite extends pulumi.ComponentResource {
    public readonly bucketName: pulumi.Output<string>;
    public readonly websiteUrl: pulumi.Output<string>;

    constructor(name: string, args: StaticSiteArgs, opts?: pulumi.ComponentResourceOptions) {
        // 1. Call super with type URN: <package>:<module>:<type>
        super("myorg:index:StaticSite", name, {}, opts);

        // 2. Create child resources with parent: this
        const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });

        const website = new aws.s3.BucketWebsiteConfigurationV2(`${name}-website`, {
            bucket: bucket.id,
            indexDocument: { suffix: args.indexDocument ?? "index.html" },
            errorDocument: { key: args.errorDocument ?? "error.html" },
        }, { parent: this });

        // 3. Expose outputs as class properties
        this.bucketName = bucket.id;
        this.websiteUrl = website.websiteEndpoint;

        // 4. Register outputs -- always the last line
        this.registerOutputs({
            bucketName: this.bucketName,
            websiteUrl: this.websiteUrl,
        });
    }
}

// Usage
const site = new StaticSite("marketing", {
    indexDocument: "index.html",
});
export const url = site.websiteUrl;
Python
import pulumi
import pulumi_aws as aws

class StaticSiteArgs:
    def __init__(self,
                 index_document: pulumi.Input[str] = "index.html",
                 error_document: pulumi.Input[str] = "error.html"):
        self.index_document = index_document
        self.error_document = error_document

class StaticSite(pulumi.ComponentResource):
    bucket_name: pulumi.Output[str]
    website_url: pulumi.Output[str]

    def __init__(self, name: str, args: StaticSiteArgs,
                 opts: pulumi.ResourceOptions = None):
        super().__init__("myorg:index:StaticSite", name, None, opts)

        bucket = aws.s3.Bucket(f"{name}-bucket",
            opts=pulumi.ResourceOptions(parent=self))

        website = aws.s3.BucketWebsiteConfigurationV2(f"{name}-website",
            bucket=bucket.id,
            index_document=aws.s3.BucketWebsiteConfigurationV2IndexDocumentArgs(
                suffix=args.index_document,
            ),
            error_document=aws.s3.BucketWebsiteConfigurationV2ErrorDocumentArgs(
                key=args.error_document,
            ),
            opts=pulumi.ResourceOptions(parent=self))

        self.bucket_name = bucket.id
        self.website_url = website.website_endpoint

        self.register_outputs({
            "bucket_name": self.bucket_name,
            "website_url": self.website_url,
        })

site = StaticSite("marketing", StaticSiteArgs())
pulumi.export("url", site.website_url)
类型 URN 格式

super() 的第一个参数是类型 URN:<package>:<module>:<type>。

部分约定示例
package组织或包名称myorg、acme、pkg
module通常为 indexindex
typePascalCase 类名StaticSite、VpcNetwork

完整示例:myorg:index:StaticSite、acme:index:KubernetesCluster

必须调用 registerOutputs

原因:如果不调用 registerOutputs(),组件在 Pulumi 控制台中会一直处于“创建中”状态,且输出不会持久化到状态中。

错误示例:

class MyComponent extends pulumi.ComponentResource {
    public readonly url: pulumi.Output<string>;

    constructor(name: string, args: MyArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:MyComponent", name, {}, opts);
        const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });
        this.url = bucket.bucketRegionalDomainName;
        // Missing registerOutputs -- component stuck "creating"
    }
}

正确示例:

class MyComponent extends pulumi.ComponentResource {
    public readonly url: pulumi.Output<string>;

    constructor(name: string, args: MyArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:MyComponent", name, {}, opts);
        const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });
        this.url = bucket.bucketRegionalDomainName;

        this.registerOutputs({ url: this.url });
    }
}
根据组件名称派生子资源名称

原因:硬编码的子资源名称会在多次实例化组件时造成冲突。

错误示例:

// Collides if two instances of this component exist
const bucket = new aws.s3.Bucket("my-bucket", {}, { parent: this });

正确示例:

// Unique per component instance
const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });

设计 args 接口

args 接口是影响最大的设计决策。它决定了使用者可以配置哪些内容,以及组件的可组合性。

将属性包装在 Input 中

原因:Input<T> 同时接受普通值和来自其他资源的 Output<T>。如果不使用它,使用者就必须通过 .apply() 手动解包输出。

错误示例:

interface WebServiceArgs {
    port: number;            // Forces consumers to unwrap Outputs
    vpcId: string;           // Cannot accept vpc.id directly
}

正确示例:

interface WebServiceArgs {
    port: pulumi.Input<number>;     // Accepts 8080 or someOutput
    vpcId: pulumi.Input<string>;    // Accepts "vpc-123" or vpc.id
}
保持结构扁平

避免使用深层嵌套的 arg 对象。扁平接口更易于使用和扩展。

// Prefer flat
interface DatabaseArgs {
    instanceClass: pulumi.Input<string>;
    storageGb: pulumi.Input<number>;
    enableBackups?: pulumi.Input<boolean>;
    backupRetentionDays?: pulumi.Input<number>;
}

// Avoid deep nesting
interface DatabaseArgs {
    instance: {
        compute: { class: pulumi.Input<string> };
        storage: { sizeGb: pulumi.Input<number> };
    };
    backup: {
        config: { enabled: pulumi.Input<boolean>; retention: pulumi.Input<number> };
    };
}
不使用联合类型

联合类型会破坏多语言 SDK 生成。Python、Go 和 C# 无法表示 string | number。

错误示例:

interface MyArgs {
    port: pulumi.Input<string | number>;  // Fails in Python, Go, C#
}

正确示例:

interface MyArgs {
    port: pulumi.Input<number>;  // Single type, works everywhere
}

如果需要接受多种形式,请使用单独的可选属性:

interface StorageArgs {
    sizeGb?: pulumi.Input<number>;      // Specify size in GB
    sizeMb?: pulumi.Input<number>;      // Or specify size in MB
}
不使用函数或回调

函数无法跨语言边界序列化。

错误示例:

interface MyArgs {
    nameTransform: (name: string) => string;  // Cannot serialize
}

正确示例:

interface MyArgs {
    namePrefix?: pulumi.Input<string>;   // Configuration instead of callback
    nameSuffix?: pulumi.Input<string>;
}
为可选属性设置默认值

在构造函数中设置合理的默认值,让使用者只需配置所需内容:

interface SecureBucketArgs {
    enableVersioning?: pulumi.Input<boolean>;   // Defaults to true
    enableEncryption?: pulumi.Input<boolean>;   // Defaults to true
    blockPublicAccess?: pulumi.Input<boolean>;  // Defaults to true
}

class SecureBucket extends pulumi.ComponentResource {
    constructor(name: string, args: SecureBucketArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:SecureBucket", name, {}, opts);

        const enableVersioning = args.enableVersioning ?? true;
        const enableEncryption = args.enableEncryption ?? true;
        const blockPublicAccess = args.blockPublicAccess ?? true;

        // Apply defaults...
    }
}

// Consumer only overrides what they need
const bucket = new SecureBucket("data", { enableVersioning: false });

暴露输出

只暴露使用者需要的内容

组件通常会创建许多内部资源。只暴露使用者需要的值,而不是每个内部资源。

错误示例:

class Database extends pulumi.ComponentResource {
    // Exposes everything -- consumers see implementation details
    public readonly cluster: aws.rds.Cluster;
    public readonly primaryInstance: aws.rds.ClusterInstance;
    public readonly replicaInstance: aws.rds.ClusterInstance;
    public readonly subnetGroup: aws.rds.SubnetGroup;
    public readonly securityGroup: aws.ec2.SecurityGroup;
    public readonly parameterGroup: aws.rds.ClusterParameterGroup;
    // ...
}

正确示例:

class Database extends pulumi.ComponentResource {
    // Exposes only what consumers need
    public readonly endpoint: pulumi.Output<string>;
    public readonly port: pulumi.Output<number>;
    public readonly securityGroupId: pulumi.Output<string>;

    constructor(name: string, args: DatabaseArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:Database", name, {}, opts);

        const sg = new aws.ec2.SecurityGroup(`${name}-sg`, { /* ... */ }, { parent: this });
        const cluster = new aws.rds.Cluster(`${name}-cluster`, { /* ... */ }, { parent: this });

        this.endpoint = cluster.endpoint;
        this.port = cluster.port;
        this.securityGroupId = sg.id;

        this.registerOutputs({
            endpoint: this.endpoint,
            port: this.port,
            securityGroupId: this.securityGroupId,
        });
    }
}
派生组合输出

使用 pulumi.interpolate 或 pulumi.concat 构建派生值:

this.connectionString = pulumi.interpolate`postgresql://${args.username}:${args.password}@${cluster.endpoint}:${cluster.port}/${args.databaseName}`;

this.registerOutputs({ connectionString: this.connectionString });

组件设计模式

合理的默认值与覆盖

将最佳实践设为默认值。允许使用者在有特定需求时覆盖默认值。

interface SecureBucketArgs {
    enableVersioning?: pulumi.Input<boolean>;
    enableEncryption?: pulumi.Input<boolean>;
    blockPublicAccess?: pulumi.Input<boolean>;
    tags?: pulumi.Input<Record<string, pulumi.Input<string>>>;
}

class SecureBucket extends pulumi.ComponentResource {
    public readonly bucketId: pulumi.Output<string>;
    public readonly arn: pulumi.Output<string>;

    constructor(name: string, args: SecureBucketArgs = {}, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:SecureBucket", name, {}, opts);

        const bucket = new aws.s3.Bucket(`${name}-bucket`, {
            tags: args.tags,
        }, { parent: this });

        // Versioning on by default
        if (args.enableVersioning !== false) {
            new aws.s3.BucketVersioningV2(`${name}-versioning`, {
                bucket: bucket.id,
                versioningConfiguration: { status: "Enabled" },
            }, { parent: this });
        }

        // Encryption on by default
        if (args.enableEncryption !== false) {
            new aws.s3.BucketServerSideEncryptionConfigurationV2(`${name}-encryption`, {
                bucket: bucket.id,
                rules: [{ applyServerSideEncryptionByDefault: { sseAlgorithm: "AES256" } }],
            }, { parent: this });
        }

        // Public access blocked by default
        if (args.blockPublicAccess !== false) {
            new aws.s3.BucketPublicAccessBlock(`${name}-public-access`, {
                bucket: bucket.id,
                blockPublicAcls: true,
                blockPublicPolicy: true,
                ignorePublicAcls: true,
                restrictPublicBuckets: true,
            }, { parent: this });
        }

        this.bucketId = bucket.id;
        this.arn = bucket.arn;
        this.registerOutputs({ bucketId: this.bucketId, arn: this.arn });
    }
}
有条件地创建资源

使用可选 args 控制是否创建子资源:

interface WebServiceArgs {
    image: pulumi.Input<string>;
    port: pulumi.Input<number>;
    enableMonitoring?: pulumi.Input<boolean>;
    alarmEmail?: pulumi.Input<string>;
}

class WebService extends pulumi.ComponentResource {
    constructor(name: string, args: WebServiceArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:WebService", name, {}, opts);

        const service = new aws.ecs.Service(`${name}-service`, {
            // ...service config...
        }, { parent: this });

        // Only create alarm infrastructure when monitoring is enabled
        if (args.enableMonitoring) {
            const topic = new aws.sns.Topic(`${name}-alerts`, {}, { parent: this });

            if (args.alarmEmail) {
                new aws.sns.TopicSubscription(`${name}-alert-email`, {
                    topic: topic.arn,
                    protocol: "email",
                    endpoint: args.alarmEmail,
                }, { parent: this });
            }

            new aws.cloudwatch.MetricAlarm(`${name}-cpu-alarm`, {
                // ...alarm config referencing service...
                alarmActions: [topic.arn],
            }, { parent: this });
        }

        this.registerOutputs({});
    }
}
组合

使用较低层级的组件构建较高层级的组件。每个层级只负责一个方面。

// Lower-level component
class VpcNetwork extends pulumi.ComponentResource {
    public readonly vpcId: pulumi.Output<string>;
    public readonly publicSubnetIds: pulumi.Output<string>[];
    public readonly privateSubnetIds: pulumi.Output<string>[];

    constructor(name: string, args: VpcNetworkArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:VpcNetwork", name, {}, opts);
        // ...create VPC, subnets, route tables...
        this.registerOutputs({ vpcId: this.vpcId });
    }
}

// Higher-level component that uses VpcNetwork
class Platform extends pulumi.ComponentResource {
    public readonly kubeconfig: pulumi.Output<string>;

    constructor(name: string, args: PlatformArgs, opts?: pulumi.ComponentResourceOptions) {
        super("myorg:index:Platform", name, {}, opts);

        // Compose lower-level components
        const network = new VpcNetwork(`${name}-network`, {
            cidrBlock: args.cidrBlock,
        }, { parent: this });

        const cluster = new aws.eks.Cluster(`${name}-cluster`, {
            vpcConfig: {
                subnetIds: network.privateSubnetIds,
            },
        }, { parent: this });

        this.kubeconfig = cluster.kubeconfig;
        this.registerOutputs({ kubeconfig: this.kubeconfig });
    }
}
提供程序透传

为多区域或多账户部署接受显式提供程序。ComponentResourceOptions 会自动将提供程序配置传递给子资源:

// Consumer passes a provider for a different region
const usWest = new aws.Provider("us-west", { region: "us-west-2" });
const site = new StaticSite("west-site", { indexDocument: "index.html" }, {
    providers: [usWest],
});

使用 { parent: this } 的子资源会自动继承提供程序。组件内部无需额外编写代码。


多语言组件

如果你的组件会由多种 Pulumi 语言(TypeScript、Python、Go、C#、Java、YAML)使用,请将其打包为多语言组件。

是否需要多语言?

请思考:“是否有人会使用与编写此组件不同的语言来使用它?”

单语言组件(无需打包):

  • 你的团队只使用一种语言,且组件仅在该代码库中使用
  • 组件仅供单个项目或 monorepo 内部使用
  • 无需 PulumiPlugin.yaml——只需直接导入类

多语言组件(必须打包):

  • 其他团队会使用不同语言来使用你的组件
  • 平台团队为可自行选择语言的开发者构建抽象
  • YAML 用户需要访问——即使你使用 TypeScript 编写,YAML 程序也需要多语言打包才能使用你的组件
  • 为组织构建共享组件库
  • 发布到 Pulumi 私有注册表或公共注册表是常见原因,但并非支持多语言所必需

常见错误:TypeScript 平台团队构建的组件只有 TypeScript 用户可以使用。如果应用开发者使用 Python 或 YAML,那么没有多语言打包的话,他们就无法使用这些组件。

设置

在组件目录中创建 PulumiPlugin.yaml,以声明运行时:

runtime: nodejs

或者使用 Python:

runtime: python
序列化限制

为确保多语言兼容性,参数必须可序列化。无论使用哪种语言编写,都须遵守以下限制:

允许不允许
string、number、boolean联合类型(string | number)
Input<T> 包装器函数和回调
基本类型的数组和映射复杂的嵌套泛型
枚举特定于平台的类型
使用多语言组件

使用者通过 pulumi package add 安装组件,该命令会自动下载提供程序插件、生成使用者所用语言的本地 SDK,并更新 Pulumi.yaml:

# From a Git repository
pulumi package add <git-repo-url>

# From a specific version tag
pulumi package add <git-repo-url>@v1.0.0

对于全新检出或 CI 环境,请运行 pulumi install,确保所有软件包依赖项均可用。使用者无需手动生成 SDK。

将 SDK 发布到软件包管理器(npm、PyPI 等)的作者可以选择使用 pulumi package gen-sdk 生成特定语言的 SDK 以供发布。大多数组件作者不需要这样做——pulumi package add 会在使用者端处理 SDK 生成。

入口点

已发布的多语言组件需要一个入口点来承载组件提供程序进程。不同语言的入口点模式各不相同。

TypeScript(runtime: nodejs):

从 index.ts 导出组件类。无需单独的入口点文件。Pulumi 会自动检查导出的类。

// index.ts -- exports are the entry point
export { StaticSite, StaticSiteArgs } from "./staticSite";
export { SecureBucket, SecureBucketArgs } from "./secureBucket";

Python(runtime: python):

创建一个 __main__.py,通过 component_provider_host 调用所有组件类:

from pulumi.provider.experimental import component_provider_host
from static_site import StaticSite
from secure_bucket import SecureBucket

if __name__ == "__main__":
    component_provider_host(
        name="my-components",
        components=[StaticSite, SecureBucket],
    )

Go(runtime: go):

创建一个 main.go 来构建并运行提供程序:

package main

import (
    "context"
    "fmt"
    "os"

    "github.com/pulumi/pulumi-go-provider/infer"
)

func main() {
    p, err := infer.NewProviderBuilder().
        WithComponents(
            infer.ComponentF(NewStaticSite),
            infer.ComponentF(NewSecureBucket),
        ).
        Build()
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    if err := p.Run(context.Background(), "my-components", "0.1.0"); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

C#(runtime: dotnet):

创建一个 Program.cs 来提供组件提供程序主机服务:

using System.Threading.Tasks;

class Program
{
    public static Task Main(string[] args) =>
        Pulumi.Experimental.Provider.ComponentProviderHost.Serve(args);
}

如需查看涵盖所有语言的完整可运行示例,请参阅 https://github.com/mikhailshilkov/comp-as-comp。

参考:https://www.pulumi.com/docs/iac/using-pulumi/pulumi-packages/


分发

根据受众选择分发方式:

受众方式操作
同一项目直接导入使用标准语言导入
同一组织私有注册表使用 pulumi package publish 发布到 Pulumi Cloud
同一组织Git 仓库使用 pulumi package add <repo> 并添加版本标签
语言生态系统包管理器发布到 npm、PyPI、NuGet 或 Maven
公共社区Pulumi Registry通过 pulumi/registry GitHub 仓库提交
Pulumi 私有注册表

私有注册表是组织组件的集中式目录。它为所有团队提供自动 API 文档、版本管理和可发现性。

将组件发布到私有注册表:

pulumi package publish https://github.com/myorg/my-component --publisher myorg

使用带 v 前缀的 git 标签为组件设置版本:

git tag v1.0.0
git push origin v1.0.0

发布时必须提供 README 文件。Pulumi 会将其用作组件在注册表中的文档页面。

使用 OIDC 身份验证,通过 GitHub Actions 自动执行发布:

name: Publish Component
on:
  push:
    tags:
      - "v*"

permissions:
  id-token: write
  contents: read

jobs:
  publish:
    runs-on: ubuntu-latest
    env:
      PULUMI_ORG: myorg
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: pulumi/auth-actions@v1
        with:
          organization: ${{ env.PULUMI_ORG }}
          requested-token-type: urn:pulumi:token-type:access_token:organization
      - run: pulumi package publish https://github.com/${{ github.repository }} --publisher ${{ env.PULUMI_ORG }}

前提条件:使用此工作流之前,请配置 GitHub OIDC 与 Pulumi Cloud 的集成。

注册表支持私有 GitHub 和 GitLab 仓库。对于非 OIDC 配置,请使用 GITHUB_TOKEN 或 GITLAB_TOKEN 环境变量进行身份验证。

私有注册表会自动为每个已发布的组件生成 SDK 文档。为组件的输入和输出添加类型注释,可丰富生成的文档(TypeScript 中使用 JSDoc,Python 中使用文档字符串,Go 中使用 Annotate() 方法)。

参考:https://www.pulumi.com/docs/idp/get-started/private-registry/

Git 仓库分发

为版本发布添加标签,方便使用者固定版本:

git tag v1.0.0
git push origin v1.0.0

使用者通过以下方式安装:

pulumi package add https://github.com/myorg/[email protected]
包管理器分发

发布特定语言的包,以便使用原生依赖管理:

  • npm:使用 npm publish 发布 TypeScript/JavaScript 包
  • PyPI:使用 twine upload 发布 Python 包
  • NuGet:使用 dotnet nuget push 发布 .NET 包
  • Maven Central:使用标准 Maven 发布流程发布 Java 包

参考:https://www.pulumi.com/docs/iac/using-pulumi/pulumi-packages/


反模式

反模式问题修正方法
在 apply() 中创建资源在 pulumi preview 中不可见将资源创建移到 apply 之外(参见 pulumi-best-practices 实践 1)
缺少 registerOutputs()组件一直处于“创建中”状态始终将其作为构造函数的最后一行调用
缺少 parent: this子资源出现在根级别为所有子资源传递 { parent: this }
参数中使用联合类型会导致 Python、Go、C# SDK 出问题使用单一类型;为不同变体设置独立属性
参数中使用函数无法跨语言序列化改用配置属性
子资源名称硬编码多个实例会发生冲突根据 ${name}-suffix 派生名称
暴露过多输出泄露实现细节仅导出使用者需要的内容
仅使用一次的组件造成不必要的抽象开销在模式重复出现之前使用内联资源
参数嵌套过深难以使用和演进保持接口扁平,并使用可选属性

快速参考

主题要点
类型 URN<package>:<module>:<type>,module 通常为 index
构造函数先调用 super(type, name, {}, opts),再创建子资源,最后调用 registerOutputs()
子资源始终使用 { parent: this },根据 ${name}-suffix 派生名称
参数接口使用 Input<T> 包装;不使用联合类型或函数;保持结构扁平
输出使用公开的只读 Output<T> 属性,仅暴露必要内容
默认值在构造函数中使用 ?? 运算符设置合理的默认值
组合将较低层级的组件组合成较高层级的组件
多语言使用 PulumiPlugin.yaml 和入口点;使用者通过 pulumi package add 添加包
分发私有注册表、git 标签、包管理器或公共 Pulumi Registry

相关技能

  • pulumi-best-practices:涵盖 Output 处理、机密和别名等 Pulumi 通用模式
  • pulumi-automation-api:用于集成测试和多堆栈工作流的编程式编排
  • pulumi-esc:集中管理组件部署所需的机密和配置

参考资料

评论

支持 Markdown