编程 Pulumi 深度实战:当基础设施即代码终于学会「说人话」——从 HCL 方言困境、资源图到自动化 API 与策略即代码的生产级完全指南(2026)

2026-07-21 14:27:26 +0800 CST views 11

Pulumi 深度实战:当基础设施即代码终于学会「说人话」——从 HCL 方言困境、资源图到自动化 API 与策略即代码的生产级完全指南(2026)

如果你写过两年以上的 Terraform,大概率经历过这样的时刻:想给团队封装一个「标准Web服务」抽象,结果只能靠 module + 一堆 count/for_each + locals 拼出一套「伪函数」;想做一点点条件逻辑,HCL 的 try() 和三元表达式把可读性啃得千疮百孔;想给基础设施写单元测试,发现根本没有趁手的工具;想把一个环境开通能力嵌进后端服务,让产品经理自己点按钮拉起一套沙箱,HCL 直接劝退。

这不是你的问题,是 DSL(领域特定语言)的天花板。而 Pulumi 给出的答案是最朴素也最激进的那一个:别再发明方言了,直接用你已经会、IDE 已经懂、编译器已经帮你查过类型的通用编程语言去写基础设施。

本文从工程视角彻底讲透 Pulumi:它到底解决了什么痛点、引擎在背后干了什么、如何用 Go / TypeScript / Python 写出可复用、可测试、可嵌入应用的生产级基础设施代码,以及如何把它落到 CI/CD、策略红线与大规模团队的真实拓扑里。


一、背景:IaC 的三次范式跃迁

1.1 从「脚本地狱」到「声明式」

基础设施即代码(Infrastructure as Code,IaC)的第一次跃迁,是把手工敲 aws cli / kubectl 的脚本,换成声明式配置。你描述「期望状态」,工具负责把现实对齐到期望状态。这一步的价值怎么强调都不过分:可版本化、可 Review、可回滚。

但声明式本身不承诺「好写」。Bash/Ansible 那种过程式脚本的问题是「顺序即逻辑、副作用满天飞」;而第一代声明式工具(CloudFormation、Terraform 早期)的问题是:配置语言太弱,复杂的真实世界逻辑(条件、循环、抽象、复用)被强行塞进一个本不擅长表达它们的格式里。

1.2 Terraform 许可证地震与 OpenTofu 的诞生

2023 年 8 月,HashiCorp 把 Terraform 从 Mozilla Public License(MPL)改为 Business Source License(BSL 1.1),意味着它不再是 OSI 认可的开源协议,商业竞争场景受到限制。社区的反应非常迅速:在 Linux 基金会牵头下,OpenTofu 作为 Terraform 的社区分支于 2023 年 11 月 GA,承诺永久开源(MPL)。

这件事的深远影响不是「多了一个 Terraform 分叉」,而是让大量团队第一次认真问了一个问题:我的 IaC 工具,是不是把我锁死在了一个供应商的节奏和许可证哲学里?

Pulumi 从第一天起就是 Apache 2.0 协议,状态后端虽然有一个商业化的 Pulumi Cloud,但你完全可以用自托管后端、甚至 Git-backed 状态,不受制于任何人的商业决策。在「许可证风险」这个维度上,Pulumi 天然站在开源一边。

1.3 HCL 在大型组织里开始吃力

HCL(HashiCorp Configuration Language)设计得很克制:它想让人人都看得懂。但克制是有代价的。当基础设施规模膨胀到「几十个微服务、跨多个账号、需要统一的安全基线、需要为不同团队提供差异化抽象」时,HCL 会暴露三个典型痛点:

  1. 抽象能力贫弱。你只能用 module 做一层薄封装,无法用「类」「函数」「泛型」「接口」去表达领域的层级结构。结果是大量 copy-paste 的 module 调用,参数透传一长串。
  2. 没有真正的类型系统variabletype 约束很弱,跨模块传递复杂结构时,编译器帮不了你,错误要等到 plan 阶段甚至 apply 阶段才暴露。
  3. 测试与复用困难。你想给一段基础设施逻辑写单元测试?HCL 没有原生的测试运行时。你想把它当成一个库发布给别的团队 npm install?做不到——HCL 模块不是「包」。

Pulumi 的核心主张正是:用通用语言(TypeScript、Python、Go、.NET、Java)写基础设施,上述问题全部回归到语言本身已经解决的领域。类型检查、抽象、测试、包管理、IDE 自动补全,全部原地复用。


二、核心概念:把「声明式」重新发明一遍

很多人第一次看 Pulumi 会困惑:它还是声明式吗?还是变回过程式脚本了?答案是:它用过程式语言的写法,表达声明式的语义。理解下面几个概念是关键。

2.1 Project / Stack / Resource / ComponentResource

  • Project:一个 Pulumi 项目,由 Pulumi.yaml 描述 nameruntime(如 nodejspythongodotnet)。它对应「一段基础设施程序」。
  • Stack:同一个 Project 的一个部署实例,相当于一套环境(dev / staging / production)。每个 Stack 有独立的 state 和独立的配置。
  • Resource:任何被 Pulumi 管理的基础设施对象(一个 S3 Bucket、一条 DNS 记录、一个 K8s Deployment)。
  • ComponentResource这是 Pulumi 区别于 Terraform module 的杀手锏。它是一个「逻辑容器」,把一组 Resource 聚合成一个更高层的概念(比如 StaticSiteKubernetesService)。它是对真实云资源的「函数封装」——你可以给它传参、做内部逻辑、返回输出,完全就是写一个类。
// Go:一个 ComponentResource 就是一个普通的 struct + 方法
type StaticSite struct {
    pulumi.ResourceState // 嵌入以继承 ComponentResource 行为
    URL                   pulumi.StringOutput
}

func NewStaticSite(ctx *pulumi.Context, name string, args *StaticSiteArgs, opts ...pulumi.ResourceOption) (*StaticSite, error) {
    component := &StaticSite{}
    err := ctx.RegisterComponentResource("acme:infra:StaticSite", name, component, opts...)
    if err != nil {
        return nil, err
    }
    // 内部创建真实资源,并把 parent 指向这个 component
    bucket, err := s3.NewBucketV2(ctx, name+"-bucket", &s3.BucketV2Args{
        Bucket: pulumi.String(args.BucketName),
    }, pulumi.Parent(component))
    if err != nil {
        return nil, err
    }
    component.URL = bucket.Bucket
    return component, nil
}

注意 pulumi.Parent(component) 这一行——它让所有子资源在状态里都挂在 StaticSite 之下,销毁、依赖、权限都跟着走。这是真正的「组合」,而不是 HCL module 那种「文本展开」。

2.2 Output<T> 与 Input<T>:部署时才知道的值

这是 Pulumi 最容易劝退新人的概念,也是它最优雅的设计之一。

云资源之间高度依赖「对方创建后才知道的值」:你创建了一个 VPC,它的 vpcIdapply 之前并不存在;你拿这个 vpcId 去建子网,再拿子网的 subnetId 去建路由表。在 Terraform 里,这种依赖由图自动解析;在 Pulumi 里,这种「未来才有的值」被建模成 Output<T>

// TypeScript:Output<T> 不是普通字符串,是一个「承诺」
const bucket = new aws.s3.BucketV2("assets", { bucket: "my-app-assets" });

// 错误写法:bucket.bucket 是 Output<string>,不能直接字符串拼接
// const url = "https://" + bucket.bucket + ".s3.amazonaws.com"; // ❌ 类型错误

// 正确写法:用 .apply 在「值就绪后」做转换
const url = bucket.bucket.apply((name) => `https://${name}.s3.amazonaws.com`);

Output<T> 是只读的「计算结果」;Input<T> 是「输入」,它既能接受普通值(如 "us-east-1"),也能接受 Output<T>。几乎所有资源的参数类型都是 Input<T>,所以「普通值」和「未来值」可以无缝混用——引擎会在运行时自动建立依赖边。

2.3 状态与后端:Cloud、自托管与 Git-backed

Terraform 的 state 是本地 .tfstate 或远端 S3 + DynamoDB 锁。Pulumi 的状态后端抽象成了一层可插拔的「Backend」:

  • Pulumi Cloud(saas):托管状态、历史、Web Console、RBAC、漂移检测 UI。
  • 自托管(Self-hosted):企业版可以把 Pulumi Service 跑在自己的集群里,满足数据不出域的合规要求。
  • 本地 / 文件 / Gitpulumi login --local 用本地文件;也可以用对象存储或 Git 仓库做状态后端。

对绝大多数中小团队,我的建议是:先用 Pulumi Cloud 的免费层把状态托管起来,把「状态锁、加密、历史」这些脏活交给它;等规模上来、合规要求明确,再切自托管或 Git-backed,迁移成本极低,因为代码一行都不用改。

2.4 Provider 与 Native Provider

Provider 是 Pulumi 与云厂商 API 之间的适配器。Pulumi 有两代 Provider:

  • Bridged Provider:用 Terraform 的 Provider 通过一层 bridge 转译过来(如经典的 @pulumi/aws 底层曾大量依赖 Terraform AWS Provider)。好处是覆盖广、跟得紧。
  • Native Provider:直接用目标语言从云厂商 OpenAPI / SDK 生成(如 @pulumi/aws-native 用 AWS Cloud Control API)。好处是更轻、更贴合云厂商原生模型、发布更快。

实际选型上,通用资源用 bridged(覆盖全),追求原生一致性或新特性用 native。两者可以混用,Pulumi 不限制。

2.5 与 Terraform 心智模型对照

维度Terraform (HCL)Pulumi (通用语言)
抽象单元module(文本展开)ComponentResource(真正的类/函数)
类型安全弱,plan 期才发现编译期/类型检查期即可发现
复用分发Git submodule / registrynpm / pip / go mod 等原生包管理
单元测试无原生运行时有(mock provider + pulumi test
嵌入应用基本不可行Automation API 原生支持
状态后端S3+DynamoDB 等可插拔(Cloud/自托管/Git/本地)
许可证BSL(Terraform)/ MPL(OpenTofu)Apache 2.0

三、架构分析:引擎到底在做什么

理解 Pulumi 不是「脚本按顺序执行」,是写好它的前提。它的运行时是一个多进程协作模型

3.1 三进程模型:语言宿主 + 引擎 + Provider

┌──────────────────┐    gRPC     ┌──────────────────┐   gRPC/HTTP   ┌─────────────┐
│  Language Host   │ ──────────► │   Pulumi Engine  │ ────────────► │  Providers  │ ──► 云厂商 API
│ (你的 Go/TS/Py)  │ ◄────────── │  (核心协调器)    │ ◄──────────── │ (aws/k8s..) │
└──────────────────┘   事件流     └──────────────────┘   资源状态     └─────────────┘
                                        │
                                        ▼
                                  状态后端 (State)
  • Language Host:运行你的程序。你的代码里每调用一次 new aws.s3.BucketV2(...),语言宿主并不直接调 AWS,而是通过 gRPC 向引擎「注册」一个资源请求。
  • Pulumi Engine:核心协调器。它收集所有资源注册请求,构建依赖图,按拓扑顺序调用 Provider 去「实际创建/更新/删除」,并把结果写回状态。
  • Provider:真正与云厂商 API 对话的插件。

关键点:你的程序跑完(register 完所有资源)后,真正的变更才由引擎按图执行。你在 main 函数里写的「顺序」只决定依赖图的「边」,不决定执行的「时刻」。这就是为什么 Pulumi 是声明式的——你描述的是「有哪些资源、它们之间什么依赖」,引擎决定「怎么落地」。

3.2 资源图(DAG)的构建与并行化

引擎收到的注册事件带有隐式依赖:

  • A 的构造参数引用了 B 的 Output → A 依赖 B;
  • 显式 dependsOn / parent → 额外边。

引擎据此构建一个有向无环图(DAG),然后拓扑排序 + 并行执行:没有依赖关系的资源会被同时下发。这就是为什么一个 200 个资源的 stack,up 时可以几秒到几十秒完成,而不是 200 次串行 API 调用。

# 控制并行度(默认会根据依赖图尽量并行)
pulumi up --parallel 20

3.3 期望状态协调(Reconciliation)与 Diff 算法

每次 up,引擎都会:

  1. 从状态后端读取「上一次的实际状态」;
  2. 用当前程序算出「期望状态」;
  3. 对每一个资源做 diff:哪些属性变了?能否原地更新?必须替换(recreate)?

Pulumi 的 diff 是基于 schema 的精细 diff——它能区分「可更新字段」和「不可变字段(变更即重建)」。例如改 S3 Bucket 的 bucket 名称(不可变),引擎会标记为 replace;改 tags,则标记为 update。这个 diff 结果会在 preview 里清晰展示,包括 +-~ 符号和具体的字段级变化。

3.4 Secret 的加密链路

Pulumi 的 Secret 不是「明文存 state,靠权限控访问」,而是默认加密写入状态。链路是:

  • 你用 pulumi config set --secret DB_PASSWORD xxx 写入的密钥,会被一个 encryption key 加密后再进 state;
  • 这个 key 可以来自 Pulumi Cloud 托管的 KMS,也可以是你自己的 KMS / age / passphrase;
  • 读取时只有拥有 key 的上下文才能解密。
# 写密钥(自动加密进 state)
pulumi config set --secret aws:accessKey AKIA......
# 在代码里直接当普通 Input 用,引擎负责解密后传给 Provider

更进一步,Pulumi ESC(Environments, Secrets, and Configuration) 把「环境 + 密钥 + 配置」本身也变成了代码,可以在多个 stack / 多个项目间组合复用,而不是在每个 stack 里重复 config set

# ESC 环境定义(myorg/prod-env)
values:
  environmentVariables:
    AWS_REGION: us-east-1
  aws:
    region: ${environmentVariables.AWS_REGION}
  secrets:
    DB_PASSWORD:
      fn::secret: ENC[AES256_GCM,data:xxxx,iv:xxxx,tag:xxxx,type:str]

然后在 Pulumi.yaml 里引用:environment: myorg/prod-env。一次定义,处处可用,密钥永不落本地明文。


四、代码实战

光讲概念没意思,直接上三种语言的真实可运行代码。

4.1 Go:定义一个生产级静态站点栈(S3 + CloudFront + OAC)

下面这段用 Go 定义了一个「关闭公开 ACL、通过 CloudFront + OAC 代理访问」的静态站点,并随手把安全基线(公开访问阻断)做了进去。

package main

import (
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
	"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/s3"
	"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/cloudfront"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		// 1) 存储桶
		bucket, err := s3.NewBucketV2(ctx, "site-bucket", &s3.BucketV2Args{
			Bucket: pulumi.String("my-app-static-site-2026"),
			Tags:   pulumi.StringMap{"Owner": pulumi.String("platform-team")},
		})
		if err != nil {
			return err
		}

		// 2) 彻底关闭公开访问(安全基线)
		_, err = s3.NewBucketPublicAccessBlock(ctx, "site-pab", &s3.BucketPublicAccessBlockArgs{
			Bucket:                bucket.ID(),
			BlockPublicAcls:       pulumi.Bool(true),
			IgnorePublicAcls:      pulumi.Bool(true),
			BlockPublicPolicy:     pulumi.Bool(true),
			RestrictPublicBuckets: pulumi.Bool(true),
		})
		if err != nil {
			return err
		}

		// 3) OAC:让 CloudFront 用 sigv4 签名访问私有桶
		oac, err := cloudfront.NewOriginAccessControl(ctx, "site-oac", &cloudfront.OriginAccessControlArgs{
			OriginAccessControlConfig: cloudfront.OriginAccessControlConfigArgs{
				Name:                       pulumi.String("site-oac"),
				OriginAccessControlOriginType: pulumi.String("s3"),
				SigningBehavior:           pulumi.String("always"),
				SigningProtocol:           pulumi.String("sigv4"),
			},
		})
		if err != nil {
			return err
		}

		// 4) 分配(CDN)
		dist, err := cloudfront.NewDistribution(ctx, "site-cdn", &cloudfront.DistributionArgs{
			Enabled:            pulumi.Bool(true),
			DefaultRootObject: pulumi.String("index.html"),
			Origins: cloudfront.DistributionOriginArray{
				&cloudfront.DistributionOriginArgs{
					DomainName:             bucket.BucketRegionalDomainName,
					OriginId:              pulumi.String("s3-origin"),
					OriginAccessControlId: oac.ID(),
				},
			},
			DefaultCacheBehaviors: cloudfront.DistributionDefaultCacheBehaviorArray{
				&cloudfront.DistributionDefaultCacheBehaviorArgs{
					TargetOriginId:       pulumi.String("s3-origin"),
					ViewerProtocolPolicy: pulumi.String("redirect-to-https"),
					AllowedMethods:       pulumi.StringArray{pulumi.String("GET"), pulumi.String("HEAD")},
					CachedMethods:        pulumi.StringArray{pulumi.String("GET"), pulumi.String("HEAD")},
					Compress:             pulumi.Bool(true),
				},
			},
		})
		if err != nil {
			return err
		}

		ctx.Export("cdnDomain", dist.DomainName)
		ctx.Export("bucketName", bucket.Bucket)
		return nil
	})
}

注意:这里没有任何「手动编排顺序」的代码。你只是声明了 4 个资源,引擎自己算出了「桶 → 公开阻断 → OAC → 分配」的依赖与并行关系。

4.2 TypeScript:用 ComponentResource 做可复用抽象

如果全公司都要建静态站点,你会希望把它封装成一个「类」,传个名字就能用。这正是 ComponentResource 的主场。

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

export interface StaticSiteArgs {
    bucketName: string;
    indexDocument?: string;
}

export class StaticSite extends pulumi.ComponentResource {
    public readonly url: pulumi.Output<string>;

    constructor(name: string, args: StaticSiteArgs, opts?: pulumi.ComponentResourceOptions) {
        super("acme:infra:StaticSite", name, {}, opts);

        const bucket = new aws.s3.BucketV2(`${name}-bucket`, {
            bucket: args.bucketName,
            tags: { Owner: "platform-team" },
        }, { parent: this });

        const oac = new aws.cloudfront.OriginAccessControl(`${name}-oac`, {
            name: `${name}-oac`,
            originAccessControlOriginType: "s3",
            signingBehavior: "always",
            signingProtocol: "sigv4",
        }, { parent: this });

        const dist = new aws.cloudfront.Distribution(`${name}-cdn`, {
            enabled: true,
            defaultRootObject: args.indexDocument ?? "index.html",
            origins: [{
                domainName: bucket.bucketRegionalDomainName,
                originId: "s3-origin",
                originAccessControlId: oac.id,
            }],
            defaultCacheBehaviors: [{
                targetOriginId: "s3-origin",
                viewerProtocolPolicy: "redirect-to-https",
                allowedMethods: ["GET", "HEAD"],
                cachedMethods: ["GET", "HEAD"],
                compress: true,
            }],
        }, { parent: this });

        this.url = dist.domainName;
        this.registerOutputs({ url: this.url });
    }
}

// 使用方:一行搞定一个站点
const docs = new StaticSite("docs", { bucketName: "acme-docs-site" });
export const docsUrl = docs.url;

这就是和 HCL module 的本质区别:调用方拿到的 docs.url 是一个有类型、可传到别处继续组合的输出,而不是一段需要在 output.tf 里手动 value = module.docs.cdn_domain 拼接的弱类型字符串。

4.3 Python + Automation API:把 Pulumi 嵌进你的应用

这是 Pulumi 最被低估的能力。Automation API 让你用代码直接驱动「preview / up / destroy」,而不必靠 shell 调用 CLI。典型场景:一个内部平台,产品经理在后台点「开通测试环境」,后端服务用 Automation API 拉起一套隔离的云资源,返回访问地址。

import pulumi
from pulumi import automation as auto
import pulumi_aws as aws

def build_program(environment: str):
    def program():
        bucket = aws.s3.BucketV2("env-bucket",
                                 bucket=f"myapp-{environment}",
                                 tags={"Owner": "platform-team"})
        pulumi.export("bucket_name", bucket.bucket)
    return program

def provision_environment(environment: str):
    stack = auto.create_or_select_stack(
        stack_name=environment,
        project_name="self-service",
        program=build_program(environment),
    )
    stack.set_config("aws:region", auto.ConfigValue(value="us-east-1"))

    # 直接 up,结果结构化返回
    up = stack.up(on_output=lambda line: print(line))  # 实时日志
    return {
        "bucket": up.outputs["bucket_name"].value,
        "summary": up.summary.resource_changes,  # 这次变更了多少资源
    }

if __name__ == "__main__":
    print(provision_environment("staging"))

这段代码可以原样跑在一个 FastAPI / Flask 服务里。基础设施不再是「运维的脚本」,而是「产品功能的一部分」。这种「基础设施即产品能力」的范式,正是平台工程(Platform Engineering)的核心。

4.4 CrossGuard:策略即代码守住红线

光有抽象还不够,大团队必须有人守红线:S3 不能公开、安全组不能 0.0.0.0/0 全开、所有资源必须带 Owner 标签。Pulumi 用 CrossGuard 把「策略」写成代码,在 up 前自动校验,违规直接阻断。

// policy/index.ts —— 用 @pulumi/policy 写策略
import { validateStack } from "@pulumi/policy";
import * as aws from "@pulumi/aws";

validateStack("require-owner-tag", (args, reportViolation) => {
    for (const r of args.resources) {
        if (r.type === "aws:s3/bucketV2:BucketV2") {
            const tags = (r.props["tags"] as Record<string, string>) || {};
            if (!tags["Owner"]) {
                reportViolation("所有 S3 Bucket 必须带 Owner 标签", r.urn);
            }
        }
        // 顺便拦掉公开的 S3 ACL
        if (r.type === "aws:s3/bucketAclV2:BucketAclV2") {
            const acl = r.props["acl"];
            if (acl === "public-read" || acl === "public-read-write") {
                reportViolation("禁止公开 ACL", r.urn);
            }
        }
    }
});

部署前 pulumi up 会自动加载策略包,任何违规都会以 error 级别阻断发布——比「靠 Review 人肉看」可靠得多。

4.5 测试:给基础设施写单元测试

Pulumi 支持 pulumi test,用 mock provider 在不真正碰云的情况下验证你的资源逻辑。下面是用 Go 写的单元测试骨架:

func TestStaticSite_CreatesBucket(t *testing.T) {
    // 用 WithMocks 注册假的 Provider,断言资源被按预期注册
    err := pulumi.RunErr(func(ctx *pulumi.Context) error {
        site, err := NewStaticSite(ctx, "test", &StaticSiteArgs{BucketName: "x"})
        if err != nil {
            return err
        }
        // 断言输出非空(真实测试里可进一步断言资源的 props)
        if site.URL == nil {
            t.Fatal("expected URL output to be set")
        }
        return nil
    }, pulumi.WithMocks("proj", "stack", mocks)) // mocks 实现 resource/read 的假行为
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
}

虽然 mock 写出来有点样板代码,但它换来的是:基础设施逻辑的回归测试可以进 CI,PR 里改了一行 ComponentResource,立刻有测试告诉你有没有改坏别的资源。这是 HCL 时代几乎不可能低成本的做到的事。


五、性能优化与生产实践

5.1 并行度、refresh 成本与 --target 手术刀

  • 并行度pulumi up --parallel N 控制同时下发的资源数。依赖链末端的独立资源会被并行打出去。默认已经尽量并行,超大 stack 可显式调大。
  • refresh 是贵操作pulumi up 默认会先 refresh(拉取云上真实状态做 diff),资源多时会很慢。CI 里可以用 pulumi up --skip-refresh 跳过,只信本地 state;但定期(如每天一次)跑一次带 refresh 的 preview 来发现漂移。
  • --target 手术刀:只想改一个资源及其依赖?pulumi up --target <urn> --target-dependents,只动相关子树,避免全量 diff 的漫长等待。
# 只更新某个特定资源及其依赖者
pulumi up --target 'urn:pulumi:prod::app::aws:s3/bucketV2:BucketV2::site-bucket' --target-dependents

5.2 漂移检测与 protect

云上资源被「手改」是常态(有人直接在控制台点了下)。Pulumi 通过 pulumi refresh 把真实状态拉回与 state 对齐,再用 pulumi preview --refresh 看「代码期望」和「现实」差在哪。

对于「绝对不能误删」的资源(生产数据库、核心桶),用 protect: true

new aws.s3.BucketV2("prod-db-backup", {
    bucket: "prod-db-backup-2026",
}, { protect: true }); // 任何 up/destroy 都不会动它,除非先手动去掉 protect

protect 会在误删前强制拦截,是生产环境的保命符。

5.3 从 Terraform 迁移:import 与 tf2pulumi

已有 Terraform 资产不必推倒重来:

# 把已有资源导入 Pulumi 管理(不重建,只接管)
pulumi import aws:s3:BucketV2 legacy-bucket my-legacy-bucket-name

# 把 .tf 代码批量转成 Pulumi(TypeScript/Python/Go)
tf2pulumi --target-language typescript

tf2pulumi 不是 100% 无损,复杂 module / 动态表达式需要人工校对,但能覆盖 70%~80% 的搬运工作,剩下的靠 import + 手工对齐 state。

5.4 大型项目的项目拓扑

两种主流组织方式:

  • Poly-repo(多项目):网络层一个 project、数据层一个 project、每个服务一个 project。优点:权限/状态边界清晰、爆炸半径小;缺点:跨项目引用要靠 stack reference。
  • Mono-repo(单项目多 stack):一个 Pulumi 项目里用多个 stack 区分环境/服务。优点:复用 ComponentResource 极方便;缺点:单 state 文件可能很大,误操作影响面宽。

我的经验法则:基础设施「地基」(账号、网络、IAM、共享存储)用独立 poly-repo + stack reference 对外暴露输出;业务服务各自 poly-repo 引用地基输出。既隔离爆炸半径,又不重复造轮子。

// 在业务项目里引用地基项目的输出
const infra = pulumi.StackReference("acme/foundation/production")
val vpcId = infra.GetStringOutput("vpcId")

5.5 CI/CD 与状态锁定

Pulumi 后端天然提供状态锁(同一 stack 同一时刻只允许一个 up),CI 里直接:

pulumi login --cloud-url <后端>
pulumi stack select production
pulumi preview --stack production   # PR 阶段只预览,贴 diff 到 PR 评论
pulumi up --stack production --yes  # 合并后自动 apply

配合 PR 预览 + 审批,基础设施的发布流程和标准应用代码完全一致。


六、总结展望:通用语言是 IaC 的终局吗

我的判断是:在「复杂、多团队、需要抽象与测试」的场景里,通用语言会是终局;在「简单、一次性、只想快速拉起」的场景里,DSL 仍有价值。两者不是你死我活,而是分层共存——这也是为什么 Pulumi 与 OpenTofu / Terraform 会长期并存:前者吃「工程复杂度」,后者吃「上手速度」。

几个值得关注的趋势:

  1. 平台工程把 IaC 推向「产品化」。Automation API 让基础设施成为后端服务的能力,开发者「自助开通、自助销毁」,平台团队退居「能力供给方」。这会是未来三年企业落地的主战场。
  2. ESC 与环境即代码。密钥、配置、环境不再是散落在各 stack 的 config set,而是可组合、可继承、可审计的代码资产。
  3. 策略即代码的常态化。CrossGuard 这类能力会从「高级团队的可选项」变成「合规的必选项」,和 CI 门禁深度绑定。
  4. AI 辅助生成。用自然语言描述「我要一个带 WAF 的 HTTPS 站点」,模型直接产出 Pulumi 代码——而通用语言因为「有编译器、有类型、能跑测试」,比 HCL 更适合做 AI 生成的「可校验产物」。

回到开头的那个问题:HCL 没做错什么,它只是把 IaC 带到了一个高度,然后触到了 DSL 的天花板。Pulumi 做的,是把天花板捅破——让写基础设施这件事,重新变成「写代码」这件你本就擅长的事。当你能用 for 循环、class 封装、pytest/go test 去管理云,你会发现:原来基础设施也可以有 DRY、有抽象、有回归测试,也可以优雅。

如果你正在被 Terraform 的 module 地狱折磨,或者被「想给基础设施写个测试」这件事劝退,不妨用 pulumi new aws-go 起一个项目,把本文的静态站点例子跑一遍。十分钟后你会回来谢我。


本文所有示例均可在 Pulumi 3.x 系列最新版中运行,Provider 以 @pulumi/aws v6 系列为准。实际部署前请结合自身云账号权限与合规要求调整。

推荐文章

Linux 网站访问日志分析脚本
2024-11-18 19:58:45 +0800 CST
thinkphp分页扩展
2024-11-18 10:18:09 +0800 CST
Mysql允许外网访问详细流程
2024-11-17 05:03:26 +0800 CST
Golang中国地址生成扩展包
2024-11-19 06:01:16 +0800 CST
全栈工程师的技术栈
2024-11-19 10:13:20 +0800 CST
pycm:一个强大的混淆矩阵库
2024-11-18 16:17:54 +0800 CST
JavaScript 策略模式
2024-11-19 07:34:29 +0800 CST
程序员茄子在线接单