ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Ansible模块实战解析:从最小执行单元到playbook编排

Ansible模块实战解析:从最小执行单元到playbook编排 最近好几个刚接触Ansible的同事问我同样一个问题Ansible的模块这么多playbook看起来就是一堆YAML到底该怎么学才能不踩坑。我给的回答很直接先把模块搞明白playbook就是一堆模块按顺序编排的清单模块是Ansible的最小执行单元模块用不好playbook写得再花哨也白搭。这篇内容我打算换个讲法不按官方文档的模块列表挨个念参数而是按真实运维场景来拆——远程执行命令用哪组模块、文件管理用哪组模块、服务状态和软件安装怎么配合、信息收集怎么玩最后用一个能直接上手的完整playbook把模块串起来。适合刚玩Ansible想系统掌握模块用法的朋友也适合写了一段时间playbook但总觉得路子不够正的运维同学。1. 模块与playbook的关系先把最小执行单元这件事想透1.1 为什么说模块是playbook的肌肉Ansible的核心思想可以浓缩成一句话无代理、SSH连接、幂等执行。这里的幂等执行全靠模块来保证。模块实际上是Ansible在目标主机上执行的一段代码多数是Python脚本它接收参数在远程主机上完成具体操作再把执行结果返回给Ansible控制端。playbook呢它本身不具备任何执行能力它只是用YAML语法把模块、主机范围、变量、条件、循环这些元素组织起来的一个编排文件。你可以把playbook理解成一份施工图纸模块是施工队每台被管理的主机就是工地。图纸画得再好施工队不行活照样干砸。所以学playbook的正确路径是先把常用模块的脾气摸透再看它们怎么在playbook里配合。我见过不少人一上来就抄网上的playbook模板结果线上出了问题连是哪一步执行失败都定位不到归根结底就是模块层面的基本功不扎实。1.2 幂等性模块和普通脚本最本质的区别普通Shell脚本你执行一遍是一份结果执行两遍可能就出问题。举例来说往配置文件里追加一行内容脚本第一次跑追加成功第二次跑又追加了一份配置就重复了。模块不一样主流模块在设计时就要求具备幂等性也就是无论执行一遍还是执行一百遍最终的目标状态都是一致的。拿file模块设置目录权限举例执行结果中会有一个字段叫changed第一次执行时目录被创建、权限被修改结果为changed: true第二次再跑因为目录和权限已经满足要求模块直接返回changed: false。这个特性在生产环境极其重要它意味着你可以放心地反复执行同一个playbook不用担心重复操作把系统搞坏。用模块写操作意图时心里要始终装着目标状态这个模型不是写我要执行这个命令而是写我要让系统变成这个状态模块来判断当前状态是否已经满足。2. command、shell、raw、script四个命令类模块怎么选才不会翻车2.1 command模块安全的默认选择command模块是Ansible默认执行的模块也就是说如果你在task里不写module名称直接写一个命令Ansible默认就当作command来处理。它最大的特点是不会经过目标主机的Shell解释器而是直接执行给定的命令和参数。这意味着两点第一像、、|、、$这种Shell特性在command模块里是无效的会被当成普通字符串第二安全性相对可控毕竟不用经过Shell解析减少了命令注入的风险。实际写playbook时command模块适合执行那些不需要管道、重定向、环境变量展开的简单命令比如systemctl status nginx、mkdir -p /data/logs、df -h这类。2.2 shell模块需要Shell特性时再上当你要执行ps aux | grep java | grep -v grep这种带管道和过滤的复杂命令时shell模块才是正确的选择。它会调用目标主机上的/bin/sh来执行整条命令字符串Shell的管道、重定向、通配符、变量展开这些特性全部可用。但这里有一个需要特别留意的点使用了shell模块就等于把整条命令的解析权完全交给了远程主机的Shell解释器。如果命令中包含用户输入的内容就存在注入风险。所以在写shell模块任务时尽量不要把没有经过校验的变量拼进命令串里尤其是还带着|和$的时候。我自己的习惯是能用command就不上shell只有确认必须用管道或重定向时才用shell模块并且把命令中可能涉及的变量用quote过滤器处理一下。对比点commandshellrawShell特性不支持完全支持完全支持依赖Python依赖依赖不依赖幂等性由命令本身决定由命令本身决定完全不做保证典型场景简单命令执行管道/重定向组合命令极简系统或网络设备2.3 script模块把本机脚本传到远端执行script模块做的事情是把控制端本机的一个脚本文件直接拷贝到目标主机上然后执行。它适合管理端已有成熟脚本、不想把脚本内容拆成一个个task的情况。实际使用时只需要在参数里指定脚本路径Ansible负责传输和执行。有个细节需要注意script模块默认在目标主机上执行脚本时也是走Shell的所以脚本里不要写需要交互式输入的逻辑否则会因为标准输入没有数据源而卡住。2.4 raw模块最后的保底手段raw模块是这里面最朴素的它不依赖目标主机上有Python解释器而是直接通过SSH执行裸命令。需要理解的使用场景是目标主机可能是一个刚装的系统Python还没装好或者是一个网络设备、小型嵌入式设备这时候常规模块全都不好用直接用raw往目标机器上打一条命令。比如你给一堆裸机装完系统发现Python环境异常没法用它跑常规模块这时就得先用raw模块确保基础环境就绪再用正常模块继续后面的配置。raw模块不保证幂等每次执行都如实上报changed所以能用其他模块的时候不要优先考虑它。3. 文件处理的四位主力file、copy、template、lineinfile 的取舍与配合3.1 file模块目录、链接、权限、删除全管file模块是文件类操作的主心骨它负责的是文件当前状态的表述而不是执行某个具体的文件操作命令。创建目录时用下面的方式声明目标目录要处于已创建的最终状态- name: 确保应用目录存在 ansible.builtin.file: path: /data/app/logs state: directory owner: app group: app mode: 0755注意mode参数这些引号是值得专门说一嘴的。YAML解析时如果不加引号0755会被当成八进制数字0o755处理在Ansible里实际引用到的值就变成了十进制的493最终设置出来的权限位和你预期的完全对不上。这个坑我踩过后来养成了给mode值都加引号的习惯。file模块还可以管理软链接把state设为link配合src参数即可。删除文件更简单把state设为absent就能确保目标路径不存在。3.2 copy模块静态文件分发的一把好手如果我们已经有了一份现成的配置文件、一段启动脚本或者一个压缩包希望从控制端分发到目标主机用copy模块就对了。- name: 分发nginx主配置 ansible.builtin.copy: src: files/nginx.conf dest: /etc/nginx/nginx.conf owner: root group: root mode: 0644 backup: yes这个backup: yes参数是我强烈建议加上的它会在目标文件已存在且内容不同时先把原文件备份一份再覆盖备份文件名会自动带上时间戳。生产环境里改配置一旦改了之后服务起不来有这个备份就能快速回滚。你需要注意copy的src行为差异src如果指向的是一个目录拷贝行为会因为末尾是否带斜杠而变化。不带斜杠时是拷贝这个目录本身及里面的所有内容带斜杠时是将目录里的内容拷贝到dest下。这个细节非常绕我建议上手前先建两个测试目录跑一遍试验比看文档十遍都管用。3.3 template模块配置文件的动态渲染引擎template是copy的进阶版。copy原样分发静态文件而template会在分发前用Jinja2模板引擎渲染内容把文件中的变量、循环、条件表达式替换成实际的值。比如要下发一个带环境差异的数据库连接配置- name: 生成应用数据库配置 ansible.builtin.template: src: templates/db_config.properties.j2 dest: /opt/app/config/db_config.properties owner: app group: app mode: 0644对应的模板文件内容长这样db.host{{ db_host }} db.port{{ db_port }} db.user{{ db_user }} db.password{{ db_password | default(change_me) }}模板文件名里的.j2后缀只是一个约定方便人和编辑器识别这是Jinja2模板Ansible本身不靠后缀判断靠的是template模块的src参数。还要记住一点Jinja2模板中获取Ansible收集到的系统信息时无需加引号直接引用变量名即可比如{{ ansible_hostname }}。3.4 lineinfile模块精准修改单行内容的利器前三个模块都是整个文件级别的操作而lineinfile专门解决只改文件里某一行的场景。最典型的是修改sshd配置- name: 开启SSH密码认证 ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: ^#?PasswordAuthentication line: PasswordAuthentication yes backup: yes运行逻辑是在文件中逐行匹配regexp找到之后用line指定的内容替换那一行如果文件里没有任何一行能匹配上默认会在文件末尾追加这行内容。需要注意的是lineinfile的regexp是Python风格的正则和Shell里的通配符不是一回事别把*的用法搞混。还有一个进阶参数backrefs它可以让line里的\1去引用正则捕获组的内容适合对原有行做局部替换这类细腻场景。4. 软件包与服务管理yum/service/systemd/firewalld的组合打法4.1 包管理模块yum、apt、pip、gem怎么选Ansible为不同操作系统准备了专用的包管理模块比如RedHat系用yum或dnf模块Debian系用apt模块Python包用pip模块。选择的原则只有一个目标主机是什么包管理体系就选用对应的模块。在RedHat类系统上安装一整套常用工具集的写法- name: 安装常用系统工具 ansible.builtin.yum: name: - vim - net-tools - lsof - unzip - gcc state: presentstate: present和latested两个状态的区别要搞清楚present表示确保已安装但不强制升级到最新版latest表示如果是旧版就升级到最新版。生产环境为了版本可控建议把state固定为present升级操作走单独的变更流程而不是在playbook里用latest把事情搞得不透明。我自己的习惯是指定明确的版本号比如nginx-1.20.1-1.el7.ngx这样可以最大程度保证环境的确定性。等到要升级的时候再手动改成新版本号顺便验证一下依赖的兼容性。4.2 service与systemd让服务保持在预期状态软件包装完了紧接着就是服务状态的管理。老一些的发行版上常用service模块通过调用SysV init脚本来管理服务。systemd模块则是专为新式systemd系统设计的支持daemon-reload、enabled、masked等新概念。现在新的项目我基本只用systemd模块对旧系统在task里做条件判断切换即可。一个标准的nginx服务启停配置- name: 确保nginx服务开机自启且已启动 ansible.builtin.systemd: name: nginx enabled: yes state: started daemon_reload: yes这个写法的好处在于它把开机自启和运行中两个状态一次性声明了。如果服务配置文件路径有变比如改了使用挂载卷的路径daemon_reload为yes可以让systemd重新加载单元文件避免服务启动失败却不知道原因。还有一个使用习惯想强调如果你给服务改了配置需要重启服务才能生效有几种触发方式最推荐的是用handlers机制。有几个请求容易让人掉进坑里直接在task里写restart然后playbook每次跑都会做一次重启容易引起服务抖动。用handlers可以只在配置实际发生变化时才去重启服务。- name: 分发nginx主配置 ansible.builtin.template: src: templates/nginx.conf.j2 dest: /etc/nginx/nginx.conf notify: restart nginx handlers: - name: restart nginx ansible.builtin.systemd: name: nginx state: restarted4.3 防火墙与SELinux容易被遗忘的部署拦路虎很多业务部署不上、访问不通查来查去最后发现是防火墙规则或SELinux没放行。playbook里考虑业务可用性把这些隐藏关卡一并管起来才稳当。放行端口用firewalld模块官方的Ansible模块列表中firewalld的写法如下- name: 放行80端口 ansible.posix.firewalld: port: 80/tcp permanent: yes immediate: yes state: enabledpermanent表示写入防火墙的持久化配置immediate表示立即生效两个参数同时设为yes才稳妥。SELinux这块如果确实不想在生产环境开启可以显式地在playbook里设置fcontext或者直接把SELinux模式设为permissive作为过渡方案。假设某台机器上安装了一个新业务软件它要监听的端口SELinux策略里没有不处理的话服务起来也被拦截。一个解决办法是安装一个自定义SELinux模块但从实际运维角度很多团队直接选择permissive模式保存精力。这个取舍得结合实际安全态势评估我不建议一刀切关掉SELinux。5. setup模块与facts变量让playbook读懂每台机器的体检报告5.1 facts到底是什么Ansible在把playbook任务下发到目标主机之前会默认在目标主机上执行一次setup模块完成信息收集。收集到的结果称为facts实际上就是一个包含了主机名、IP、操作系统版本、CPU核数、内存大小、磁盘挂载等大量信息的JSON数据集。执行一条命令可以完整看到这台机器被Ansible收集了哪些信息ansible 192.168.1.10 -m setup返回的数据量非常大包含几百个字段常用的像是ansible_facts.ansible_default_ipv4.address默认IPv4地址、ansible_facts.ansible_distribution_version系统版本、ansible_facts.ansible_processor_vcpusCPU核数等都是高频率使用的。5.2 在playbook中使用facts做差异化配置facts最大的价值在于让你的playbook能自动适配不同配置的机器而不必为每台机器单独写一套配置。比如要根据CPU核数设置应用线程池大小- name: 计算并写入线程池配置 ansible.builtin.template: src: templates/app.conf.j2 dest: /opt/app/conf/app.conf vars: thread_pool_size: {{ ansible_facts.ansible_processor_vcpus * 2 }}模板中引用变量时写法如下thread.pool.size{{ thread_pool_size }}这样当这台机器有4核CPU时线程池就是8另外一台机器有8核时线程池自动变成16。整个过程不需要为不同机型维护多份配置。5.3 刷新facts与关闭facts收集的取舍如果你在playbook运行过程中通过模块改了主机名或IP默认的facts不会自动更新。要拿到最新的信息可以在task中显式调用setup模块重新收集- name: 重新收集facts ansible.builtin.setup:还有一个经常被忽略的性能优化点如果playbook里完全用不到facts可以在play级别设置gather_facts: false。这样做能明显加快执行速度尤其是管理几百台主机时收集facts的开销累加起来相当可观。但要注意关闭facts之后模板和条件判断里就不要再引用任何ansible_*变量了因为压根没收集肯定取不到值。6. 用户、定时任务与日志三条常规运维也离不开的平行线6.1 user模块管理账号批量创建用户时user模块是基本手段。它可以一并设置用户组、家目录、Shell和密码。密码参数有一个注意点明文密码不被接受必须传入一个符合目标系统规则的密文哈希通常使用mkpasswd命令生成。- name: 创建运维用户 ansible.builtin.user: name: ops groups: wheel append: yes shell: /bin/bash create_home: yes password: {{ your_password | password_hash(sha512) }}这里的password_hash过滤器会在控制端直接生成SHA512哈希给到目标主机写入shadow文件避免密码以明文形式出现在playbook里。如果你用了Vault加密整个playbook不写这个过滤器也可以但习惯上我还是会做一层哈希。append: yes这个参数同样的容易被忽略它表示把用户追加到群组中覆盖原来的groups属性用错的话会把用户的属组配置重置。6.2 cron模块维护定时任务批量下发要在目标主机上配置的定时任务cron模块是标准答案- name: 添加日志清理定时任务 ansible.builtin.cron: name: 清理超过7天的应用日志 minute: 30 hour: 2 job: /usr/bin/find /data/app/logs -type f -mtime 7 -exec rm -f {} \\; user: appcron的幂等性和命令模块一样依赖任务本身的特性cron模块本身会按任务名来判断目标主机上是否已存在相同任务。如果你把job里的命令改了但name没有变模块会认为这个定时任务已经存在不会做任何修改。这是非常容易踩的坑所有cron任务变更时记得同样调整name字段或者先删除再添加。6.3 利用fetch模块备份远程关键文件和copy正好相反fetch模块是把目标主机上的文件拉到控制端来最典型的用途是备份远程节点的配置和日志。- name: 拉取nginx配置到本地备份 ansible.builtin.fetch: src: /etc/nginx/nginx.conf dest: /backup/nginx_conf/这个模块在批量备份场景下很实用。它默认会在dest下按主机名/source完整路径的目录结构保存文件比如/backup/nginx_conf/192.168.1.10/etc/nginx/nginx.conf能防止不同主机之间的文件互相覆盖。7. 把模块串起来一个nginx集群初始化的完整playbook拆解7.1 编一个能直接落地的示例纸上谈兵讲完了用一个实际可用的playbook把刚才的模块在真实流程里串起来。这个例子是一个新上线nginx前置机集群的初始化任务包含系统初始化、安装nginx、下发配置、启动服务、校验状态五个阶段。--- - name: Nginx集群上线初始化 hosts: nginx_nodes become: yes gather_facts: yes vars: nginx_port: 80 app_name: demo-web deploy_dir: /data tasks: - name: 1. 安装基础依赖包 ansible.builtin.yum: name: - vim - net-tools - tcpdump - lsof state: present - name: 2. 创建部署目录 ansible.builtin.file: path: {{ deploy_dir }}/{{ app_name }}/logs state: directory owner: nginx group: nginx mode: 0755 - name: 3. 安装nginx ansible.builtin.yum: name: nginx state: present - name: 4. 下发nginx主配置 ansible.builtin.template: src: templates/nginx.conf.j2 dest: /etc/nginx/nginx.conf owner: root group: root mode: 0644 backup: yes notify: restart nginx - name: 5. 下发站点配置 ansible.builtin.template: src: templates/app.conf.j2 dest: /etc/nginx/conf.d/{{ app_name }}.conf owner: root group: root mode: 0644 notify: reload nginx - name: 6. 防火墙放行端口 ansible.posix.firewalld: port: {{ nginx_port }}/tcp permanent: yes immediate: yes state: enabled - name: 7. 确保nginx运行并开机自启 ansible.builtin.systemd: name: nginx enabled: yes state: started daemon_reload: yes - name: 8. 校验nginx监听端口 ansible.builtin.shell: ss -lntp | grep {{ nginx_port }} register: nginx_check changed_when: false - name: 9. 输出校验结果 ansible.builtin.debug: msg: nginx正在监听端口 {{ nginx_port }}{{ nginx_check.stdout }} handlers: - name: restart nginx ansible.builtin.systemd: name: nginx state: restarted - name: reload nginx ansible.builtin.systemd: name: nginx state: reloaded7.2 这份playbook里的几个设计细节目录创建前先确认nginx用户存在这里假设nginx包里已创建实际线上如果是从裸机初始化建议在安装nginx后再执行目录创建或者在创建目录前用user模块显式声明用户存在。这里把目录创建放在yum安装之前是为了演示file模块用法如果你直接抄这个示例记得按实际顺序调整。我把端口校验这个task的changed_when设成了false。shell模块默认只要命令执行了就报告changedtrue但ss命令本质上是个只读查询命令它不该被标记为发生变更否则每次跑playbook这个task都显示有变化看着烦且容易掩盖真实变更。加了这个参数之后只有命令执行失败才报错执行成功就显示ok干净很多。模板文件中的nginx配置可以写成这样演示Jinja2的变量引用server { listen {{ nginx_port }}; server_name {{ ansible_facts.hostname }}; access_log /data/{{ app_name }}/logs/access.log; error_log /data/{{ app_name }}/logs/error.log; location / { root /usr/share/nginx/html; index index.html index.htm; } }这里顺手把facts里的主机名用上了每个节点的server_name自动变成它自己的主机名不用为每台机器单独写配置。7.3 执行这个playbook的正确姿势执行前先做语法检查和模拟执行这两步千万别省# 第一步语法检查 ansible-playbook --syntax-check nginx_init.yaml # 第二步dry run只看会做什么改动不实际执行 ansible-playbook -C nginx_init.yaml # 第三步正式执行限制只跑一台机器先验证再全量 ansible-playbook nginx_init.yaml --limit 192.168.1.11 # 第四步全量执行 ansible-playbook nginx_init.yaml首次在陌生环境跑playbook我强烈建议先加--limit限制到单台机器验证等确认没有问题再全量执行。-C模拟模式在很多模块下并不完全准确比如shell模块在-C模式下仍然可能执行部分命令但至少能过滤掉大部分低级的逻辑错误。整个执行过程的输出里重点关注task前面的状态图标ok表示已满足目标状态未做改动changed表示该模块实际执行了变更failed不用解释就是失败了。如果全是ok说明目标主机已经处于playbook声明的目标状态这正是Ansible幂等性的优秀表现。8. 编写playbook时最值得记住的几个习惯先讲一点最容易被轻忽的模块版本。Ansible官方将模块区分为ansible.builtin.*内置核心模块和ansible.posix.*POSIX相关模块等命名空间。建议在playbook中带上命名空间前缀来引用模块例如写作ansible.builtin.yum而不仅是yum这是Ansible 2.10之后推荐的写法避免旧写法在升级时失效。再提一个是执行速度问题。gather_facts默认开启但你不一定每次都用到facts里的所有字段。如果确认这套playbook不需要facts加gather_facts: false可以省掉几百毫秒到几秒不等的时间几十台几百台机器累积下来差距非常大。反过来如果你后面模板里要引用系统信息记得先确认facts收集没有关掉。写playbook时注释也要带着目标状态的思路。我见过有人在task下方写重启nginx之类的注释但核心的notify/handler逻辑放在哪却没人解释。注释应当写清楚为什么这个task要这样写比如这里用lineinfile而不是copy是因为该文件里的其他配置由不同系统共同管理不能整份覆盖这样后来接手的同事才能理解当初的决策依据。还有一个实战验证很有效的小技巧写新的playbook或模块任务时先在一台临时测试机上反复执行确认幂等性确实成立。也就是跑了第一次之后立刻跑第二次正常情况下第二次的结果应该零changed或只保留无法避免的少量changed项。如果第二次还报大量changed说明你的任务写法可能有隐藏问题通过压低变更次数来检验使用模块的正确性是我个人比较推荐的检查手段。最后说说报错处理的小经验。模块执行失败时Ansible默认会终止整个playbook在当前主机上的执行。如果你希望某类失败不要中断整个流程比如某个服务在个别节点上不存在又被停了导致报错可以结合ignore_errors: yes或failed_when来做弹性处理。但使用这些要克制否则错误会被掩盖掉过了一段时间环境出状况再来查看反而定位不出来原因。
返回列表