去年冬天我接手了一台谁也不敢碰的机器,12 台同配置的云服务器,Nginx 站点配置却有三份不同的版本。运维同事在脚本里写着「先备份再改」,改完凌晨由发布脚本统一 rsync 一次配置文件到全网那次同步,把我白天手工调好的两个参数直接覆盖回旧值,第二天早高峰,12 个站点里有 3 个开始 502。
事故复盘会上我才真正搞明白问题所在:我们管理机器的方式是「人肉改配置」,机器不知道自己该是什么样子,人也记不住 12 台机器各自的差异。这不是某个人手滑,而是整套工作方式的地基塌了。后来我们把服务器的配置、用户、防火墙规则、监控采集项全部写成代码交给 Terraform 管,12 台机器的配置从此只有一份,凌晨那次 rsync 也随之删掉了。这篇把整个过程和踩过的坑写下来,给还在手改配置的同学一个能落地的思路。
如果你手上是十几台甚至几十台机器要统一配置,这台可以先了解一下:狗云DOGYUN(高性价比云服务器,多机房可选)、RackNerd(全球VPS,特价常推)。机器多不多不是重点,重点是你的配置能不能复现。
一、配置漂移是怎么一点点发生的
先说清楚问题本质。我们管十几台服务器,配置实际上是分散在三个地方的:一是代码仓库里的配置文件模板,二是每台机器上手工改过的实际文件,三是跑批脚本里的 sed 替换逻辑。这三份东西各自演化,谁也不认识谁。
漂移的起点往往很小。同事 A 为了临时排查某个 IP 的问题,在 3 号机的 Nginx 里加了个 location;同事 B 为了兼容老客户端加了几个 header;某次扩容新开的两台机器直接拷贝了老机器的 conf 文件,带着三个月前的历史包袱上线。半年之后再去看,12 台机器的 Nginx 配置已经没有一台是完全一样的。
更麻烦的是,你根本不知道谁改过。`stat` 能看到文件修改时间,但看不了「谁改的、改了什么、为什么改」。配置文件进了版本库也没用因为线上真正生效的那份,从来没人提交过。
这时候还剩两条路:一是继续人肉维护,靠记忆和文档硬撑;二是引入「基础设施即代码」这套装甲,把机器的期望状态写成文件,让工具负责对比和执行。第二条路一开始看着重,投入产出比要过一段时间才体现出来,但规模越大越划算。

二、Terraform 到底在管什么
Terraform 的定位是「声明式的基础设施管理工具」。你在 HCL 文件里描述「我想要什么」比如这台机器应该有 2 核 4G、装 nginx、开 22 和 443 端口、挂一块 100G 数据盘然后执行 apply,工具会算出「现状」和「期望」的差距,并把这个差距消掉。
它的核心概念有四个:
Provider 是对接云平台的插件,比如阿里云、腾讯云、AWS 的 provider,负责真正调用 API 去创建资源。Resource 是你要管理的资源声明,比如一块云盘、一个安全组规则。State(状态文件) 记录着「现在实际有什么」这份账本,Terraform 靠它做增量对比,不用每次都全量重建。Module(模块) 是可复用的资源集合封装,比如「一台带 nginx 的 Web 服务器」打包成一个模块,换个变量就能在别的环境下复用。
日常用的命令就四条:`terraform init` 初始化(下载 provider、准备后端)、`terraform plan` 看将要发生什么、`terraform apply` 真正执行、`terraform destroy` 拆干净。真正高频用的是 plan它输出的那份 diff 就是「配置漂移」的体检报告。
这里有个很多人误解的点:Terraform 不只管「创建」,它同样管「删除和修改」。资源定义里少写了某台机器的挂载盘,apply 之后那台机器上的数据盘可能就被卸载甚至销毁。生产环境里 apply 之前一定要先看 plan 输出,确认 diff 里的每一个动作是你想要的。

三、第一步:先让一台机器归管
别一上来就想接管 60 台机器,那是在给自己挖坑。我们的做法是挑一台最不重要的测试机开始,先写一个只包含两个资源的小配置:
terraform {
required_providers {
alicloud = { source = "aliyun/alicloud", version = "~> 1.190" }
}
}
provider "alicloud" { region = "cn-hangzhou" }
resource "alicloud_instance" "web" {
instance_name = "web-01"
instance_type = "ecs.g7.large"
image_id = "ubuntu_22_04_x64"
vswitch_id = var.vswitch_id
}
变量用 variables.tf 声明,敏感信息用环境变量注入而不是硬编码在文件里。写好之后 init、plan,确认输出里只有「+ create」这一类动作,再 apply。
接下来是很多人会卡住的一步:已经手工存在的机器,怎么纳入管理?Terraform 提供了 import 机制,把已有资源「导入」到状态里,之后它就归 Terraform 管了。import 只建立账本,不动机器上的任何东西,所以是安全的。但导入的 block 写法比较嗦,官方有生成工具可以直接出:
terraform import alicloud_instance.web i-xxxxxxxxxxxx
我们当时是一台台导的,导完在每台机器上跑一次 `terraform plan`,只要输出是空的(或者只有你在代码里显式写好的差异),就说明这台机器的状态和代码完全对得上。只有 plan 全绿,这台机器才算真正「归管」。
四、扩到 12 台:for_each 与模块
一台机器验证完之后,第二步是把重复的部分收进 module,把「有哪些机器」用变量描述。一个 Web 服务器模块大概长这样:
module "web" {
source = "./modules/web"
count = length(var.machines)
instance_name = var.machines[count.index].name
...
}
然后 variables.tf 里写一份清单:
variable "machines" {
type = list(object({
name = string
vswitch_id = string
disk_size = number
}))
}
更推荐 for_each,因为它用机器名做 key,改动某台机器时 state 不会整体位移。用 count 的话,中间删掉一台机器,后面所有实例的索引都会变,Terraform 会认为是「大规模重建」,虽然实际不会真的删机器,但 diff 看着很吓人。
跑 terraform plan 的时候,重点看「No changes」这一行。如果它显示要改、要销毁,先停下来看 diff,绝大多数时候那是变量写错或者 state 丢了。

五、state 文件放哪儿,这是最容易出事的地方
state 文件是 Terraform 的账本,丢了就等于失忆:它会认为所有资源都不存在,然后 plan 出一堆「+ create」,apply 下去就是在生产环境上重复创建一堆同名资源,轻则报错,重则把数据搞乱。
所以 state 必须放在团队共享的远端后端上,比如对象存储的 S3 兼容后端(带版本控制)、或者云厂商的 Terraform 托管服务。关键是要打开两样东西:版本控制,万一状态被写坏可以回滚到上一个版本;状态锁,防止两个人同时跑 apply,把账本改花。
远程后端的配置写在 backend 块里:
terraform {
backend "oss" {
bucket = "tf-state"
key = "prod/terraform.tfstate"
region = "cn-hangzhou"
}
}
初始化时加 -reconfigure 参数切换后端:terraform init -reconfigure。
还有一条铁律:state 文件里会明文存储很多敏感信息,数据库密码、云厂商 AK/SK 都在里面。state 的访问权限要比普通对象存储还严,别丢在公开可读的桶里。
六、和现有的运维流程怎么衔接
我们没有试图用 Terraform 替换掉所有东西。工具链分工是这样的:Terraform 管「资源与基线」机器规格、磁盘、端口规则、用户、部署哪些包;Ansible 负责「一次性的批量动作」比如滚动重启、改某个应用版本、批量执行一条运维命令。两者的边界其实很好划:凡是要写成「期望状态」的,都交给 Terraform;凡是「跑一次就完事」的,交给 Ansible。
执行流程我们改成了这样:改配置 → 本地 terraform plan 出 diff → 提交代码让另一个人 review → CI 里跑 apply → 流水线负责通知。这样做的最大好处是,配置变更有记录、有审批、能在几秒内复现出「线上到底应该长什么样」,半夜排障的时候不用再靠猜。
顺带解决了一个历史遗留问题:我们把「凌晨全量 rsync 配置文件」这个定时任务删掉了。那脚本本来是为了保证所有机器配置一致,但它只会覆盖,永远不知道差异从哪来。改用 Terraform 之后,一致性由 plan 保证,需要批量执行的动作才进 Ansible,职责一分开,问题自然少了一半。

七、几个不踩就不知道疼的坑
一是 apply 前一定看 plan。Terraform 的设计哲学是「plan 只读、apply 才写」,把 plan 输出贴到群里 review 是最低成本的保险。
二是别在 state 里手改。用 terraform state rm 或 state mv 这些正规命令操作,直接编辑 state 文件会写出损坏的 JSON,apply 时报错还不告诉你坏在哪。
三是工作区别乱用。terraform workspace 用来隔离 dev/staging/prod,但每个工作区都是一份独立的 state,多人协作时很容易在错误的 workspace 上执行 apply。建议在 CI 里显式指定 workspace,不要依赖本地默认值。
四是模块版本要 pin。本地模块写成 source = "./modules/web" 是可以的,但一旦从 registry 或 Git 拉模块,一定要指定版本或 commit 标签,否则上游一更新,你这边 apply 的结果就变了。
五是 provider 版本别乱升。大版本之间字段经常有 breaking change,在生产环境用的 provider 要和初始化时保持一致,升级走单独一轮变更。
八、写在最后
回头看那次凌晨回滚的事故,技术上它只是一次 rsync 覆盖,真正贵的是我们没有能力快速回答「线上现在到底配置成什么样」这个问题。Terraform 解决的就是这个:它不保证你永不出错,但它保证任何一台机器的实际状态,你花几秒钟就能问出来,并且能一键改回去。
当然它也有门槛:需要团队统一思想,需要改掉「改完就完事」的坏习惯,需要有人愿意维护这些代码。规模小的时候(比如就三五台机器、一个人维护),搞这一套可能确实不划算;但只要机器数量过了十台、或者开始有人离职交接,这笔账迟早要算。工具本身是开源免费的,HashiCorp 官方的 Terraform 和 OpenTofu 两个版本在常规使用上基本一致,多一个选择也多条路。
如果你手上也是一堆「每台都不太一样」的服务器,从挑一台测试机开始,先把一个最简单的资源用 import 纳管,跑一次 plan 看它是不是全绿。看见绿色的 No changes 那一刻,你大概就明白这条路值不值得走了。


喜欢
顶
难过
囧
围观
无聊



