7.Ansible自动化之-实施任务控制

7.Ansible.实施任务控制

实验环境

准备 Ansible 工作环境:

# 创建工作目录web并进入
[bq@controller ~]$ mkdir web && cd web

# 创建Ansible配置文件ansible.cfg,定义默认参数
[bq@controller web]$ cat > ansible.cfg <<'EOF'
[defaults]
remote_user = bq          # 默认远程登录用户为bq
inventory = ./inventory   # 指定inventory文件路径为当前目录的inventory

[privilege_escalation]
become = True             # 允许提权(切换到root)
become_user = root        # 提权目标用户为root
become_method = sudo      # 提权方式为sudo
become_ask_pass = False   # 提权时不询问密码(需提前配置sudo免密)
EOF

# 创建inventory文件,定义受管节点列表
[bq@controller web]$ cat > inventory <<'EOF'
controller   # 控制节点自身(可作为受管节点)
node1        # 受管节点1
node2        # 受管节点2
node3        # 受管节点3
node4        # 受管节点4
EOF

编写循环任务

循环的核心作用是减少重复代码:当需要对多个对象执行相同操作时(比如创建多个用户、安装多个软件),无需写多个重复任务,只需用循环迭代一个列表即可。

Ansible 通过loop关键字实现循环,迭代过程中用item变量表示当前元素。

简单循环(列表迭代)

场景:创建用户 jane 和 joe,均加入 wheel 组。

如果不使用循环,需要写两个任务:

---
- name: 不使用循环创建用户
  hosts: node1
  gather_facts: no  # 不收集受管节点信息(加快执行速度)
  tasks:
    - name: 创建用户jane
      user:
        name: "jane"   # 用户名
        groups: "wheel"  # 所属组
        state: present   # 确保用户存在
    - name: 创建用户joe
      user:
        name: "joe"
        groups: "wheel"
        state: present

用 loop 改写后(更简洁):

- name: 使用loop循环创建用户
  hosts: node1
  gather_facts: no
  tasks:
    - name: 批量创建用户
      user:
        name: "{{ item }}"  # item代表循环中的当前用户(jane或joe)
        groups: "wheel"
        state: present
      loop:  # 循环的用户列表
         - jane
         - joe

也可以将列表定义为变量,再通过loop引用(更易维护):

- name: 用变量定义循环列表
  hosts: node1
  gather_facts: no
  tasks:
    - name: 批量创建用户
      user:
        name: "{{ item }}"
        groups: "wheel"
        state: present
      loop: "{{ users }}"  # 引用变量users
      vars:  # 定义变量users为用户列表
        users:
          - jane
          - joe
循环散列 / 字典列表

当需要迭代的元素包含多个属性时(比如用户不仅有名称,还有所属组、家目录等),可以用字典列表(每个元素是一个键值对集合),通过item.键名获取属性。

场景:创建用户 jane(属于 wheel 组)和 joe(属于 root 组)。

yaml

- name: 循环字典列表创建用户
  hosts: node1
  gather_facts: no
  vars:  # 定义用户信息列表(每个元素是一个字典)
    users:
      - name: jane    # 用户名
        groups: wheel # 所属组
      - name: joe
        groups: root
  tasks:
    - name: 批量创建带属性的用户
      user:
        name: "{{ item.name }}"   # 取当前字典的name属性
        groups: "{{ item.groups }}"  # 取当前字典的groups属性
        state: present
      loop: "{{ users }}"  # 循环用户信息列表
较早样式的循环关键字(了解即可)

Ansible 2.5 之前常用with_开头的关键字(如with_items)实现循环,现在推荐用loop。以下是常见旧语法对比:

旧语法说明对应新语法(loop)
with_items迭代列表,会自动展开嵌套列表loop(但loop不会展开嵌套列表,需注意区别)
with_list迭代列表,不展开嵌套列表loop(行为一致)
with_together按索引合并多个列表(如 [1,2] 和 [a,b] 合并为 (1,a),(2,b))`loop: "{{ list1zip(list2)list }}"`
with_indexed_items迭代列表时同时返回索引(如 a 的索引 0,b 的索引 1)`loop: "{{ itemsenumerate }}"`
with_sequence生成数字序列(如 1-5)loop: "{{ range(1,6) }}"

示例:with_items 与 loop 的区别
with_items会展开嵌套列表,而loop不会:

# with_items会将嵌套列表[ [jane, joe] ]展开为[jane, joe],创建两个用户
- name: with_items示例(旧语法)
  hosts: node1
  tasks:
    - name: 创建用户
      user: name="{{ item }}" state=present
      with_items:
        - [jane, joe]  # 嵌套列表会被展开

# loop不会展开嵌套列表,会把[jane, joe]当作一个元素,导致错误(用户名不能是列表)
- name: loop错误示例
  hosts: node1
  tasks:
    - name: 创建用户(会失败)
      user: name="{{ item }}" state=present
      loop:
        - [jane, joe]  # 嵌套列表不会被展开,item是整个列表
Do-Until 循环(重试直到条件满足)

用于重复执行任务直到满足条件(如等待服务启动、节点上线)。

场景:每隔 1 秒检测 node2 是否可 ping 通,最多重试 20 次。

- name: 检测node2是否可达
  hosts: node1  # 在node1上执行ping命令
  gather_facts: no
  tasks:
    - shell: ping -c1 -w 2 node2  #  ping node2(1个包,超时2秒)
      register: result  # 保存命令结果到变量result
      until: result.rc == 0  # 条件:返回码为0(成功)时停止重试
      retries: 20  # 最大重试次数
      delay: 1  # 重试间隔(秒)
循环与注册变量(register)

循环任务的结果会被整合到注册变量的results属性中,可通过二次循环获取每个迭代的结果。

场景:循环输出字符串,然后打印每个输出的内容。

---
- name: 循环与注册变量示例
  hosts: node1
  gather_facts: no
  tasks:
    - name: 循环输出字符串
      shell: "echo 这是我的元素:{{ item }}"  # 输出当前元素
      loop:
        - one
        - two
      register: result  # 注册结果(包含所有迭代的信息)
    
    - name: 打印完整结果
      debug:
        var: result  # 查看result的结构(包含results列表)
    
    - name: 打印每个迭代的输出
      debug:
        msg: "上一步的输出:{{ item.stdout }}"  # 取每个迭代的stdout
      loop: "{{ result.results }}"  # 循环result中的results列表

编写条件任务

条件任务用于根据特定条件决定是否执行任务(比如根据主机系统版本安装不同软件、根据磁盘空间决定是否部署服务)。

核心关键字是when,后面跟判断条件(如变量值、命令结果、系统信息等)。

常见判断条件
条件类型示例说明
等于(字符串)ansible_machine == "x86_64"系统架构是否为 x86_64
等于(数字)max_memory == 512内存是否为 512MB
比较大小min_memory > 256内存是否大于 256MB
变量是否定义min_memory is defined变量 min_memory 是否存在
变量是否为空min_memory is none变量 min_memory 已定义但值为空
布尔值判断run_task变量 run_task 为 true 时成立
取反not run_task变量 run_task 为 false 时成立
包含关系ansible_distribution in supported_os系统发行版是否在支持列表中
文件属性/etc/hosts is file路径是否为普通文件
实用示例

1. 布尔变量判断
当变量run_my_task为 true 时执行任务:

---
- name: 布尔条件示例
  hosts: node1
  gather_facts: no
  vars:
    run_my_task: true  # 定义布尔变量
  tasks:
    - name: 条件执行的任务
      debug:
        msg: "任务执行了!"
      when: run_my_task  # 条件:变量为true

2. 变量是否定义
判断卷组research是否存在,存在则创建逻辑卷,否则提示错误:

---
- name: 检查卷组并创建逻辑卷
  hosts: node1
  tasks:
    - name: 创建4000MB的逻辑卷
      lvol:
        vg: research  # 卷组名称
        lv: data      # 逻辑卷名称
        size: 4000    # 大小(MB)
      when: ansible_lvm.vgs.research is defined  # 条件:卷组research存在
    
    - name: 提示卷组不存在
      debug:
        msg: "卷组research不存在"
      when: ansible_lvm.vgs.research is not defined  # 条件:卷组不存在

3. 命令结果判断
根据df命令结果判断根目录可用空间,足够则安装软件:

---
- name: 根据磁盘空间安装软件
  hosts: node1
  gather_facts: no
  tasks:
    - name: 获取根目录可用空间(KB)
      shell: df / | awk 'NR==2 {print $4}'  # 取df结果第二行第四列(可用空间)
      register: fs_size  # 保存结果到变量
    
    - name: 安装mariadb-server(空间足够时)
      dnf:
        name: mariadb-server
        state: present
      when: fs_size.stdout | int >= 300000  # 条件:可用空间≥300000KB

4. 循环与条件结合
对多个挂载点循环,仅处理根目录(/)且可用空间足够时安装软件:

---
- name: 循环+条件示例
  hosts: node1
  tasks:
    - name: 根目录空间足够时安装mariadb
      yum:
        name: mariadb-server
        state: latest
      loop: "{{ ansible_mounts }}"  # 循环所有挂载点(从facts获取)
      when: 
        - item.mount == "/"  # 仅处理根目录
        - item.size_available > 300000000  # 可用空间>300MB(单位:字节)

Ansible Handlers(处理程序)

Handlers 是被其他任务 “通知” 后才执行的任务,通常用于 “配置变更后需要重启服务” 等场景(如修改 nginx 配置后重启 nginx)。

特点:

  • 只有通知它的任务执行成功且状态为changed时,Handlers 才会运行;
  • 即使被多个任务通知,Handlers 也只会执行一次;
  • 所有任务执行完毕后,Handlers 才会统一运行(除非用meta模块强制提前执行)。
基础用法

场景:安装 httpd 后,若配置有变更则重启服务。

---
- name: 部署web服务器
  hosts: node1
  tasks:
    - name: 安装httpd
      yum:
        name: httpd
        state: present
      notify:  # 若此任务状态为changed,通知Handlers中的"重启apache"
        - 重启apache

    - name: 安装httpd手册
      yum:
        name: httpd-manual
        state: present
      notify:  # 再次通知同一个Handler
        - 重启apache
    
    - debug: 
        msg: 所有任务执行完毕(Handlers还未运行)

  handlers:  # 定义Handlers
    - name: 重启apache  # Handler名称(需与notify中的名称一致)
      service:
        name: httpd
        state: restarted  # 重启服务
        enabled: yes      # 设置开机自启

执行结果

  • 第一次执行:两个 yum 任务均为changed,Handlers 被通知,最终执行一次 “重启 apache”;
  • 第二次执行:yum 任务状态为ok(已安装),Handlers 不被通知,不执行。
强制立即执行 Handlers(meta 模块)

默认 Handlers 在所有任务执行完毕后运行,若需中途执行,可使用meta: flush_handlers

场景:安装数据库后,立即启动服务再创建用户。

---
- name: 部署数据库
  hosts: node1
  tasks:
    - name: 安装mariadb
      yum:
        name:
          - mariadb-server
          - python3-PyMySQL
        state: present
      notify:
        - 启动并启用数据库

    - meta: flush_handlers  # 强制立即执行已通知的Handlers
    
    - name: 创建数据库用户bq
      mysql_user:
        name: bq
        password: redhat
        host: "%"  # 允许远程登录

  handlers:
    - name: 启动并启用数据库
      service:
        name: mariadb
        state: started
        enabled: yes
调用多个 Handlers

可通过列表或分组(listen)通知多个 Handlers。

方式 1:列表通知
直接在notify中列出多个 Handler 名称:

---
- name: 部署服务
  hosts: node1
  tasks:
    - name: 安装httpd相关包
      yum:
        name:
          - httpd
          - httpd-manual
        state: present
      notify:
        - 启动apache
        - 启用apache  # 通知两个Handlers
  
  handlers:
    - name: 启动apache
      service: name=httpd state=started
    - name: 启用apache
      service: name=httpd enabled=yes

方式 2:分组通知(listen)
listen给 Handlers 分组,notify中指定组名即可触发所有分组的 Handler:

---
- name: 部署服务
  hosts: node1
  tasks:
    - name: 安装httpd相关包
      yum:
        name:
          - httpd
          - httpd-manual
        state: present
      notify:
        - 启动并启用apache  # 通知组名
  
  handlers:
    - name: 启动apache
      service: name=httpd state=started
      listen: 启动并启用apache  # 归为同一组
    - name: 启用apache
      service: name=httpd enabled=yes
      listen: 启动并启用apache  # 归为同一组

处理任务错误

Ansible 默认会在任务失败后终止当前主机的后续任务,以下是常见的错误处理方式。

忽略错误(ignore_errors)

让任务失败后仍继续执行后续任务,适用于 “预期可能失败” 的场景。

---
- name: 忽略错误示例
  hosts: node1
  tasks:
    - name: 安装不存在的包(会失败)
      yum:
        name: notexistpackage  # 不存在的包
        state: present
      ignore_errors: yes  # 忽略此任务的错误
      register: result  # 保存结果
    
    - name: 提示包不存在
      debug:
        msg: "包notexistpackage不存在"
      when: result is failed  # 若上一步失败,则执行此任务
强制执行 Handlers(force_handlers)

默认情况下,若任务失败,已通知的 Handlers 不会执行。force_handlers: yes可强制执行。

---
- name: 强制执行Handlers
  hosts: node1
  force_handlers: yes  # 即使任务失败,Handlers仍执行
  tasks:
    - name: 成功任务(通知Handler)
      command: /bin/true  # 成功执行
      notify: 重启sshd
    
    - name: 失败任务(安装不存在的包)
      yum:
        name: notexistpkg
        state: latest  # 会失败

  handlers:
    - name: 重启sshd
      service: name=sshd state=restarted  # 即使有任务失败,仍会执行
主动触发失败(fail 模块)

通过fail模块主动让任务失败,通常结合when条件使用。

- name: 主动失败示例
  hosts: node1
  gather_facts: no
  tasks:
    - debug:
        msg: 任务1执行中
    - fail:  # 此任务必定失败
        msg: "主动触发失败!"  # 失败提示信息
    - debug:  # 不会执行(上一步已失败)
        msg: 任务3执行中
自定义失败条件(failed_when)

默认任务返回非 0 退出码时失败,failed_when可自定义失败条件(如根据命令输出判断)。

场景:执行脚本/root/adduser,若输出含 “failed” 则任务失败。

环境准备:

# 在node1上创建脚本
[root@node1 ~]# cat /root/adduser 
#!/bin/bash
# 尝试创建用户devops,成功则输出success,失败则输出failed
useradd devops &> /dev/null
if [ $? -eq 0 ];then
  echo "add user devops success"
else
  echo "add user devops failed"
fi
[root@node1 ~]# chmod +x /root/adduser  # 加执行权限

Playbook:

- name: 自定义失败条件
  hosts: node1
  tasks:
    - shell: /root/adduser  # 执行脚本
      register: result  # 保存输出
      failed_when: "'failed' in result.stdout"  # 输出含"failed"则任务失败
自定义 changed 条件(changed_when)

默认命令成功执行即状态为changedchanged_when可自定义何时标记为changed(如根据输出判断)。

场景:执行数据库升级脚本,仅当输出含 “Success” 时标记为changed

- name: 自定义changed条件
  hosts: node1
  tasks:
    - name: 升级数据库
      shell: /usr/local/bin/upgrade-database  # 执行升级脚本
      register: result
      changed_when: "'Success' in result.stdout"  # 输出含"Success"则标记为changed
      notify: 重启数据库  # 只有changed时才通知

  handlers:
    - name: 重启数据库
      service: name=mariadb state=restarted

若希望任务始终不标记为changed(如查询操作),可设置changed_when: false

- name: 不标记为changed
  hosts: node1
  tasks:
    - name: 查看系统版本
      shell: cat /etc/redhat-release
      changed_when: false  # 始终为ok,不会触发Handlers

Ansible Block(任务块)

Block 用于将多个任务分组,可统一设置条件、错误处理等,类似编程中的 “代码块”。

核心用法:

  • block:定义主要任务;
  • rescueblock中任务失败时执行的 “补救” 任务;
  • always:无论blockrescue是否成功,始终执行的任务。
基础示例

场景:升级数据库,失败则回滚,最后始终重启数据库。

- name: 数据库升级流程
  hosts: node1
  tasks:
    - block:  # 主要任务:升级数据库
        - name: 执行数据库升级
          shell: /usr/local/lib/upgrade-database
      rescue:  # 补救任务:升级失败时回滚
        - name: 回滚数据库
          shell: /usr/local/lib/revert-database
      always:  # 始终执行:重启数据库
        - name: 重启数据库
          service: name=mariadb state=restarted
      when: ansible_distribution == "RedHat"  # 条件作用于整个block
实战案例:创建逻辑卷

需求:在所有受管节点上创建逻辑卷,满足:

  1. 若卷组research存在:
    • 尝试创建 4000MiB 的逻辑卷data
    • 若失败(空间不足),则创建 800MiB 的逻辑卷;
    • 格式化逻辑卷为 ext4,挂载到/data
  2. 若卷组research不存在,提示错误。

实验流程

  1. 检查卷组research是否存在;
  2. 存在则进入 block:尝试创建 4000MiB LV,失败则 rescue 中创建 800MiB LV;
  3. 无论创建成功与否,always 中执行格式化、创建目录、挂载操作;
  4. 不存在则直接提示错误。

Playbook

---
- name: 创建并使用逻辑卷
  hosts: all
  tasks:
    - block:  # 卷组存在时执行
        - name: 尝试创建4000MiB的LV
          lvol:
            vg: research  # 卷组名称
            lv: data      # 逻辑卷名称
            size: 4000    # 大小(MiB)
      rescue:  # 创建失败时执行(如空间不足)
        - debug:
            msg: "无法创建4000MiB的逻辑卷,改为创建800MiB"
        - name: 创建800MiB的LV
          lvol:
            vg: research
            lv: data
            size: 800
      always:  # 始终执行(格式化、挂载)
        - name: 格式化LV为ext4
          filesystem:
            fstype: ext4
            dev: /dev/research/data  # LV路径
        - name: 创建/data目录
          file:
            path: /data
            state: directory
        - name: 挂载LV到/data
          mount:
            path: /data
            src: /dev/research/data
            fstype: ext4
            state: mounted  # 永久挂载(写入fstab)
      when: ansible_lvm.vgs.research is defined  # 条件:卷组存在
    
    - name: 卷组不存在时提示
      debug:
        msg: "卷组research不存在"
      when: ansible_lvm.vgs.research is not defined  # 条件:卷组不存在

实施 Tags(标签)

Tags 用于只执行 Playbook 中的部分任务(无需修改 Playbook,通过命令行指定标签即可),适合大型 Playbook 的部分更新。

核心关键字:tags,后跟标签名称(可多个)。

基础用法

场景:给 “安装 httpd” 和 “安装 postfix” 任务打标签,可单独执行。

---
- name: 标签示例
  hosts: node1
  gather_facts: no
  tasks:
    - name: 安装httpd
      yum:
        name: httpd
        state: latest
      tags: webserver  # 打标签webserver

    - name: 安装postfix
      yum:
        name: postfix
        state: latest
      tags: mailserver  # 打标签mailserver

    - name: 打印调试信息
      debug:
        msg: "这是一个无标签任务"

执行命令

# 查看所有标签
[bq@controller web]$ ansible-playbook playbook.yaml --list-tags

# 只执行带webserver标签的任务
[bq@controller web]$ ansible-playbook playbook.yaml --tags webserver

# 执行所有任务,除了带webserver标签的
[bq@controller web]$ ansible-playbook playbook.yaml --skip-tags webserver
特殊标签
标签说明
always总是执行,除非用--skip-tags always跳过
never从不执行,除非用--tags never指定
all执行所有任务(默认,除never外)
tagged只执行带标签的任务
untagged只执行无标签的任务

示例always标签的任务总是执行:

---
- name: 特殊标签示例
  hosts: node1
  tasks:
    - name: 总是执行的任务
      debug:
        msg: "我总会执行"
      tags: always  # 无论--tags如何指定,都会执行

如涉及版权问题请联系作者处理!!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值