很久之前, 参加过公司的一个内部培训用Terraform实现了一个云资源管理平台。好久没 用,基本上把他的语法都忘完了。最近开始用 Pulumi 管理云资源,它和 Terraform 的思路完全不同 —— 直接用真正的编程语言(TypeScript/Python/Go)写基础设施代 码,而不是学新的 DSL。所以记录基础概念和常见用法,方便以后快速回顾。

核心概念

概念说明类比
Resource云资源(VM、S3、数据库)对象实例
Stack独立环境(dev/staging/prod)环境变量
State资源当前状态(实际 vs 期望)数据库快照
Output异步值(资源 ID、IP 等)Promise

Resource:云资源

Resource 是你要创建的云资源,比如 S3 bucket、EC2 实例、数据库等。每个 Resource 有三个参数:

new aws.s3.Bucket(
  "my-bucket",          // 1. 名称(Pulumi 内部标识)
  { acl: "private" },   // 2. 配置(传给云 API)
  { protect: true }     // 3. 选项(可选,防止误删等)
);

关键:代码只是声明,运行 pulumi up 才会真正调用云 API 创建资源。

Stack:环境隔离

Stack 是同一份代码部署到不同环境的实例。比如同一份代码,用 dev Stack 部署到开发环境,用 prod Stack 部署到生产环境。

pulumi stack init dev      # 创建 dev Stack
pulumi stack select dev    # 切换到 dev
pulumi up                  # 部署到 dev 环境

每个 Stack 可以有不同的配置(实例规格、副本数等):

const config = new pulumi.Config();
const instanceType = config.require("instanceType");
 
const server = new aws.ec2.Instance("web", {
  instanceType,  // dev 用 t2.micro,prod 用 t3.large
});

State:状态管理

State 是 Pulumi 记录的资源快照,用于对比「代码里写的」和「云上实际的」是否一致。如果不一致,Pulumi 会帮你修改云资源。

常用操作:

pulumi refresh  # 同步 State(有人手动改了云资源)
pulumi import aws:s3/bucket:Bucket my-bucket existing-id  # 导入已存在的资源

Output:异步值

资源的属性(如 ID、IP)在创建前不存在,所以不能直接用,必须用 Output<T> 类型处理。

const bucket = new aws.s3.Bucket("data");
 
bucket.id  // Output<string>,不是 string
 
// ✅ 用 apply
bucket.id.apply(id => console.log(id));
 
// ✅ 用 interpolate 拼接字符串
const url = pulumi.interpolate`https://${bucket.id}.s3.amazonaws.com`;
 
// ✅ 传给其他资源(Pulumi 自动处理 Output)
new aws.s3.BucketObject("file", { bucket: bucket.id });

多环境配置

一套代码部署多个环境,用 Stack 区分。每个 Stack 有独立的配置文件 Pulumi.<stack>.yaml,这样就能用同一份代码、不同配置部署到 dev/staging/prod 环境。

工作原理

Pulumi 入口文件 (index.ts) → 读取配置 → 创建云资源
    ↑
    └─ 根据当前 Stack 读取对应的配置文件

说明

  • index.ts 是 Pulumi 项目的入口文件(定义基础设施的代码,不是业务代码)
  • 文件名不一定要叫 index.ts,可以在 Pulumi.yaml 中通过 main 字段指定其他名字
  • 约定俗成通常用 index.ts(Node.js)或 __main__.py(Python)

完整示例:三环境部署

下面演示如何用一份代码管理三个环境。

项目结构

my-app/
├── index.ts              # Pulumi 入口文件(定义基础设施资源)
├── Pulumi.yaml           # 项目配置
├── Pulumi.dev.yaml       # Dev 环境配置
├── Pulumi.staging.yaml   # Staging 环境配置
└── Pulumi.prod.yaml      # Prod 环境配置

项目配置(所有环境共享)

Pulumi.yaml 定义项目名和运行时:

name: my-app
runtime: nodejs
# main: src/infra.ts  # 可选:指定其他入口文件(默认是 index.ts)

环境配置(每个环境独立)

每个环境有独立的配置文件,定义实例规格、副本数等。配置格式是 <项目名>:<配置名>

Pulumi.dev.yaml

config:
  aws:region: us-east-1
  my-app:instanceType: t2.micro
  my-app:dbSize: "20"
  my-app:replicas: "1"

Pulumi.prod.yaml

config:
  aws:region: us-west-2
  my-app:instanceType: t3.large
  my-app:dbSize: "200"
  my-app:replicas: "5"
  my-app:dbPassword:
    secure: AAABAxxxxxxxx  # 加密后的值

代码(读取配置)

代码通过 config.require() 读取配置,Pulumi 会根据当前 Stack 自动读取对应的配置文件。

index.ts(Pulumi 入口文件):

const config = new pulumi.Config();
const stack = pulumi.getStack();
 
// 读取配置(从 Pulumi.<stack>.yaml)
const instanceType = config.require("instanceType");
const replicas = config.requireNumber("replicas");
 
// 创建云资源(EC2 服务器)
const servers = Array.from({ length: replicas }, (_, i) =>
  new aws.ec2.Instance(`web-${i}`, {
    instanceType,  // dev 用 t2.micro,prod 用 t3.large
    tags: { Environment: stack },
  })
);
 
// 导出 Output(部署后会显示在终端,可通过 pulumi stack output 查看)
export const serverIps = servers.map(s => s.publicIp);

export 的作用:

  • 部署完成后,Pulumi 会在终端显示这些值
  • 可以用 pulumi stack output serverIps 命令查看
  • 其他 Pulumi 项目可以引用这些值(StackReference)

注意:这不是你的应用业务代码(如 Express、NestJS),而是定义云资源的代码(创建 EC2、S3、数据库等)。

部署流程

第一步是创建 Stack 和配置,后续只需切换 Stack 并 pulumi up 即可。

# 第一次:创建 Stack
pulumi stack init dev
pulumi stack init prod
 
# 配置(或手动编辑 Pulumi.<stack>.yaml)
pulumi stack select dev
pulumi config set instanceType t2.micro
pulumi config set replicas 1
 
pulumi stack select prod
pulumi config set instanceType t3.large
pulumi config set replicas 5
pulumi config set dbPassword my-secret --secret  # 自动加密
 
# 部署到 Dev
pulumi stack select dev
pulumi up  # 读取 Pulumi.dev.yaml,部署到 Dev
 
# 部署完成后会显示 Output:
# Outputs:
#   serverIps: ["54.123.45.67"]
 
# 查看 Output
pulumi stack output serverIps  # 显示服务器 IP 列表
pulumi stack output            # 显示所有 Output

关键点

  • pulumi stack select <name> 切换环境
  • 切换后,所有操作(up/preview/destroy)都针对当前 Stack
  • 每个 Stack 有独立的 State,互不影响
  • 配置文件自动匹配:pulumi stack select dev → 读取 Pulumi.dev.yaml

简化版:代码里硬编码配置

如果觉得配置文件麻烦,可以直接在代码里判断 Stack 名称:

const stack = pulumi.getStack();
 
const envConfig = {
  dev: { instanceType: "t2.micro", replicas: 1 },
  prod: { instanceType: "t3.large", replicas: 5 },
}[stack];
 
const server = new aws.ec2.Instance("web", {
  instanceType: envConfig.instanceType,
});

部署:

pulumi stack init dev && pulumi stack init prod
pulumi stack select dev && pulumi up
pulumi stack select prod && pulumi up

注意:这种方式配置写死在代码里,修改配置需要改代码。敏感信息(密码)建议还是用配置文件加密存储。

  • 切换后,所有操作(up/preview/destroy)都针对当前 Stack
  • 每个 Stack 有独立的 State,互不影响
  • 配置文件自动匹配:pulumi stack select dev → 读取 Pulumi.dev.yaml

简化版:代码里硬编码配置

不想用配置文件,可以直接在代码里判断:

const stack = pulumi.getStack();
 
const envConfig = {
  dev: { instanceType: "t2.micro", replicas: 1 },
  prod: { instanceType: "t3.large", replicas: 5 },
}[stack];
 
const server = new aws.ec2.Instance("web", {
  instanceType: envConfig.instanceType,
});

部署:

pulumi stack init dev && pulumi stack init prod
pulumi stack select dev && pulumi up
pulumi stack select prod && pulumi up

依赖与复用

自动依赖

Pulumi 会根据资源之间的引用关系自动建立依赖顺序。比如 Subnet 引用了 VPC 的 ID,Pulumi 就知道要先创建 VPC。

const vpc = new aws.ec2.Vpc("vpc", { cidrBlock: "10.0.0.0/16" });
const subnet = new aws.ec2.Subnet("subnet", {
  vpcId: vpc.id,  // 自动依赖:subnet 等 vpc 创建完
  cidrBlock: "10.0.1.0/24",
});

组件复用

把常用的资源组合封装成 Component,可以重复使用。比如把”安全组 + EC2 + 公网 IP”封装成一个 WebServer 组件。

class WebServer extends pulumi.ComponentResource {
  public url: pulumi.Output<string>;
  
  constructor(name: string, port: number) {
    super("custom:WebServer", name);
    
    const sg = new aws.ec2.SecurityGroup(`${name}-sg`, {
      ingress: [{ fromPort: port, toPort: port, cidrBlocks: ["0.0.0.0/0"] }],
    }, { parent: this });
    
    const server = new aws.ec2.Instance(`${name}-vm`, {
      instanceType: "t2.micro",
      vpcSecurityGroupIds: [sg.id],
    }, { parent: this });
    
    this.url = pulumi.interpolate`http://${server.publicIp}:${port}`;
  }
}
 
// 使用封装好的组件
const web = new WebServer("app", 80);
export const url = web.url;

Pulumi vs Terraform

维度PulumiTerraform
语言真实编程语言(TS/Python/Go)HCL(专用 DSL)
抽象用函数、类封装逻辑用 module
测试单元测试(Jest/pytest)集成测试为主
学习曲线会编程就能上手需学 HCL 语法
生态较新,社区小成熟,资源多

选择建议

  • 团队已熟悉某门语言 → Pulumi
  • 需要复杂逻辑(循环、条件、算法) → Pulumi
  • 追求稳定、成熟方案 → Terraform

常用操作

日常使用 Pulumi 的常见命令:

# 预览变更(看看会改什么,但不执行)
pulumi preview
 
# 部署(真正执行变更)
pulumi up
 
# 删除所有资源
pulumi destroy
 
# 刷新 State(同步云上实际状态)
pulumi refresh
 
# 导入已存在资源到 Pulumi 管理
pulumi import aws:s3/bucket:Bucket my-bucket existing-bucket-name
 
# 使用本地 State(不用登录云端)
pulumi login --local

Resource Options(资源选项):

一些常用的资源行为控制:

// 阻止误删(生产数据库)
new aws.rds.Instance("prod-db", {...}, { protect: true });
 
// 忽略外部变更(自动伸缩的字段)
new aws.autoscaling.Group("asg", {...}, { 
  ignoreChanges: ["desiredCapacity"] 
});

参考: