ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Ansible 2.9.27生产环境实战:安装、Playbook与踩坑排查

Ansible 2.9.27生产环境实战:安装、Playbook与踩坑排查

简介:面向 CentOS 7 / RHEL 7 系统管理员与运维工程师,提供 Ansible 2.9.27 完整离线部署包,适合在无外网或内网隔离环境中快速搭建自动化运维平台。压缩包共 29 个文件,以 RPM 安装包为主,包含 ansible 主程序及 python-paramiko、PyYAML、sshpass 等关键依赖,另有 gz/bz2 格式的 yum 仓库元数据与 repomd.xml 索引,整体仅 19.29MB,便于传输和离线安装。该版本延续 Ansible 无代理特性,通过 SSH 协议即可管理大量主机,支持用 YAML 编写 Playbook 统一完成系统配置、软件部署、服务启停等任务;内置的 copy、yum、template、service 等常用模块,能满足批量化日常运维需求。包中对 RHEL7 系列依赖链做了整理,可避免因缺少依赖导致的安装失败,企业用户可直接将本地目录配置为 yum 源进行批量安装。已有 635 人学习下载,既适合初学者系统了解自动化运维,也可作为生产环境快速部署的实用工具包。

1. ansible-2.9.27:它为什么还在生产环境服役

登录某生产环境的控制节点,输入ansible --version,屏幕返回ansible 2.9.27——这个画面在很多一线运维手里并不陌生。明明 Ansible 已经发到更高的版本,为什么 2.9.27 还在大批服务器上稳稳跑着?答案很现实:它足够稳定、边界清楚、资料好查,而且很多存量 Playbook 就是照着这一代语法写的。这个标题讲的就是把版本锁死在 ansible-2.9.27 之后的完整打法:怎么装、怎么配置、怎么写 Playbook、遇到玄学错误怎么排查,以及它到底还能不能扛住新需求。适合批量管理几十到几千台主机、正在维护老 Playbook、又不想被升级折腾的运维和开发。

2. 为什么选 2.9.27:兼容性边界与三条判断

2.1 2.9 系列在 Ansible 迭代里的位置

Ansible 2.9 是模块体系拆分前最后一条完整主线。2.9 之后,模块慢慢被拆进 collection,Playbook 写法开始出现“要不要带 fully qualified collection name”的争论,依赖关系也变复杂。反过来看 2.9,它的模块是整包随发行版提供的,ansible-doc -l一敲就是全量模块列表,不用额外装 collection,这对内网离线环境极其友好。

2.9.27 是 2.9 分支的收尾修复版。它不是新功能版,而是把前面十几轮暴露的问题集中修掉后的“最终形态”。对运维来说,版本号末尾的修复版本往往才是最值得长期用的:功能不再膨胀,行为已经固定,网上能搜到的报错样本最全。很多团队的线上 Ansible 停留在 2.9.27,不是因为懒,而是因为升级到 2.10+ 意味着每个模块都要重新回归,收益却只是用不上几个新特性。

2.2 控制端与被管端的 Python 兼容矩阵

决定 2.9.27 能不能跑,先看两端的 Python。控制端需要 Python 2.7 或 3.5 及以上,也就是说现在用 Python 3.9、3.10 装它没问题;被管端情况复杂一些,基础模块要 Python 2.6 以上,但有相当一部分模块在 2.6 上表现不完整,官方实际上更推荐 2.7 或 3.5 及以上。

位置常见可用 Python需要重点关注的配置
控制端Python 2.7 / 3.5+用虚拟环境锁版本,避免系统 Python 被污染
被管端Python 2.6+,模块多要求 2.7;3.5+ 通用ansible_python_interpreter=/usr/bin/python3
被管端无 Python只能跑raw模块做引导先用raw装 Python,再走正常模块

这一条直接影响后面所有 Playbook 的适配。例如在老的 CentOS 6 上/usr/bin/python指向 2.6,而某个模块内部用了 2.7 才有的语法,就会出现模块执行失败;在只装了 Python 3 的新镜像上,默认解释器路径又可能指向不存在的/usr/bin/python。所以 2.9.27 的 inventory 里,“显式指定解释器”通常排在第一位。

2.3 什么时候坚持 2.9.27,什么时候换新版

三条判断供参考。第一,现有 Playbook 是否大量基于 2.9 语法、是否依赖未拆分的核心模块;如果是,留在 2.9.27 是成本最低的选择。第二,是否明确需要 collection 分发的新模块、是否要对接新版生态;如果不是,就没有升级的动力。第三,团队是否有多套环境需要统一版本;多数情况下各环境拉到同一版本号 2.9.27,比追新版本更容易保持一致。

我见过一个模拟项目X,控制端还是老 Python 环境,被管端从 CentOS 6 一直混到新版云镜像,整套 Playbook 全是yum、service、template这类传统模块。换成新版后要逐个模块回归,最后结论仍是留在 2.9.27。反过来,如果项目一开始就要用新模块体系里的功能,比如某些定向发布的插件,那就别硬用 2.9.27,应该评估直接上新主线。

3. 把 2.9.27 跑起来:安装、cfg 与最小 inventory

3.1 用虚拟环境安装 ansible-2.9.27

安装 Ansible 最常见也最可控的方式是拉一个独立的 Python 虚拟环境,然后把版本钉死在 2.9.27。不要图省事直接pip install ansible装进系统 Python,全局安装会把权限、依赖、版本全搅在一起,后面想换版本很被动。

python3 -m venv /opt/ansible-2.9.27 source /opt/ansible-2.9.27/bin/activate pip install --upgrade pip pip install "ansible==2.9.27" ansible --version

逻辑说明:先建虚拟环境,激活后所有 Python 包都落在/opt/ansible-2.9.27下,不污染系统解释器。升级 pip 是为了避免老 pip 拉取依赖时出错。最后一条命令验证安装结果,输出里会显示ansible 2.9.27。参数说明:--upgrade pip不是必须的,但在 CentOS 7 这类自带 pip 较老的系统上建议执行;"ansible==2.9.27"把版本精确锁定,避免 pip 顺手装了更新版本导致行为不一致。

安装完成后建议把PATH写进.bashrc或直接使用绝对路径调用。我一般会加一行export PATH=/opt/ansible-2.9.27/bin:$PATH,这样任何终端登录控制节点都能直接执行ansible。

只要依赖的 Python 解释器在 3.5 以上,这个安装方式在绝大多数系统上都能顺利走通。安装过程经常卡在编译某个依赖上,比如装了旧版 openssl 导致 cryptography 构建失败,把编译工具链补全再重装即可。

3.2 ansible.cfg:先写对再动手

控制节点第一次跑不起来,十有八九是没配ansible.cfg。Ansible 查找配置文件的顺序是当前目录、用户家目录、/etc/ansible/ansible.cfg,我习惯把ansible.cfg放在每个项目目录里,随 Playbook 一起走,避免不同项目互相干扰。

[defaults] inventory = hosts host_key_checking = False retry_files_enabled = True remote_user = ops become = True become_method = sudo timeout = 30 [ssh_connection] pipelining = True ssh_args = -o ControlMaster=auto -o ControlPersist=60s

逻辑说明:inventory指定同目录下的主机清单文件;host_key_checking = False跳过首次 SSH 指纹确认,省得批量管新主机时卡在交互确认上;remote_user指定默认登录用户;become = True让 Playbook 默认启用提权,省去每个任务重复写become。timeout控制 SSH 连接等待时限,避免连不上的时候长时间挂起。

参数说明:pipelining = True会把模块执行和传输合并成一个 SSH 通道,大批量执行时提速明显,但要求被管端目录/tmp可写;ControlPersist=60s保留空闲连接 60 秒,连续执行多个任务时复用连接。如果被管端 sudo 需要密码,这里还要在 inventory 或运行时指定密码,不建议写死在 config 文件里。

host_key_checking = False是一个典型的安全权衡:内网批量环境收益大于风险,但公网环境应改为预先在控制端.ssh/known_hosts里写入主机指纹,而不是关掉校验。另外retry_files_enabled会在 Playbook 失败时生成.retry文件,方便重跑指定失败主机。

3.3 最小 inventory:三种写法与验证命令

Inventory 决定 Ansible 认识哪些主机,常见写法是 INI 格式,按组划分。以下是最小可用的 hosts 文件。

[web] 192.0.2.10 ansible_user=ops ansible_become=yes 192.0.2.11 [db] 192.0.2.20 [all:vars] ansible_python_interpreter=/usr/bin/python3

逻辑说明:[web]和[db]是两个组,组名在 Playbook 里可以直接作为 hosts 目标使用。第二行没有写主机变量,会走[all:vars]里的默认解释器配置。参数说明:ansible_user指定 SSH 用户;ansible_become=yes表示该主机默认提权;ansible_python_interpreter是 2.9 系列里最重要的一个变量,直接决定被管端用哪个 Python 跑模块。

写好后先摸一遍连通性,再验证被管端事实收集是否正常。

ansible -i hosts web -m ping ansible -i hosts all -m setup -a "filter=ansible_distribution*"

第一条命令用ping模块检查连接与登录是否正常,能返回pong说明 SSH、用户、提权链路都通。第二条命令调用setup模块收集系统事实,filter=ansible_distribution*只保留发行版相关信息,用来确认被管端 Python 解释器是否生效。如果第二行报module failed,多半是解释了器路径错了。

另一种 inventory 写法是把变量拆到目录里:group_vars/all.yml、group_vars/web.yml,适合变量多、需要版本管理的场景。目录方式比把全部变量塞进 hosts 文件更清晰,且同名变量优先级按 主机变量 > 组变量 > all 变量 的顺序覆盖。

4. 写第一个 Playbook:模板、执行策略与三层校验

4.1 目录结构:把配置和代码分开

Playbook 不是单一 YAML 文件,我习惯按项目建目录。一个典型结构如下。

web-ops/ ├── ansible.cfg ├── hosts ├── group_vars/ │ └── web.yml ├── templates/ │ └── nginx_reverse_proxy.conf.j2 └── site.yml

逻辑说明:group_vars/web.yml存 web 组专用的变量,templates放 Jinja2 模板,site.yml是入口 Playbook。这样做的价值是后续加服务器时不用翻 YAML 找变量,模板和任务分离也让非 Python 背景的同事能看懂改动点。2.9.27 对这个结构的支持很完整,不需要额外配置。

4.2 用模板下发 Nginx 反向代理配置

模板的作用是把“可变的部分”留成占位符,由变量注入。下面是一个最小 Nginx 反向代理模板。

server { listen 80; server_name {{ nginx_server_name }}; location / { proxy_pass http://{{ nginx_upstream }}; proxy_set_header Host $host; } }

逻辑说明:{{ nginx_server_name }}和{{ nginx_upstream }}是 Jinja2 变量,渲染时由 Ansible 从 变量源取值。$host是 Nginx 自身的变量,不会被 Ansible 模板引擎解析。参数说明:nginx_server_name填写对外域名,nginx_upstream填写后端服务地址,比如127.0.0.1:8080。

对应的 Playbook 如下。

--- - name: deploy nginx reverse proxy hosts: web become: yes vars: nginx_server_name: example.com nginx_upstream: "127.0.0.1:8080" tasks: - name: install nginx yum: name: nginx state: present - name: template reverse proxy config template: src: nginx_reverse_proxy.conf.j2 dest: /etc/nginx/conf.d/reverse_proxy.conf notify: - restart nginx handlers: - name: restart nginx systemd: name: nginx state: restarted daemon_reload: yes

逻辑说明:hosts: web表示只在这个组执行;become: yes提权;vars是任务内的临时变量。第一个任务用yum模块装 Nginx,第二个任务把模板渲染到目标路径,notify触发同名 handler。参数说明:template模块的src是模板文件相对路径,dest是目标机绝对路径;notify只有当配置文件内容发生变化时才会触发restart nginx,内容没变不会重启服务,这是幂等设计的关键。

如果目标机是 Debian 系,把yum换成apt即可。2.9.27 的yum/apt模块在使用上都很稳定,主要坑在于被管端默认 Python 环境,前面配置文件里已通过ansible_python_interpreter解决。

4.3 执行策略:serial、forks 与 become 的配合

批量执行时最怕一组挂掉拖垮全部。2.9.27 用serial控制每批次执行的机器数,生产环境建议设为 2 或 3,先在少量机器上验证再滚动到全部。

- name: rolling deploy hosts: web become: yes serial: 2 forks: 5 tasks: - name: reload nginx systemd: name: nginx state: reloaded

逻辑说明:serial: 2表示每批只拿 2 台机器执行,一批成功后才进入下一批;forks: 5表示批次内最多并行跑 5 个任务节点。become: yes指定默认提权。参数说明:当被管端用户需要 sudo 密码时,可以额外指定-K参数或通过 inventory 里的ansible_become_pass传密码,后者不建议明文写在 hosts 文件里。

serial和forks不是同一个概念,前者控制批次大小,后者控制同一批里的并行度。如果机器数量少、网络环境差,可以把forks调低,避免并发 SSH 把控制端或交换机打满。我在模拟项目X里就遇到过forks=20时控制端 CPU 突然飙高,改成serial: 3, forks: 5后稳定完成滚动更新。

4.4 三层校验:语法检查、dry-run 与 diff

Playbook 写完后不要直接跑全量,先用校验命令过三关。依次执行以下命令。

ansible-playbook -i hosts site.yml --syntax-check ansible-playbook -i hosts --list-hosts site.yml ansible-playbook -i hosts site.yml --check --diff

逻辑说明:第一行只做 YAML 语法检查,不连主机,能快速暴露拼写和结构错误。第二行确认 Playbook 的目标主机范围,避免把改动波及到不该动的组。第三行进入 dry-run,--check让模块只预测变更不真正执行,--diff显示文件前后差异。参数说明:--check对纯配置类任务很有用,但不是所有模块都支持 check 模式,command类模块在 check 下也会执行;--diff在任务涉及template、copy时输出文件差异,方便 review。

我执行 Playbook 的习惯是先把目标机用-l限定一台,例如ansible-playbook -i hosts site.yml -l 192.0.2.10,确认这台机器上渲染结果符合预期,再去掉-l跑全组。这个习惯省下的后悔药比什么技巧都值钱。

5. 2.9.27 排查:五个高频踩坑与修复记录

5.1 SSH 连接被 host key 弹窗卡死

现象:执行ansible web -m ping时任务长时间不返回,手动 SSH 登录却要确认指纹;通过被管端大量新增主机时尤其明显。原因:默认host_key_checking开启,首次连接未知主机时出现交互确认,而 Ansible 是非交互执行,无法响应确认导致挂起。解决:在ansible.cfg里加host_key_checking = False,或预先用ssh-keyscan采集指纹写入known_hosts。讲真,内网场景关掉它最常见,但要意识到这是安全让步,公网环境应改为预采集指纹。

5.2 模块报错找不到 Python 解释器

现象:执行setup或任何模块任务时报类似FAILED! => {"msg": "module (and its dependencies) is missing"},或者后台日志显示/usr/bin/python不存在。原因:新云镜像默认没有 Python 2,而 Ansible 在 2.9 里默认找/usr/bin/python。解决:在 inventory 的[all:vars]下固定ansible_python_interpreter=/usr/bin/python3。另一种情况是被管端只有 Python 2.6,此时基础模块能跑,部分依赖 2.7 特性的模块仍会失败,只能换路径或改用raw模块引导。

5.3 提权后环境变量和 PATH 丢失

现象:Playbook 里通过become提权后执行command提示找不到某个命令,或者部署的服务起不来;直接 SSH 上去手动跑却没问题。原因:sudo 默认重置环境变量,PATH、HOME都变了,Ansible 提权后拿到的不是原用户环境。解决:在配置里加become_flags: -E保留环境,或者在ansible.cfg的[privilege_escalation]段统一设置。注意-E可能引入安全风险,生产环境更推荐把完整路径写进命令,或把需要的变量显式传进 Playbook。

5.4 YAML 语法检查报错,冒号惹的祸

现象:Playbook 语法检查报Syntax Error while loading YAML,定位到某一行看似正常的字符串。原因:YAML 会把“冒号加空格”解析为字典,比如msg: 状态: 正常里第二个冒号会触发解析错误。解决:给带冒号的字符串加引号,例如写成msg: "状态: 正常"。路径里带冒号也一样,比如dest: /etc/nginx/conf.d/rev: proxy.conf,必须整体加引号。这种报错在 2.9.27 里经常被误判为模板问题,排查时先看冒号。

5.5 sshpass 未安装导致密码认证失败

现象:inventory 里配置了ansible_password或使用了-k参数,执行时报to use the 'ssh' connection type with passwords, you must install the sshpass program。原因:控制端没有安装 sshpass 工具,Ansible 无法通过 ssh 的密码认证方式连接。解决:在控制端用系统包管理器安装,例如基于红帽的是yum install -y sshpass,基于 Debian 的是apt install -y sshpass。安装后重新执行即可。生产环境仍建议使用密钥认证,密码方式只适合临时任务或切换认证期的过渡方案。

这些坑的共同点是:报错信息看着吓人,但根因都不是 2.9.27 本身的问题,而是环境配置没有对齐。遇到奇怪报错先检查三处——SSH 握手、Python 解释器、提权方式,能省下大量时间。

6. 进阶:动态 inventory 与固定版本的自检习惯

6.1 用一段脚本做动态 inventory

主机清单不一定要写死在 hosts 文件里。当机器数量大、分组来自内部系统或需要频繁变化时,可以用动态 inventory 脚本让 Ansible 每次执行时实时获取。Ansible 2.9.27 对动态 inventory 的协议很成熟:脚本支持--list输出全量主机与分组,支持--host输出指定主机变量,返回 JSON 即可。

#!/usr/bin/env python3 # -*- coding: utf-8 -*- import json import sys def build_inventory(): inventory = { "web": { "hosts": ["192.0.2.10", "192.0.2.11"] }, "db": { "hosts": ["192.0.2.20"] }, "_meta": { "hostvars": { "192.0.2.10": {"ansible_python_interpreter": "/usr/bin/python3"}, "192.0.2.11": {"ansible_python_interpreter": "/usr/bin/python3"}, "192.0.2.20": {"ansible_python_interpreter": "/usr/bin/python3"} } } } return inventory if __name__ == "__main__": if len(sys.argv) > 1 and sys.argv[1] == "--host": print("{}") else: print(json.dumps(build_inventory()))

逻辑说明:脚本输出web、db两个组和_meta里的主机变量;--host分支返回空对象表示没有额外主机变量。参数说明:脚本需要可执行权限,chmod +x inventory.py后,在ansible.cfg把inventory = hosts改成inventory = ./inventory.py,之后所有ansible、ansible-playbook命令都会自动调用脚本。

动态 inventory 的坑在于脚本报错时 Ansible 给到的提示不够直观。可以先手动执行python3 inventory.py --list验证 JSON 合法性,再接入 Ansible。真实环境里这个脚本可以换成读取内部 CMDB 接口,或扫描云厂商 API 生成主机列表,核心逻辑不变。

6.2 固定版本的三条自检习惯

我最后想补一个长期养成的习惯,它比任何模块技巧都重要。第一,控制节点上用虚拟环境钉死版本,任何新人登录都不会意外升级全局包。第二,每次换控制机或重建环境后,先跑一遍基线任务——ansible all -m ping加一个--syntax-check的 Playbook——确认版本与库存行为一致,再跑正式业务。第三,Playbook 里尽量少用latest作为安装状态,版本一旦漂移,问题排查成本会指数级上升。

以上这套组合拳背后是我的一次翻车教训:某次新控制机顺手升级到了新版 Ansible,原 Playbook 里的一批模块语法在新版下行为变化,批量执行时把老服务的配置改出了偏差,事后只能逐台回滚。从那以后我坚持一个原则:自动化管机器的地方,Ansible 自己的版本必须先被自动化管住。这套做法希望能替你把同样的一跤挡在门外,希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进