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
元件複用 程式語言原生 Module CDK Construct Construct
學習曲線 中高

總結:Pulumi用真正的程式語言定義基礎設施,讓IaC擁有了迴圈、條件、型別檢查和單元測試的能力。ComponentResource元件抽象+Output非同步鏈+Policy as Code合規檢查三位一體,讓基礎設施程式碼從「設定檔案」進化為「工程化程式碼」。2026年Pulumi AI和Automation API的成熟讓基礎設施管理更加智慧和自動化。


線上工具推薦

本站提供瀏覽器本地工具,免註冊即可試用 →

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