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 中使用 | 使用 apply 或 interpolate |
| 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
在线工具推荐
- JSON 格式化:编辑 Pulumi 配置时,使用 /zh-CN/json/format 格式化 JSON
- Base64 编码:处理云凭证和 Secret 时,使用 /zh-CN/encode/base64 编解码
- 哈希计算:验证配置文件完整性时,使用 /zh-CN/encode/hash 计算哈希值
总结: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#云资源管理