Ansible自动化运维实战:批量部署与Playbook模块详解 半夜两点被电话叫醒三台新上线的服务器等着部署环境你一台一台登录、敲命令、等结果再手工修改配置、重启服务——我相信做过运维的人都经历过这种时刻。我之前也一直这么干直到一次批量扩容被某个导师狠狠吐槽手工操作记录都写不清才下决心把Ansible批量部署这套东西老老实实补上了。这篇博文就来实打实地拆解Ansible的安装过程以及日常批量部署中最常用的那些模块怎么用、为什么这么用顺着一条实际可复现的路径把整套流程讲透适合刚开始接触自动化运维、或者虽然用过Ansible但还没系统梳理过模块体系的读者参考。1. 批量部署这件事的本质把人肉操作翻译成声明式描述1.1 手工运维的痛点和Ansible的解题思路手工登录服务器逐台执行命令最大的问题不是慢而是不可控。三台机器可能因为系统版本差异、环境变量不同、上一步执行失败导致状态不一致等到排查的时候根本不知道哪台机器处于什么状态。Ansible的解题思路和传统脚本有本质区别它不关心你怎么一步步操作而是让你直接描述最终这台机器应该是什么样子——装好哪些包、配置文件内容是什么、服务处于什么状态。剩下的操作细节由Ansible自己推导。这个思想和Docker的镜像即部署思路有相似之处但Ansible不要求你重新封装应用运行环境它面向的是现有基础设施的管理。你有一批已经存在的物理机或虚拟机系统装好了、网络通了Ansible就在这个基础上做持续声明期望状态这件事。每执行一次它都会检查当前状态和期望状态的差异只有存在差异才去修正这就是Ansible模块默认具备的幂等性。手动运维很难做到这一点因为你不知道上一秒的改动是否生效也不容易判断哪些命令需要重跑。1.2 为什么不用自研脚本或其它自动化工具可能有人会问我写个shell脚本配合SSH循环不是也能批量部署吗短期看确实可以但脚本一旦变复杂问题就来了脚本只在执行成功和执行失败之间二选一无法表达该装的时候装、不该装的时候跳过这类状态语义脚本的变量作用域、错误处理、并行控制全靠自己维护每加一台机器类型就得重写分支。Ansible把这一整套通用能力封装好了主机清单负责定义管理哪些机器模块负责定义执行什么操作Playbook负责定义按什么顺序做参数和变量负责处理不同机器的差异。你只需要关注业务逻辑本身。另外像配置管理类的其他工具我也简单对比过。有些方案需要在被管节点上安装常驻代理进程这让网络拓扑复杂化也带来了额外的安全审计面一些工具使用自定义语言写配置学习成本不低。Ansible走的是纯SSH加Python这条路控制端装一个客户端即可被管节点只要支持SSH和Python2.7以上或Python3就能管理这个准入门槛在大部分场景下都是最低的。2. 安装Ansible控制端的环境准备和三条实战路线2.1 系统包管理器安装安装Ansible时控制端首选Linux系统。Windows上虽然能通过WSL使用但原生体验一般生产环境基本不考虑。以常见的Rocky、AlmaLinux、CentOS系为例最省事的安装方式是用系统包管理器。先启用EPEL软件源然后直接安装sudo yum install -y epel-release sudo yum install -y ansibleUbuntu、Debian系的命令则是sudo apt update sudo apt install -y ansible系统包管理器安装的版本通常相对保守好处是依赖关系由系统自己处理升级时跟随源更新即可适合生产环境求稳的情形。需要注意的是有些最小化系统默认没有安装Python3Ansible执行模块时需要Python解释器必要的时候先补上sudo yum install -y python32.2 pip方式安装与版本管理如果你需要紧跟Ansible新功能或者要在同一台机器上管理多个Ansible版本建议用pip安装Python发行版python3 -m venv /opt/ansible-venv source /opt/ansible-venv/bin/activate pip install ansible我习惯用虚拟环境而不是直接往系统Python里塞依赖。原因是Ansible的依赖库版本和系统其它工具可能冲突比如PyYAML、Jinja2都有版本要求放在虚拟环境里可以隔离得一干二净。需要切换版本时直接建一个新的虚拟环境即可互不干扰。安装完成后验证一下ansible --version这个命令会输出Ansible版本号、Python版本号、配置文件搜索路径等关键信息。初次使用建议重点关注配置文件路径那一项因为它决定你后面写的配置放在哪里才能被读取。2.3 ansible.cfg基础配置Ansible的配置文件搜索顺序依次是当前目录的ansible.cfg、当前用户目录下的~/.ansible.cfg、系统级/etc/ansible/ansible.cfg。默认情况下只要你在哪个目录下执行ansible命令就会优先使用那个目录下的配置。这给了项目级配置很大的灵活性——不同项目可以用不同的inventory、不同的用户、不同的SSH参数。下面是一个适合起步的配置文件模板[defaults] inventory ./hosts remote_user root host_key_checking False forks 10 pipelining True逐行解释一下inventory指定主机清单文件的路径写法可以是相对路径或绝对路径。remote_user默认登录用户省得每次在命令行里重复指定。host_key_checking第一次SSH连接时的指纹确认交互式界面会卡住批量任务测试环境建议设为False。forks并行执行的最大主机数默认5日常管理几十台机器时调到10左右通常够用。pipelining开启SSH管道化减少每次执行模块时的SSH连接开销对批量任务效率提升非常明显。2.4 安装完成后的最小验证配置文件和主机清单都准备好之后先跑一个最简单的测试ansible all -m ping注意这里ping模块并不是ICMP的ping而是检查目标主机是否满足Ansible执行条件能否SSH登录、Python解释器是否可用、能否拿到系统信息。返回pong即代表链路通了。这一步通过后面所有模块的操作才有意义。3. 批量部署的前置条件免密登录与Inventory主机清单3.1 SSH免密连接批量操作的地基Ansible的远程执行基于SSH如果每一台机器都需要交互式输入密码批量部署就没有意义。所以第一步就是把控制端到被管理节点的SSH免密登录配好。生成密钥对并分发公钥的操作如下ssh-keygen -t ed25519 -C ansible-manager -f ~/.ssh/ansible_ed25519 ssh-copy-id -i ~/.ssh/ansible_ed25519.pub root192.168.1.101在生成密钥时我建议使用ed25519算法相比RSA 2048/4096密钥更短、安全性更好SSH客户端支持也足够广泛。如果遇到老旧的设备不支持ed25519再退回RSA。如果你管理的机器较多逐台执行ssh-copy-id还是麻烦可以做一个简单循环批量分发公钥while read host; do ssh-copy-id -i ~/.ssh/ansible_ed25519.pub $host; done server_list.txtserver_list.txt每行放一个主机地址。这里仍然需要逐台输入一次密码但至少省去了重复创建密钥的时间。如果你的环境不允许root直接SSH登录那就单独创建一个运维账号赋予sudo权限然后在ansible.cfg中配置remote_user opsuser become True become_method sudo become_user rootAnsible通过become机制实现权限提升相当于在远程执行sudo命令。这个做法更符合最小权限原则比我直接用root管理要安全得多。3.2 Inventory主机清单机器分组的核心Inventory是Ansible管理主机的通讯录。默认路径是/etc/ansible/hosts但更推荐在项目目录下自建hosts文件用绝对路径或相对路径指向它。基础格式如下[web] 192.168.1.101 192.168.1.102 ansible_port2222 [db] 192.168.1.201 ansible_ssh_usermysqladmin [infra:children] web db方括号里的是组名下面每行一个主机。主机行可以附加连接参数比如自定义SSH端口、登录用户、别名等。组可以嵌套用:children标识的子组会把子组内所有主机聚合到父组中。实际使用中我还常借助分组做差异化管理。比如web组要装Nginx、db组要装MySQLplaybook里通过hosts字段指定组名即可配置、变量都可以按组隔离这一点是脚本循环很难做到的。3.3 用ad-hoc命令先探路在写Playbook之前先用ad-hoc命令验证连通性和参数配置是效率最高的做法。比如查看所有web组主机的系统发行版ansible web -m command -a cat /etc/os-releasecommand模块是执行命令最简单的模块但后面会讲到它有局限性。或者用setup模块快速抓取主机信息ansible web -m setupsetup模块返回的是极其详细的主机事实信息包括CPU架构、内存大小、网络接口、挂载点、系统版本等这是后续Playbook中判断系统变量的数据来源。初次使用建议只对单台机器执行输出很长别一次性对几十台跑会刷屏。4. 常用模块逐个拆解每个模块解决哪一类问题4.1 执行类模块command、shell与raw批量执行命令是最基础的需求Ansible里有三个模块干这件事新手经常混淆。command是默认模块也就是你在ansible命令里不指定-m时默认使用的模块。它执行命令时不经过shell所以不支持管道、重定向、通配符等shell特性。好处是行为可预期不会因为目标机器的shell类型不同产生歧义。缺点是遇到|、、这些符号时会直接报错或得不到预期结果。shell模块则通过/bin/sh执行命令支持管道、重定向、通配符适合执行相对复杂的命令组合ansible web -m shell -a df -h | grep /dataraw模块不走Ansible的模块机制直接把原始命令通过SSH丢过去执行适用于目标机器没有Python等特殊场景。多数情况用不上但当你管理一台只有busybox的设备时它就是救命稻草。实操建议是能用command解决的事情一律不要用shell因为shell命令的解析结果依赖目标环境幂等性更差比如echo hello /tmp/test.txt每次执行都会追加一行重复执行结果不同排查时容易被误导。4.2 文件类模块copy、template、lineinfile批量部署中最常见的需求是分发配置文件和模板化渲染配置。copy模块用于将控制端的文件复制到目标主机- name: 分发Nginx主配置 ansible.builtin.copy: src: ./files/nginx.conf dest: /etc/nginx/nginx.conf owner: root group: root mode: 0644注意src如果是目录copy会复制目录本身如果只想复制目录内容需要加directory_mode或在src末尾加/。这个细节经常让新手踩坑——copy后目标结构比预期多套一层目录。template模块和copy类似区别在于它使用Jinja2模板引擎渲染变量。比如模板文件里写server_name {{ domain_name }};执行时Ansible会把变量替换成实际值。当不同环境的主机需要不同配置时template就是标准答案- name: 渲染应用配置文件 ansible.builtin.template: src: ./templates/app.conf.j2 dest: /opt/app/app.conf mode: 0640lineinfile模块用于在文件中精确修改某一行。比如存在多个相同键值的配置文件不想整文件覆盖时用它最合适- name: 修改SSH端口 ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: ^#Port line: Port 22022lineinfile的regexp参数用来定位要修改的行line参数是修改后的完整内容。这个模块天然具备幂等性如果目标行内容已经匹配重复执行不会重复添加。4.3 软件包管理模块yum、apt与package装软件包是批量部署的重要环节。Ansible为不同系统提供了对应模块yum模块对应RPM系系统apt模块对应Debian系系统而package模块是一个抽象层会自动根据目标系统识别应该调用yum还是apt。yum模块的基本用法- name: 安装Nginx ansible.builtin.yum: name: nginx state: presentstate参数有几个关键取值present保证安装但不会升级到最新版。latest总是升级到软件源中的最新版本。absent卸载该软件包。用latest要慎重。生产环境批量更新某个软件包可能导致不可控的行为尤其是内核、数据库这类核心软件。我更倾向于在测试环境验证新版本之后再显式指定版本号。apt模块类似稍微不同的地方是可以指定update_cache参数相当于先执行apt update再安装。Ubuntu系统上首次使用apt模块时需要加上这个参数否则会提示找不到软件包。4.4 服务管理模块service与systemd安装完软件包就要启动服务并设置开机自启。service模块是传统的服务管理模块对SysVinit和systemd都能兼容- name: 启动Nginx并设为开机自启 ansible.builtin.service: name: nginx state: started enabled: true当目标系统确定使用systemd时用systemd模块操作更精细可以控制服务的restart、daemon-reload等动作- name: 重新加载systemd并重启服务 ansible.builtin.systemd: name: nginx state: restarted daemon_reload: true这两种模块支持的服务名称以系统的unit名为准不是进程名。写Nginx的unit名是nginx写MySQL有时是mysqld有时是mysql这个因发行版而异。建议在执行第一个服务管理任务之前先用shell模块查一下systemctl list-unit-files | grep -i 服务名确认。4.5 系统配置模块user、cron与firewalld批量创建用户在企业环境非常常见- name: 创建部署用户 ansible.builtin.user: name: deployer groups: wheel shell: /bin/bash create_home: true append: trueappend关键字很关键默认情况下user模块设置groups参数会覆盖用户原有属组加上appendtrue才是追加到附加组列表。部署新环境时覆盖没问题但已有用户的机器上操作时特别容易把用户从sudo组里踢出去这个坑我实际踩过。cron模块用于管理系统计划任务- name: 添加日志清理任务 ansible.builtin.cron: name: cleanup logs minute: 30 hour: 2 job: /usr/local/bin/cleanup.sh重复执行cron模块不会叠加计划任务它会根据name字段找到已有条目内容变化时更新存在时保持不变。管理远程计划任务时这个模块比手工echo到crontab要安全得多。firewalld模块用于管理防火墙规则- name: 放行80端口 community.general.firewalld: port: 80/tcp permanent: true state: enabled immediate: truepermanent表示写入持久化规则immediate表示立即生效。生产环境的防火墙规则变更建议先在测试机验证避免放行范围写错导致服务不可达。4.6 数据拉取与同步fetch与synchronize批量部署不只是把文件推过去有时候也需要把远程机器的文件拉回来分析。fetch模块负责从目标主机拉取单个文件到控制端本地- name: 收集所有主机的日志文件 ansible.builtin.fetch: src: /var/log/nginx/access.log dest: ./collected_logs/{{ inventory_hostname }}/它会按主机名创建目录保存拉取的文件避免多台机器文件重名互相覆盖。跨主机做文件目录级别的同步可以用synchronize模块它封装了rsync支持增量同步。比如把一台机器的某个目录分发到其它机器- name: 同步数据目录到web组 ansible.posix.synchronize: src: /data/shared/ dest: /data/shared/ delete: true需要注意synchronize模块的src是控制端路径dest是目标主机路径。加上deletetrue时目标目录中多出的文件会被删除保证完全镜像。这个参数对数据安全影响极大用之前必须确认目录底细。5. 实战一个完整的批量部署Playbook案例5.1 从需求到任务分解模块是积木Playbook是把积木按顺序拼起来的地方。下面用一个批量部署Nginx静态站点的例子串一下全流程。手头的需求是在web组五台机器上安装Nginx、写入一个基于变量的站点配置、放行80端口、启动服务、最后从其中一台机器拉取日志做分析。任务分解完基本是这样的清单安装Nginx软件包创建站点目录分发站点配置模板分发首页HTML文件放行80端口启动Nginx并设为开机自启拉取一份访问日志用于检查5.2 Playbook文件结构与写法在项目目录里创建deploy_nginx.yml- name: 批量部署Nginx静态站点 hosts: web become: true vars: domain_name: www.example-site.local site_root: /var/www/mysite tasks: - name: 安装Nginx ansible.builtin.yum: name: nginx state: present - name: 创建站点根目录 ansible.builtin.file: path: {{ site_root }} state: directory owner: root group: root mode: 0755 - name: 渲染Nginx站点配置 ansible.builtin.template: src: ./templates/default.conf.j2 dest: /etc/nginx/conf.d/default.conf mode: 0644 notify: reload nginx - name: 分发首页文件 ansible.builtin.copy: src: ./files/index.html dest: {{ site_root }}/index.html mode: 0644 - name: 放行80端口 community.general.firewalld: port: 80/tcp permanent: true state: enabled immediate: true - name: 启动Nginx并设置开机自启 ansible.builtin.service: name: nginx state: started enabled: true handlers: - name: reload nginx ansible.builtin.service: name: nginx state: reloadedtemplates/default.conf.j2内容示例server { listen 80; server_name {{ domain_name }}; root {{ site_root }}; index index.html; }5.3 执行Playbook的完整过程在项目目录下执行ansible-playbook deploy_nginx.yml输出结果会逐个显示每个任务在每台机器上的执行状态对应三种颜色含义绿色代表该任务在该机器上没有变更黄色代表改变了系统状态红色代表报错。不要以为全绿就万事大吉——全绿只能说明系统和期望状态一致不代表业务逻辑一定正确。真正要判断的是可能完全不可用的配置被执行后到底是什么效果那就需要用检查模式先看一遍。5.4 用--check和--diff验证变更影响Playbook写好后先别急着正式执行我强烈建议执行检查模式ansible-playbook deploy_nginx.yml --check --diff--check模式会模拟执行不实际修改远程机器只报告如果执行会有什么变化。--diff会展示文件类任务的具体内容差异。对template、copy这类文件任务尤其有用你能提前看到控制端文件和目标端文件会有哪些不同。这个组合是上线前最有价值的防线。需要说明的是并非所有模块都完整支持check模式比如command、shell、raw这类纯执行模块在check模式下通常直接跳过。但文件、包、服务类模块大多支持得不错可以放心使用。5.5 验证部署结果执行完成后在控制端验证一下所有web主机的Nginx进程状态ansible web -m command -a systemctl is-active nginx如果全部返回active再进行一次HTTP探测ansible web -m shell -a curl -s -o /dev/null -w %{http_code} http://127.0.0.1/输出200表示部署成功。最后拉取日志ansible web -m fetch -a src/var/log/nginx/access.log dest./logs/ flatyesfetch模块配合flatyes时dest直接作为存储路径如果不加flat则会在dest下按主机名建子目录。在日志收集这种场景里按主机名归档是更合理的组织方式丢失主机信息反而不好排查。6. 踩坑记录与完整排查链路6.1 现象批量任务卡在SSH指纹确认第一次跑ansible命令时很常见任务卡住不动控制端没有输出任何错误过很久才报Host key verification failed。原因就是host_key_checking默认开启第一次连接陌生主机时SSH会交互式询问是否信任指纹而Ansible的批量任务是非交互式的直接卡住。排查链路是先手动SSH到目标机器确认能通、能看到指纹确认再看控制端ansible.cfg有没有被读取到最后确认配置项是否生效可以用命令查看ansible localhost -m debug -a msg{{ lookup(config, host_key_checking) }}如果输出显示False但实际仍然卡住看看是不是当前目录的ansible.cfg没有被加载——可以用ansible --version确定最终生效的配置文件路径。这类问题多半是配置文件的放置位置不对。6.2 现象command模块执行管道符命令报错我刚开始用Ansible时写过这样的命令ansible web -m command -a ps aux | grep nginx结果每台机器都报错错误信息指向特殊字符无法解析。原因前面已经说了command不经过shell管道符被当作普通字符传给程序当然不可能工作。排查链路看错误类型就能定位如果报unknown option或No such file or directory大概率是特殊字符问题。改用法是换成shell模块或者改用其它模块。这里要说一下用shell模块虽然简单粗暴但如果有更具体的模块能描述意图建议优先用模块。比如查看服务状态用service模块的state信息就比在shell里grep进程列表更可靠。6.3 现象重复执行Playbook后配置文件被反复改动某次部署完以后连续两次执行同一个Playbook部分机器总是显示一份配置有变更。排查后发现模板里有一个动态时间戳变量# Updated at {{ ansible_date_time.iso8601 }}这个变量每次执行都会变化导致模板渲染结果和上次不同。Ansible发现文件内容变化就会重新写入于是每次执行都出现变更。这在语义上是正确的因为文件确实每次都不同但从配置管理角度这是一个反模式——配置文件应该是可重复生成、内容稳定的。解法是去掉时间戳或者把这个信息写进单独的文件不混入配置文件。另外用ansible提供的--check模式可以发现每次执行都会变的任务因为check模式下看到的是预期的变化量。如果分析一个任务老是显示changed说明它没有做到真正的幂等。6.4 现象批量几百台机器时速度越来越慢一台一台顺序执行确实慢。Ansible默认的并行进程数是5如果管理100台机器会明显感知到耗时。排查后通过两个参数优化forks可以调大受控制端CPU、网络连接数限制一般50以下没问题pipelining开启后每次模块执行省去了SFTP上传临时文件的过程速度改善很可观。另外还要检查SSH连接是否需要每次都做DNS反向解析如果内网DNS配置不当SSH登录会卡在gethostbyaddr阶段。解决办法是在sshd_config里设置UseDNS no这个细节一般没人提醒排查时容易忽略。6.5 排错的通用方法找准确错误信息再动手无论什么问题我的经验是不要凭感觉改配置。先控制单台主机运行并加verbose参数ansible-playbook playbook.yml --limit 192.168.1.101 -vvv-vvv能看到SSH连接、模块参数、执行结果这些完整细节。缩小范围到单台主机、然后提升日志详细度、最后再改配置验证按这个顺序排查大多数问题十分钟内都能定位。最后再分享一点使用体会经过这些实战踩坑我最大的感受是Ansible的价值不在于帮你省掉几行命令而是把「我手动操作过很多次」变成了「我明确知道每台机器处于什么状态」。批量部署从依赖记忆和运气变成了可审计、可重放、可回滚的工程过程。你现在完全可以先把最基本的安装、主机清单、copy、template、service这几个点跑通去管理一台真实的测试机器再慢慢扩展到其它模块。过程中遇到报错先按上面说的排查链路走一遍把错误信息完整读明白再动手基本都能找到出路。如果后续有空我还可以继续整理变量与secret管理、角色复用、以及如何把Playbook接入CI/CD流程这些进阶内容那又是另一个值得细聊的话题了。