Chapter 03

TypeScript 实战:类型安全的 IaC

利用 TypeScript 强类型系统,在编译时发现 IaC 错误

为什么选 TypeScript 写 Pulumi?

TypeScript 是 Pulumi 生态中最成熟的语言选择。Pulumi 的所有官方示例和大多数组件都优先提供 TypeScript 版本。TypeScript 的强类型系统与 Pulumi 的 Input<T>/Output<T> 类型系统完美配合,能在编译阶段就发现资源属性类型错误,而不必等到 pulumi up 失败后才发现。

项目初始化与 SDK 安装

# 创建 TypeScript + AWS 项目
mkdir my-ts-infra && cd my-ts-infra
pulumi new aws-typescript

# 手动安装 SDK(如果向已有项目添加云平台支持)
npm install @pulumi/pulumi @pulumi/aws

# 添加 Kubernetes 支持
npm install @pulumi/kubernetes

# 添加 GCP 支持
npm install @pulumi/gcp

# 编译 TypeScript(可选,pulumi up 会自动编译)
npx tsc --noEmit  # 只做类型检查,不输出文件

tsconfig.json 推荐配置

{
  "compilerOptions": {
    "strict": true,
    "target": "ES2020",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "bin"
  },
  "include": ["*.ts"]
}

TypeScript 项目结构

my-ts-infra/ ├── index.ts # 主入口:导出资源引用 ├── network.ts # VPC、子网、安全组 ├── compute.ts # EC2 实例 ├── storage.ts # S3、RDS ├── Pulumi.yaml # 项目配置 ├── Pulumi.dev.yaml # dev stack 配置 ├── package.json ├── tsconfig.json └── node_modules/

Input<T> 与 Output<T> 类型系统

类型定义

// Pulumi 类型系统的核心
type Input<T> = T | Promise<T> | Output<T>;
// Input<T> 可以是:
//   - 普通值(直接传 string/number/boolean)
//   - Promise(异步解析的值)
//   - Output(另一个资源的输出)

interface InstanceArgs {
    ami: Input<string>;              // 接受字符串或 Output<string>
    instanceType: Input<string>;
    subnetId?: Input<string>;        // 可选的 Input
    vpcSecurityGroupIds?: Input<Input<string>[]>;
}

强类型资源属性的好处

import * as aws from "@pulumi/aws";

// 创建安全组
const sg = new aws.ec2.SecurityGroup("web-sg", {
    ingress: [{
        fromPort: 80,
        toPort: 80,
        protocol: "tcp",
        cidrBlocks: ["0.0.0.0/0"],
        // 错误示例:
        // protocol: "invalid",  // ❌ TypeScript 编译错误!
        // fromPort: "80",       // ❌ 类型错误,应该是 number
    }],
    egress: [{
        fromPort: 0,
        toPort: 0,
        protocol: "-1",
        cidrBlocks: ["0.0.0.0/0"],
    }],
});

// 创建 EC2 实例(引用安全组 Output)
const instance = new aws.ec2.Instance("web-server", {
    ami: "ami-0c02fb55956c7d316",
    instanceType: aws.ec2.InstanceType.T3_Micro,  // 枚举类型,IDE 自动补全!
    vpcSecurityGroupIds: [sg.id],   // Output<string> 自动被 Input<string> 接受
});

pulumi.all():合并多个 Output

import * as pulumi from "@pulumi/pulumi";

const db = new aws.rds.Instance("db", { /* ... */ });
const bucket = new aws.s3.BucketV2("assets");

// 合并多个 Output 为一个
const config = pulumi.all([db.endpoint, bucket.id, db.port])
    .apply(([endpoint, bucketId, port]) => ({
        databaseUrl: `postgres://user@${endpoint}:${port}/db`,
        assetsBucket: bucketId,
    }));

// pulumi.interpolate:模板字符串中使用 Output
const dbUrl = pulumi.interpolate
    `postgres://admin:${dbPassword}@${db.endpoint}:${db.port}/mydb`;

// 导出复合配置
export const appConfig = config;

实战:VPC + EC2 + Security Group 完整网络架构

架构图

┌─────────────────────────────────────────────────────┐ │ AWS VPC (10.0.0.0/16) │ │ │ │ ┌─────────────────────┐ ┌────────────────────┐ │ │ │ Public Subnet │ │ Private Subnet │ │ │ │ 10.0.1.0/24 │ │ 10.0.2.0/24 │ │ │ │ │ │ │ │ │ │ ┌───────────────┐ │ │ ┌─────────────┐ │ │ │ │ │ EC2 Web │ │ │ │ RDS DB │ │ │ │ │ │ (port 80) │ │ │ │ (port 5432)│ │ │ │ │ └───────────────┘ │ │ └─────────────┘ │ │ │ └─────────────────────┘ └────────────────────┘ │ │ │ │ │ Internet Gateway │ └─────────────────────────────────────────────────────┘

network.ts — VPC 网络资源

// network.ts
import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";

// 1. 创建 VPC
export const vpc = new aws.ec2.Vpc("main", {
    cidrBlock: "10.0.0.0/16",
    enableDnsHostnames: true,
    enableDnsSupport: true,
    tags: { Name: "main-vpc" },
});

// 2. 创建 Internet Gateway
export const igw = new aws.ec2.InternetGateway("igw", {
    vpcId: vpc.id,
    tags: { Name: "main-igw" },
});

// 3. 创建公共子网(两个可用区,实现高可用)
const azs = ["us-east-1a", "us-east-1b"];
export const publicSubnets = azs.map((az, i) =>
    new aws.ec2.Subnet(`public-${az}`, {
        vpcId: vpc.id,
        cidrBlock: `10.0.${i + 1}.0/24`,
        availabilityZone: az,
        mapPublicIpOnLaunch: true,
        tags: { Name: `public-${az}`, Type: "public" },
    })
);

// 4. 创建路由表(公共子网 → Internet Gateway)
const publicRt = new aws.ec2.RouteTable("public-rt", {
    vpcId: vpc.id,
    routes: [{
        cidrBlock: "0.0.0.0/0",
        gatewayId: igw.id,
    }],
    tags: { Name: "public-rt" },
});

// 5. 关联路由表到公共子网
publicSubnets.forEach((subnet, i) =>
    new aws.ec2.RouteTableAssociation(`public-rta-${i}`, {
        subnetId: subnet.id,
        routeTableId: publicRt.id,
    })
);

compute.ts — EC2 实例与安全组

// compute.ts
import * as aws from "@pulumi/aws";
import { vpc, publicSubnets } from "./network";

// 安全组:允许 HTTP + SSH
export const webSg = new aws.ec2.SecurityGroup("web-sg", {
    vpcId: vpc.id,
    description: "Allow HTTP and SSH",
    ingress: [
        { fromPort: 80, toPort: 80, protocol: "tcp", cidrBlocks: ["0.0.0.0/0"] },
        { fromPort: 443, toPort: 443, protocol: "tcp", cidrBlocks: ["0.0.0.0/0"] },
        { fromPort: 22, toPort: 22, protocol: "tcp", cidrBlocks: ["10.0.0.0/8"] },
    ],
    egress: [{
        fromPort: 0, toPort: 0, protocol: "-1", cidrBlocks: ["0.0.0.0/0"],
    }],
    tags: { Name: "web-sg" },
});

// 查询最新 Amazon Linux 2 AMI
const ami = aws.ec2.getAmiOutput({
    mostRecent: true,
    owners: ["amazon"],
    filters: [
        { name: "name", values: ["amzn2-ami-hvm-*-x86_64-gp2"] },
        { name: "virtualization-type", values: ["hvm"] },
    ],
});

// EC2 实例(user_data 安装 Nginx)
export const webServer = new aws.ec2.Instance("web-server", {
    ami: ami.id,
    instanceType: aws.ec2.InstanceType.T3_Micro,
    subnetId: publicSubnets[0].id,
    vpcSecurityGroupIds: [webSg.id],
    userData: `#!/bin/bash
yum update -y
yum install -y nginx
systemctl start nginx
systemctl enable nginx`,
    tags: { Name: "web-server", Role: "webserver" },
});

index.ts — 主入口与输出

// index.ts
import * as pulumi from "@pulumi/pulumi";
import { vpc, publicSubnets } from "./network";
import { webServer, webSg } from "./compute";

// 导出关键信息
export const vpcId = vpc.id;
export const publicSubnetIds = publicSubnets.map(s => s.id);
export const webServerPublicIp = webServer.publicIp;
export const webServerUrl = pulumi.interpolate`http://${webServer.publicIp}`;
export const securityGroupId = webSg.id;
# 部署
pulumi up

# 查看输出
pulumi stack output webServerPublicIp
pulumi stack output webServerUrl

# 查看所有输出(JSON 格式)
pulumi stack output --json
TypeScript 严格模式建议

在 tsconfig.json 中开启 "strict": true 可以获得最强的类型保护,Pulumi 的类型定义完全兼容严格模式。strict: true 会启用 noImplicitAnystrictNullChecks 等一系列检查,让你在 pulumi up 之前就能发现潜在的配置错误。

本章小结

本章核心要点