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 |
| 元件複用 | 程式語言原生 | Module | CDK Construct | Construct |
| 學習曲線 | 中 | 低 | 中高 | 中 |
總結:Pulumi用真正的程式語言定義基礎設施,讓IaC擁有了迴圈、條件、型別檢查和單元測試的能力。ComponentResource元件抽象+Output非同步鏈+Policy as Code合規檢查三位一體,讓基礎設施程式碼從「設定檔案」進化為「工程化程式碼」。2026年Pulumi AI和Automation API的成熟讓基礎設施管理更加智慧和自動化。
線上工具推薦
- JSON格式化:/zh-TW/json/format
- Hash計算:/zh-TW/encode/hash
- cURL轉程式碼:/zh-TW/dev/curl-to-code
本站提供瀏覽器本地工具,免註冊即可試用 →
#基础设施即代码#Pulumi#IaC#TypeScript IaC#2026#DevOps