2025:基础设施即代码正当时

4 阅读7分钟

前文提到 2025 年的一大收获是使用 Pulumi 实践了基础设施即代码。今天就来分享一下这一年的实践心得。

要有权限

实践基础设施即代码最重要的前提就是有足够的权限。年初时我加入一个新建的组织,因为这是一个全新的团队,没有沉重的历史包袱,我拥有足够的权限来自由探索和实验。

但写到现在,组织已经逐渐复杂化了。各种部门、规范、流程相继而来,问题也随之显现。有人开始提出各种个性化需求而不愿遵循规范,修改 Pulumi 代码显得繁琐;更麻烦的是,一些人开始绕过代码直接手工管理 AWS 环境。这样做的结果就是:并非所有人都愿意或有能力维护 Pulumi 代码,IaC 的价值也就逐步被侵蚀。

要足够快

表面上看只是一个参数修改,但在成熟的组织中,这背后往往涉及一系列的规范要求和流程审批。传统基础设施部门之所以能快速响应,其实是因为他们早已为常见任务制定了标准 SOP 并构建了自动化工具。

在我的情况下,规范相对简化,直接登录 AWS 看似最快。但既然选择了 Pulumi,用代码速度真的更快吗?在 2023 年时我还不确定,但到了 2025 年,答案很明确:写代码反而是最高效的方式。

没有 AI 编程工具的话,学习 Pulumi 将是一个漫长的过程。Pulumi 本身入门容易,但实际项目中往往涉及相互关联的多个资源,你需要既精通 API,还要理解这些 API 背后的 AWS 服务概念。这需要大量的时间投入才能掌握。

幸好已经是 2025 年了。从 VSCode Copilot、Roo Code、Windsurf,再到后来的 Trae、Kiro 和 Antigravity,AI 编程工具的进化令人印象深刻。年初时这些工具还显得有些不稳定,但经过一年的发展和大量 Vibe Coding 实践,我发现 Pulumi 代码已经变得相当易于编写了。

不过,有过 Vibe Coding 经验的开发者都知道,要让代码长期稳定可维护,绝不能只是堆砌代码,必须有良好的架构设计。下面就分享一些我的实践心得。

使用 S3 后端

Pulumi 必须有一个地方存储状态文件。对于大多数企业而言,Pulumi 官方云和本地文件都不太合适,使用 S3 是最佳选择。配置示例如下:

backend:
  url: s3://some-prod-pulumi/infra?region=ap-southeast-1&awssdk=v2&profile=prod

使用配置文件

最初我有点纠结:既然是基础设施即代码,为什么还要加配置文件呢?但在真正用 Pulumi 编写资源配置后,我意识到高级编程语言虽然强大,却不如 DSL 那样简洁直观。因此抽象和分层就变得必要。例如,我将 EC2 的各种配置参数整理成了 YAML 配置:

...
    instanceGroups:
      - namePrefix: bastion
        subnetId: private-subnet-1
        ami: ami-0b8607d2721c94a77
        volume_size: 30
        ext_volume_size: 50
        privateIps:
          - 10.62.21.110
        instanceType: t3.micro
      - namePrefix: vpn
        subnetId: public-subnet-1
        ami: ami-08b138b7cf65145b1
        instanceType: t3.small
        volume_size: 30
        ext_volume_size: 20
        privateIps:
          - 10.62.21.21
...

此时,配置文件负责定义各个 EC2 实例的差异,而代码中实现所有的共性逻辑和规范。职责分离清楚,更易维护。

模块化设计

起初需求很分散,我索性把所有代码都写在了 __main__.py 中。一是因为还没有分模块的必要,二是懒得折腾。

后来出现了一个新需求:要把某个应用相关的基础设施代码独立出来,使其能在客户环境中一键部署。考虑到现有代码还会持续演变,我不想维护两套代码,所以决定重新组织代码结构。既要支持在现有环境中运行,也要能独立部署。

这时高级编程语言的优势就显现出来了。我轻松地将代码拆分成多个 Python 模块,放在不同的文件中,逻辑更清晰,复用性也更高。

显式管理依赖关系

虽然这条看起来理所当然,但还是值得强调一下。初期我没有意识到需要显式定义依赖,但 Pulumi 其实已经帮我们做了很多。当资源使用另一个资源的输出时,依赖关系是隐含的。

不过当多个资源之间没有明确的变量引用,但却有逻辑依赖关系时,就必须显式声明。比如 EKS 的 Storage Class 依赖 CSI Addon,就需要这样指定:

    efs_storage_class = k8s.storage.v1.StorageClass(f"{prefix}-efs-sc",
        metadata={
            "name": "efs-sc"  # 这是将在 PVC 中引用的 StorageClass 名称
        },
        provisioner="efs.csi.aws.com",
        parameters={
            "provisioningMode": "efs-ap",
            "fileSystemId": efs_file_system.id, # 引用上面创建的 EFS 文件系统 ID
            "directoryPerms": "700",
        },
        opts=pulumi.ResourceOptions(provider=k8s_provider, depends_on=[efs_csi_addon])
    )

忽略无关差异

Pulumi 在更新基础设施前会比对自身记录的状态和当前代码执行的结果,只对差异部分进行更新。这在大多数情况下运行良好,但有些差异其实无关紧要,不需要更新。此时就需要用 transformations 方法处理。aws-load-balancer-controller 就是典型例子:

...
        opts=pulumi.ResourceOptions(
            provider=k8s_provider, 
            depends_on=[nodegroup_a, nodegroup_b],
            transformations=[ignore_changes],
        )

该方法定义为:

    def ignore_changes(args: ResourceTransformationArgs):
        if args.type_ == "kubernetes:admissionregistration.k8s.io/v1:ValidatingWebhookConfiguration" or args.type_ == "kubernetes:admissionregistration.k8s.io/v1:MutatingWebhookConfiguration":
            return ResourceTransformationResult(
                props=args.props,
                opts=ResourceOptions.merge(args.opts, ResourceOptions(
                    ignore_changes=[
                        "metadata.annotations.template",
                        "webhooks[*].clientConfig",
                    ],
                )))
        if args.type_ == "kubernetes:core/v1:Secret" :
            return ResourceTransformationResult(
                props=args.props,
                opts=ResourceOptions.merge(args.opts, ResourceOptions(
                    ignore_changes=[
                        "data",
                    ],
                )))

如果不设置忽略规则,每次 Pulumi 更新都会报告这些资源有变化,这完全没有必要。

合理打标签

给资源打标签非常重要,尤其是在成本分析时。用 Pulumi 修改标签简直太方便了——只需改一下代码,免去了在 AWS 界面上逐个操作的麻烦。

高级用法:编程式调用

Pulumi 还支持不通过命令行直接调用,让你完全掌控其生命周期。我曾尝试 Pulumi + Dagger 的组合,用纯 Python 编写了包含基础设施初始化的 CI/CD 程序,虽然可行,但实际上 Pulumi 在管理大型 Stack 时价值更大。对于一些边边角角的基础设施变更,直接调用相关 API 反而更轻量。

总结

随着团队规模扩大,公司的传统习惯也逐渐蔓延,基础设施即代码的实践可能会被逐步侵蚀。但这很正常——IaC 本身就是 DevOps 文化的一部分,需要整个组织理念的转变。DevOps 不仅仅是让开发人员做运维工作,而是需要全局统筹,由更专业的人来主导这种转变。

对我个人而言,现在已经没必要去改变别人了。有了 AI 的加持,我能一句话就生成完全无误的基础设施代码栈,这本身就够让我满足了。

回顾这一年的经历,我发现一个有趣的现象:做技术久了,会意识到宇宙的宏大奥秘藏在最细微的粒子中。一个看似微不足道的细节,可能会决定一次伟大变革的成败。就像过去的 Job、Process、Thread 的出现,都无形中改变了整个 IT 生态。基础设施即代码是基础设施领域的终极答案,但它必然要伴随相应的组织和文化变革才能真正发挥价值。

最后,还是有些感触。前段时间我用 AI 制作了一首歌曲,献给像我一样的老技术人。感兴趣的朋友可以听听:向晚的光。如果链接不可用,也可以直接在网易云音乐搜索“向晚的光”。