跳到主要内容

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: $&#125;&#125; secrets.PULUMI_ACCESS_TOKEN &#125;&#125;

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

优势PulumiTerraform
类型TS 强类型HCL 弱类型
调试标准 IDE / debuggerterraform console
生态复用 npmprovider
学习已会 TS学 HCL
团队前端友好业界标准
招聘较窄较广

中小前端团队 Pulumi。大团队 / 多云 / 已有 Terraform 资产 → Terraform。

13. 常见反模式

  • 不用 Component:重复代码
  • state 本地不加锁:团队协作崩
  • secret 不用 requireSecret:state 明文存
  • 生产 stack 任何人能 apply:必须 environment + 审批
  • 生成动态名 Math.random():每次 apply 创建新资源
  • 跨 stack 引用混乱:用 StackReference

14. 延伸阅读