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.722.03 LTS 系列普遍在 3.9 及以上OpenCloudOS 8 系列默认 Python 3.69 系列则上到 3.9 甚至更高。而 ansible-core 从 2.14 开始把控制节点的门槛提到了 Python 3.92.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 LTS3.72.9 ~ 2.12系统仓库安装欧拉 22.03 LTS3.9 ~ 3.102.14 ~ 2.16虚拟环境 pipOpenCloudOS 83.62.9 ~ 2.11系统仓库安装OpenCloudOS 93.9 ~ 3.112.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-core2.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-core2.16.6 -d /tmp/ansible-wheels -i https://镜像源地址/simple # 目标机 pip install --no-index --find-links/tmp/ansible-wheels ansible-core2.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 ControlMasterauto -o ControlPersist300s -o PreferredAuthenticationspublickey逐个说理由。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 不能要求 ttyrequiretty。老版本系统默认开着需要在 sudoers 里关掉新版本基本都默认关闭了。fact_caching那块是给大规模环境准备的。默认每次都重新收集 facts几百台机器光收集就要跑好几分钟。开了 jsonfile 缓存之后一天内重复执行可以跳过收集用gather_facts: no加缓存读取。缓存有效期设 86400 秒超过就重新收避免机器配置变了还拿着旧数据做判断。stdout_callback yaml纯粹是为了可读性。默认的default回调输出是一行 JSON 挤在一起改完 yaml 之后每个任务的返回结构清晰展开排错时能一眼看到是哪个字段不对。配合profile_tasks还能看到每个任务耗时优化 playbook 的时候特别有用。参数默认值建议值调整理由forks520~50提升并发需配套调 fd 上限timeout1015~30跨网段连接留余量pipeliningFalseTrue减少 SSH 往返提速明显host_key_checkingTrueFalse内网免交互新机器频繁上线场景必需gatheringimplicitsmart配合 facts 缓存retry_files_enabledTrueFalse避免目录堆积 .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 StrictHostKeyCheckingno [control] ctrl-01 ansible_host10.0.0.10 [webservers] web-[01:08] ansible_host10.0.1.[11:18] [dbservers] db-01 ansible_host10.0.2.11 db-02 ansible_host10.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_varsall→ inventory group_vars具体组→ inventory host_vars → play vars → task vars → 命令行-e。记住一条就够范围越小优先级越高命令行最高。调试时用ansible-inventory --host web-01和ansible -m debug -a varxxx能看到最终生效的值比猜快得多。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 deploy10.0.1.11 # 验证 ssh -i ~/.ssh/id_ed25519_ops -o BatchModeyes deploy10.0.1.11 echo okBatchModeyes这个参数很关键它让 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 filteransible_distribution* ansible all -m setup -a filteransible_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这个模块不在核心包里需要提前装 collectionansible-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 密码和密钥交给 vaultplaybook 里迟早要出现密码、密钥、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 firewallpython3-firewalldnf install -y python3-firewallNo module named dnfpython3-dnfdnf install -y python3-dnfNo module named selinuxpython3-libselinuxdnf install -y python3-libselinuxFailed to import the required Python library (psutil)python3-psutildnf install -y python3-psutilcryptography 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加forks30就已经足够了没必要追极限。连接复用那里ControlPersist300s的意思是 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 \ 21 | 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 rollbacktrue切换到恢复分支。更重的变更比如升级系统包会提前打快照或者保留旧版本包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跑一遍养成这个习惯之后很多低级错误在到达生产环境之前就被拦住了。踩过的坑多了就会发现配置管理工具的价值不在于快而在于改之前你能知道会发生什么。