阶段三 · 基础设施即代码

基础设施即代码(IaC)

一句话总结

前两个阶段管的是"集群跑什么",但集群本身,网络,数据库,DNS 这些底座从哪来?
基础设施即代码(IaC)把"开服务器,建网络"也变成可版本化,可复现的声明式代码--其灵魂是一个你在前面没见过的新概念:状态(State).

前置回顾

阶段二我们让应用自动,安全地跑在了 Kubernetes 集群上.但有个一直被当作前提,从没追问的问题:那个集群本身是谁创建的? 它所在的 VPC 网络,它连的云数据库,它用的域名和证书--这些"底座"如果还靠人在云厂商控制台上点鼠标创建,那我们前面建立的"可复现,可追溯"就有个手工的地基.本篇是阶段三,也是整个 CI/CD 系列的收尾:把声明式思想从"应用"贯彻到"基础设施".

问题:点控制台建出来的基础设施,没人说得清

先看没有 IaC 的团队怎么准备基础设施--业界戏称 "ClickOps"(点点运维):

需要一套生产环境:登录云控制台 → 点"创建 VPC" → 点"创建子网" → 点"创建 K8s 集群" → 点"创建数据库" → 配安全组 → 配负载均衡 → 配 DNS...几十次点击,散落在十几个控制台页面里.

三个月后,要再开一套一模一样的灾备环境:"当时那个集群的网络是怎么配的来着?" → 没人记得;"安全组开了哪些端口?" → 得一个个翻;"为什么这个参数是这个值?" → 当时点的人离职了.结果:照着记忆又点一遍,开出来一套"看起来差不多但其实不一样"的环境.

这和第 01 篇手工发布,第 05 篇手动改集群是同一个病:依赖人的记忆和手工操作,不可复现,不可追溯,无法 review,会漂移.解药也是同一个思路--把它变成声明式代码.

ClickOps 像凭记忆手工搭一座房子,每次盖出来都不太一样,且没有图纸;IaC 像先画好施工蓝图(代码),施工队照图施工--同一张图,在哪盖,盖几次,都是一模一样的房子.

IaC 是什么:用代码声明基础设施

基础设施即代码(Infrastructure as Code,IaC)的主张:把服务器,网络,数据库,DNS 等基础设施,用代码声明出来,由工具读取代码去自动创建和管理.

注意这里又是熟悉的声明式(第 05 篇)--你描述"我要什么基础设施",而不是"一步步怎么点".以 Terraform 为例,声明"一台云服务器 + 一个数据库"大致长这样:

# main.tf -- 声明期望的基础设施(HCL 语言)
resource "aws_instance" "web" {
ami = "ami-0abc123"
instance_type = "t3.medium" # 我要一台这样的机器
tags = { Name = "web-server" }
}

resource "aws_db_instance" "db" {
engine = "postgres"
instance_class = "db.t3.small" # 我要一个这样的数据库
allocated_storage = 20
}
$ terraform plan     # 预览:对比"代码声明"和"云上现状",告诉你它打算做什么
$ terraform apply # 执行:调用云 API,把这些资源真正创建出来

这套代码可以进 Git,可以 review,可以在任意地方 apply 出一模一样的环境--ClickOps 的所有毛病都被治好了.但 IaC 有一个 Kubernetes 世界里不存在 的独特难题,理解它才算真正理解 IaC.

IaC 的灵魂:状态(State)

这是本篇最关键,也最容易被初学者忽略的概念.先抛出一个问题:

一个要命的问题

apply 创建了一台服务器.现在你把代码里那台服务器删掉,再 apply.工具怎么知道"云上那台已经存在的机器"是之前由它创建,现在该删掉的,而不是别人手工建的,不该乱动的?它怎么知道"代码里的 aws_instance.web"对应"云上那台具体的 i-0abc123 实例"?

答案是:IaC 工具维护一份 状态文件(State)--一份"我管理的代码资源 ↔ 云上真实资源"的映射账本.每次 apply,它做的其实是三方对比:

IaC 的核心循环:代码,状态,现实三方对账

┌─ 代码(期望):我声明要 2 台机器

├─ 状态(记录):我上次创建了 1 台,叫 i-0abc123 ──┐
│ ├─▶ diff → 计划:再建 1 台
└─ 云上(现实):实际有 1 台 i-0abc123 ──┘

正是这份状态,让 IaC 能算出"从现状到期望,需要增,删,改哪些资源".terraform plan 展示的就是这个 diff.

为什么 Kubernetes 不需要你管状态,Terraform 却要?

对比第 05 篇:K8s 的"期望状态"和"实际状态"都存在 etcd 里,由集群自己持续调谐--状态是 K8s 内建托管的,你无感.而 Terraform 管的是外部的云资源,云厂商不会替它记"哪些资源归这套代码管",所以 Terraform 必须自己在外部存一份状态账本.这就是 IaC 比 K8s 多出来的心智负担,也是几乎所有 IaC 事故的源头.

状态带来的两个现实难题

  • 状态必须共享且加锁:状态默认是本地一个文件.但团队协作时,每个人本地一份状态会彻底打架.所以生产实践中状态要放在远程后端(如 S3 + DynamoDB 锁,Terraform Cloud),并加锁防止两人同时 apply 把账本搞乱.
  • 状态文件含敏感信息:数据库密码等可能被明文记进状态.因此状态后端必须加密,严格管控访问--它的敏感级别等同于密钥(回想第 02,04 篇对机密的谨慎).

永远不要手工编辑状态文件

手改状态会让"账本"和"现实"错位,后续 apply 可能误删生产资源或重复创建.要调整状态有专门命令(如 terraform state mv/rmimport),但日常应避免直接碰它.把状态当成只能由工具读写的数据库,而不是一个可以随便改的文本文件.

三个主流工具的定位与取舍

IaC 工具不少,挑三个有代表性,思路各异的,理解它们的取舍比记语法重要.

Terraform:行业事实标准

用专门的声明式语言 HCL 描述基础设施,通过 "Provider"(插件)对接几乎所有云厂商和服务(AWS,GCP,Azure,甚至 Cloudflare,GitHub).

  • 优点:生态最庞大,Provider 最全,社区资料最多;声明式,跨云通用,是事实标准
  • 取舍:要学一门新语言 HCL;表达复杂逻辑(循环,条件)时 HCL 略显笨拙

关于 OpenTofu

2023 年 Terraform 改用非开源许可后,社区 fork 出了完全开源的 OpenTofu,用法与 Terraform 基本兼容.理念和机制(尤其状态)完全一致,本篇所讲对两者都适用.

Pulumi:用通用编程语言写基础设施

Pulumi 让你用 Python,TypeScript,Go 等真实编程语言来声明基础设施,而非专用 DSL.

# 用 Python 声明,可以用真正的循环,函数,条件
import pulumi_aws as aws

for i in range(3): # 想建 3 台?用语言原生的循环
aws.ec2.Instance(f"web-{i}",
instance_type="t3.medium",
ami="ami-0abc123")
  • 优点:复用你已掌握的语言和工具链(IDE,测试,包管理);复杂逻辑表达自然
  • 取舍:灵活性是双刃剑--能写出难以维护的"基础设施程序";生态和社区规模不及 Terraform.注意它底层同样有"状态"概念,前面讲的状态难题一个都不少.

Crossplane:把基础设施变成 K8s 资源

Crossplane 思路最特别:它让你用 Kubernetes 的方式(写 YAML,kubectl apply)来管理云基础设施.一个云数据库,在 Crossplane 里就是一个 K8s 自定义资源:

apiVersion: database.aws.crossplane.io/v1beta1
kind: RDSInstance # 一个"云数据库",但它是 K8s 资源
metadata:
name: my-postgres
spec:
forProvider:
engine: postgres
dbInstanceClass: db.t3.small

这带来一个杀手级优势:它复用 K8s 的控制器和 etcd 来做调谐和状态管理--于是"状态"问题自动消失了,和第 05 篇 K8s 资源一样由集群托管.更妙的是,它能被 ArgoCD 直接管理:

Crossplane = 让 GitOps 也能管基础设施

因为云资源被表达成了 K8s 资源,第 05 篇的 ArgoCD 可以像同步 Deployment 一样同步它们.于是"创建一个数据库"也变成了"改 Git → ArgoCD 自动同步"--应用和基础设施,统一在同一套 GitOps 工作流里.这正是它最吸引云原生团队的地方.代价是:需要一个 K8s 集群来运行 Crossplane 本身(先有鸡还是先有蛋--通常用一个独立的"管理集群"来解决).

Terraform Pulumi Crossplane
怎么写 HCL 专用语言 Python/TS/Go 等 K8s YAML
状态在哪 自管(远程后端) 自管(类似) K8s etcd 托管,无感
持续调谐 否(apply 才动) 否(up 才动) 是(控制器持续纠偏)
接 GitOps 需额外集成 需额外集成 原生(就是 K8s 资源)
最适合 通用,跨云,团队标准 偏好用代码,逻辑复杂 已全面 K8s 化,想统一 GitOps

IaC 与 GitOps:把整条链路彻底打通

IaC 让基础设施变成了代码,自然该和阶段二的工作流合流.两种程度:

程度一:IaC 进 CI/CD 流水线..tf 代码放进 Git,提 PR 时自动跑 terraform plan 把"将要发生的变更"贴到 PR 上供 review,合并后自动 apply.基础设施变更从此和应用代码一样:有 review,有审计,可回滚.

程度二:用 Crossplane 把基础设施纳入 GitOps. 如前所述,基础设施直接成为 ArgoCD 同步的对象,和应用共用一套声明式管线.

┌──────────────────────────────────────────────────┐
│ 底座层(阶段三 · IaC) │
├──────────────────────────────────────────────────┤
│ • Terraform/Crossplane 声明<br/>VPC,K8s 集群,数据库,DNS │
└──────────────────────────────────────────────────┘
┌──────────────────┐
│ 应用层(阶段一,二) │
├──────────────────┤
│ • CI 构建可信镜像(阶段一) │
└──────────────────┘

关系: B ──→ 应用层(阶段一,二)

至此,从一行代码提交,到承载它的服务器,网络,集群,每一层都是声明式的代码,都在 Git 里,都可复现可审计.这就是现代云原生交付的完整图景.

整个 CI/CD 系列的收束

回头看这八篇,其实是同一个核心思想在不同层次的反复应用--用声明式代码替代人工操作,设好客观标准让机器自动决策:

阶段 解决的层次 同一思想的体现
一 · CI 与制品(01-04) 代码 → 可信镜像 流水线代替手工构建,门禁代替人工评审
二 · GitOps 交付(05-07) 镜像 → 安全上线 Git 声明状态,控制器调谐,指标驱动放量
三 · IaC(08) 底座 → 可复现的基础设施 代码声明基础设施,状态对账,apply 调谐

三个阶段层层向下:阶段一造出可信的"货",阶段二把货安全地送上"车"(集群),阶段三则保证"路和车"(基础设施)本身也是按图纸建的,可随时重建.人的角色,从"亲手操作每一步",变成"定义标准,审查变更,处理例外".

快速回顾

  • 要解决的问题:ClickOps(点控制台建基础设施)不可复现,不可追溯,会漂移--和手工发布同病
  • IaC:用声明式代码描述服务器/网络/数据库,工具读代码自动创建,可进 Git,可 review,可复现
  • 状态(State)是灵魂:一份"代码资源 ↔ 云上真实资源"的映射账本,让工具能 diff 出增删改;K8s 内建托管状态故无感,Terraform 管外部云资源必须自管状态
  • 状态的坑:必须远程共享 + 加锁,含敏感信息要加密,绝不手工编辑
  • 三工具取舍:Terraform(HCL,事实标准,生态最全)/ Pulumi(通用语言,灵活但需自律)/ Crossplane(K8s YAML,状态由 etcd 托管,原生接 GitOps)
  • 合流:IaC 进流水线(PR 上看 plan,合并后 apply),或用 Crossplane 让基础设施也走 ArgoCD,实现应用+底座的统一声明式管理

IaC 和配置管理工具(Ansible,Chef)是一回事吗?

侧重点不同.IaC(Terraform 等)偏供给(Provisioning)--从无到有创建基础设施资源(开机器,建网络,建集群),是声明式的.Ansible 这类配置管理在已有机器上做配置(装软件,改配置文件),偏过程式.实践中常配合:Terraform 先把机器开出来,Ansible 再上去装东西.不过在云原生时代,"装软件"这件事很大程度被容器镜像(第 03 篇)取代了,配置管理的地盘在收缩.

已经手工建了一堆资源,现在想用 IaC 接管,要全删了重建吗?

不用.IaC 工具提供 import 能力:把已存在的云资源"导入"到状态账本里,并补写对应代码,之后就由 IaC 管理了,无需删除重建.这正体现了状态的作用--import 本质就是在账本里登记"这个云上资源从此归这套代码管".对存量环境迁移到 IaC,这是标准做法.

动手练习

  1. 体验状态账本 装 Terraform(或 OpenTofu),用 local / random 这类不花钱的 Provider 写几个资源,跑 planapply,然后打开生成的 terraform.tfstate 看看状态账本长什么样.
  2. 观察销毁推断 把代码里某个资源删掉,再 plan,观察它如何依据状态判断"这个该销毁";apply 后再看状态变化.
  3. 漂移检测 故意手工改一下已创建资源的某个属性,再 plan,观察 Terraform 如何检测到"现实偏离了代码"并打算纠正--对照第 05 篇的"自愈"体会异同.
  4. 远程状态后端 把状态后端从本地改为远程(如用 Terraform Cloud 免费层),理解"团队为何不能各存一份本地状态".
  5. 串联全系列 画一张图:一次"新增一个微服务"的完整旅程,从 IaC 准备底座,到阶段一 CI,到阶段二 GitOps 上线,每一步分别由哪个工具,哪份代码负责.