Pulumi + TypeScript IaC 实战:2026年开发者基础设施即代码完全指南

DevOps

2026年,为什么 Pulumi + TypeScript 对开发者更友好

Terraform 用 HCL 定义基础设施,这是一种领域特定语言(DSL)。开发者需要学习新语法、处理字符串插值、忍受缺乏 IDE 支持的痛苦。Pulumi 让你用真正的编程语言定义基础设施——TypeScript 意味着你有类型检查、IDE 自动补全、npm 生态、条件逻辑和循环,一切你熟悉的东西。

维度 Terraform (HCL) Pulumi (TypeScript) CDK (TypeScript)
语言 HCL (DSL) TypeScript TypeScript
类型安全 完整 完整
IDE 支持 有限 完整 完整
条件逻辑 count/for_each if/else/for if/else/for
模块复用 Terraform Module npm 包 Construct
状态管理 State File Pulumi Cloud/自托管 CDK Cloud
秘密管理 .tfvars Config secrets Context
测试 terraform test Mocha/Jest Jest
依赖管理 package.json package.json
错误提示 运行时 编译时+运行时 编译时+运行时
学习曲线 低(HCL简单) 低(用熟悉的语言) 中(Construct抽象)
厂商绑定 AWS绑定

Pulumi 架构:引擎、语言主机、资源提供者

┌─────────────────────────────────────────────────────┐
│              Pulumi CLI (pulumi)                      │
│  ┌────────────┐  ┌────────────┐  ┌────────────┐     │
│  │ Engine     │  │ Language   │  │ Resource   │     │
│  │ (编排核心) │──│ Host       │──│ Provider   │     │
│  │            │  │ (Node.js)  │  │ (gRPC)     │     │
│  └────────────┘  └────────────┘  └────────────┘     │
└──────┬───────────────────────────────────┬──────────┘
       │                                   │
┌──────▼──────┐  ┌────────────┐  ┌────────▼────────┐
│ Pulumi Cloud │  │ AWS/GCP/   │  │ State Backend   │
│ (可选)       │  │ Azure API  │  │ (本地/S3/Cloud) │
└─────────────┘  └────────────┘  └─────────────────┘

核心组件

  • Engine:Pulumi 的核心编排引擎,负责资源依赖图、并行创建、状态更新
  • Language Host:运行用户程序的语言运行时(Node.js for TypeScript)
  • Resource Provider:与云厂商 API 通信的 gRPC 插件(如 pulumi-aws、pulumi-gcp)

完整项目搭建

初始化项目

# 安装 Pulumi CLI
curl -fsSL https://get.pulumi.com | sh

# 登录 Pulumi Cloud(或自托管后端)
pulumi login

# 创建 TypeScript 项目
mkdir my-infra && cd my-infra
pulumi new aws-typescript

# 项目结构
# ├── Pulumi.yaml          # 项目元数据
# ├── Pulumi.dev.yaml      # dev stack 配置
# ├── Pulumi.prod.yaml     # prod stack 配置
# ├── index.ts             # 基础设施定义
# ├── package.json
# └── tsconfig.json

Pulumi.yaml

name: my-infra
runtime: nodejs
description: My infrastructure with Pulumi and TypeScript

package.json

{
  "name": "my-infra",
  "main": "index.ts",
  "scripts": {
    "preview": "pulumi preview",
    "up": "pulumi up --yes",
    "destroy": "pulumi destroy --yes",
    "test": "mocha -r ts-node/register test/**/*.spec.ts"
  },
  "dependencies": {
    "@pulumi/pulumi": "^3.130.0",
    "@pulumi/aws": "^6.60.0",
    "@pulumi/awsx": "^2.10.0"
  },
  "devDependencies": {
    "@types/mocha": "^10.0.0",
    "mocha": "^10.0.0",
    "ts-node": "^10.9.0",
    "typescript": "^5.5.0"
  }
}

部署 AWS 资源

VPC + ECS + RDS 完整示例

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

const env = pulumi.getStack();

// VPC
const vpc = new awsx.ec2.Vpc("main-vpc", {
    cidrBlock: "10.0.0.0/16",
    subnets: [
        { type: "public", cidrMask: 24 },
        { type: "private", cidrMask: 24 },
    ],
    tags: { Environment: env },
});

// RDS 数据库
const dbSubnetGroup = new aws.rds.SubnetGroup("db-subnet", {
    subnetIds: vpc.privateSubnetIds,
    tags: { Environment: env },
});

const db = new aws.rds.Instance("app-db", {
    engine: "postgres",
    engineVersion: "16.4",
    instanceClass: "db.t3.medium",
    allocatedStorage: 100,
    storageType: "gp3",
    dbName: "appdb",
    username: "appadmin",
    password: pulumi.Config.requireSecret("db-password"),
    dbSubnetGroupName: dbSubnetGroup.name,
    vpcSecurityGroupIds: [vpc.vpc.defaultSecurityGroupId],
    skipFinalSnapshot: true,
    tags: { Environment: env },
});

// ECR 仓库
const repo = new aws.ecr.Repository("app-repo", {
    imageScanningConfiguration: { scanOnPush: true },
    tags: { Environment: env },
});

// ECS 集群
const cluster = new aws.ecs.Cluster("app-cluster", {
    tags: { Environment: env },
});

// ECS 服务
const appService = new awsx.ecs.FargateService("app-service", {
    cluster: cluster.arn,
    desiredCount: env === "prod" ? 3 : 1,
    taskDefinitionArgs: {
        container: {
            name: "app",
            image: repo.latestImageDigest,
            portMappings: [{ containerPort: 8080 }],
            environment: [
                { name: "DATABASE_URL", value: pulumi.interpolate`postgresql://${db.username}:${db.password}@${db.endpoint}/appdb` },
                { name: "NODE_ENV", value: env },
            ],
            cpu: 256,
            memory: 512,
        },
    },
    subnetIds: vpc.privateSubnetIds,
    securityGroups: [vpc.vpc.defaultSecurityGroupId],
    tags: { Environment: env },
});

// ALB
const alb = new awsx.elasticloadbalancingv2.ApplicationLoadBalancer("app-alb", {
    vpc: vpc,
    subnetIds: vpc.publicSubnetIds,
    defaultTargetGroup: {
        port: 8080,
    },
    tags: { Environment: env },
});

// 导出
export const apiUrl = alb.loadBalancer.dnsName;
export const dbEndpoint = db.endpoint;
export const ecrRepoUrl = repo.repositoryUrl;

部署 GCP 资源

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

const project = gcp.config.project;
const region = gcp.config.region || "us-central1";

// GKE 集群
const cluster = new gcp.container.Cluster("app-cluster", {
    project: project,
    location: region,
    initialNodeCount: 3,
    nodeConfig: {
        machineType: "e2-medium",
        oauthScopes: [
            "https://www.googleapis.com/auth/cloud-platform",
        ],
        labels: { environment: pulumi.getStack() },
    },
    ipAllocationPolicy: {
        useIpAliases: true,
    },
});

// Cloud SQL
const db = new gcp.sql.DatabaseInstance("app-db", {
    project: project,
    region: region,
    databaseVersion: "POSTGRES_16",
    settings: {
        tier: "db-f1-micro",
        diskSize: 20,
        diskType: "PD_SSD",
        ipConfiguration: {
            ipv4Enabled: true,
        },
    },
});

// Cloud Storage
const bucket = new gcp.storage.Bucket("app-assets", {
    project: project,
    location: region,
    uniformBucketLevelAccess: true,
    lifecycleRules: [{
        action: { type: "Delete" },
        condition: { age: 365 },
    }],
});

export const clusterName = cluster.name;
export const dbName = db.name;
export const bucketName = bucket.name;

部署 Azure 资源

import * as azure from "@pulumi/azure";
import * as azuread from "@pulumi/azuread";

const resourceGroup = new azure.core.ResourceGroup("app-rg", {
    location: "East Asia",
    tags: { Environment: pulumi.getStack() },
});

// AKS 集群
const aks = new azure.containerservice.KubernetesCluster("app-aks", {
    resourceGroupName: resourceGroup.name,
    location: resourceGroup.location,
    defaultNodePool: {
        name: "default",
        nodeCount: 3,
        vmSize: "Standard_D2s_v3",
    },
    identity: { type: "SystemAssigned" },
    tags: { Environment: pulumi.getStack() },
});

// Azure SQL
const sqlServer = new azure.mssql.SqlServer("app-sql", {
    resourceGroupName: resourceGroup.name,
    location: resourceGroup.location,
    version: "12.0",
    administratorLogin: "sqladmin",
    administratorLoginPassword: pulumi.Config.requireSecret("sql-password"),
});

const sqlDb = new azure.mssql.Database("app-db", {
    serverId: sqlServer.id,
    skuName: "S1",
    maxSizeMb: 1024,
});

export const aksFqdn = aks.fqdn;
export const sqlEndpoint = sqlServer.fullyQualifiedDomainName;

Stack 管理和配置

多环境 Stack

# 创建 Stack
pulumi stack init dev
pulumi stack init staging
pulumi stack init prod

# 设置配置
pulumi config set aws:region us-east-1
pulumi config set db-password "secret-value" --secret
pulumi config set instance-count 3

# 切换 Stack
pulumi stack select prod

# 预览变更
pulumi preview

# 部署
pulumi up

# 查看输出
pulumi stack output apiUrl

Stack 配置文件

# Pulumi.dev.yaml
config:
  aws:region: us-east-1
  my-infra:instance-count: "1"
  my-infra:db-password:
    secure: AAABAN0T+...(加密值)

# Pulumi.prod.yaml
config:
  aws:region: us-east-1
  my-infra:instance-count: "3"
  my-infra:db-password:
    secure: AAABAN1T+...(加密值)

代码中使用配置

const config = new pulumi.Config();
const instanceCount = config.requireNumber("instance-count");
const dbPassword = config.requireSecret("db-password");

// 环境特定逻辑
const isProd = pulumi.getStack() === "prod";
const dbInstanceClass = isProd ? "db.r6g.xlarge" : "db.t3.medium";
const desiredCount = isProd ? instanceCount : 1;

测试基础设施:Pulumi Testing Framework

单元测试

// test/infra.spec.ts
import * as pulumi from "@pulumi/pulumi";
import { describe, it, expect } from "mocha";

// Mock Pulumi 的输出
pulumi.runtime.setMocks({
    newResource: (args) => ({
        id: `${args.name}-id`,
        state: args.inputs,
    }),
    call: (args) => args.inputs,
});

describe("Infrastructure", () => {
    it("should create VPC with correct CIDR", async () => {
        const vpc = new awsx.ec2.Vpc("test-vpc", {
            cidrBlock: "10.0.0.0/16",
        });
        const cidr = await pulumi.output(vpc.vpc.cidrBlock).promise();
        expect(cidr).to.equal("10.0.0.0/16");
    });

    it("should use larger DB in production", async () => {
        const isProd = true;
        const dbInstanceClass = isProd ? "db.r6g.xlarge" : "db.t3.medium";
        expect(dbInstanceClass).to.equal("db.r6g.xlarge");
    });

    it("should create ECS service with correct desired count", async () => {
        const env = "prod";
        const desiredCount = env === "prod" ? 3 : 1;
        expect(desiredCount).to.equal(3);
    });
});

Policy 测试(合规检查)

// policy/index.ts
import * as pulumi from "@pulumi/pulumi";
import { PolicyPack, validateResource } from "@pulumi/policy";

new PolicyPack("infra-policies", {
    policies: [
        {
            name: "no-public-s3-buckets",
            description: "S3 buckets must not be publicly accessible",
            enforcementLevel: "mandatory",
            validateResource: validateResourceOfType(aws.s3.Bucket, (bucket, args) => {
                if (args.acl === "public-read" || args.acl === "public-read-write") {
                    return "S3 bucket must not be publicly accessible";
                }
            }),
        },
        {
            name: "required-tags",
            description: "All resources must have Environment tag",
            enforcementLevel: "mandatory",
            validateResource: (args) => {
                const tags = args.props.tags as Record<string, string> | undefined;
                if (!tags || !tags.Environment) {
                    return "All resources must have an Environment tag";
                }
            },
        },
        {
            name: "db-encryption-required",
            description: "RDS instances must have encryption enabled",
            enforcementLevel: "mandatory",
            validateResource: validateResourceOfType(aws.rds.Instance, (instance, args) => {
                if (!args.storageEncrypted) {
                    return "RDS instance must have storage encryption enabled";
                }
            }),
        },
    ],
});

Pulumi vs Terraform vs CDK 对比

维度 Pulumi Terraform CDK
语言 TS/Python/Go/C# HCL TS/Python/Java/C#
状态 Pulumi Cloud/自托管 State File CDK Cloud
Provider 200+ 3000+ AWS为主
测试 原生支持 terraform test Jest
秘密 Config secrets SOPS/Vault Context
导入现有 pulumi import terraform import cdk import
模块生态 npm/PyPI Terraform Registry Construct Hub
多云 原生 原生 AWS为主
团队协作 Pulumi Cloud Remote State CDK Cloud
商业模式 开源+Cloud 开源+Cloud 开源

5 个常见陷阱

1. 输出值在运行时不可用

Pulumi 的 Output<T> 是异步的,不能在运行时直接访问值。

// 错误:直接使用 Output
const endpoint = db.endpoint;  // Output<string>
console.log(endpoint);  // 输出:Promise(不是实际值)

// 正确:使用 apply 或 interpolate
const dbUrl = pulumi.interpolate`postgresql://${db.endpoint}/appdb`;

// 或在 apply 中使用
db.endpoint.apply(ep => {
    console.log(`DB endpoint: ${ep}`);
});

2. 资源依赖未正确声明

Pulumi 自动推断依赖,但有时需要显式声明。

// 隐式依赖(Pulumi 自动推断)
const db = new aws.rds.Instance("db", { /* ... */ });
const app = new aws.ecs.Service("app", {
    // 如果 environmentVariables 引用了 db.endpoint,依赖自动建立
});

// 显式依赖
const app = new aws.ecs.Service("app", {
    // ...
}, { dependsOn: [db] });

3. Stack 配置中明文存储秘密

# 错误:明文存储密码
pulumi config set db-password "my-secret-password"

# 正确:使用 --secret 标记
pulumi config set db-password "my-secret-password" --secret

# 在代码中使用
const dbPassword = config.requireSecret("db-password");

4. 忽略 destroy 保护

生产环境应启用资源保护,防止意外删除。

const db = new aws.rds.Instance("app-db", {
    // ...
    deletionProtection: true,  // 防止意外删除
    skipFinalSnapshot: false,  // 删除前创建快照
    finalSnapshotIdentifier: `app-db-final-${Date.now()}`,
});

5. 并行部署导致竞态条件

多个 Stack 同时部署同一资源可能导致冲突。

# 使用 Pulumi Cloud 的并发锁
pulumi login  # Pulumi Cloud 自动管理锁

# 自托管后端需要配置锁
pulumi login s3://my-pulumi-state?region=us-east-1

10 个错误排查

# 错误现象 可能原因 排查方法
1 requireSecret 返回加密值 在非 apply 中使用 使用 applyinterpolate
2 资源创建超时 云 API 限流 检查并行度,添加重试
3 Provider 版本不兼容 package.json 版本过旧 pulumi plugin install 更新
4 Stack 状态损坏 部署中断 pulumi state unprotect 修复
5 类型错误 TypeScript 类型不匹配 tsc --noEmit 检查
6 循环依赖 资源间互相引用 重构资源依赖图
7 配置缺失 Stack 配置未设置 pulumi config 检查
8 权限不足 云凭证权限不够 检查 IAM 角色
9 Drift 检测 手动修改了云资源 pulumi refresh 同步状态
10 导入现有资源失败 资源 ID 不正确 pulumi import 检查 ID 格式
# 通用排查命令
# 查看当前 Stack 状态
pulumi stack

# 预览变更
pulumi preview --diff

# 查看资源历史
pulumi stack history

# 刷新状态(检测 drift)
pulumi refresh

# 导出状态
pulumi stack export > state.json

# 查看所有资源
pulumi stack --show-resources

在线工具推荐


总结:Pulumi + TypeScript 让开发者用熟悉的编程语言定义基础设施,享受类型安全、IDE 支持和 npm 生态。2026年,Pulumi 已支持 200+ Provider,覆盖 AWS、GCP、Azure 等主流云平台。关键实践:正确处理 Output 的异步性、使用 --secret 保护敏感配置、启用资源删除保护、编写 Policy 做合规检查、使用 Testing Framework 验证基础设施逻辑。对于熟悉 TypeScript 的开发者,Pulumi 是比 Terraform 更自然、更高效的 IaC 选择。

本站提供浏览器本地工具,免注册即可试用 →

#Pulumi#IaC#TypeScript#基础设施即代码#AWS#GCP#Azure#云资源管理