先说结论,免得你看到一半骂我
去年 8 月我接手了公司一套跑了五年多的运维自动化体系。接手那天的 inventory 是 824 台 ECS,横跨 3 个 region,playbook 目录下 417 个 yaml 文件,最大的一个文件 2300 多行。跑一次全量 ansible-playbook -i prod site.yml 要 47 分钟,成功率大概 78%,剩下那 22% 里有一半是"上次执行到一半挂了留下的脏状态"。
那时候我还没意识到问题的本质,我以为是我 playbook 写得烂。后来花三个月做了两件事:把 417 个 playbook 拆成 63 个 Pulumi 项目 + 21 个 Ansible role,以及把全量执行时间从 47 分钟压到 6 分 20 秒。但真正值钱的不是这些数字,是我中间想明白的一个边界问题——Ansible 和 Terraform 这类工具的分界线,不在"有没有 agent",而在"谁拥有这台机器"。
我一开始的错:把 Ansible 当万能胶
说个具体的坑,你大概率也踩过。
我们用 Ansible 管一批 Kafka broker,涉及 server.properties 的修改。早期写法是 lineinfile:
- lineinfile:
path: /data/kafka/config/server.properties
regexp: '^log.retention.hours='
line: 'log.retention.hours=168'
这个写法在"第一次执行"的时候没问题。问题出在灰度扩容那批新机器上:出厂镜像里的 server.properties 是从模板生成的,log.retention.hours 那一行后面跟了个空格和注释 # 7 days。regexp 匹配到了,line 替换掉整行——注释没了。但另一个 playbook 里有个 task 会根据注释判断"是否已初始化",于是它认为新机器没初始化,重新跑了一遍 format。数据没了。
这不是 Ansible 的错,是我把"机器内部状态的最终一致性"和"机器是否存在"这两件事混在一个工具里做。Ansible 的模型是"我告诉你目标状态,你自己算 diff",但它的 diff 计算是基于模块各自的幂等实现,不是基于一个全局 state。lineinfile 认为它幂等了,因为在它的世界观里那一行确实符合预期,它不知道还有别的 playbook 在读注释。
这就是那三个月最大的收获:Ansible 处理的是机器内部的配置漂移,它假设机器已经存在;Terraform/Pulumi 处理的是机器本身的生命周期,它假设机器内部是黑盒。一旦你让 Ansible 去创建机器、让 Terraform 去装 nginx,两边的 state 就开始互相打架。
见过最离谱的一个 case,是有人用 Terraform 的 remote-exec provisioner 装 Docker,再用 Ansible 改 daemon.json,再用 Terraform 的 null_resource 去重启 Docker。正常运行时看着挺美,直到 Terraform state 丢了——我们真丢过一次,S3 backend 的 DynamoDB 锁表被误删。机器还在跑,但 Terraform 认为它们不存在,下一次 apply 会创建一批新的,而 Ansible 会把新老机器一起改。824 台里有 37 台在两小时内被装了两次 Docker,端口冲突,Kafka 集群的 controller 选主来回跳了四次。
拆完之后的分工线,和一套能照抄的步骤
给团队定的规则就三条:
- Terraform/Pulumi 管"存在与不存在":VPC、子网、安全组、ECS 实例、RDS、SLB、DNS 记录、IAM。简单说,任何你在云控制台里能点出来的东西。
- Ansible 管"存在之后长什么样":systemd unit、内核参数、文件权限、用户、crontab、配置文件内容。
- 中间那条线用一个"机器自我描述"的接口焊死:我们用 cloud-init + 实例 tag,Ansible 的 dynamic inventory 按 tag 分组,绝不按 IP 分组。
实际迁移顺序是这样的,你可以对着改:
第一步,别急着拆 playbook,先给 inventory 打标。我们花了两周,把 824 台按 role(kafka/nginx/mysql)、env(prod/staging)、owner、managed_by(terraform/manual)四个维度打 tag。这一步最枯燥但最值钱,因为后面所有拆分都依赖它。当时发现有 63 台机器的 owner 是空的——这些机器没人认领,已经在跑生产流量了。
第二步,把"创建类"任务从 Ansible 里挖出来。搜这几个模块,出现在哪个 playbook 里,那段逻辑就挪走:ec2、ec2_instance、gcp_compute_instance、os_server、docker_container、mysql_db(如果指云 RDS)。我们挖出 89 个 task,涉及 11 个 playbook。
第三步,用 Pulumi 而不是 Terraform 重建这批。这里说个可能挨骂的观点:如果团队是 Python/TypeScript 背景的运维开发,Pulumi 的迁移痛苦程度大概只有 Terraform 的三分之一。我们用 Pulumi 的 Python SDK 重写,因为能直接 import 已有的 boto3 封装。代价是 Pulumi 的 state 默认在 Pulumi Cloud,自建 backend 得自己搭 S3 兼容服务——我们最后用 pulumi login s3://xxx 配 MinIO,踩了没开 versioning 的坑,第一次 state 覆盖导致 12 台机器的定义丢了。
第四步,Ansible 侧用 roles 收敛,禁止 site.yml 级别的全量跑。417 个 playbook 按 role 合并成 21 个,每个 role 只跑自己那组 tag 的机器。命令行从
ansible-playbook -i prod site.yml
改成
ansible-playbook -i inventory/prod.py --limit 'role_kafka:&env_prod' playbooks/kafka.yml
第五步,加一个漂移检测定时任务。ansible-playbook --check --diff 每天凌晨跑一遍,结果推到 Prometheus 的 pushgateway,漂移超过阈值进告警。这条是整套体系里我最满意的部分,因为它把"配置漂移"从一个玄学概念变成了可观测指标,不用等人半夜被叫起来。
数字对比,以及一个反直觉的结论
拆完之后的实际数字:
- 全量执行时间:47 分钟 → 6 分 20 秒(单 role 平均 42 秒)
- 文件数量:417 个 yaml → 63 个 Pulumi 项目 + 21 个 Ansible role
- 变更成功率:78% → 96.4%(口径:一次执行内无 failed task 且次日漂移检测通过)
- 回滚时间:平均 25 分钟(手工)→ 4 分钟(
pulumi destroy --target配合 role 版本回退) - 半夜被叫起来处理配置类故障:每月 6~8 次 → 每月 0~1 次
但反直觉的结论在这儿:迁移完之后,我们并没有变得"更自动化",反而手动操作变多了。
因为原来那套 Ansible 什么都管,大家习惯写个 playbook 解决一切,包括那些本来应该走工单、走审批的变更。拆分之后,很多"创建类"操作被强制走 Pulumi 的 PR 流程,要 review 要 approve,表面上慢了。但半年下来,误操作导致的故障从 11 起降到 2 起,而且这 2 起都是人在控制台点的,不是代码跑的。
所以我现在的观点是:运维开发的核心指标不该是"自动化覆盖率",而是"不可逆操作的审批覆盖率"。一个能一键删库的自动化脚本,比手工删库危险一百倍。Ansible 的问题从来不是它不够强大,是它太容易被写成"一键做任何事",而且没有天然的审批边界。
如果你现在只有 10 台机器
上面那套别抄。10 台机器你上 Terraform + Pulumi + Ansible + 漂移检测,运维成本比手工还高。
我的分界线大概是:
- 少于 20 台,且不涉及自动扩缩容:Ansible + 一个 git 仓库够了,别碰 IaC。
- 20~100 台,或有多环境:Terraform(或 Pulumi)管云资源,Ansible 管机内配置。这时候开始值得投入。
- 100 台以上,或有合规要求:加上漂移检测、state 加密、变更审批。这时候瓶颈已经不是工具,是流程。
- 500 台以上:开始考虑要不要写自己的平台。我的建议是尽量别写,除非你的业务本身就是卖这个的。
我们踩过的坑里,最贵的不是工具选型错,是在没有流程的情况下先上了自动化。824 台机器里那 417 个 playbook,大概有 60 个是"某个人为了解决某次故障临时写的,后来没人敢删"。这些脚本平时不跑,但一旦有人跑全量 site.yml,它们就会执行。这才是最阴的雷。
差不多就这些。评论区可以问具体问题,我尽量回。