服务器配置到底是「装好之后持续收敛」还是「每次都从镜像重建」?这是配置管理领域最根本的路线之争。Ansible 与 SaltStack 代表收敛式路线,容器与镜像烘焙代表不可变路线。本文不站队,而是把两条路线的适用边界讲清楚,并给出 Ansible 的核心模型、幂等语义、Role 组织与混合策略的落地做法。
目录
- 1. 配置管理的两条路线
- 2. Ansible 核心模型
- 3. 幂等与收敛
- 4. Playbook 与 Role 组织
- 5. SaltStack 与大规模编排
- 6. 不可变基础设施
- 7. 状态漂移与合规
- 8. 混合策略与迁移
- 9. 案例与最佳实践
1. 配置管理的两条路线
1.1 收敛式:让机器持续趋向目标状态
收敛式配置管理(Ansible、SaltStack、Puppet、Chef)的核心是声明目标状态,由工具反复执行直到机器达到并保持该状态。机器是「活的」,配置会随时间漂移,工具负责把它拉回来。
1.2 不可变式:让机器一次性成形
不可变基础设施(镜像烘焙、容器、NixOS)的核心是「构建一个完整的机器镜像,部署时直接替换」。机器一旦启动就不再修改,要变更就重建一个新版本再切换。
| 维度 | 收敛式 | 不可变式 |
|---|---|---|
| 变更方式 | 就地修改 | 替换重建 |
| 状态来源 | 工具持续收敛 | 镜像即真相 |
| 漂移风险 | 高,需持续巡检 | 低,重启即复原 |
| 回滚方式 | 反向收敛,易失败 | 切回旧镜像,秒级 |
| 启动速度 | 快 | 慢(需构建镜像) |
| 适合场景 | 长生命周期主机、有状态服务 | 无状态服务、弹性伸缩 |
1.3 不是非此即彼
现实中很少二选一。常见做法是:无状态服务用不可变镜像,有状态服务与数据库主机用收敛式管理,边缘设备与长生命周期主机两者结合。选型的关键是问「这台机器的生命周期和变更频率」。
2. Ansible 核心模型
2.1 无代理与推送式
Ansible 最大的特点是 Agentless:被管理节点不需要安装常驻代理,通过 SSH(Linux)或 WinRM(Windows)推送模块执行。这降低了纳管成本,也让它成为跨异构环境的首选。
2.2 模块、任务、Play
三个层级:模块(module)是执行单元,任务是模块的调用,Play 是一组任务的有序集合,Playbook 是 Play 的列表。所有编排都是这三个概念的组合。
- name: configure web servers
hosts: webservers
become: true
tasks:
- name: ensure nginx installed
ansible.builtin.package:
name: nginx
state: present
- name: deploy config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: reload nginx
handlers:
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
2.3 Inventory 与变量优先级
Inventory 描述主机分组,变量决定每台机器的具体配置。Ansible 的变量有十多个优先级层级,理解优先级是避免「变量被意外覆盖」的关键。
2.4 执行模式与并发
Ansible 默认按 inventory 顺序执行,可用 forks 提高并发。滚动更新场景要用 serial 控制批次,配合 max_fail_percentage 在失败过多时中止,避免一次性把整个集群改坏。
- hosts: webservers
serial: 2 # 每批 2 台
max_fail_percentage: 25 # 失败超过 25% 则中止
tasks:
- name: deploy config
ansible.builtin.template:
src: app.conf.j2
dest: /etc/app/app.conf
3. 幂等与收敛
3.1 幂等的定义
幂等指「执行一次和执行多次的结果相同」。幂等是 Ansible 的核心契约:第二次运行一个已满足状态的 playbook,所有任务都应报告 ok 而非 changed。
3.2 changed 与 ok 的语义
Ansible 用三种状态报告结果:ok(状态已满足,未改动)、changed(本次做了改动)、failed(执行失败)。收敛性可以从 changed 的数量观察:稳定后 changed 应趋近于零。
收敛过程观察
首次运行 :changed 数量多,机器从初始态向目标态靠拢
再次运行 :changed 趋近 0,说明已收敛
若持续 changed :说明存在非幂等任务或外部因素在改配置
3.3 破坏幂等的常见写法
- 用 shell 命令代替专用模块(如用
command: useradd而非user模块)。 - 无条件追加内容(lineinfile 不指定 regexp)。
- 每次生成带时间戳的文件名。
4. Playbook 与 Role 组织
4.1 用 Role 做复用单元
Role 是 Ansible 的复用与封装单元,把任务、模板、变量、handler 打包成可被多个 playbook 引用的组件。
roles/
nginx/
tasks/main.yml # 任务入口
handlers/main.yml # 处理器
templates/ # Jinja2 模板
defaults/main.yml # 默认变量(可覆盖)
vars/main.yml # 高优先级变量
meta/main.yml # 依赖声明
4.2 defaults 与 vars 的区别
defaults 是低优先级的默认值,期望被外部覆盖;vars 是高优先级,用于 role 内部的固定值。搞反了会导致「变量覆盖不生效」的经典困惑。
4.3 分层组织
大型项目按「角色分层」组织 playbook:基础层(用户、时区、安全基线)、中间层(运行时、监控代理)、应用层(具体业务)。层与层之间用 tags 分离,支持只跑某一层。
| 组织方式 | 适合规模 | 特点 |
|---|---|---|
| 单 playbook | 小规模 | 简单,但难复用 |
| Role 拆分 | 中等规模 | 可复用、可组合 |
| Collection | 大规模/跨团队 | 可分发、带版本 |
| 多仓库 + 依赖 | 超大规模 | 需配套的依赖管理 |
5. SaltStack 与大规模编排
5.1 SaltStack 的模型
SaltStack 采用 Master-Minion 架构,Minion 常驻在被管理节点上,通过消息总线接收指令。相比 Ansible 的推送式,它在超大规模(数千节点)下的并发与响应更有优势。
5.2 Ansible 与 SaltStack 对比
| 维度 | Ansible | SaltStack |
|---|---|---|
| 架构 | 无代理、推送 | 有代理、消息总线 |
| 语言 | YAML + Jinja2 | YAML + Jinja2 + Python |
| 规模 | 数百到数千 | 数千到数万 |
| 实时性 | 拉取式,按需执行 | 事件驱动,可实时响应 |
| 学习曲线 | 平缓 | 稍陡 |
| 典型用途 | 通用配置与部署 | 大规模状态管理 |
5.3 选型建议
中小规模、异构环境、希望零代理,选 Ansible。数万节点、需要实时事件响应与集中状态管理,选 SaltStack。二者都支持声明式状态,理念相通,迁移成本主要在语法细节。
5.4 状态与事实采集
两个工具都支持采集被管理节点的事实(facts/grains):操作系统版本、内存、网络接口、已装包等。事实是编写条件化配置的基础,也是做资产盘点的数据来源。定期把事实汇总入库,就能得到一份动态的资产清单。
6. 不可变基础设施
6.1 镜像烘焙
不可变基础设施把配置前置到镜像构建阶段:用 Packer、Dockerfile 或 Nix 把操作系统、运行时、应用一次性打包成镜像,部署时只做替换,不再就地修改。
不可变流水线
1. 构建镜像(Packer/Dockerfile/Nix)
2. 对镜像做安全扫描与合规校验
3. 推送镜像到制品仓库
4. 部署新镜像实例,加入负载均衡
5. 摘除并销毁旧实例
6.2 为什么回滚变简单
不可变的核心收益是回滚可靠:旧镜像还在仓库里,回滚就是「切回旧版本再拉起来」,不需要反向执行一堆配置步骤。收敛式的回滚则要「撤销之前的修改」,往往不可靠。
6.3 不可变也有代价
- 构建时间:每次变更都要重新构建镜像,比就地改配置慢。
- 状态处理:有状态服务的数据无法随镜像丢弃,需要外部存储。
- 调试不便:不能直接 SSH 进去改,得改代码重新构建。
7. 状态漂移与合规
7.1 漂移是怎么发生的
即使有配置管理,机器仍会漂移:有人手工改了配置、某个脚本临时加了文件、依赖自动升级。漂移积累到一定程度就会引发「明明配置对却行为不对」的诡异故障。
7.2 检测漂移
两种手段:定期以 check 模式跑 playbook(--check --diff)看有多少 would-change;或用专门工具(如 Ansible 的 fact 采集、云厂商的配置合规服务)做基线比对。
漂移检测周期建议
生产环境:每日一次 check 模式巡检
核心主机:每小时一次关键项巡检
变更窗口后:立即跑一次全量巡检
7.3 合规基线
把安全基线(如 SSH 配置、审计规则、内核参数)写进 role,定期收敛并出合规报告。合规不是一次性通过审计,而是持续保持在基线上。
8. 混合策略与迁移
8.1 什么时候用哪个
| 服务类型 | 推荐策略 | 理由 |
|---|---|---|
| 无状态 Web 服务 | 不可变镜像 | 弹性伸缩、秒级回滚 |
| 数据库/中间件 | 收敛式 | 有状态、不宜重建 |
| 长生命周期主机 | 收敛式 | 无法频繁重建 |
| 边缘/IoT 设备 | 混合 | 镜像更新 + 配置收敛 |
| 一次性任务 | 容器 | 用完即弃 |
8.2 渐进迁移路径
从收敛式迁向不可变不要一步到位。可以先「镜像化打包但保留 SSH」,再逐步禁止运行时修改,最后完全关闭登录入口,让镜像成为唯一真相。
8.3 共享的真相来源
无论走哪条路线,配置的真相来源都应该在版本控制里。Ansible 的 playbook 和镜像的构建脚本都是代码,都要走评审、测试与流水线。
9. 案例与最佳实践
9.1 落地 Checklist
□ 按服务生命周期选路线:无状态用镜像、有状态用收敛
□ Ansible 优先用专用模块,避免 command/shell 破坏幂等
□ 用 changed 数量观察收敛,稳定后应趋近 0
□ 用 Role/Collection 做复用,defaults 与 vars 分清
□ 定期跑 check 模式巡检,量化状态漂移
□ 安全基线写进 role,持续收敛并出合规报告
□ 不可变镜像必须过安全扫描与合规校验再入库
□ 配置真相来源统一放版本控制,走评审与流水线
9.2 常见坑与对策
| 坑 | 现象 | 对策 |
|---|---|---|
| 非幂等任务 | 每次运行都 changed | 改用专用模块 |
| 变量优先级混乱 | 覆盖不生效 | 梳理 defaults 与 vars |
| 手工改生产配置 | 漂移积累出诡异故障 | 巡检 + 禁止手工改 |
| 镜像不扫就上 | 漏洞随镜像扩散 | 入库前强制扫描 |
| 有状态服务用不可变 | 数据丢失 | 状态外置或用收敛式 |
| 一次全量迁移 | 迁移期故障频发 | 渐进迁移,先并行 |
| 回滚靠反向收敛 | 回滚失败率高 | 优先设计可替换方案 |
小结
配置管理的本质问题是「机器的真相从哪里来」:收敛式让工具持续把机器拉向声明状态,不可变式让镜像成为唯一真相。Ansible 以无代理、幂等、Role 复用的模型成为收敛式路线的首选;不可变基础设施则以镜像烘焙和替换重建换取可靠回滚。实战中最优解通常是混合:无状态服务走镜像、有状态服务走收敛、配置真相统一进版本控制,并用定期巡检把状态漂移控制在可接受范围内。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。