Pulumi基础设施即代码实战:5个模式用TypeScript管理云资源
DevOps
Pulumi:用真正的编程语言管理基础设施
Terraform用HCL写配置,遇到循环/条件/复用就得写一堆workaround。Pulumi让你用TypeScript/Python/Go等真正的编程语言定义基础设施,循环、条件、函数、类——所有编程能力都可用。2026年,Pulumi已支持80+云提供商,Pulumi AI可自动生成基础设施代码,Pulumi Deployments实现GitOps自动化部署。
本文将从5种核心模式出发,带你完成项目搭建→组件抽象→多云部署→状态管理→GitOps的全链路实战。
核心概念
| 概念 | 说明 |
|---|---|
| Pulumi | 用编程语言定义基础设施的IaC工具 |
| Stack | 同一项目的不同环境(dev/staging/prod) |
| Resource | 云资源的抽象表示 |
| ComponentResource | 自定义组件,封装多个Resource |
| Provider | 云提供商插件(AWS/GCP/Azure/K8s) |
| State | 基础设施状态的持久化存储 |
| Output | 异步值,表示资源创建后的属性 |
| Config | Stack级别的配置键值对 |
问题分析:Pulumi IaC的5大挑战
- 学习曲线:从HCL迁移到TypeScript需要思维转换
- Output处理:Output不能直接当字符串用,需要理解异步链
- 状态管理:状态文件损坏或冲突的恢复
- 组件设计:如何抽象可复用的基础设施组件
- 多云编排:不同云提供商资源的依赖和协调
分步实操:5种Pulumi IaC模式
模式1:Pulumi项目与基础资源
// index.ts - Pulumi项目入口
import * as pulumi from '@pulumi/pulumi';
import * as aws from '@pulumi/aws';
import * as awsx from '@pulumi/awsx';
const config = new pulumi.Config();
const environment = config.get('environment') || 'dev';
const vpcCidr = config.get('vpcCidr') || '10.0.0.0/16';
const vpc = new aws.ec2.Vpc('main-vpc', {
cidrBlock: vpcCidr,
enableDnsHostnames: true,
enableDnsSupport: true,
tags: {
Name: `${environment}-vpc`,
Environment: environment,
},
});
const publicSubnets = [0, 1, 2].map((i) => {
return new aws.ec2.Subnet(`public-${i}`, {
vpcId: vpc.id,
cidrBlock: `10.0.${i}.0/24`,
availabilityZone: aws.getAvailabilityZonesOutput().names[i],
mapPublicIpOnLaunch: true,
tags: { Name: `${environment}-public-${i}`, Environment: environment },
});
});
const privateSubnets = [0, 1, 2].map((i) => {
return new aws.ec2.Subnet(`private-${i}`, {
vpcId: vpc.id,
cidrBlock: `10.0.${i + 10}.0/24`,
availabilityZone: aws.getAvailabilityZonesOutput().names[i],
tags: { Name: `${environment}-private-${i}`, Environment: environment },
});
});
const cluster = new aws.ecs.Cluster('main-cluster', {
tags: { Name: `${environment}-cluster`, Environment: environment },
});
const sg = new aws.ec2.SecurityGroup('app-sg', {
vpcId: vpc.id,
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'] },
],
egress: [
{ protocol: '-1', fromPort: 0, toPort: 0, cidrBlocks: ['0.0.0.0/0'] },
],
tags: { Name: `${environment}-app-sg`, Environment: environment },
});
export const vpcId = vpc.id;
export const clusterArn = cluster.arn;
export const securityGroupId = sg.id;
模式2:ComponentResource组件抽象
// components/ecs-service.ts
import * as pulumi from '@pulumi/pulumi';
import * as aws from '@pulumi/aws';
interface EcsServiceArgs {
clusterArn: pulumi.Input<string>;
subnets: pulumi.Input<string>[];
securityGroups: pulumi.Input<string>[];
imageName: pulumi.Input<string>;
cpu: number;
memory: number;
port: number;
desiredCount: number;
environment: string;
}
export class EcsService extends pulumi.ComponentResource {
public readonly serviceArn: pulumi.Output<string>;
public readonly albDns: pulumi.Output<string>;
constructor(name: string, args: EcsServiceArgs, opts?: pulumi.ComponentResourceOptions) {
super('custom:ecs:Service', name, {}, opts);
const alb = new aws.lb.LoadBalancer(`${name}-alb`, {
subnets: args.subnets,
securityGroups: args.securityGroups,
internal: false,
loadBalancerType: 'application',
tags: { Name: `${args.environment}-${name}-alb`, Environment: args.environment },
}, { parent: this });
const targetGroup = new aws.lb.TargetGroup(`${name}-tg`, {
port: args.port,
protocol: 'HTTP',
vpcId: args.subnets[0],
targetType: 'ip',
healthCheck: {
path: '/health',
interval: 30,
timeout: 5,
healthyThreshold: 2,
unhealthyThreshold: 3,
},
tags: { Name: `${args.environment}-${name}-tg`, Environment: args.environment },
}, { parent: this });
const listener = new aws.lb.Listener(`${name}-listener`, {
loadBalancerArn: alb.arn,
port: 80,
protocol: 'HTTP',
defaultActions: [{
type: 'forward',
targetGroupArn: targetGroup.arn,
}],
}, { parent: this });
const taskDefinition = new aws.ecs.TaskDefinition(`${name}-task`, {
family: `${args.environment}-${name}`,
networkMode: 'awsvpc',
requiresCompatibilities: ['FARGATE'],
cpu: args.cpu.toString(),
memory: args.memory.toString(),
containerDefinitions: pulumi.jsonStringify([{
name: name,
image: args.imageName,
essential: true,
portMappings: [{ containerPort: args.port, protocol: 'tcp' }],
logConfiguration: {
logDriver: 'awslogs',
options: {
'awslogs-group': `/ecs/${args.environment}-${name}`,
'awslogs-region': 'us-east-1',
'awslogs-stream-prefix': 'ecs',
},
},
}]),
}, { parent: this });
const service = new aws.ecs.Service(`${name}-svc`, {
cluster: args.clusterArn,
desiredCount: args.desiredCount,
launchType: 'FARGATE',
taskDefinition: taskDefinition.arn,
networkConfiguration: {
awsvpcConfiguration: {
subnets: args.subnets,
securityGroups: args.securityGroups,
assignPublicIp: true,
},
},
loadBalancers: [{
targetGroupArn: targetGroup.arn,
containerName: name,
containerPort: args.port,
}],
}, { parent: this });
this.serviceArn = service.id;
this.albDns = alb.dnsName;
this.registerOutputs({
serviceArn: this.serviceArn,
albDns: this.albDns,
});
}
}
模式3:多云部署
// multi-cloud.ts
import * as pulumi from '@pulumi/pulumi';
import * as aws from '@pulumi/aws';
import * as gcp from '@pulumi/gcp';
const config = new pulumi.Config();
const cloud = config.get('cloud') || 'aws';
function createAwsInfrastructure() {
const bucket = new aws.s3.Bucket('data-bucket', {
forceDestroy: true,
tags: { Name: 'data-bucket' },
});
const db = new aws.rds.Instance('database', {
engine: 'postgres',
engineVersion: '16.1',
instanceClass: 'db.t3.micro',
allocatedStorage: 20,
username: 'admin',
password: config.requireSecret('dbPassword'),
skipFinalSnapshot: true,
});
return { bucketArn: bucket.arn, dbEndpoint: db.endpoint };
}
function createGcpInfrastructure() {
const bucket = new gcp.storage.Bucket('data-bucket', {
location: 'US',
forceDestroy: true,
});
const db = new gcp.sql.DatabaseInstance('database', {
databaseVersion: 'POSTGRES_16',
settings: { tier: 'db-f1-micro' },
deletionProtection: false,
});
return { bucketName: bucket.name, dbConnection: db.connectionName };
}
const infra = cloud === 'aws' ? createAwsInfrastructure() : createGcpInfrastructure();
export const bucketOutput = 'bucketArn' in infra ? infra.bucketArn : infra.bucketName;
export const dbOutput = 'dbEndpoint' in infra ? infra.dbEndpoint : infra.dbConnection;
模式4:状态管理与灾难恢复
// pulumi.state-config.ts
// 使用S3后端存储状态
const backendConfig = {
url: 's3://pulumi-state-bucket/my-project',
};
// pulumi stack init my-project-dev
// pulumi config set aws:region us-east-1
// pulumi up
# 状态锁定冲突处理
pulumi stack import --file backup-state.json
# 状态文件备份与恢复
pulumi stack export --file state-backup-$(date +%Y%m%d).json
# 强制解锁(谨慎使用)
pulumi stack unlock
# 刷新状态与实际资源同步
pulumi refresh
# 销毁所有资源
pulumi destroy
// 状态漂移检测
import * as pulumi from '@pulumi/pulumi';
pulumi.runtime.setOptions({
stackTransform: (args) => {
args.resource.options.protect = true;
return Promise.resolve(args);
},
});
模式5:Pulumi GitOps与自动化
# .github/workflows/pulumi-deploy.yml
name: Pulumi Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
preview:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: pulumi/actions@v5
with:
command: preview
stack-name: my-project-staging
comment-on-pr: true
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_KEY }}
deploy:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
needs: []
steps:
- uses: actions/checkout@v4
- uses: pulumi/actions@v5
with:
command: up
stack-name: my-project-prod
skip-install: false
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_KEY }}
// Policy as Code - 合规检查
import * as pulumi from '@pulumi/pulumi';
import { PolicyPack, validateResourceOfType } from '@pulumi/policy';
new PolicyPack('infra-policies', {
policies: [
{
name: 'no-public-egress',
description: 'Security groups must not allow unrestricted egress',
enforcementLevel: 'mandatory',
validateResource: validateResourceOfType(aws.ec2.SecurityGroup, (sg, args, reportViolation) => {
for (const rule of sg.egress || []) {
if (rule.cidrBlocks?.includes('0.0.0.0/0')) {
reportViolation('Security group allows unrestricted egress');
}
}
}),
},
{
name: 'encryption-required',
description: 'S3 buckets must have encryption enabled',
enforcementLevel: 'mandatory',
validateResource: validateResourceOfType(aws.s3.Bucket, (bucket, args, reportViolation) => {
if (!bucket.serverSideEncryptionConfiguration) {
reportViolation('S3 bucket must have server-side encryption enabled');
}
}),
},
{
name: 'tag-compliance',
description: 'All resources must have required tags',
enforcementLevel: 'advisory',
validateResource: (args, reportViolation) => {
const tags = args.props.tags || {};
if (!tags.Environment) {
reportViolation('Resource missing required tag: Environment');
}
},
},
],
});
避坑指南
坑1:Output不能当字符串用
// ❌ 错误:直接拼接Output
const url = `https://${alb.dnsName}/api`; // 类型错误
// ✅ 正确:使用pulumi.interpolate或apply
const url = pulumi.interpolate`https://${alb.dnsName}/api`;
// 或
const url = alb.dnsName.apply(dns => `https://${dns}/api`);
坑2:资源依赖未声明
// ❌ 错误:隐式依赖,可能创建顺序错误
const db = new aws.rds.Instance('db', { ... });
const app = new aws.ecs.Service('app', {
environment: [{ name: 'DB_HOST', value: db.endpoint }], // 隐式依赖
});
// ✅ 正确:显式声明依赖
const app = new aws.ecs.Service('app', {
environment: [{ name: 'DB_HOST', value: db.endpoint }],
}, { dependsOn: [db] });
坑3:Secret明文存储
// ❌ 错误:密码明文
const db = new aws.rds.Instance('db', {
password: 'my-password-123',
});
// ✅ 正确:使用Config Secret
const password = config.requireSecret('dbPassword');
const db = new aws.rds.Instance('db', {
password: password,
});
坑4:未设置资源保护
// ❌ 错误:生产数据库无保护,可能被误删
const db = new aws.rds.Instance('prod-db', {
skipFinalSnapshot: true,
});
// ✅ 正确:启用保护
const db = new aws.rds.Instance('prod-db', {
skipFinalSnapshot: false,
finalSnapshotIdentifier: 'prod-db-final',
}, { protect: true });
坑5:状态文件未加密
# ❌ 错误:本地文件后端,未加密
pulumi login --local
# ✅ 正确:使用Pulumi Cloud或加密S3后端
pulumi login
# 或
pulumi login s3://pulumi-state-bucket?region=us-east-1
报错排查
| 序号 | 报错信息 | 原因 | 解决方法 |
|---|---|---|---|
| 1 | Cannot read property of Output |
Output未使用apply | 使用pulumi.interpolate或apply |
| 2 | Resource already exists |
状态与实际不一致 | pulumi import导入现有资源 |
| 3 | Conflict: stack is locked |
状态锁定冲突 | pulumi stack unlock |
| 4 | 403 Access Denied |
AWS凭证无效 | 检查AWS_ACCESS_KEY_ID配置 |
| 5 | Provider credential failed |
云提供商认证失败 | 验证Provider配置 |
| 6 | Destroy failed: protected resource |
资源被保护 | pulumi state unprotect |
| 7 | Stack not found |
Stack不存在 | pulumi stack init创建 |
| 8 | Plugin not found |
Provider插件缺失 | pulumi plugin install |
| 9 | Drift detected |
状态与实际漂移 | pulumi refresh同步 |
| 10 | Secret exposed in state |
Secret未加密 | 使用requireSecret和加密后端 |
进阶优化
- Pulumi AI:自然语言描述自动生成基础设施代码
- Automation API:在应用中嵌入Pulumi,实现自助服务平台
- Pulumi Deployments:Git推送自动触发部署
- CrossGuard:Policy as Code合规检查
- Pulumi ESC:环境密钥和配置集中管理
对比分析
| 维度 | Pulumi | Terraform | CDK for Terraform | AWS CDK |
|---|---|---|---|---|
| 语言 | TS/Python/Go/C# | HCL | TS/Python/Java | TS/Python/Java |
| 状态管理 | Pulumi Cloud/S3 | S3/Consul/Cloud | Terraform Cloud | CloudFormation |
| 测试 | 单元测试+集成测试 | terraform test | CDK测试 | CDK测试 |
| 多云 | 80+提供商 | 3000+提供商 | 3000+提供商 | AWS only |
| 组件复用 | 编程语言原生 | Module | CDK Construct | Construct |
| 学习曲线 | 中 | 低 | 中高 | 中 |
总结:Pulumi用真正的编程语言定义基础设施,让IaC拥有了循环、条件、类型检查和单元测试的能力。ComponentResource组件抽象+Output异步链+Policy as Code合规检查三位一体,让基础设施代码从"配置文件"进化为"工程化代码"。2026年Pulumi AI和Automation API的成熟让基础设施管理更加智能和自动化。
在线工具推荐
- JSON格式化:/zh-CN/json/format
- Hash计算:/zh-CN/encode/hash
- cURL转代码:/zh-CN/dev/curl-to-code
本站提供浏览器本地工具,免注册即可试用 →
#基础设施即代码#Pulumi#IaC#TypeScript IaC#2026#DevOps