1. 先搞清楚:这两套系统上的 Ansible 到底难在哪
一批新机器上架,系统盘里装的不是同一个发行版——一部分是 OpenCloudOS,一部分是欧拉(openEuler),版本还横跨 8 和 9、20.03 和 22.03。机器到位之后,第一件事不是装业务,而是把基线统一掉:时区、时间同步、账号、防火墙策略、内核参数、常用工具包。这种活儿用脚本堆 bash 也能干,但干到第三轮你就会发现脚本里全是if [ -f xxx ]的判断,改一次跑一次都得重新校对。Ansible 就是我在这类场景下的首选:无代理、走 SSH、幂等、配置即文档,新同事接手看 inventory 和 playbook 就能明白这台机器被改过什么。适合谁看?手上有几台到几百台 Linux 机器、想用配置管理替代手工脚本、又不想引入重型平台的人。下面这些内容,是我在两套系统混合的环境里反复装、反复配、反复踩坑之后整理出来的东西,包含版本选型、安装路径、配置细节和报错处理,照着做基本能一次跑通。
1.1 同源不同枝:先认清两套发行版的差异点
OpenCloudOS 和欧拉都属于 RPM 体系、用 dnf 管包、systemd 管服务、配置文件路径高度一致,所以很多操作可以共用一套命令。但它们毕竟是两条独立的发行版线路,仓库里打好的软件包、默认 Python 版本、SELinux 默认策略、以及对第三方仓库的兼容程度,都存在肉眼可见的区别。这些区别不体现在日常dnf install上,而是集中爆发在 Ansible 控制节点的 Python 版本要求上——Ansible 的核心执行引擎是 Python 写的,控制节点版本不够,pip 直接给你一个Requires-Python >=3.9的红字,装都装不上。
具体到实际遇到的版本:欧拉 20.03 LTS 系列默认带的是 Python 3.7,22.03 LTS 系列普遍在 3.9 及以上;OpenCloudOS 8 系列默认 Python 3.6,9 系列则上到 3.9 甚至更高。而 ansible-core 从 2.14 开始把控制节点的门槛提到了 Python 3.9,2.16 之后进一步抬到 3.10。这意味着什么?意味着你在欧拉 20.03 上想直接 pip 装最新版 ansible-core,大概率是装不上的,要么退回系统仓库提供的老版本,要么自己额外装一个高版本 Python 解释器并给它单独建虚拟环境。这个判断必须在动手之前做完,否则后面全是返工。
被控节点的要求就宽松得多。Ansible 推模块过去执行时,目标机上只要有可用的 Python 3 解释器就够了,官方对较新版本的最低要求大致在 Python 3.6 上下,个别模块(比如某些依赖新版标准库的)会要求更高。老一点的系统如果只有 Python 2,就得在ansible.cfg里显式指定interpreter_python,或者干脆先在被控机上补一个 Python 3。我在一套老系统上就吃过这个亏:控制节点一切正常,ping模块能通(因为 ping 走的是 raw 方式,不需要 Python),但一跑dnf模块就报/usr/bin/python: not found,排查了半天才发现是解释器路径的问题。
1.2 无代理模式的取舍:为什么不用装 agent 的方案
Ansible 最吸引人的地方就是无代理。不需要在几百台机器上装一个常驻进程,不需要额外开监听端口,不需要担心 agent 版本和控制端不匹配,SSH 通就能干活。对于一套刚上架、还没接监控、防火墙策略也还没最终确定的环境来说,这个特性省掉的事情太多了——你不用担心 agent 包在某个发行版上没有对应版本,也不用为了升级 agent 去做一轮全量滚动。
代价也是实打实的。第一是性能,每次任务执行都要新建 SSH 连接(除非开了连接复用),几百台机器并发跑起来,控制节点的 CPU 和文件描述符都会被顶到高位。第二是依赖 SSH 的稳定性,网络抖一下、sshd 的 MaxStartups 卡一下,整个 play 就会挂掉几台。第三是没法做常驻式的事件驱动,Ansible 是推模式,你不跑它就不动,做不到 agent 那种实时上报。所以我的判断标准很简单:需要秒级响应和状态回传的场景,无代理方案不合适;需要批量下发配置、统一基线、做一次性变更的场景,Ansible 是最省心的选择。
1.3 控制节点和被控节点,角色千万别混
新手最容易犯的错是把控制节点也塞进 inventory 里当被控机用。技术上可行,但一旦你的 playbook 里写了重启服务、改防火墙、调内核参数这类操作,控制节点自己把自己玩挂,后面的任务就全断了。我现在的做法是严格分开:挑一到两台配置相对好的机器专门做控制节点,inventory 里用[control]单独分组,业务 playbook 的hosts永远指向业务分组,绝不写all。
| 角色 | 数量建议 | 配置要求 | 关键依赖 |
|---|---|---|---|
| 控制节点 | 1 主 1 备 | 4 核 8G 起,磁盘留 50G 给日志和临时文件 | Python 3.9+、ansible-core、SSH 客户端 |
| 被控节点 | 不限 | 按业务定 | Python 3.6+、sshd、sudo |
| 跳板/中继 | 视网络分区而定 | 低 | SSH 转发能力 |
控制节点的磁盘容易被忽略。ansible-playbook默认会在当前目录生成.retry文件,开了log_path之后日志会持续增长,加上临时传输的模块文件,跑上几个月就能吃掉几个 G。我一般会在ansible.cfg里把retry_files_enabled关掉,日志按月切割,这是后话,但规划阶段就该想到。
2. 动手之前,把环境摸清楚再装
装 Ansible 这件事本身只有几行命令,真正花时间的是前置确认。我见过太多"装完了跑不通"的情况,追根溯源八成是版本不匹配、账号权限没配好、或者网络根本不通。花二十分钟做一轮体检,比事后花两小时排查划算得多。
2.1 版本矩阵:先确定你能装哪个 Ansible
这一步的核心是搞清楚两件事:控制节点的 Python 版本,以及你想要哪个 Ansible 版本。这两个是硬约束,其余的都能商量。我的建议是优先保证控制节点能跑较新的 ansible-core,因为新版本在错误提示、模块兼容性、collection 管理上都好很多;如果系统 Python 实在上不去,就老老实实用系统仓库里的版本,不要硬凹。
| 发行版线路 | 典型默认 Python | 可行的 ansible-core 版本区间 | 推荐安装方式 |
|---|---|---|---|
| 欧拉 20.03 LTS | 3.7 | 2.9 ~ 2.12 | 系统仓库安装 |
| 欧拉 22.03 LTS | 3.9 ~ 3.10 | 2.14 ~ 2.16 | 虚拟环境 pip |
| OpenCloudOS 8 | 3.6 | 2.9 ~ 2.11 | 系统仓库安装 |
| OpenCloudOS 9 | 3.9 ~ 3.11 | 2.15 ~ 2.17 | 虚拟环境 pip |
表里的区间是经验值,实际以官方发布说明为准。有一个细节值得注意:ansible这个包和ansible-core不是一回事。前者是"全家桶",把大量常用 collection 打包在一起,装完体积好几百兆;后者只有核心引擎,collection 按需安装。在受控环境里我更倾向于用ansible-core加requirements.yml精确声明依赖,装什么、什么版本一目了然,不会因为某个 collection 悄悄升级导致 playbook 行为变化。
2.2 十条命令做完基础体检
下面这套检查我基本是条件反射地在每台新机器上跑一遍,几分钟的事,能挡掉后面绝大部分莫名其妙的故障。
cat /etc/os-release # 确认发行版和版本号 uname -r # 内核版本,判断是否需要额外驱动 python3 -V # 解释器版本,决定 Ansible 版本上限 which python3 # 解释器路径,后面写进配置 dnf repolist # 仓库是否可用、有没有缺 timedatectl # 时区和时间同步状态 hostnamectl # 主机名是否规范 ip -br a # 网卡与地址 df -h /var /opt # 磁盘余量 ulimit -n # 文件描述符上限timedatectl这条别跳过。时间不同步会导致 SSH 认证偶发失败、日志时间线错乱、某些依赖时间戳的模块判定异常。我有一次在一批虚拟机上排查"随机几台连不上"的问题,最后发现是其中三台的时间漂了十几分钟,SSH 的密钥校验直接拒绝。
ulimit -n也要看。默认值可能是 1024,你后面想把forks调到 50,每个并发连接会占用若干文件描述符,加上 SSH 进程本身的开销,1024 很容易顶满,表现就是跑到一半报Too many open files。控制节点我一般调到 65535,写在/etc/security/limits.d/下的独立配置文件里,比改主配置安全。
2.3 运维账号与提权方式,提前定死
Ansible 连过去用什么账号、怎么提权,这两件事必须在第一批任务之前定下来,中途改会牵动所有 playbook。我的标准做法是:建一个专用运维账号,全环境同名,SSH 密钥登录,sudo 免密执行全部命令。
useradd -m -s /bin/bash deploy mkdir -p /home/deploy/.ssh && chmod 700 /home/deploy/.ssh # 把控制节点的公钥写入 authorized_keys chmod 600 /home/deploy/.ssh/authorized_keys chown -R deploy:deploy /home/deploy/.ssh # 免密 sudo echo 'deploy ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/90-deploy chmod 440 /etc/sudoers.d/90-deploy visudo -c # 语法校验,别省这一步注意:
/etc/sudoers.d/下的文件权限必须是 0440 且属主为 root,权限不对 sudo 会直接忽略该文件,你会看到"明明写了却还是要密码"的诡异现象。
免密 sudo 听起来危险,但在受控的运维网段里,它换来的是自动化流程不中断。如果你所在的环境对安全要求更高,可以退一步只对特定命令免密,比如只放行systemctl、dnf、firewall-cmd这几个,其余保持密码。代价是 playbook 里不能出现任意 shell 命令,得改成白名单内的调用,写起来会啰嗦一些,但可控性更强。
3. 安装路线选型:三条路,各有各的适用场景
装 Ansible 的方式不止一种,选哪条路取决于你的网络环境、版本要求和运维习惯。我把常用的三条路都走了一遍,各自的坑也记录下来了。
3.1 系统仓库直装:快,但要接受版本偏旧
最省事的方式,两条命令结束战斗。
dnf makecache dnf install -y ansible ansible --version优点是包由发行版维护,依赖关系、SELinux 策略、systemd 单元都替你处理好了,出问题也容易找到同版本的人。缺点是版本通常落后上游一到两年,一些新模块和新参数用不了。我在欧拉 20.03 和 OpenCloudOS 8 这类 Python 版本偏低的环境里基本都走这条路,装完是 2.9 系列,够用,写 playbook 的时候注意别用 2.10 之后才有的语法就行。
判断能不能用的标准很简单:ansible-doc里能查到你要用的模块,参数也对得上,那就没问题。实在需要新特性,可以在 playbook 里做版本判断,用when: ansible_version.full is version('2.12', '>=')分开写两条任务,比强升版本稳。
3.2 虚拟环境配合 pip:我最常用的方式
Python 3.9 以上的环境,我一律推荐这条路。核心思路是用 venv 隔离出一个独立的 Python 环境,Ansible 装在里面,不和系统包管理器打架,升级、回滚、并行装多个版本都很方便。
# 装 venv 和编译依赖(部分包需要) dnf install -y python3 python3-pip python3-devel gcc libffi-devel openssl-devel # 建环境,路径固定在 /opt 下方便统一管理 python3 -m venv /opt/ansible-venv source /opt/ansible-venv/bin/activate # 升级 pip,换成你所在网络里速度快的镜像源 pip install --upgrade pip -i https://<镜像源地址>/simple # 只装核心引擎,版本按前面对照表来 pip install "ansible-core==2.16.*" # 装常用 collection ansible-galaxy collection install ansible.posix community.general隔离带来的好处在升级时最明显。有一次需要临时验证一个 playbook 在旧版本下的行为,我直接再建一个 venv 装 2.14,两套环境并存,激活哪个用哪个,完全不影响生产环境正在用的那套。如果当初是 dnf 装的,就得先卸再装,中间还可能把依赖链搞乱。
环境建好之后,把激活命令写进运维账号的.bashrc,或者干脆在/usr/local/bin/下放个软链指向 venv 里的可执行文件,这样不同登录会话都能用。我更喜欢后者,简单直接:
ln -sf /opt/ansible-venv/bin/ansible /usr/local/bin/ansible ln -sf /opt/ansible-venv/bin/ansible-playbook /usr/local/bin/ansible-playbook ln -sf /opt/ansible-venv/bin/ansible-galaxy /usr/local/bin/ansible-galaxy注意:软链方式会让所有用户共用同一个 venv。如果不同团队需要不同版本,还是老老实实用激活脚本,或者给每条命令加绝对路径。
3.3 离线环境:把包搬进去
内网隔离环境没法直接联网装,这时候需要提前在一台同架构、同发行版的联网机器上把包下全。dnf 和 pip 都提供了下载依赖的能力。
# dnf:下载 ansible 及其全部依赖到本地目录 dnf install -y --downloadonly --downloaddir=/tmp/ansible-rpms ansible # 打包带走 tar -czf ansible-rpms.tar.gz -C /tmp ansible-rpms # 目标机解压后本地安装 tar -xzf ansible-rpms.tar.gz -C /tmp dnf install -y --disablerepo='*' /tmp/ansible-rpms/*.rpmpip 那条路类似,用pip download把 wheel 包下全:
pip download ansible-core==2.16.6 -d /tmp/ansible-wheels -i https://<镜像源地址>/simple # 目标机 pip install --no-index --find-links=/tmp/ansible-wheels ansible-core==2.16.6离线安装最容易翻车的地方是"依赖不全"。--downloadonly在某些仓库配置下会漏掉间接依赖,装的时候报缺包。我的经验是把目标机上dnf install的完整输出先跑一遍(用--assumeno让它在确认前停下),把依赖列表抄下来核对,或者直接在一台空白的同版本机器上验证一遍离线安装包,通过了再搬到内网。
4. 配置写对了,后面能省一半的事
Ansible 默认行为在单机试验时没问题,放到几十上百台机器的环境里就各种别扭:输出太长看不清、每次都要手动指定 inventory、连接超时太短、日志没地方存。这些问题都能通过一份ansible.cfg解决。
4.1 先约定目录结构
目录结构不是形式主义,它决定了半年后你还能不能快速找到某个变量是在哪里定义的。我固定用下面这套:
/opt/ansible/ ├── ansible.cfg ├── inventory/ │ ├── hosts.ini │ └── hosts-prod.ini ├── group_vars/ │ ├── all/ │ │ ├── main.yml │ │ └── vault.yml │ ├── webservers.yml │ └── dbservers.yml ├── host_vars/ │ └── node-01.yml ├── roles/ ├── playbooks/ │ ├── site.yml │ └── baseline.yml ├── templates/ ├── files/ ├── logs/ └── requirements.yml关键在于ansible.cfg和 inventory 放在同一级,Ansible 会从当前工作目录开始向上查找配置文件。只要在/opt/ansible下执行命令,永远用的是这套配置,不会误用系统级的/etc/ansible/ansible.cfg。这个细节我踩过坑:有一次在/root下直接跑命令,加载的是系统默认配置,连的是另一套 inventory,把测试环境当生产跑了一遍,好在只是收集信息,没造成实质影响。
4.2 ansible.cfg 里值得动的那几个参数
默认配置大部分不用改,下面这几个是我每套环境都会调整的。
[defaults] inventory = ./inventory/hosts.ini remote_user = deploy host_key_checking = False forks = 30 timeout = 15 gathering = smart fact_caching = jsonfile fact_caching_connection = ./cache fact_caching_timeout = 86400 stdout_callback = yaml callbacks_enabled = timer, profile_tasks log_path = ./logs/ansible.log retry_files_enabled = False interpreter_python = auto_silent deprecation_warnings = False [privilege_escalation] become = True become_method = sudo become_user = root become_ask_pass = False [ssh_connection] pipelining = True ssh_args = -o ControlMaster=auto -o ControlPersist=300s -o PreferredAuthentications=publickey逐个说理由。host_key_checking = False关掉的是首次连接时的指纹确认,在内网、机器频繁重建的环境里必须关,否则一批新机器上线,每个都要人工敲 yes,自动化就断了。代价是失去对中间人攻击的防护,所以我只在可控网段里这么设,跨公网的环境会保留校验并预置 known_hosts。
forks是并发数,默认 5。调到 30 是我在 4 核控制节点上的经验值,再往上 SSH 进程的创建开销开始显现,收益递减。这个值要和控制节点的 CPU 核数、ulimit -n、被控端 sshd 的MaxStartups一起看,单方面调大只会换来连接拒绝。
pipelining = True是性能优化里性价比最高的一项。开启后 Ansible 不再为每个模块单独创建临时文件并通过 SFTP 传输,而是把模块内容通过 SSH 的标准输入直接喂给远端 Python,减少往返次数。实测在中等规模环境里能省掉 30% 到 50% 的执行时间。但它有个前置条件:远端 sudo 不能要求 tty(requiretty)。老版本系统默认开着,需要在 sudoers 里关掉;新版本基本都默认关闭了。
fact_caching那块是给大规模环境准备的。默认每次都重新收集 facts,几百台机器光收集就要跑好几分钟。开了 jsonfile 缓存之后,一天内重复执行可以跳过收集,用gather_facts: no加缓存读取。缓存有效期设 86400 秒,超过就重新收,避免机器配置变了还拿着旧数据做判断。
stdout_callback = yaml纯粹是为了可读性。默认的default回调输出是一行 JSON 挤在一起,改完 yaml 之后每个任务的返回结构清晰展开,排错时能一眼看到是哪个字段不对。配合profile_tasks还能看到每个任务耗时,优化 playbook 的时候特别有用。
| 参数 | 默认值 | 建议值 | 调整理由 |
|---|---|---|---|
| forks | 5 | 20~50 | 提升并发,需配套调 fd 上限 |
| timeout | 10 | 15~30 | 跨网段连接留余量 |
| pipelining | False | True | 减少 SSH 往返,提速明显 |
| host_key_checking | True | False(内网) | 免交互,新机器频繁上线场景必需 |
| gathering | implicit | smart | 配合 facts 缓存 |
| retry_files_enabled | True | False | 避免目录堆积 .retry 文件 |
4.3 inventory 的分组和变量分层
inventory 有两种写法,INI 和 YAML。小规模用 INI 顺手,规模上去之后 YAML 更好维护,尤其是嵌套分组和变量内联。我一般用 INI 做主清单,变量单独放group_vars。
[all:vars] ansible_python_interpreter=/usr/bin/python3 ansible_ssh_common_args='-o StrictHostKeyChecking=no' [control] ctrl-01 ansible_host=10.0.0.10 [webservers] web-[01:08] ansible_host=10.0.1.[11:18] [dbservers] db-01 ansible_host=10.0.2.11 db-02 ansible_host=10.0.2.12 [oe20] db-01 [ocos9] web-01 web-02 [all:children] webservers dbservers这里有两个实用技巧。第一,主机名支持范围展开,web-[01:08]会自动生成八台,配合ansible_host的[11:18]一一对应,省得手敲。第二,我用发行版维度单独建了oe20和ocos9两个分组,因为这两套系统的包名、服务名偶尔有差异,做条件分支时按分组判断比按 facts 判断更快,也更直观。
变量优先级是另一个容易糊涂的地方。从低到高大致是:role defaults → inventory group_vars(all)→ inventory group_vars(具体组)→ inventory host_vars → play vars → task vars → 命令行-e。记住一条就够:范围越小优先级越高,命令行最高。调试时用ansible-inventory --host web-01和ansible -m debug -a var=xxx能看到最终生效的值,比猜快得多。
4.4 SSH 免密:别用密码硬扛
Ansible 支持密码认证,但每次连接都要输密码或者用--ask-pass,自动化就无从谈起。免密密钥是标配。
# 控制节点生成密钥,ed25519 比 rsa 短且快 ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_ed25519_ops # 分发到被控节点 ssh-copy-id -i ~/.ssh/id_ed25519_ops.pub deploy@10.0.1.11 # 验证 ssh -i ~/.ssh/id_ed25519_ops -o BatchMode=yes deploy@10.0.1.11 'echo ok'BatchMode=yes这个参数很关键,它让 SSH 在需要交互时直接失败而不是卡住等输入。批量分发密钥的时候,如果某台机器的权限没配好,加了这个参数会立刻报错退出,不加就是整个脚本挂在那里等超时。
密钥分发完之后,先别急着关密码登录。我一般会留一周的观察期,确认所有自动化流程都走通了,再统一在 sshd 配置里关掉PasswordAuthentication,重启服务前用另一个会话验证一遍能连上,避免把自己锁在外面。
5. 从 ping 通到真正跑起来
安装和配置都到位之后,先用最简单的命令验证链路,再逐步增加复杂度。这个顺序不能颠倒,尤其是几十台机器的环境,一次性上复杂 playbook,出问题根本定位不到是哪一层。
5.1 连通性验证的三板斧
第一条命令永远是 ping 模块。注意这个 ping 不是 ICMP,是 Ansible 自带的一个探活模块,它会连过去、推一小段 Python 代码、拿到返回,能验证 SSH 通、Python 可用、权限够。
ansible all -m ping返回pong说明链路没问题。如果这里就挂了,先别往下走,把报错信息看清楚:UNREACHABLE是网络或 SSH 层,FAILED一般是 Python 或权限层,两条路排查方向完全不同。
第二条是收集 facts,顺便看看两套系统的差异。
ansible all -m setup -a 'filter=ansible_distribution*' ansible all -m setup -a 'filter=ansible_python*'输出会告诉你每台机器的发行版、版本号、Python 路径。这一步在混合环境里特别重要,因为后面的条件判断全靠这些字段。
第三条是确认 inventory 解析正确。
ansible-inventory --graph ansible-inventory --list | head -50--graph用树形展示分组关系,一眼能看出有没有主机挂错组;--list输出完整的变量合并结果,排查变量覆盖问题很有用。
5.2 一个可以直接抄的初始化 playbook
链路通了之后,先做一个不改动任何业务配置的基线 playbook,只做时间同步、基础工具、防火墙策略这几件低风险的事。
--- - name: 基础环境基线初始化 hosts: all become: true gather_facts: true vars: ntp_server: "ntp.internal.example" base_packages: - vim-enhanced - tar - chrony - firewalld - python3-firewall tasks: - name: 校验发行版家族,防止误操作 ansible.builtin.assert: that: - ansible_facts['os_family'] == "RedHat" fail_msg: "当前主机不是 RPM 体系,终止执行" success_msg: "系统家族校验通过" - name: 安装基础工具包 ansible.builtin.dnf: name: "{{ base_packages }}" state: present - name: 渲染 chrony 配置 ansible.builtin.template: src: templates/chrony.conf.j2 dest: /etc/chrony.conf owner: root group: root mode: '0644' backup: true notify: 重启 chronyd - name: 启动并设置开机自启 ansible.builtin.service: name: "{{ item }}" state: started enabled: true loop: - chronyd - firewalld - name: 放行 SSH 服务 ansible.posix.firewalld: service: ssh permanent: true state: enabled immediate: true handlers: - name: 重启 chronyd ansible.builtin.service: name: chronyd state: restarted第一段assert是我后来加的习惯。有一次把 playbook 误指向了一台装有容器运行时的机器,包管理器不是 dnf,结果任务跑到一半失败,留下半截状态。加了这个断言之后,不匹配的机器直接被跳过,一行报错说清楚原因,比执行到中间崩掉强得多。
template任务里的backup: true也值得说。它会在目标文件旁边生成一个带时间戳的备份,改错了能立刻还原。对于/etc/chrony.conf这种改错了会导致时间同步失效、进而影响一堆依赖时间的组件的文件,多留一份备份的成本可以忽略。代价是目录里会堆备份文件,定期清理就行。
notify配合handlers是 Ansible 的经典设计:只有template任务真的产生了变更(文件内容有差异)才会触发 handler,配置没变就不重启服务。这个机制避免了每次跑 playbook 都无脑重启一遍服务,在几百台机器的规模下差别巨大。
ansible.posix.firewalld这个模块不在核心包里,需要提前装 collection:
ansible-galaxy collection install ansible.posix如果环境不允许装 collection,退一步用ansible.builtin.command调firewall-cmd,但要做幂等判断,比如先查--query-service再决定是否放行,写法会啰嗦不少。
5.3 幂等性:判断 playbook 写得好不好的硬指标
同一个 playbook 连跑两遍,第二遍的changed应该是 0。做不到这一点的任务,迟早会在某次批量执行中给你制造意外。最常见的破坏幂等的写法就是滥用 shell。
# 反面例子:每次都执行,每次都报 changed - name: 解压安装包 ansible.builtin.shell: tar -xzf /tmp/app.tar.gz -C /opt正确写法是用专门的模块,或者加上creates参数让它在目标已存在时跳过。
# 写法一:用专门模块 - name: 解压安装包 ansible.builtin.unarchive: src: /tmp/app.tar.gz dest: /opt remote_src: true # 写法二:必须用 shell 时加 creates - name: 初始化数据库 ansible.builtin.shell: /opt/app/bin/init-db.sh args: creates: /opt/app/data/.initializedcreates指定一个文件路径,存在就跳过执行,这是给一次性操作打标记的通用手法。removes是反过来的逻辑。这两个参数在必须调外部脚本时特别有用。
还有一个容易被忽略的点:文件权限和属主。copy、template、file这些模块如果只写了path和state,没写mode,那它就不会去改权限,但也不会报错,你会以为改好了其实没动。养成习惯,凡是涉及文件的模块,mode一定显式写出来,用字符串形式'0644'而不是数字644——数字形式在某些版本里会被当成八进制之外的解析,导致权限变成一堆乱七八糟的值。
5.4 密码和密钥交给 vault
playbook 里迟早要出现密码、密钥、token。明文写在group_vars里再提交到版本库,是安全审计里最常见的扣分项。Ansible 自带的 vault 能解决这个问题,加密后的文件可以放心提交。
# 新建一个加密文件 ansible-vault create group_vars/all/vault.yml # 编辑已有文件 ansible-vault edit group_vars/all/vault.yml # 加密单个字符串,直接贴进 yml ansible-vault encrypt_string '实际密码' --name 'db_password' # 运行时提供密码 ansible-playbook site.yml --ask-vault-pass密码本身怎么管?--ask-vault-pass每次手输,适合低频操作。高频执行用密码文件,权限设成 0400,路径通过vault_password_file写进ansible.cfg,或者用环境变量ANSIBLE_VAULT_PASSWORD_FILE指过去。密码文件本身绝对不能进版本库,.gitignore里第一行就该是它。
6. 报错现场:这些坑我基本都趟过
Ansible 的报错信息有时代码很长,但真正有用的就一两行。下面按类别整理了高频问题,附上我的定位思路。
6.1 连不上:SSH 层的五种典型状况
UNREACHABLE! => {"msg": "Failed to connect to the host via ssh: Permission denied (publickey)."}公钥没分发到、或者属主权限不对。检查目标机/home/deploy/.ssh/是不是 700、authorized_keys是不是 600、属主是不是 deploy。SSH 对权限极其敏感,权限过宽会直接忽略密钥文件,而且不给你任何提示。
UNREACHABLE! => {"msg": "Host key verification failed."}host_key_checking没关,或者换了机器但 known_hosts 里还留着旧指纹。清理对应条目或者直接关掉校验。机器频繁重建的环境,建议把ansible_ssh_common_args里的StrictHostKeyChecking也一并设成 no,双保险。
UNREACHABLE! => {"msg": "ssh: connect to host 10.0.1.11 port 22: Connection timed out"}网络不通或者 sshd 没起来。先用nc -zv 10.0.1.11 22确认端口,再登过去看服务状态。别一上来就怀疑 Ansible 配置,九成是网络或服务的问题。
ssh_exchange_identification: Connection closed by remote host并发太高把被控端的 sshd 顶了。检查/etc/ssh/sshd_config里的MaxStartups,默认是10:30:100,意思是超过 10 个未认证连接开始随机拒绝,到 30 个拒绝一半,100 个全拒。控制节点forks开到 50 的时候这个默认值肯定不够,得调到30:60:200之类。同时看下MaxSessions,单连接多路复用时也会受限。
Too many open files控制节点的文件描述符上限不够。ulimit -n确认,改/etc/security/limits.d/下的配置,注意要重新登录才生效,ulimit命令临时改只对当前 shell 有效。
6.2 Python 解释器引发的连锁反应
FAILED! => {"msg": "The module failed to execute correctly, you probably need to set the interpreter."}最典型的解释器问题。原因是被控机上python3不在默认 PATH,或者干脆只有python。ansible.cfg里设interpreter_python = auto_silent能让 Ansible 自己去探测,探测不到就静默失败。稳妥做法是在 inventory 的[all:vars]里显式指定:
[all:vars] ansible_python_interpreter=/usr/bin/python3如果某些机器的路径不一样,在host_vars里单独覆盖。我曾经遇到一台机器上/usr/bin/python3是个指向不存在目标的软链,实际解释器在/usr/local/bin/python3.9,这种就只能单机覆盖。
SyntaxError: invalid syntax这条出现在模块执行阶段,通常是 Python 版本太低,跑不了新版本 Ansible 生成的模块代码。ansible-core 2.15 之后对目标节点的 Python 要求提高了一截,老系统上要么降 Ansible 版本,要么给目标机补一个新解释器并用interpreter_python指过去。
6.3 模块依赖缺失:一张表查完
很多模块不是纯 Python 实现的,它依赖系统里对应的 Python 绑定包。控制节点上装好了没用,被控节点也得有。
| 报错关键词 | 缺失依赖 | 处理方式 |
|---|---|---|
No module named 'firewall' | python3-firewall | dnf install -y python3-firewall |
No module named 'dnf' | python3-dnf | dnf install -y python3-dnf |
No module named 'selinux' | python3-libselinux | dnf install -y python3-libselinux |
Failed to import the required Python library (psutil) | python3-psutil | dnf install -y python3-psutil |
cryptography is required | 对应 wheel 包 | pip install cryptography到目标解释器 |
这张表我贴在运维文档里,新人遇到直接查。更省事的做法是把这些包写进基线 playbook,用dnf模块统一装上,新机器上线自动具备。
提示:装依赖包本身也得用 dnf 模块,而 dnf 模块依赖 python3-dnf。如果目标机连这个都没有,就只能先用
raw模块装依赖,因为raw是唯一不需要 Python 的模块。这个先后顺序在最精简的系统镜像上很关键。
6.4 并发调大之后的性能反噬
forks从 5 调到 50 之后,很多人会遇到"执行更快了但也更容易失败"的情况。这不是错觉,背后有几个具体原因。
一是 SSH 连接建立的瞬时压力。50 个并发意味着控制节点同时发起 50 次 TCP 握手和密钥交换,CPU 会有一个明显的尖峰。控制节点配置不够的话,尖峰期间其他操作都会变慢。二是被控端 sshd 的连接数限制,前面提过的MaxStartups。三是网络设备,某些交换机或安全组对新建连接速率有限制,超了就丢包。
我的调优顺序是:先开pipelining和ControlPersist,这两个是纯收益,没有副作用;再逐步调forks,每次加 10,观察执行时间和失败率,找到拐点;最后才考虑上多控制节点分片。绝大多数中小规模环境,pipelining加forks=30就已经足够了,没必要追极限。
连接复用那里ControlPersist=300s的意思是 SSH 保持连接 5 分钟,期间同一台机器的多个任务复用这条连接,省掉重复握手。跑长 playbook 时效果非常明显,实测能把执行时间砍掉近一半。
7. 跑顺之后,让它长期能用
一次性把机器配好只是开始,真正的考验是半年后你还能不能快速改一个配置、加一批机器、回滚一次变更。
7.1 用 role 把重复的东西收起来
所有任务都堆在一个 playbook 里,写到第三个环境你就想重写。role 是官方的拆分方案,每个 role 管一件事,变量、模板、任务都在自己的目录里。
# 生成骨架 ansible-galaxy role init roles/common生成的结构里,defaults/main.yml是默认变量(优先级最低,方便覆盖),vars/main.yml是高优先级变量(一般放不希望被覆盖的常量),tasks/main.yml是任务入口,handlers、templates、files各归各位。调用的时候在site.yml里引用:
--- - name: 全站基线 hosts: all roles: - common - name: Web 层 hosts: webservers roles: - common - nginx写 role 的经验是:一个 role 只做一件能被一句话描述清楚的事。common管基础包和时区,chrony管时间同步,hardening管安全基线,别把三样混在一起。混在一起之后,你会遇到"我只想跑时间同步但不想动安全配置"的场景,然后就得靠tags补救,越补越乱。
依赖用requirements.yml声明,装的时候一条命令:
collections: - name: ansible.posix version: ">=1.5.0" - name: community.general version: ">=7.0.0" roles: - name: common src: <内部代码仓库地址> version: v1.2.0ansible-galaxy install -r requirements.yml版本号一定要写死或者写最小范围,别留空。留空意味着每次安装都拉最新,某天上游发了个不兼容的版本,你的 playbook 就会在没有任何改动的情况下突然挂掉。这种问题排查起来极其难受,因为"我什么都没改"。
7.2 执行记录、审计与回滚准备
log_path只是最基础的一层。生产环境的变更,我还会额外做三件事。
第一件是保留完整的执行日志,带时间戳和操作人。
mkdir -p logs ansible-playbook -i inventory/hosts.ini playbooks/site.yml \ -f 30 \ 2>&1 | tee "logs/$(date +%Y%m%d-%H%M%S)-baseline.log"第二件是用--check --diff先看一遍影响面。--check是空跑,只报告会做什么变更不真的执行,--diff会把文件内容的前后差异打出来。这两个参数组合起来,等于给变更做了一次预演。
ansible-playbook -i inventory/hosts.ini playbooks/site.yml --check --diff --limit webservers注意--check不是万能的,有些模块不支持检查模式,会直接跳过或者报不支持,比如某些command任务。第一次用的 playbook,我建议先在单台测试机上真跑一遍,再上--check看全局。
第三件是回滚预案。涉及配置文件的变更,我会要求对应的模板任务开backup: true,并在 playbook 里加一个rollback变量,出问题的时候用-e rollback=true切换到恢复分支。更重的变更(比如升级系统包)会提前打快照或者保留旧版本包,dnf history能查到每次事务的 ID,用dnf history undo <id>回退,这条命令救过我一次。
7.3 变更节奏:先灰度再全量
有了工具之后最大的诱惑就是"一把梭"。几百台机器一条命令全推,确实爽,但一旦有问题就是全站故障。我的固定节奏是:单台验证 → 小批量灰度 → 全量。
# 第一步:单台 ansible-playbook playbooks/site.yml --limit web-01 # 第二步:一个分组里挑三台 ansible-playbook playbooks/site.yml --limit 'web-[01:03]' # 第三步:观察指标,确认无异常 # 第四步:全量 ansible-playbook playbooks/site.yml -f 30--limit支持模式匹配,web-*、webservers:!web-01(排除某台)、oe20:&dbservers(两个分组的交集),这些写法在灰度时特别方便。&是交集,!是排除,:是并集,记住这三个符号够用了。
还有一个我后来加上的习惯:给风险较高的任务打tags,全量执行时跳过,需要的时候单独跑。
- name: 重启业务服务 ansible.builtin.service: name: app state: restarted tags: [never, restart-app]never是个特殊标签,意思是"默认永不执行",必须显式--tags restart-app才会跑。这个配合灰度流程非常好用,日常基线执行不会误重启业务。
ansible-playbook playbooks/site.yml --tags restart-app --limit web-01.retry文件在这里也有用武之地。全量执行时有几台失败,Ansible 会生成site.retry记录失败的主机,修好问题之后用--limit @site.retry只重跑这几台,不用全量再来一遍。不过我在配置里关掉了自动生成,需要的时候手动加--retry-files或者就地--limit @xxx.retry,避免目录里堆一堆过期的 retry 文件。
8. 关于混合环境的一点个人习惯
两套发行版混着跑,最容易出问题的地方不是安装,而是"我以为它们一样"。包名差一个字、服务单元名不一样、默认路径挪了位置,这些差异在小规模测试时根本暴露不出来,等到几百台一起跑才会集中爆发。我现在的做法是把差异显式写进变量层,而不是散落在任务里做when判断:按发行版分组建group_vars,把包名列表、服务名、配置文件路径这些易变项全部提到变量里,任务本身保持干净。这样新增一个发行版线路,只需要加一个变量文件,playbook 一行都不用改。另一件事是每次动 playbook 之前先在单台上--check --diff跑一遍,养成这个习惯之后,很多低级错误在到达生产环境之前就被拦住了。踩过的坑多了就会发现,配置管理工具的价值不在于"快",而在于"改之前你能知道会发生什么"。