Pulumi 架构深度解析

当你想用「真正的」编程语言来定义基础设施时,会发现 Terraform 的 HCL 总是有些力不从心。

比如,你想在循环中根据条件创建不同类型的资源;你想复用一段逻辑但又不想用 Terraform 的模块系统;你想写测试,却发现 HCL 的测试框架几乎不存在。

Pulumi 就是为了解决这些问题而生的:用你熟悉的编程语言来定义云基础设施

Pulumi 是什么

Pulumi 是一个开源的基础设施即代码(IaC)平台,支持使用 TypeScript、Python、Go、Java、C# 等真正的编程语言来定义云资源。

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

const vpc = new aws.ec2.Vpc("main", {
    cidrBlock: "10.0.0.0/16",
    enableDnsHostnames: true,
    enableDnsSupport: true,
    tags: {
        Name: "main-vpc",
        Environment: "prod",
    },
});

const subnet = new aws.ec2.Subnet("public", {
    vpcId: vpc.id,
    cidrBlock: "10.0.1.0/24",
    availabilityZone: "us-east-1a",
    mapPublicIpOnLaunch: true,
});

核心架构

整体架构

flowchart TD
    subgraph User["用户代码"]
        A[TypeScript/Python/Go]
    end

    subgraph Pulumi["Pulumi 引擎"]
        B[程序运行时]
        C[资源图构建]
        D[变更计划生成]
        E[状态管理]
    end

    subgraph Providers["Providers"]
        F[AWS Provider]
        G[GCP Provider]
        H[Kubernetes Provider]
    end

    subgraph Backend["Backend"]
        I[Pulumi Service]
        J[S3/Blob Storage]
        K[Self-managed]
    end

    User --> B
    B --> C
    C --> D
    D --> E
    E --> I
    D --> F
    D --> G
    D --> H

Pulumi vs Terraform 架构对比

flowchart LR
    subgraph Terraform["Terraform"]
        A1[HCL 配置] --> A2[Terraform Core]
        A2 --> A3[Provider Plugins]
    end

    subgraph Pulumi["Pulumi"]
        B1[TypeScript/Go/Python] --> B2[Pulumi SDK]
        B2 --> B3[Resource Providers]
    end

    A2 -.->|状态文件| A4[State]
    B2 -.->|状态| B4[State]

关键区别

维度TerraformPulumi
语言HCL(自定义 DSL)TypeScript/Python/Go/Java/C#
程序结构声明式配置块命令式程序
状态管理独立状态文件Pulumi Service 或自托管
依赖解析自动构建 DAG程序执行顺序 + SDK 追踪
条件逻辑受限完整支持

编程模型

基本概念

flowchart TD
    A[Program] --> B[Resource]
    A --> C[ComponentResource]
    A --> D[Provider]

    B --> E[Managed Resources]
    C --> F[Custom Components]
    D --> G[Infrastructure APIs]

资源类型

managed-resources.ts
import * as aws from "@pulumi/aws";

// 托管资源:由 Provider 管理
const instance = new aws.ec2.Instance("web", {
    ami: "ami-12345678",
    instanceType: "t3.micro",
    tags: { Name: "web-server" },
});
component-resource.ts
import * as pulumi from "@pulumi/pulumi";

// 自定义组件:组合多个资源
class Network extends pulumi.ComponentResource {
    public readonly vpcId: pulumi.Output<string>;
    public readonly subnetIds: pulumi.Output<string[]>;

    constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
        super("my:Network", name, {}, opts);

        const vpc = new aws.ec2.Vpc("main", {
            cidrBlock: "10.0.0.0/16",
        }, { parent: this });

        const subnets: aws.ec2.Subnet[] = [];
        for (let i = 0; i < 3; i++) {
            const subnet = new aws.ec2.Subnet(`subnet-${i}`, {
                vpcId: vpc.id,
                cidrBlock: `10.0.${i}.0/24`,
            }, { parent: this });
            subnets.push(subnet);
        }

        this.vpcId = vpc.id;
        this.subnetIds = pulumi.all(subnets.map(s => s.id));
    }
}

const network = new Network("prod");

依赖追踪

dependencies.ts
import * as aws from "@pulumi/aws";

// Pulumi 自动追踪资源依赖
const vpc = new aws.ec2.Vpc("main", {
    cidrBlock: "10.0.0.0/16",
});

// subnet 依赖 vpc,Pulumi 自动处理
const subnet = new aws.ec2.Subnet("public", {
    vpcId: vpc.id,  // pulumi 自动追踪这个依赖
    cidrBlock: "10.0.1.0/24",
});

// instance 依赖 subnet
const instance = new aws.ec2.Instance("web", {
    subnetId: subnet.id,  // pulumi 自动追踪
    ami: "ami-12345678",
});

// 使用 Output
export const instanceIp = instance.publicIp;

Output 和 Input

inputs-outputs.ts
import * as pulumi from "@pulumi/aws";

// Input: 接受字符串或 Output<string>
type VpcIdInput = string | pulumi.Input<string>;

// Output: 代表一个异步计算的值
const vpcId: pulumi.Output<string> = vpc.id;

// 在字符串中引用 Output
const subnetCidr = pulumi.interpolate`10.0.1.0/24`;

// 使用 apply 处理 Output
vpc.id.apply(id => {
    console.log(`VPC ID: ${id}`);
    return id;
});

// 使用 all 处理多个 Output
pulumi.all([vpc.id, subnet.id]).apply(([vpcId, subnetId]) => {
    return `VPC: ${vpcId}, Subnet: ${subnetId}`;
});

状态管理

Pulumi Service(托管后端)

# 登录 Pulumi Service
pulumi login

# 创建新项目
pulumi new typescript

# 部署
pulumi up
flowchart TD
    A[Pulumi CLI] --> B[Pulumi Service]
    B --> C[State Storage]
    B --> D[Policy as Code]
    B --> E[Access Control]
    B --> F[Audit Logs]

自托管后端

self-managed-backend.ts
// 使用 S3 作为状态后端
import * as pulumi from "@pulumi/pulumi";

// pulumi.stack.ts 中配置
export const pulumiConfig = {
    backend: {
        url: "s3://my-pulumi-state-bucket",
    },
};
# 登录 S3 后端
pulumi login s3://my-pulumi-state-bucket

# 或使用 Azure Blob
pulumi login azblob://my-pulumi-state-container

# 或使用 Google Cloud Storage
pulumi login gs://my-pulumi-state-bucket

状态锁定

# Pulumi 自动处理状态锁定
pulumi up

# 输出
Updating (prod)
     Type                     Name                 Plan       Info
     pulumi:pulumi:Stack      myapp-prod
     ...

# 锁定由 Pulumi Service 自动管理
# 其他人尝试同时操作会得到错误

程序执行模型

程序执行流程

sequenceDiagram
    participant CLI as Pulumi CLI
    participant SDK as Pulumi SDK
    participant Prog as User Program
    participant State as State Backend
    participant Provider as Provider

    CLI->>SDK: 初始化
    SDK->>Prog: 执行 Program
    Prog->>SDK: new Resource()
    SDK->>State: 注册资源
    State-->>SDK: 注册完成
    Prog->>SDK: 完成
    SDK->>CLI: 返回资源图
    CLI->>Provider: 查询实际状态
    Provider-->>CLI: 状态差异
    CLI->>CLI: 生成计划
    CLI->>CLI: 等待用户确认
    CLI->>Provider: 执行变更
    Provider-->>State: 更新状态

预览 vs 更新

# 预览:执行程序但不实际创建资源
pulumi preview

# 更新:执行程序并应用变更
pulumi up

# 销毁:删除所有资源
pulumi destroy
$ pulumi preview

Previewing update (dev)

View Live: https://app.pulumi.com/myorg/myapp/dev/previews/12345

     Type                      Name            Plan
     +   pulumi:pulumi:Stack   myapp-dev      create
     +   ├─ aws:ec2:Vpc        main           create
     +  └─ aws:ec2:Subnet  public         create
     +   └─ aws:ec2:Instance  web            create

Resources:
    + 4 to create

Do you want to proceed? [No]  y

配置管理

Stack 配置

Pulumi.yaml
name: myapp
runtime: typescript
description: My application
Pulumi.dev.yaml
config:
  aws:region: us-east-1
  myapp:instanceType: t3.micro
  myapp:desiredCapacity: "1"
Pulumi.prod.yaml
config:
  aws:region: us-east-1
  myapp:instanceType: t3.medium
  myapp:desiredCapacity: "3"
读取配置.ts
import * as pulumi from "@pulumi/pulumi";

const config = new pulumi.Config("myapp");

const instanceType = config.get("instanceType") || "t3.micro";
const desiredCapacity = config.requireNumber("desiredCapacity");

const asg = new aws.autoscaling.Group("web", {
    minSize: 1,
    maxSize: desiredCapacity,
    instanceType: instanceType,
});

Secret 配置

# 创建 secret 配置
pulumi config set --secret dbPassword "super-secret"

# 配置加密存储
读取
const config = new pulumi.Config("myapp");

// 自动解密
const dbPassword = config.requireSecret("dbPassword");

// 类型安全
dbPassword.apply(password => {
    console.log(`DB Password length: ${password.length}`);
    return password;
});

组件资源

创建可复用组件

component.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

export interface WebServerArgs {
    vpcId: pulumi.Input<string>;
    subnetIds: pulumi.Input<string[]>;
    instanceType?: string;
    desiredCapacity?: number;
}

export class WebServer extends pulumi.ComponentResource {
    public readonly url: pulumi.Output<string>;
    public readonly loadBalancerArn: pulumi.Output<string>;

    constructor(name: string, args: WebServerArgs, opts?: pulumi.ComponentResourceOptions) {
        super("my:WebServer", name, {}, opts);

        const securityGroup = new aws.ec2.SecurityGroup("web", {
            vpcId: args.vpcId,
            ingress: [
                { protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] },
                { protocol: "tcp", fromPort: 443, toPort: 443, cidrBlocks: ["0.0.0.0/0"] },
            ],
        }, { parent: this });

        const asg = new aws.autoscaling.Group("web", {
            vpcZoneIdentifiers: args.subnetIds,
            launchTemplate: {
                id: this.createLaunchTemplate(args),
            },
            minSize: 1,
            maxSize: args.desiredCapacity || 3,
        }, { parent: this });

        this.url = pulumi.interpolate`http://${asg.id}`;
        this.loadBalancerArn = asg.arn;
    }

    private createLaunchTemplate(args: WebServerArgs): aws.ec2.LaunchTemplate {
        // ...
    }
}

使用组件

main.ts
import { WebServer } from "./component";

const webServer = new WebServer("prod", {
    vpcId: network.vpcId,
    subnetIds: network.subnetIds,
    instanceType: "t3.medium",
    desiredCapacity: 5,
});

export const webUrl = webServer.url;

动态 Providers

什么是动态 Providers

动态 Providers 允许你用任何语言实现自定义资源类型:

dynamic-provider.ts
import * as pulumi from "@pulumi/pulumi";

const provider: pulumi.dynamic.ResourceProvider = {
    async create(inputs) {
        // 创建资源的逻辑
        return { id: "new-resource-id" };
    },
    async update(id, olds, news) {
        // 更新逻辑
        return { outs: news };
    },
    async delete(id) {
        // 删除逻辑
    },
};

class ConfigMap extends pulumi.dynamic.Resource {
    constructor(name: string, props: any, opts?: pulumi.ResourceOptions) {
        super(provider, "ConfigMap", props, opts);
    }
}

const configMap = new ConfigMap("my-config", {
    data: { key: "value" },
});

测试

单元测试

__tests__/network.test.ts
import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";
import * as network from "../network";

pulumi.runtime.setMocks({
    newResource: (args: pulumi.runtime.MockResourceArgs): { id: string; state: any } => {
        return {
            id: `${args.name}-id`,
            state: { ...args.inputs, arn: `arn:aws:ec2:123456789:vpce/${args.name}` },
        };
    },
    call: (args: pulumi.runtime.MockCallArgs) => args.inputs,
    getStack: () => "dev",
    getProject: () => "myapp",
});

describe("Network", () => {
    it("creates a VPC with correct CIDR", () => {
        const net = new network.Vpc("test", { cidr: "10.0.0.0/16" });
        expect(net.vpcId).toBeDefined();
    });
});

集成测试

__tests__/integration.test.ts
import * as pulumi from "@pulumi/pulumi";

async function testResource() {
    const stack = new pulumi.StackReference("myorg/myapp/dev");

    const vpcId = await stack.getOutputValue("vpcId");
    console.log(`VPC ID: ${vpcId}`);

    // 验证资源存在
    expect(vpcId).toBeTruthy();
}

与 Terraform 对比

维度TerraformPulumi
学习曲线HCL 简单,但高级特性复杂需要熟悉编程语言
状态管理手动配置 BackendPulumi Service 或自托管
调试plan 输出有限可用调试器
测试Terratest(Go)原生测试框架(jest/go test 等)
灵活性受 HCL 限制完整编程语言能力
生态更成熟(Provider 更多)快速增长
下一步

想详细对比 Pulumi 和 Terraform?请阅读 Pulumi vs Terraform 对比