Pulumi-TypeScript基础设施
1. 概念
Pulumi = 用真实编程语言(TS / Python / Go)写基础设施。前端团队最大的吸引力:
- TypeScript 类型提示,IDE 自动补全
- 用循环 / 条件 / 函数(HCL 限制多)
- 复用已有 npm 生态
2. 起步
brew install pulumi
pulumi version
# 登录(可用免费 SaaS 或自托管 backend)
pulumi login # SaaS
pulumi login s3://my-pulumi-state # 自托管 S3
pulumi login azblob://...
pulumi login --local # 本地(开发)
# 创建项目
mkdir my-infra && cd my-infra
pulumi new aws-typescript # 选模板
3. 完整 AWS 示例
// index.ts
import * as aws from '@pulumi/aws'
import * as awsx from '@pulumi/awsx'
// VPC + 子网(awsx 高阶包装)
const vpc = new awsx.ec2.Vpc('main', {
cidrBlock: '10.0.0.0/16',
numberOfAvailabilityZones: 2,
natGateways: { strategy: 'Single' },
})
// 安全组
const webSg = new aws.ec2.SecurityGroup('web', {
vpcId: vpc.vpcId,
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'] }],
})
// ALB
const alb = new awsx.lb.ApplicationLoadBalancer('alb', {
subnetIds: vpc.publicSubnetIds,
})
// ECS 集群 + Fargate 服务
const cluster = new aws.ecs.Cluster('main')
const service = new awsx.ecs.FargateService('frontend', {
cluster: cluster.arn,
taskDefinitionArgs: {
container: {
name: 'web',
image: 'ghcr.io/myorg/frontend:latest',
cpu: 256,
memory: 512,
essential: true,
portMappings: [{ containerPort: 80, targetGroup: alb.defaultTargetGroup }],
},
},
desiredCount: 3,
networkConfiguration: {
subnets: vpc.privateSubnetIds,
securityGroups: [webSg.id],
},
})
// 输出
export const url = alb.loadBalancer.dnsName
4. 常用命令
pulumi up # plan + apply
pulumi up --yes # 不交互
pulumi preview # 只看变更
pulumi destroy # 销毁
pulumi stack ls # 看所有 stack
pulumi stack output url # 看输出
pulumi config set region us-east-1
pulumi config set --secret dbPassword "xxx" # 加密存储
pulumi refresh # 同步真实状态
5. Stack(环境)
每个环境一个 stack:
pulumi stack init dev
pulumi stack init staging
pulumi stack init prod
pulumi stack select prod
pulumi up
每个 stack 独立 state 和配置。
# Pulumi.prod.yaml
config:
aws:region: us-east-1
my-infra:replicas: 5
my-infra:dbPassword:
secure: AAABABxxx # 自动加密
6. 配置和 Secret
import * as pulumi from '@pulumi/pulumi'
const config = new pulumi.Config()
const region = config.require('region')
const replicas = config.requireNumber('replicas')
const dbPassword = config.requireSecret('dbPassword')
// Secret 自动加密存 state,输出时遮蔽
new aws.rds.Instance('db', {
password: dbPassword,
})
7. 复用:Component Resource
// components/StaticSite.ts
import * as pulumi from '@pulumi/pulumi'
import * as aws from '@pulumi/aws'
export class StaticSite extends pulumi.ComponentResource {
public readonly url: pulumi.Output<string>
constructor(name: string, args: { domain: string }, opts?: pulumi.ComponentResourceOptions) {
super('myorg:web:StaticSite', name, {}, opts)
const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this })
const cdn = new aws.cloudfront.Distribution(`${name}-cdn`, {
origins: [{
domainName: bucket.bucketRegionalDomainName,
originId: 'S3',
}],
// ...
}, { parent: this })
this.url = cdn.domainName
this.registerOutputs({ url: this.url })
}
}
// 使用
import { StaticSite } from './components/StaticSite'
const site = new StaticSite('frontend', { domain: 'app.example.com' })
export const url = site.url
8. 多云
import * as aws from '@pulumi/aws'
import * as alicloud from '@pulumi/alicloud'
import * as cloudflare from '@pulumi/cloudflare'
// 一个 stack 同时管多云
const s3 = new aws.s3.Bucket('logs')
const oss = new alicloud.oss.Bucket('cn-data', {})
const dnsRecord = new cloudflare.Record('app', {
zoneId: 'xxx',
name: 'app',
type: 'CNAME',
content: oss.extranetEndpoint,
})
9. 调用现有 npm 包
import * as fs from 'fs'
import { sortBy } from 'lodash-es'
// 读 yaml 配置
import yaml from 'js-yaml'
const config = yaml.load(fs.readFileSync('app-config.yaml', 'utf8'))
// 动态生成资源
config.apps.forEach((app) => {
new aws.lambda.Function(`fn-${app.name}`, { ... })
})
完整 npm 生态可用,HCL 做不到。
10. CI/CD
# .github/workflows/pulumi.yml
on:
pull_request:
push: { branches: [main] }
jobs:
preview:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- uses: pulumi/actions@v5
with:
command: preview
stack-name: prod
env:
PULUMI_ACCESS_TOKEN: $}} secrets.PULUMI_ACCESS_TOKEN }}
apply:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production # 需审批
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- uses: pulumi/actions@v5
with:
command: up
stack-name: prod
11. State Backend
# Pulumi SaaS(默认,免费团队 100MB)
pulumi login
# AWS S3
pulumi login s3://my-pulumi-state?region=us-east-1
# Azure Blob
pulumi login azblob://state?storage_account=mypulumi
# 本地(仅开发)
pulumi login --local
生产用 SaaS 或对象存储 + 加密。
12. Pulumi vs Terraform
| 优势 | Pulumi | Terraform |
|---|---|---|
| 类型 | TS 强类型 | HCL 弱类型 |
| 调试 | 标准 IDE / debugger | terraform console |
| 生态 | 复用 npm | provider |
| 学习 | 已会 TS | 学 HCL |
| 团队 | 前端友好 | 业界标准 |
| 招聘 | 较窄 | 较广 |
中小前端团队 Pulumi。大团队 / 多云 / 已有 Terraform 资产 → Terraform。
13. 常见反模式
- 不用 Component:重复代码
- state 本地不加锁:团队协作崩
- secret 不用
requireSecret:state 明文存 - 生产 stack 任何人能 apply:必须 environment + 审批
- 生成动态名 Math.random():每次 apply 创建新资源
- 跨 stack 引用混乱:用 StackReference
14. 延伸阅读
- Pulumi 文档
- Pulumi Examples
- Pulumi vs Terraform
- 模块 12 Terraform 入门与进阶