
最近有位朋友问我Ansible到底值不值得花时间认真学。我给出的回答是你先别问值不值得先回想一下上周加班是不是有大半天都耗在一台台服务器上执行几乎一模一样的命令。他沉默了一会儿然后才开始认真看自动化相关的东西。我自己开始系统学习 Ansible起因也是一次批量变更任务。当时要同时调整几十台主机的NTP配置、替换一份监控脚本、再更新防火墙规则。按老办法来做就是开好几个终端窗口登录一台、执行一遍、核对一遍、再登出反反复复。整个过程不仅枯燥而且非常容易漏。后来花了一个周末把Ansible的Playbook基础过了一遍写出了一个简单的批量配置剧本效果立竿见影。从那一刻起我意识到自动化运维不是可有可无的锦上添花而是一种基础能力。这篇文章算是我自己的Ansible学习总结不打算照抄文档而是把这段时间实际使用、踩坑、调优的真实经验整理出来。内容包括为什么最终选了它、Playbook里的关键设计怎么理解、高频模块怎么用顺手、变量和清单怎么组织不容易出错以及我反复遇到的几个坑和对应的排查思路。适合刚开始接触自动化运维的人也适合那些已经能写简单剧本但总感觉差点意思的同行。1. 从手动批量操作到声明式自动化我为什么最终选了Ansible1.1 手动批量操作最大的问题不是慢而是不可重复很多刚接触自动化的人第一反应是觉得痛点在于慢。确实逐台登录服务器执行操作很费时间但它的问题本质不是慢而是不一致。同样一个变更上午配置的机器和下午配置的机器很可能存在细微差别不同的人执行同一份操作文档也会因为各自习惯产生偏差。举个例子改完防火墙规则以后有些人习惯用重启防火墙的方式让它生效有些人只用reload。看起来都生效了但重启会打断现有连接在跑了业务的环境里这就是一次小事故。还有更隐蔽的是版本漂移。这个月手动改了配置文件下个月又手动改了一次中间有没有同事临时手工动过、改了什么内容基本没人说得清。机器越积越多最后没人敢碰。Ansible这类配置管理工具真正解决的问题是你把期望的目标状态写进剧本每次执行都朝这个目标状态收敛。变更从此变得可复现、可审计至少不再依赖某个人头脑里的记忆。1.2 我的选型比较过程Ansible、SaltStack、Puppet、Chef选型之前我先明确了自己当时的真实需求。总结下来就三条不想引入太重的额外基础设施组件、学习成本不能太高、现有网络环境下能快速跑起来。带着这几个条件我简单研究了市面上主流的几款配置管理工具给自己做了一张对比表工具架构配置方式学习门槛典型场景Ansible无代理端SSH直连YAML剧本较低批量配置、临时任务、交付流程中的自动化步骤SaltStack需要部署minion端Master-SlaveYAML/Jinja2中等大规模基础设施实时管理Puppet需要部署agentMaster-AgentPuppet DSL中高长期状态管理、组织级合规策略Chef需要部署agent工作站-服务器-节点Ruby DSL高高定制化基础设施自动化这里不是要贬低SaltStack或Puppet。SaltStack 这种带常驻 minion 的设计在规模非常大、对实时性要求极高的场景下确实有天然优势Puppet 对状态管理和合规审计的专注也很深适合大型企业长期持续治理。但对大多数中小团队来说为了管理几十台几百台机器去额外维护一整套 Master-minion 架构投入产出比确实不高。Ansible 这种无需常驻代理端的方式让我可以把精力放在我要系统处于什么状态上而不是怎么维持这套工具本身。1.3 无代理设计与SSH相互配合的实际体验Ansible 没有独立的服务端和代理端它通过 SSH 连到目标主机执行任务。理解这一点非常重要因为很多诡异的问题都源于对这个架构的误判。它的执行过程大致是控制端读取 Playbook解析出任务列表然后通过 SSH 把模块代码和运行所需的数据推送到目标节点在目标节点上执行模块再把结果返回给控制端。用一句不严谨但好理解的话来说它更像一位熟练的远程执行者而不是在被管机器里埋下的常驻工具。这种架构有几个非常实际的好处。首先是部署成本低被管节点几乎不用预装额外软件只要有 SSH 服务、有可用的 Python 环境就能跑。其次是权限模型简单可以沿用原有用户和 sudo 体系不必单独准备一套账号。还有一点很实用临时性任务特别方便。比如临时检查一批机器的内存使用和内核版本直接写个临时任务拉回来就行不需要事先部署任何东西。局限性也同样真实。因为依赖 SSH如果控制端和目标主机之间的网络质量差、延迟高执行效率会明显下降目标节点 Python 环境太老或者缺失基础组件也会导致模块执行失败。所以我后来都会在受管机器上尽量保证有一个可用的、版本不太落后的 Python 环境同时把 SSHD 的一些连接限制参数调整到合理范围。2. 决定写剧本前先搞懂状态和幂等到底怎么回事2.1 过程式脚本与声明式剧本的根本区别很多人第一次看到 Ansible Playbook会觉得它不太像脚本。确实传统 Shell 脚本描述的是怎么做事先执行哪个命令再判断结果然后执行下一个命令。它关心的是操作过程本身。而 Ansible Playbook 描述的是事情应该是什么样的比如nginx 服务必须是运行状态这个配置文件的内容必须是指定内容。用一个生活例子来理解会更直观。假设你要打扫房间过程式做法是拿起扫帚扫地、把桌面物品归位、叠好被子。每一步都是明确动作。声明式做法则不同你只需要设定目标状态是房间整洁至于用吸尘器还是用拖把完全可以自由选择。Ansible 做的事情就是不断检查当前状态距离目标状态差多少只执行那些必要的收敛动作。这个差异引出了配置管理领域非常重要的一个词幂等性。幂等意味着一个操作执行一次和执行十次最终结果是一致的。比如你手动执行一条防火墙 reload 命令执行十次也就是 reload 十次看起来没破坏什么但中间一旦有配置未加载完就可能引入问题。而 Ansible 的 service 模块在执行时会先检查服务状态如果服务已经在运行且配置没有变化它会直接返回 OK而不是盲目重复执行。这才是我愿意把变更交给工具来跑的根本原因。2.2 Playbook的基本骨架tasks、plays与模块从结构上讲Ansible Playbook 的核心组织单位是 play一个 play 包含一系列 task每个 task 调用一个具体模块。一个最简单剧本大概是这个样子- hosts: web_servers become: yes tasks: - name: 安装nginx apt: name: nginx state: present - name: 启动nginx服务 service: name: nginx state: started enabled: yes里面的每个字段都有实际意义。hosts 指定在哪些主机上执行become: yes 表示执行任务时进行提权因为安装软件、启动服务通常需要 root 权限tasks 是任务列表每个任务里的模块参数描述的是期望状态。apt 模块负责管理软件包service 模块负责管理服务。刚开始学时我不理解为什么每个 task 都要写一个 name后来发现这个习惯对排查问题的帮助极大。执行日志里的任务名就是线索没有命名的话出现错误时你得自己猜是哪一步挂的。命名起得好一行日志就能定位到具体位置。become 的用法需要单独提醒。Ansible 默认用当前 SSH 用户连接目标机器如果你的账号没有提权能力大量系统级操作会直接失败。become: yes 类似 sudo但建议在主机清单或 Playbook 中明确配置 become_user、become_method不要全局无脑开提权否则权限审计会非常混乱。2.3 handlers到底是什么别再当成普通task用Handlers 是 Playbook 里一套容易被忽略但极其实用的机制。它在普通 tasks 执行完毕后仅当某个任务明确发出触发信号时才运行。最典型的使用场景是配置文件内容发生变化后自动重启一次服务。我最早犯的一个错误是把 handler 当成普通 task 放在任务序列里结果发现它总是最后才执行行为表现很怪。后来才理解handlers 的触发逻辑是某个任务通过 notify 字段通知对应的 handler所有通知统一等到 play 结束时集中处理。这样做的好处是如果你一次修改了多个配置文件所有改动都完成后只需要重启一次服务而不是每改一个文件就重启一次。在大批量变更的场景下这种收敛逻辑很有价值。需要特别注意handler 只有在前置任务状态是 changed 时才会被触发。如果任务执行结果是 ok 而不是 changed它不会给对应 handler 发通知。很多人测试时发现服务没有按预期重启开始怀疑 handler 写错了其实只是因为文件内容根本没有真正变化。这恰恰是幂等性的正常表现——不需要动的时候绝不多动。3. 盘点我使用频率最高的几个模块和实战心得3.1 文件与内容管理copy、template、fileAnsible 之所以好用很大程度要归功于模块把常见系统操作封装得很好。文件处理方面我最常用的三个模块是 copy、template 和 file。copy 模块用来把本地文件推送到远端。比如推送一份新的 nginx 配置- name: 推送nginx配置 copy: src: files/nginx.conf dest: /etc/nginx/nginx.conf owner: root group: root mode: 0644owner、group、mode 这些属性我强烈建议写全。刚开始我经常漏写 mode结果文件推送过去了权限不对服务起不来排查半天才发现是权限问题。template 模块和 copy 模块很像但它支持 Jinja2 模板语法可以在文件里插入变量。比如给每台机器生成带有各自主机名的欢迎文件。当你需要根据每台机器不同情况动态生成内容时template 是最顺手的工具。模块内部会先渲染模板再和远端现有文件做对比内容没变化就不会触发写入这天然避免了反复覆盖文件的问题。file 模块则用来管理文件本身的属性创建目录、创建软链接、删除文件、修改属主权限等。比如- name: 确保应用目录存在 file: path: /opt/myapp state: directory mode: 0755这几个模块组合起来日常的配置分发需求基本都能覆盖。3.2 服务与包管理service、apt、yum只要涉及装软件、起服务这两个动作就离不开 service 模块和包管理模块。在 Ubuntu/Debian 系统上我用 apt在 CentOS/RHEL 系统上我用 yum。它们的用法非常接近核心参数是 name 和 state。state 的可选值包括 present、latest、absent。present 表示保证已安装不会轻易升级现有版本latest 则会把包升级到仓库里的最新版本。如果要跑生产环境我建议慎用 latest自动化过程中无意把某个依赖包升到新版本可能带来一系列非预期的影响。安装指定版本可以写成 nginx1.18.0 类似格式只要仓库支持就行。service 模块用来管理系统服务。常用的参数有 name、state 和 enabled。state 可以写 started、stopped、restarted、reloadedenabled 表示是否开机自启。这里有实战经验要分享当配置文件变更后需要让服务重新加载配置时尽量不要在 task 里直接写 restarted。如果每次执行剧本都是无条件重启服务哪怕配置没有变化也会造成不必要的中断。更合理的做法是配合 notify 和 handler文件真变了才重启或重载。service 模块对 state 参数的处理本身不是幂等的尤其是 restarted用它做无条件重启的场景要格外小心。3.3 命令与脚本的边界command、shell、script有些需求确实没有现成模块只能执行一段命令。这时会用到 command 模块。它和 shell 模块的区别在于command 不会经过目标机器的 shell 去解释执行因此像管道、重定向这类操作符都不能直接用shell 模块会调用 shell 执行灵活性强但风险也更大。我的原则是能用专用模块就不用 command必须用命令时优先用 command 而不是 shell。原因很简单专用模块通常实现了幂等和变化检测而 command 和 shell 类模块每次执行都会显示 changed这意味着同样的剧本跑两遍它都会报告有变化审计日志里很难判断是否真的动了系统。如果确实要执行命令尽量用 creates 参数或 when 条件做保护。比如- name: 初始化基础目录 command: /usr/local/bin/init_dir.sh args: creates: /data/appcreates 表示当指定路径已经存在时就不再执行这个命令减少重复执行的风险。script 模块则用于把控制端的一个脚本复制到远端执行适合那些很难拆解成模块的改造脚本。好处是不需要先手动拷贝再执行一条任务完成。但它同样不保证幂等需要你自己对场景做好保护。4. 变量、清单与事实批量管理时真正拉开差距的地方4.1 Inventory清单的组织方式如果说 Playbook 是剧本那 Inventory 清单就是演员表。它告诉 Ansible 要管理哪些主机、这些主机属于哪个分组、每个分组的变量是什么。最常用的写法是 INI 格式可读性好也简洁[web_servers] web-01 ansible_host10.0.0.11 web-02 ansible_host10.0.0.12 [database_servers] db-01 ansible_host10.0.0.21 [all_servers:children] web_servers database_servers这里有两个经验要重点分享。第一尽量不要在 Inventory 里直接写 SSH 密码更不要顺手提交到 Git 仓库。要么配置 SSH 密钥认证要么用 ansible-vault 加密敏感文件。第二善用组嵌套。web_servers、database_servers 都挂在 all_servers 组下面后续要对所有服务器做批量变更时直接面向 all_servers 这个组执行即可不需要反复列主机清单。提醒一旦 Inventory 文件进入版本管理任何敏感信息的泄漏风险都会被无限放大。宁可多花十分钟配置密钥认证也不要图省事把密码留在清单里。4.2 变量作用域与优先级谁覆盖谁Ansible 变量的来源非常广命令行参数、play 中的 vars、vars_files、组变量、主机变量、facts 事实、角色默认变量等。刚开始我经常踩的坑是改了半天的变量不生效实际上是被同名的高优先级变量覆盖了。优先级大致从低到高排序是这样角色默认值 组变量 主机变量 play 中的 vars 显式传入的 -e 参数。实际使用中我把随环境变化的内容放在组变量和主机变量里把固定逻辑放在 play 中。遇到临时需要覆盖的场景就用 -e 参数比如某次发版需要指定一个特殊版本号直接在命令行覆盖不用改动任何文件。需要特别留意的是Ansible 不同版本之间变量优先级的细节差异不小。尤其涉及角色依赖时更不要凭感觉推断。稳妥做法是在实际环境里用 debug 模块验证当前变量值是什么。我几乎每天都会用它确认变量解析结果这比翻文档回忆规则高效得多。4.3 facts与gather_facts别让信息收集成为瓶颈Ansible 默认会在执行 play 前先采集目标主机的事实信息这些 facts 包括操作系统发行版、内核版本、CPU、内存、IP 地址等采集后以变量形式供整个 play 使用。举例- name: 输出目标主机总内存 debug: msg: {{ ansible_memtotal_mb }} MB不过 facts 采集是有成本的。控制端要远程运行一系列采集逻辑每台主机都要消耗几秒。当主机数量多时这个时间会被显著放大甚至占据整体执行时间的相当比例。如果剧本本身根本用不到 facts可以在 play 级别加 gather_facts: no 跳过采集速度提升立竿见影。但也不能一律禁掉比如模板里要引用主机名、系统类型时没有 facts 是跑不动的。我的做法是先快速判断剧本是否依赖 facts大范围批量执行时能关就关某些场景还能用 gather_subset 对采集范围做裁剪只取必要字段。这几个优化组合下来执行耗时会明显下降到可接受范围内。5. 让我踩了半个月坑才逐渐摸清的五个高频问题5.1 YAML里的危险字符冒号、换行与缩进YAML 看起来简单实际隐藏了不少细节。最常见的问题就是值里带冒号和特殊字符。比如一个普通字符串里包含冒号不加引号可能会被解析成嵌套结构包含 # 的字符串也可能被当成注释截断。我习惯的做法是只要值里含有冒号、#、大括号等潜在危险字符一律加引号包住。多行文本使用块标量语法比如text: |这样换行不会被折叠。刚开始为了省事直接用 vim 写剧本缩进经常出问题后来统一改成两个空格缩进并配置了语法校验这类问题基本绝迹。5.2 权限问题become报了权限不足却查不出原因become 机制是 Ansible 管理端向受管服务器进行权限升级的主要方式默认类似 sudo。遇到权限报错我通常按这个顺序排查确认 SSH 登录用户是否存在于目标机的 sudo 组中确认 sudo 规则是否对本次要执行的命令做了限制确认 Playbook 里的 become 是放在 play 级还是 task 级task 级配置是否意外覆盖了 play 级配置查看目标机 sudo 相关日志确认 Ansible 实际以什么身份在跑任务检查 sudoers 文件里是否有 requiretty 这类限制项。有一次我排查了很久最后发现不是用户名和权限规则的问题而是目标主机的 sudoers 里配置了 requiretty。这个选项会让远程通过 SSH 执行 sudo 时因为没有可用的终端而失败。去掉这条配置后一切恢复正常。这类问题不算常见但一旦碰上没有经验会非常卡人。5.3 when条件判断复杂逻辑容易变成括号地狱when 用来做条件执行。它原生支持 and、or 运算也允许括号分组。但 YAML 和 Jinja2 混在一起之后很容易写出让人看不懂的表达式。比如- name: 仅在支持的发行版安装htop apt: name: htop state: present when: - ansible_os_family Debian - ansible_distribution_major_version | int 20这里缩进列表里的多个条件之间默认是 and 关系这是常用的写法。如果确实要表达复杂的优先级逻辑建议先测试好再拆成清晰的子条件不要把一大串括号全塞进一个表达式里。另外变量尚未定义时直接拿来判断会报错通常用 default 过滤器兜底例如when: some_var | default(false) | bool。这不是银弹但至少可以让未定义变量在条件判断里表现得可控一些。5.4 变量取不到值debug模块是排查的第一工具没有哪种调试手段比得上直接看变量的实际值。当我觉得变量取值不对时第一反应永远是加一个 debug 任务- debug: var: my_variable也可以格式化输出更多上下文- debug: msg: 当前环境的group值是{{ my_variable }}反复 debug 可以快速确认问题出在变量优先级、变量名拼写还是 facts 缺失。在真正执行变更之前我还会先跑语法检查ansible-playbook -i inventory.ini playbook.yml --syntax-check然后再用 --check 模式做一次预演。这样可以避免把明显的语法错误和逻辑错误直接抛到生产环境里。5.5 幂等性被破坏同一套剧本第二次执行竟然报错这是最让人头疼的问题。看起来完全正确的剧本跑第二次却失败了。绝大多数情况下原因是某个任务依赖了一个只有第一次执行时才会成立的临时状态。比如第一次执行时创建了一个目录并往里面写入配置第二次执行时目录已经存在但写配置的任务期望从模板重新生成内容如果中间变量发生了变化或者目录里有额外文件就可能报错。面对这种情况我的建议是尽量把任务设计成始终朝目标状态收敛而不是先判断存在与否再做分支处理。避免用 shell 写复杂的 if 判断优先使用模块的幂等参数比如 copy 的 force、file 的 state、template 的 validate。实在无法避免一次性操作时用 creates 声明这个文件存在就不再执行这是简单又可靠的方法。6. 性能优化与日常习惯把Ansible用得舒服一点6.1 并发控制forks参数怎么调才合适Ansible 默认的并发数是 5也就是同一时刻最多并行处理 5 台主机。如果你需要管理几十上百台机器默认值确实让人着急。可以在 ansible.cfg 里调整[defaults] forks 20但需要注意并不是越大越好。并发数太高控制端要同时维护大量 SSH 连接CPU、文件描述符和网络带宽都可能成为瓶颈目标机器那边如果有大量任务同时提权执行也容易影响正常业务。建议从默认值开始在测试环境逐步提高观察控制端 CPU、网络连接和任务总耗时这几个指标找到适合自己环境的并发数而不是一味追求最大。6.2 检查模式、语法检查与详细日志正式执行之前一定要习惯用检查模式ansible-playbook -i inventory.ini playbook.yml --check在这个模式下Ansible 不会真的修改系统只会模拟判断每个 task 是否会发生变更。它非常适合变更上线之前的人肉评审。但也要明确检查模式不是百分之百准确凡是涉及 shell、command 这类模块时它无法真正预判执行结果因为命令本身根本没有运行。所以 --check 模式适合观察框架性改动不适合用来替代完整验证。除 --check 之外-v、-vv、-vvv详细模式也是排错利器。任务失败时用-vvv能看到完整的 SSH 传输内容、模块执行输出和错误信息。很多莫名其妙的问题把-vvv日志完整翻一遍往往比盯着红字猜原因有效得多。6.3 用ansible-vault保护敏感信息Playbook 里难免要处理密码、密钥、token 这类敏感数据。直接明文放在 Inventory 或 vars 文件里并不是好习惯。Ansible 自带的 ansible-vault 可以对整个文件做加密。ansible-vault encrypt group_vars/production.yml执行 play 时加入--ask-vault-pass或--vault-password-file选项提供解密口令。这里强烈建议通过环境变量或外部密钥管理方式传递解密密码这样敏感信息不会落进 Git也不会被写在各种脚本日志里。不过要清醒认识到ansible-vault 的安全边界有限。它只负责静态文件内容的加密保护运行时解密后的数据依然存在于进程内存里。对于极高敏感度的凭据建议对接专门的密钥管理系统集中管理Ansible 从系统中读取凭据避免让真实密钥出现在任何远端文件里。6.4 角色与目录规范为后续可维护性留够余地随着 Playbook 数量变多把所有内容堆在几个大文件里维护难度会快速上升。Ansible 提供角色的机制我个人采用的目录约定大致是这样ansible_project/ ├── ansible.cfg ├── inventory/ │ ├── production │ └── staging ├── playbooks/ │ ├── site.yml │ └── nginx.yml └── roles/ ├── nginx/ │ ├── tasks/ │ ├── templates/ │ ├── handlers/ │ └── vars/ └── common/角色把安装配置 nginx这件完整的事情连同任务、模板、handler、变量统一收进一个目录可以被多个 play 复用。虽然初期拆分目录会多花一些时间但后续维护和团队协作省下来的时间远超前期投入。写角色时要特别注意角色默认变量和 play 变量之间的优先级关系这两者混在一起最容易出问题。6.5 从个人使用体验看最值得先养成的几个习惯用过一段时间之后我最大的体会是学 Ansible 最关键的并不是背模块参数而是把思维从一台台执行操作转换到定义系统的目标状态。如果你只是把以前的 Shell 脚本逐行翻译成 Playbook那它对你来说就是另一个命令包装工具价值会大打折扣。再分享一个适合刚入门的人的小技巧先在自己的笔记本上装一个虚拟机或容器环境用 Ansible 管理这一台机器就足够了。不需要什么生产环境做实验一台机器就能把 Playbook、变量、facts、handler 这些核心概念全部过一遍。等这些基础彻底理解了再上真实服务器批量执行时底气完全不一样。自动化运维这条路早走一步和晚走一步区别很快就能体现出来。