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大挑战

  1. 学习曲线:从HCL迁移到TypeScript需要思维转换
  2. Output处理:Output不能直接当字符串用,需要理解异步链
  3. 状态管理:状态文件损坏或冲突的恢复
  4. 组件设计:如何抽象可复用的基础设施组件
  5. 多云编排:不同云提供商资源的依赖和协调

分步实操: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和加密后端

进阶优化

  1. Pulumi AI:自然语言描述自动生成基础设施代码
  2. Automation API:在应用中嵌入Pulumi,实现自助服务平台
  3. Pulumi Deployments:Git推送自动触发部署
  4. CrossGuard:Policy as Code合规检查
  5. 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的成熟让基础设施管理更加智能和自动化。


在线工具推荐

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

#基础设施即代码#Pulumi#IaC#TypeScript IaC#2026#DevOps