Pulumi 入门:原理、组件与 IaC 工作流

This article is extracted from the chat log with AI. Please identify it with caution.

说明:本文由 Codex 根据作者提供的主题、素材与公开资料辅助生成,属于 AI 生成/整理内容,非作者原创。请读者自行甄别并交叉验证。

Pulumi 解决什么问题#

Pulumi 是一套 Infrastructure as Code 工具。

IaC 的核心目标是:

用代码描述基础设施的期望状态,
再由工具把真实云资源推进到这个状态。

传统方式里,创建云资源可能依赖:

  • 控制台手动点击。
  • 临时脚本。
  • 运维文档。
  • 零散的 CLI 命令。

这些方式的问题是:

  • 很难复现。
  • 很难审查变更。
  • 很难知道当前环境和预期环境是否一致。
  • 很难在开发、测试、生产环境之间保持结构一致。

Pulumi 的思路是:

基础设施也应该像应用代码一样被编写、复用、审查、测试和部署。

它和 Terraform、CloudFormation、ARM/Bicep 等工具属于同一类问题空间,但 Pulumi 的明显特点是使用通用编程语言来描述资源。

Pulumi 的特点#

Pulumi 支持 TypeScript、JavaScript、Python、Go、.NET、Java、YAML 等语言。

这意味着可以用普通代码组织基础设施逻辑:

  • 使用函数封装重复资源。
  • 使用类或组件抽象一组资源。
  • 使用条件、循环和类型系统表达复杂配置。
  • 使用包管理器复用已有库。
  • 在同一套工程体系中做测试、格式化和代码审查。

例如 TypeScript 中可以写:

import * as aws from "@pulumi/aws";

const bucket = new aws.s3.Bucket("assets");

export const bucketName = bucket.bucket;

这段代码不是普通业务逻辑,而是在向 Pulumi 注册一个云资源:

希望当前 Stack 里存在一个名为 assets 的 S3 Bucket。

Pulumi 后续会比较当前状态和期望状态,决定创建、更新、替换还是删除资源。

Pulumi 由哪些部分组成#

从使用者角度看,Pulumi 至少包括几层。

Pulumi Program#

Pulumi Program 是描述基础设施的代码。

它可以是:

  • index.ts
  • main.py
  • main.go
  • Program.cs
  • Pulumi.yaml

这个程序运行时会创建资源对象。

注意,这里的“创建资源对象”不等于立刻调用云厂商 API 创建真实资源。

更准确地说:

Pulumi Program 执行后,会向 Pulumi 引擎注册资源声明。

Language Host#

Language Host 负责运行 Pulumi Program。

例如:

  • TypeScript/JavaScript 使用 Node 语言宿主。
  • Python 使用 Python 语言宿主。
  • Go 使用 Go 语言宿主。

Language Host 的工作是:

运行用户代码;
捕获代码里注册的资源;
把资源注册请求发给 Pulumi 部署引擎。

Deployment Engine#

Deployment Engine 是 Pulumi 的核心调度者。

它负责比较:

当前 Stack 的状态
vs
Pulumi Program 本次表达的期望状态

然后决定每个资源应该执行什么操作:

  • create
  • update
  • replace
  • delete
  • same

它还会根据资源依赖关系决定执行顺序。

如果两个资源之间没有依赖,Pulumi 可以并行处理它们;如果一个资源依赖另一个资源的输出,则必须按依赖顺序执行。

Resource Provider#

Resource Provider 负责和具体平台交互。

例如:

  • AWS Provider 调 AWS API。
  • Azure Provider 调 Azure API。
  • Kubernetes Provider 调 Kubernetes API。
  • Cloudflare Provider 调 Cloudflare API。

Pulumi 引擎本身不直接实现每个云资源的创建逻辑,而是通过 Provider 插件完成。

这也解释了为什么同样是 Pulumi,不同云厂商资源的行为和耗时可能差异很大:

Pulumi 负责调度和状态;
Provider 负责具体 API;
云平台决定底层资源的实际生命周期。

State#

State 是 Pulumi 理解当前基础设施的关键。

Pulumi 会记录每个 Stack 的资源状态,例如:

  • 资源 URN。
  • 资源 ID。
  • 输入参数。
  • 输出结果。
  • 依赖关系。
  • Secret 标记。

有了 State,Pulumi 才知道:

这次代码里的资源,和上次已经创建过的真实资源,是不是同一个资源。

没有 State,Pulumi 就无法可靠判断应该创建新资源还是更新旧资源。

Backend#

Backend 是保存 State 的地方。

常见选择包括:

  • Pulumi Cloud。
  • 本地文件。
  • S3、Azure Blob、Google Cloud Storage 等对象存储。

Backend 不只是一个文件位置,还承担并发更新协调、状态读写、加密配置等职责。

生产环境通常不建议随意使用本地状态,因为本地状态更容易丢失,也不利于多人协作。

Stack#

Stack 是 Pulumi Program 的一个独立实例。

常见 Stack:

dev
staging
prod

同一套代码可以部署到多个 Stack:

同一个 Pulumi Program
  -> dev Stack
  -> staging Stack
  -> prod Stack

每个 Stack 有自己的:

  • 配置。
  • Secret。
  • State。
  • 资源集合。

因此 Stack 是环境隔离的核心单位。

Pulumi 的执行流程#

执行:

pulumi up

大致会发生这些事:

  1. Pulumi CLI 读取当前 Project 和 Stack。
  2. CLI 启动对应语言的 Language Host。
  3. Language Host 执行 Pulumi Program。
  4. 程序中的资源声明被发送给 Deployment Engine。
  5. Engine 读取当前 Stack 的 State。
  6. Engine 计算 diff,生成计划。
  7. Provider 根据计划调用云平台 API。
  8. 操作完成后,Engine 更新 State。

这个过程可以简化成:

代码 -> 资源声明 -> diff -> 云 API -> 新状态

Preview 和 Up 的区别#

pulumi preview 只计算将要发生什么。

pulumi preview

它用于回答:

如果现在执行,会创建、修改、替换、删除哪些资源?

pulumi up 则会真正执行。

pulumi up

实际使用中,建议先 preview,再 up。

这和应用开发里的代码审查类似:

先看 diff,再合并。

Pulumi 如何实现幂等#

IaC 工具常说的幂等,不是说“每次执行都什么也不做”,而是说:

重复执行同一份期望状态,最终应该收敛到同一个基础设施状态。

Pulumi 的幂等依赖几个条件。

资源逻辑名称稳定#

Pulumi 资源通常有一个逻辑名称:

const bucket = new aws.s3.Bucket("assets");

这里的 "assets" 是 Pulumi 资源名的一部分。

如果它每次运行都随机变化:

const bucket = new aws.s3.Bucket(`assets-${Date.now()}`);

Pulumi 会认为这是一个新资源。

结果可能是:

  • 旧资源从本次程序中消失,被计划删除。
  • 新资源被计划创建。
  • 某些资源因云平台唯一名称冲突而失败。

所以资源逻辑名称应该稳定。

输入参数稳定#

如果资源输入每次运行都变化,Pulumi 就会认为期望状态发生了变化。

例如:

tags: {
  buildTime: new Date().toISOString(),
}

这种值会导致每次 pulumi up 都出现 diff。

如果这个字段不是基础设施真实需要的一部分,就不应该放进资源输入。

Provider 行为稳定#

Pulumi Engine 会根据 Provider 返回的信息判断资源差异。

如果某个 Provider 对某些字段处理不稳定,或者云平台 API 返回值本身会漂移,也可能导致反复 diff。

这种情况下通常需要:

  • 检查资源字段是否真的需要托管。
  • 使用 ignoreChanges 忽略外部系统会改写的字段。
  • 使用稳定的输入配置。
  • 通过 pulumi refresh 同步状态。

随机资源 ID 会破坏幂等吗#

要区分两类 ID。

云平台生成的物理 ID#

很多云资源创建后会得到云平台返回的 ID。

例如:

vpc-xxxx
subnet-xxxx
lb-xxxx

这些 ID 是资源创建后才知道的。

只要 Pulumi State 保存了它们,后续运行就可以通过 State 找回对应资源。

这不会破坏幂等。

程序每次生成的逻辑名称或输入#

如果代码每次执行都生成随机资源名、随机配置或随机业务 ID,就会破坏 Pulumi 对“同一个资源”的识别。

例如:

const name = `worker-${Math.random()}`;
new cloud.ProviderResource(name, { name });

这类写法会让 Pulumi 每次看到一个新资源。

更好的方式是:

  • 逻辑名称使用稳定值。
  • 需要随机但长期稳定的值时,使用专门的随机资源并让它进入 State。
  • 业务侧唯一后缀通过配置传入,而不是每次运行动态生成。

Pulumi 的自动命名#

Pulumi 对很多资源支持自动命名。

例如逻辑名是:

assets

真实云资源名可能带有自动后缀:

assets-3f2a91b

这种后缀由 Pulumi 管理,目的是减少命名冲突,并帮助资源替换时先创建新资源再删除旧资源。

如果手动指定云资源的物理名称,替换时可能遇到名称冲突。

这也是为什么一些资源如果禁用自动命名或固定物理名,变更策略会更敏感。

Automation API 是什么#

常规使用方式是命令行:

pulumi preview
pulumi up
pulumi destroy

Automation API 则是把这些能力变成 SDK。

也就是说,可以在自己的程序里调用 Pulumi:

创建 Stack;
设置配置;
执行 preview;
执行 up;
读取输出;
执行 destroy。

它适合这些场景:

  • 在自定义平台里提供基础设施创建能力。
  • 在 CI/CD 中更细粒度地控制部署流程。
  • 做集成测试时动态创建和销毁环境。
  • 构建内部 CLI 或 Web 控制台。

需要注意的是,Automation API 仍然是驱动 Pulumi 引擎,不是绕过 Pulumi 直接调云平台 API。

Pulumi 和 Terraform 的关键差异#

这里不展开完整对比,只抓最关键的差异。

维度PulumiTerraform
配置语言通用编程语言或 YAMLHCL
抽象方式函数、类、包、组件module、表达式
类型系统依赖宿主语言HCL 类型系统
执行方式运行程序,注册资源解析配置,构建计划
状态每个 Stack 独立 State每个 Workspace/State 后端管理
生态重点多语言 SDK 与组件抽象声明式配置与 Provider 生态

Pulumi 的优势在于:

  • 对开发者友好,尤其是熟悉 TypeScript/Python/Go 的团队。
  • 复杂逻辑表达能力强。
  • 容易复用语言生态里的测试、包管理和工程工具。
  • 适合构建平台化的 IaC 能力。

它的代价是:

  • 代码自由度更高,也更容易写出难以审查的动态逻辑。
  • 团队需要约束资源命名、随机值、组件边界和配置方式。
  • 对 IaC 初学者来说,语言运行时和 Pulumi 运行时容易混淆。

使用 Pulumi 的实践建议#

1. 保持资源逻辑名称稳定#

不要把时间戳、随机数、临时输入拼进资源逻辑名。

// 好
new aws.s3.Bucket("assets");

// 不好
new aws.s3.Bucket(`assets-${Date.now()}`);

2. 把环境差异放进 Stack 配置#

例如:

pulumi config set instanceType t3.small
pulumi config set replicas 3

不要在代码里到处写:

if (stack === "prod") {
  ...
}

必要的环境分支可以有,但最好集中管理。

3. 先 preview 再 up#

对生产环境尤其重要。

pulumi preview
pulumi up

关注是否有意外的 deletereplace

4. 不要手工修改 State#

State 是 Pulumi 的核心账本。

除非非常明确知道后果,否则不要直接改 State 文件。

如果真实环境和 State 不一致,优先考虑:

pulumi refresh

5. 谨慎处理 Secret#

敏感配置应该使用 Pulumi 的 Secret 机制:

pulumi config set --secret dbPassword ...

不要把密码、Token、私钥写进代码或普通配置。

常见误解#

误解一:Pulumi 程序执行就是创建资源#

不准确。

Pulumi 程序执行时主要是在注册资源声明。真正的创建、更新和删除由 Deployment Engine 和 Provider 根据 diff 执行。

误解二:用了 TypeScript 就可以随便写动态逻辑#

不建议。

IaC 代码最重要的是可预测、可审查、可重复执行。

过度动态会降低可维护性。

误解三:Stack 名称不同就一定完全隔离#

Pulumi State 是隔离的,但云平台账号、区域、命名空间、VPC、Kubernetes 集群等外部资源仍可能共享。

是否真正隔离,取决于资源本身如何配置。

误解四:随机物理 ID 一定破坏幂等#

云平台创建后返回的物理 ID 不会破坏幂等,因为 Pulumi 会记录在 State 中。

真正危险的是程序每次运行都生成新的逻辑名称或输入参数。

小结#

Pulumi 可以概括为:

用通用编程语言描述基础设施,
用 Stack 隔离环境,
用 State 记录现实世界,
用 Engine 计算差异,
用 Provider 调云平台 API,
最终让基础设施收敛到代码表达的期望状态。

理解 Pulumi 的关键不是背命令,而是理解这条链路:

Program -> Engine -> Provider -> Cloud API -> State

只要资源命名稳定、配置边界清晰、State 管理可靠,Pulumi 就能成为一套比较自然的 IaC 工程化工具。

参考资料#

本文共 3961 字,创建于 Jun 24, 2026

相关标签: Cloud, DevOps, ByAI