第 4-3 章 Linux Ansible 实践梳理

作者: Brinnatt 分类: ARM64 Linux CICD 研发 发布时间: 2025-04-01 10:37

前面通过理论和实践结合对 ansible 有了一些了解,在生产上用起来不会有什么大的问题。不过,ansible 还有很多功能和细节没有涉及到,生产上遇到了再说,下面只是针对实践中主要的功能梳理一下,用起来更顺。

4.1、变量

Ansible 的变量很复杂,因为 Ansible 的变量来源很多,查一下官方手册对变量的介绍,相当震撼,还搞了一个优先级,不过前面我们提到的一些变量也能把 ansible 用起来,不必非得都了解。

4.1.1、访问列表、字典变量

Ansible 中经常需要访问列表和字典类型的变量。例如下面的字典类型:

p: 
  a: aa
  b: bb

files:
  - /tmp/a.txt
  - /tmp/b.txt

对于这类变量的访问,Ansible 中提供了两种方式:

  1. 按照 Python 字典或列表的索引下标方式访问。例如 p["a"]files[0]

  2. 按照对象访问方式。例如 p.bfiles.0

值得一提的是,如果字典的 key 名称和 Python 的字典方法名冲突了,就会有问题。比如:

p: 
  a: aa
  b: bb
  keys: "tic"

如果用 p.keys 来访问这个字典变量会出错,因为 Python 的字典类型有一个称为 keys 的方法名。

4.1.2、–extra-vars 选项

ansible-playbook 命令的 -e 选项或 --extra-vars 选项可定义变量或引入变量文件。

# 定义单个变量
$ ansible-playbook -e 'var1="value1"' xxx.yml

# 定义多个变量
$ ansible-playbook -e 'var1="value1" var2="value2"' xxx.yml

# 引入单个变量文件
$ ansible-playbook -e '@varfile1.yml' xxx.yml

# 引入多个变量文件
$ ansible-playbook -e '@varfile1.yml' -e '@varfile2.yml' xxx.yml

因为是通过选项的方式来定义变量的,所以它所定义的变量是全局的,对所有 play 都有效。

通常来说不建议使用 -e 选项,因为这对用户来说是不透明也不友好的,要求用户记住定义了哪些变量,冲突可能性也比较大。

4.1.3、inventory 变量

在解析 inventory 时,会收集 inventory 相关的变量。

inventory 变量主要分为两种:

  1. 连接目标节点时的行为控制变量,即决定如何连接目标节点

  2. 主机变量

行为控制变量(如 ansible_portansible_host 等)用于指定 Ansible 端连接目标节点时的连接参数,可设置的参数项比较多,可参见官方手册

inventory 的主机变量有多种定义途径,例如直接在 inventory 文件中为某个主机定义变量,也可以在主机组中定义变量,主机组变量会在解析 inventory 的时候整理到主机变量中去。此外还可以将变量定义在 host_vars/group_vars/ 目录内。

# 在主机上直接定义变量
[dev]
192.168.200.42 aaa=333 bbb=444 ansible_port=22
192.168.200.43
192.168.200.44

# 在主机组上定义变量
[dev:vars]
xxx=555
yyy=666
ansible_port=22

# 也可以在特殊的主机组ungrouped和all上定义变量
[all]
zzz=777

主机变量除了可以直接定义在 inventory 文件中,还可以定义在和 inventory 文件同目录的 host_varsgroup_vars 目录中,其中 host_vars 目录中定义主机变量,group_vars 目录中定义主机组变量。

例如,默认的 inventory 文件是 /etc/ansible/hosts,那么可以在 /etc/ansible 目录下创建 host_varsgroup_vars 目录,并在其中创建一些主机、主机组的变量文件或变量目录:

$ tree /etc/ansible/
/etc/ansible/
├── ansible.cfg
├── hosts
├── group_vars
│   ├── all.yml         # all主机组变量文件
│   ├── nginx           # nginx主机组变量目录,其内所有文件都会被读取
│   │   ├── main.yml    # nginx的主配置变量文件
│   │   └── vhost.yml   # nginx的虚拟主机配置变量文件
│   └── php.yml         # php主机组变量文件
└── host_vars
    └── 192.168.200.42.yml  # 192.168.200.42节点的变量文件

上面的目录结构已经解释完所有内容了,这里总结下:

  1. 定义在 group_vars/ 目录中的变量文件可以是普通文件(比如 all.yml、php.yml),也可以是变量目录(比如 nginx 目录)。
  2. 如果是目录,则目录名必须和主机组名相同,目录中的所有文件都会在解析 inventory 的时候被读取。
  3. 如果是普通文件,则文件名可带可不带后缀,不带后缀时,文件名和主机组名相同,带后缀时,前缀和主机组名相同,后缀只允许 .yml .yaml .json
  4. host_vars/ 目录只能为每个节点都单独定义属于它们的变量文件。

记住比较实用的 group_vars/host_vars/ 存放位置:

  1. inventory 文件的同目录
  2. playbook 文件的同目录

所以,下面和 playbook 文件 lnmp.yml 同目录层次的 group_vars/ 也是有效的:

# tree -L 2 -F .
.
├── common.yml
├── group_vars/
│   └── all.yml
├── inventory_lnmp
├── lnmp.yml
├── mysql.yml
├── nginx.yml
├── php.yml
└── roles/
    ├── common/
    ├── mysql/
    ├── nginx/
    └── php/

在 inventory 文件同目录下创建 {group,host}_vars/ 好理解,为什么还要支持在 playbook 同目录下允许这两个目录呢?主要原因还是为了扩充 Role 的能力。

lnmp 实践中,在 nginx Role 中想要跨 Role 访问 php Role 中的变量 phpfpm_port,其中方法之一也是最友好的方法,就是在 playbook 文件的同目录下提供 {group,host}_vars/,并在其中设置多个 Role 共享的变量。

也就是说,在 playbook 文件同层次的 {group,host}_vars/ 下定义主机或主机组变量,可以实现 playbook 全局变量的功能。任何人都能一眼看到这两个目录,而且只要看到 playbook 文件同目录下有这两个目录,就知道它们定义了全局变量,是所有 Role 都能访问到的变量。

需要说明或提醒几点:

  1. 不要忘记 all 主机组的存在,为 all 主机组设置变量表示为所有节点设置主机变量

  2. 主机变量是绑定在主机上的,和 play、task 没有关系,所以这些变量都是全局变量,甚至节点 A 执行任务时还能访问节点 B 的主机变量。

  3. 所有的主机变量都可以通过 ansible-inventory 工具列出来。

  4. 所有变量,包括主机变量,都保存在 Ansible 的变量表 hostvars 中,通过这个全局变量表,任何一个节点都能访问其它节点的变量。

4.1.4、Role 变量

Role 中主要有两个地方定义变量:

  1. roles/ROLE_NAME/defaults/main.yml

  2. roles/ROLE_NAME/vars/main.yml

需要提醒大家,Role defaults 变量的优先级非常低,几乎可以被其它任何同名变量覆盖。

Role 变量都是 play 级别的变量。换句话说,如果 play 中执行了 Role 之后还有 tasks 指令的任务,则 tasks 的任务中可以引用 Role 中的变量。

---
- hosts: localhost
  gather_facts: false
  roles: 
    - role: test_role
  tasks: 
    - debug: 
        var: var_from_role

4.1.5、play 变量

play 级别可以通过 varsvars_filesvars_prompt 指令来定义变量。因为它们属于 play 级别,所以只在当前 play 有效。另一方面,每个 play 都有选中的目标节点,所以所有选中的目标节点都能访问这些 play 变量。

关于 varsvars_files 前面已经解释过,所以不再解释,这里简单介绍下 vars_prompt 指令的用法。

vars_prompt 指令用于交互式提示用户输入数据,并将输入内容赋值给指定的变量。

---
- hosts: localhost
  gather_facts: false
  vars_prompt: 
    - name: username
      prompt: "Your Name?"
      private: no
      default: "root"

    - name: passwd
      prompt: "Your Password"
  tasks: 
    - debug: 
        msg: "username: {{username}}, password: {{passwd}}"

上面定义了两个变量 usernamepasswd,都会提示用户输入对应的值。private: no 表示不要隐藏用户输入的字符(默认会隐藏),default 表示指定变量的默认值。

vars_prompt 主要用于保护隐私数据,比如密码,有时候也用于交互式选择,比如让用户自己输入要安装的软件包名称。它的用法不难,更详细的用法参见官方手册

4.1.6、task 变量

task 变量有多种定义方式,稍作总结:

  1. register 指令

  2. set_fact 指令

  3. vars 指令

  4. include_vars 指令

它们的用法都介绍过,下面给个示例看一眼即可:

---
- hosts: localhost
  gather_facts: false
  tasks: 
    # var.yml变量文件中定义了变量a
    - include_vars: 
        file: var.yml

    - shell: echo tictoc
      register: res
    - set_fact: name="{{res.stdout}}"

    - debug:
        msg: "a: {{a}}, name: {{name}}, age: {{age}}"
      vars:
        age: 18
        gender: male

4.1.7、block 变量

block 作为一个特殊的层次级别,它也支持定义变量,只不过这个 block 层次的变量只对当前 block 内的所有任务有效。

---
- hosts: localhost
  gather_facts: false
  tasks: 
    - block:
        - debug:
           var: name
      vars: 
        name: "toc"

4.1.8、预定义特殊变量

Ansible 作为一个功能复杂的程序,它自身也维护了一些暴露给用户的预定义变量,这些变量都是特殊变量(官方也称为魔法变量),它们都是能直接访问的变量,且对用户只读。其实在之前的文章中已经接触了好几个这类变量,比如 hostvars

写 playbook 的时候如果想要用关于 play、role、task、inventory、host 等 Ansible 内部信息,可以查找预定义变量官方手册

- ansible_forks
表示最大的进程数。这也暗含了多少个节点作为一批。(如果忘记了一批是什么意思,可回介绍playbook的那一章末尾复习)

- hostvars
保存了inventory中所有主机和主机变量

- inventory_hostname
当前执行任务的节点在inventory中的主机名

- inventory_hostname_short
当前执行任务的节点在inventory中的短主机名

- inventory_dir
inventory文件所在的目录

- inventory_file
inventory文件名

- group_names
当前正在执行任务的节点所在主机组列表,注意是一个列表,因为一个节点可能存在于多个主机组

- groups
inventory中所有的主机组以及各组内的主机列表

- ansible_play_batch
当前play中可执行任务的主机列表。Ansible动态维护该变量,默认情况下执行任务失败或连接失败的节点会从此变量中移除

- ansible_play_hosts
等价于ansible_play_batch

- play_hosts
已废弃,等价于ansible_play_batch

- playbook_dir
playbook所在目录,该playbook是ansible-playbook命令所执行的playbook,而不是import_playbook导入的playbook

- ansible_play_name
当前正在执行的play的name。Ansible 2.8才添加的变量

- ansible_play_hosts_all
当前play所选中的所有节点,等价于ansible_play_batch + 失败的节点

- ansible_play_role_names
当前play中包含的Role列表。注意,因依赖关系而隐式导入的Role不在列表内

- role_names
已废弃,等价于ansible_play_role_names

- ansible_role_names
当前play中包含的Role列表,包括因依赖关系而隐式导入的Role

- role_name
当前正在执行的Role的名称

- role_path
当前正在执行的Role的路径

ansible_run_tags
所有--tags筛选出来的tag列表

ansible_skip_tags
所有--skip_tags筛选出来的tag列表

ansible_version
Ansible版本号信息,是一个字典,字典的key: full, major, minor, revision以及string

- omit
这是一个非常特殊的变量,可直接忽略一个模块的参数。通常结合Filter和`default(omit)`使用。用法见下文

下面介绍下 omit 变量的用法:

- name: touch files with an optional mode
  file:
    dest: "{{ item.path }}"
    state: touch
    mode: "{{ item.mode | default(omit) }}"
  loop:
    - path: /tmp/foo
    - path: /tmp/bar
    - path: /tmp/baz
      mode: "0444"

上面的示例通过迭代的方式创建多个文件,其中迭代创建前两个文件时,将以 umask 值来设置所创建文件的权限,而第三个文件因为存在 mode,所以将权限设置为 0444。{ item.mode | default(omit) } 的作用是:如果 item.mode 不存在,则忽略 file 模块的 mode 参数,否则 mode 参数则生效。

4.1.9、变量作用域

前面介绍了几种主要的变量类型,除了需要知道它们的用法之外,还需要搞清楚这些变量的生效范围,也即它们的作用域。其实在前面介绍各种变量的时候都提到过它们各自的生效范围,这里做个总结。

Ansible中变量主要有五种作用域概念:

  1. 全局作用域:Ansible 配置文件、环境变量、命令行选项 -e,--extra-vars 设置的变量都是全局变量。

  2. Play 作用域:整个 Play 中都有效的变量,vars_filesvars_prompt、play 级别的 vars 以及 Role 的变量,它们都是 play 级别的变量。

  3. 主机变量:绑定在各主机上的变量,各种方式定义的 inventory 变量、Facts 信息变量(这个就划分在这吧)、set_factregisterinclude_vars 都是主机变量。

  4. 任务变量:只在当前任务中生效的变量,task 级别的 vars 定义的变量属于任务变量。

  5. block 变量:只在当前 block 内生效,block 级别的 vars 定义的变量属于 block 变量。

最后还有预定义特殊变量未分类,这些变量由 Ansible 自身内部维护,有些是全局变量,有些是 play 变量,有些是主机变量,所以不方便对它们分类。

4.2、handler

前面介绍过 handler,这里再对其做一点补充,如何触发多个 handler 任务。

另外,还有一个关于”如何解决因某任务失败而导致 handler 未执行”的问题,该内容将在后文介绍异常和错误处理的时候再做补充。

如何触发执行多个 handler 任务?比如,将重启 nginx 的 handler 分为多步:

  1. 检查 nginx 语法
  2. 检查 nginx 进程是否已存在
  3. 如果 nginx 进程已存在,则 reload
  4. 如果 nginx 进程还不存在,则 start

第一种实现方式,在 handler 任务中使用 notify,将多个任务链在一起。

# check config file syntax
- name: "reload nginx step 1"
  shell: |
    nginx -c /etc/nginx/nginx.conf -t
  changed_when: true
  notify: "reload nginx step 2"

# check nginx process is started or not
- name: "reload nginx step 2"
  shell: |
    killall -0 nginx &>/dev/null
  notify: 
    - "start nginx"
    - "reload nginx"
  changed_when: true
  register: step2
  failed_when: false

# start nginx when nginx process is not running
- name: "start nginx"
  shell: |
    nginx -c /etc/nginx/nginx.conf
  when: step2.rc == 1

# reload nginx when nginx is running
- name: "reload nginx"
  shell: |
    nginx -s reload -c /etc/nginx/nginx.conf
  when: step2.rc == 0

第二种方式,在触发 handler 处定义 handler 列表:

- template: 
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify: 
    - check nginx syntax
    - check nginx process
    - start nginx
    - reload nginx

注意,handler 任务的执行顺序不是根据 notify 顺序决定的,而是根据 handler 任务的定义顺序。

然后定义如下 handlers:

- name: "check nginx syntax"
  shell: |
    nginx -c /etc/nginx/nginx.conf -t
  changed_when: true

- name: "check nginx process"
  shell: |
    killall -0 nginx &>/dev/null
  changed_when: true
  register: step2
  failed_when: false

- name: "start nginx"
  shell: |
    nginx -c /etc/nginx/nginx.conf
  when: step2.rc == 1

- name: "reload nginx"
  shell: |
    nginx -s reload -c /etc/nginx/nginx.conf
  when: step2.rc == 0

第三种方式,在每个 handler 任务中使用 Ansible 2.2 提供的 listen 指令,它可以监听 notify 发送的信息:

- template: 
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify: start or reload nginx

然后定义如下 handlers,每个 handler 都定义 listen: start or reload nginx,只要 notify 发送 start or reload nginx,则这些任务都会被触发。

- name: "check nginx syntax"
  shell: |
    nginx -c /etc/nginx/nginx.conf -t
  changed_when: true
  listen: start or reload nginx

- name: "check nginx process"
  shell: |
    killall -0 nginx &>/dev/null
  changed_when: true
  register: step2
  failed_when: false
  listen: start or reload nginx

- name: "start nginx"
  shell: |
    nginx -c /etc/nginx/nginx.conf
  when: step2.rc == 1
  listen: start or reload nginx

- name: "reload nginx"
  shell: |
    nginx -s reload -c /etc/nginx/nginx.conf
  when: step2.rc == 0
  listen: start or reload nginx

4.3、when 条件判断

4.3.1、同时满足多个条件

按照一般编程语言的语法,结合多个条件判断语句时要么使用逻辑与符号(通常是 and&&),要么使用逻辑或符号(通常是 or||)。Ansible 当然也支持这种结合方式,例如:

when: item > 3 and item < 10

但如果是想要同时满足多个条件,则可以将这些条件以列表的方式提供。例如:

---
- hosts: localhost
  gather_facts: false
  tasks: 
    - debug: 
        var: item
      when:
        - item > 3
        - item < 5
      loop: [1,2,3,4,5,6]

4.3.2、按条件导入文件

Linux 发行版不同,服务名称、软件包名称等都不相同,对于这种因环境不同而配置不同任务的场景,通常是为不同环境编写不同任务文件或不同变量文件,然后根据when的环境判断加载不同环境的文件。

以 Redhat 和 Debian 系列安装 Apache httpd 软件包的任务文件为例:

---
- hosts: localhost
  gather_facts: yes
  tasks:
    - include_tasks: RedHat.yml
      when: ansible_os_family == "RedHat"
    - include_tasks: Debian.yml
      when: ansible_os_family == "Debian"

4.4、循环迭代

4.4.1、with_list

最简单的循环就是迭代一个列表。例如,在 Ansible 本地端的 /tmp 目录下创建两个文件:

---
- hosts: localhost
  gather_facts: false
  tasks: 
    - file:
        name: "/tmp/{{item}}"
        state: touch
      with_list: 
        - "filename1"
        - "filename2"

与上面 with_list 等价的 loop 语法:

- file:
    name: "/tmp/{{item}}"
    state: touch
  loop: 
    - "filename1"
    - "filename2"

4.4.2、with_items

有时候列表中会嵌套列表,如果想要迭代嵌套列表结构,使用 with_items。注意,它也可以迭代普通非嵌套列表,所以它可以完全替代 with_list

---
- hosts: localhost
  gather_facts: false
  vars: 
    a: [b, c, [d, e], f]
  tasks: 
    - debug: 
        var: item
      with_items: "{{a}}"

注意,with_items 只压平嵌套列表的第一层,不会递归压平第二层、第三层…

[b, c, [d, e], f, [g, h, [i, j], k]]
# with_items压平后得到:
[b, c, d, e, f, g, h, [i, j], k]

with_items 等价的 loop 指令的写法为:

loop: "{{ nested_list | flatten(levels=1) }}"

由上面的写法可推测,筛选器函数 flatten() 默认会递归压平所有嵌套列表,如果只是压平第一层,需指定参数 levels=1

4.4.3、with_flattened

递归压平所有嵌套层次:

---
- hosts: localhost
  gather_facts: false
  vars: 
    a: [b, c, [d, e], f, [g,h,[i,j], k]]
  tasks: 
    - debug: 
        var: item
      with_flattened: "{{a}}"

4.4.4、with_dict

with_dict 用于迭代一个字典结构,迭代时可以使用 item.key 表示每个字典元素的 key,item.value 表示每个字典元素的 value。

---
# test.yml
- hosts: localhost
  gather_facts: false
  vars: 
    users:
      junmajinlong_key:
        name: junmajinlong
        age: 18
      fairy_key:
        name: fairy
        age: 22
  tasks:
    - debug:
        msg: "who: {{item.key}} && 
              name: {{item.value.name}} && 
              age: {{item.value.age}}"
      with_dict: "{{users}}"

with_dict 等价的 loop 指令有两种写法:

loop: "{{lookup('dict', users)}}"
loop: "{{users | dict2items}}"

4.4.5、循环控制loop_control

loop_control指令可以控制循环时的特性,该指令有一些参数,每种参数都是一个控制开关。

4.4.5.1、label 参数

使用 label 参数可以自定义迭代时显示的内容来替代默认显示的 item。例如:

---
- hosts: localhost
  gather_facts: false
  vars: 
    mylist: [11,22]
  tasks:
    - shell: echo {{item}}
      loop: "{{ mylist }}"
      register: res
    - debug: 
        var: item.stdout
      loop: "{{res.results}}"
      loop_control:
        label: "toc"

对比一下,看使用 label 和不使用 label 的区别。

4.4.5.2、pause 参数

loop_control 的 pause 参数可以控制每轮迭代之间的时间间隔。

---
- hosts: localhost
  gather_facts: false
  vars: 
    mylist: [11,22]
  tasks:
    - debug: 
        var: item
      loop: "{{mylist}}"
      loop_control:
        pause: 1

这表示第一轮迭代后,等待一秒,再进入第二轮迭代。

4.4.5.3、index_var 参数

index_var 参数可以指定一个变量,这个变量可以记录每轮循环迭代过程中的索引位,也即表示当前是第几轮迭代。

---
- hosts: localhost
  gather_facts: false
  vars: 
    mylist: [11,22]
  tasks:
    - debug: 
        msg: "index: {{idx}}, value: {{item}}"
      loop: "{{mylist}}"
      loop_control:
        index_var: idx

通过 index_var,可以进行一些条件判断。比如只在第一轮循环时执行某任务:

---
- hosts: localhost
  gather_facts: false
  vars: 
    mylist: [11,22]
  tasks:
    - debug: 
        msg: "index: {{idx}}, value: {{item}}"
      when: idx == 0
      loop: "{{mylist}}"
      loop_control:
        index_var: idx

4.4.5.4、extended 参数

从 Ansible 2.8 开始,还支持使用 extended 参数来获取更多循环的信息。

loop_control:
  extended: yes

打开 extended 的开关后,将可以在循环内部使用下面一些变量。

变量名 含义
ansible_loop.allitems 循环中所有的item
ansible_loop.index 本轮迭代的索引位,即第几轮迭代(从1开始计数)
ansible_loop.index0 本轮迭代的索引位,即第几轮迭代(从0开始计数)
ansible_loop.revindex 本轮迭代的逆向索引位(距离最后一个item的长度,从1开始计数)
ansible_loop.revindex0 本轮迭代的逆向索引位(距离最后一个item的长度,从0开始计数)
ansible_loop.first 如果本轮迭代是第一轮,则该变量值为True
ansible_loop.last 如果本轮迭代是最后一轮,则该变量值为True
ansible_loop.length 循环要迭代的轮数,即item的数量
ansible_loop.previtem 本轮迭代的前一轮的item值,如果当前是第一轮,则该变量未定义
ansible_loop.nextitem 本轮迭代的下一轮的item值,如果当前是最后一轮,则该变量未定义

来个示例,循环迭代一个包含 4 个元素的列表,并在第一轮迭代时输出上面所有变量的值。

---
- hosts: localhost
  gather_facts: false
  tasks: 
    - debug:
        msg: 
          - "ansible_loop.allitems: {{ansible_loop.allitems}}"
          - "ansible_loop.index: {{ansible_loop.index}}"
          - "ansible_loop.index0: {{ansible_loop.index0}}"
          - "ansible_loop.revindex: {{ansible_loop.revindex}}"
          - "ansible_loop.revindex0: {{ansible_loop.revindex0}}"
          - "ansible_loop.first: {{ansible_loop.first}}"
          - "ansible_loop.last: {{ansible_loop.last}}"
          - "ansible_loop.length: {{ansible_loop.length}}"
          - "ansible_loop.previtem: {{ansible_loop.previtem | default('')}}"
          - "ansible_loop.nextitem: {{ansible_loop.nextitem}}"
      when: ansible_loop.first
      loop: "{{mylist}}"
      loop_control:
        extended: yes
      vars: 
        mylist: ['a','b','c','d']

上面的 when 指令使用了 ansible_loop.first 来判断是否是第一轮迭代,通过 ansible_loop.index 也可以判断。此外,刚才也介绍过使用 index_var 的方式来判断处于第几轮判断。

另外,上面的 msg 参数使用了列表形式,如果不使用列表,debug 输出的时候,所有内容都会输出在同一行中,而使用列表则每项都单独成行输出。

4.5、异常

任何程序都有 bug,如果你没有遇到,说明你的程序没有遇到合适的时间,地点,场景,或者用户。

默认情况下,Ansible 端无法连接某个节点时、节点执行某个任务失败时,Ansible 都会将这个节点从活动节点列表中(play_hosts)移除,以避免该节点继续执行之后的任务。

用户可以去修改 Ansible 对这种异常现象的默认处理方式,比如遇到错误也不让该节点退出舞台,而是继续执行后续任务,又或者某节点执行任务失败就让整个 play 都失败。

4.5.1、fail 模块

使用 fail 模块,可以人为制造一个失败的任务。

---
- hosts: nginx
  gather_facts: no
  tasks: 
    - fail:
        msg: "oh, not me"
      when: inventory_hostname == groups['nginx'][0]
    - debug: 
        msg: "hello"

上面的 fail 会任务失败,并使得此节点不会执行后续任务,但其它节点会继续执行任务。

4.5.2、assert 模块

对于当满足某某条件时就失败的逻辑,可以使用 fail 模块加 when 指令来实现,也可使用更为直接的 assert 模块进行断言。

---
- hosts: localhost
  gather_facts: no
  tasks: 
    - assert:
        that:
          - 100 > 20
          - 200 > 200
        fail_msg: "oh, not me"
        success_msg: "oh, it's me"

其中 that 参数接收一个列表,用于定义一个或多个条件,如果条件全为 true,则任务成功,只要有一个条件为 false,则任务失败。fail_msg 定义任务失败时的信息,success_msg 定义任务成功时的信息。

4.5.3、ignore_errors

当某个任务执行失败(或被 Ansible 认为失败,比如通过返回值判断)时,如果不想让这个失败的任务导致节点退出,可以使用 ignore_errors 指令来忽略失败。

---
- hosts: localhost
  gather_facts: no
  tasks:
    - shell: ls /tmp/klakas/kjlasd8293
      ignore_errors: yes
    - debug: 
        msg: "hello world"

4.5.4、failed_when

当条件表达式为 true 时任务强制失败,当条件表达式为 false 时,任务强制不失败。

例如,下面的示例中不管 shell 模块是否正确执行,都认为这个任务成功执行:

---
- hosts: localhost
  gather_facts: no
  tasks:
    - shell: ls /tmp/klakas/kjlasd8293
      failed_when: false
    - debug: 
        msg: "hello world"

failed_when 经常会和 shell 或 command 模块以及 register 指令一起使用,用来手动定义失败的退出状态码。比如,退出状态码为 0 1 2 都认为任务成功执行,其它状态码都认为执行失败。

- shell: COMMAND
  register: res
  failed_when: res.rc not in (0, 1, 2)

如果这时候去查看 res 变量,将会发现多出了一项 failed_when_result

{
  "changed": true,
  "cmd": "ls /tmp/klakas/kjlasd8293",
  "delta": "0:00:00.002469",
  "end": "2020-01-17 00:18:49.437885",
  "failed": false,
  "failed_when_result": false,
  "msg": "non-zero return code",
  "rc": 2,
  "start": "2020-01-17 00:18:49.435416",
  "stderr": "ls: cannot access /tmp/klakas/kjlasd8293: No such file or directory",
  "stderr_lines": [
      "ls: cannot access /tmp/klakas/kjlasd8293: No such file or directory"
  ],
  "stdout": "",
  "stdout_lines": []
}

failed_when_result 记录了 failed_when 指令的渲染结果是 true 还是 false,本例中渲染结果为 false。

此外,failed_whenwhen 一样都可以将多个条件表达式写成列表的形式来表示逻辑与。例如:

- shell: xxxxxx
  register: res
  failed_when: 
    - res.rc != 0 
    - res.stdout == "yyyyy"

4.5.5、rescue 和 always

Ansible 允许在任务失败的时候,去执行某些任务,还允许不管任务失败与否,都执行某些任务。这功能类似于编程语言的 try…catch 异常捕获。

关于这两个指令:

1、rescue 和 always 都是 block 级别的指令

2、rescue 表示 block 中任意任务失败后,都执行 rescue 中定义的任务,但如果 block 中没有任务失败,则不执行 rescue 中的任务

3、always 表示 block 中任务无论失败与否,都执行 always 中定义的任务

---
- hosts: localhost
  gather_facts: no
  tasks: 
    - block:
        - fail: 
        - debug: msg="hello world"
      rescue:
        - debug: msg="rescue1"
        - debug: msg="rescue2"
      always:
        - debug: msg="always1"
        - debug: msg="always2"

block 中的 fail 任务会失败,于是跳转到 rescue 中开始执行任务,然后再跳转到 always 中执行任务。

如果注释掉 block 中的 fail 模块任务,则 block 中没有任务失败,于是 rescue 中的任务不会执行,但是在执行完 block 中所有任务后会跳转到 always 中继续执行任务。

4.5.6、any_errors_fatal

如果想让某个失败的任务直接导致整个 play 的失败,可在 play 级别使用 any_errors_fatal 指令。

---
- hosts: nginx
  gather_facts: no
  any_errors_fatal: true
  tasks: 
    - fail:
        msg: "oh, not me"
      when: inventory_hostname == groups['nginx'][0]
    - debug: 
        msg: "hello"

- hosts: localhost
  gather_facts: no
  tasks: 
    - debug: 
        msg: "HELLO WORLD"

any_errors_fatal 设置为 true 后,nginx 组第一个节点只要一开始执行 fail 任务,整个 playbook 中所有后续任务都将不再执行,就连其它 play 也一样不执行。

注意观察 playbook 的执行结果,它将提示 ”NO MORE HOSTS LEFT”:

.........
TASK [fail] *********************
fatal: [192.168.200.42]: FAILED! => {"changed": false, "msg": "oh, not me"}
skipping: [192.168.200.43]
skipping: [192.168.200.44]

NO MORE HOSTS LEFT **************

PLAY RECAP *************
.........

4.5.7、异常打断 handler 处理

如果某节点执行某任务失败,但在失败任务之前已经触发了 handler,那么该节点将因为失败而无法执行 handler 任务。

有时候这种默认的异常处理并非理想的处理方式。比如 copy 了一个 nginx 配置文件并触发了重启 Nginx 服务的 handler,但是在重启之前执行某个任务失败了,那么该节点将不会重启 nginx,但配置文件确实已经拷贝过去了且发生改变了。

可以在命令行上使用 --force-handlers 选项,也可在 play 级别使用 force_handlers: true 指令,它们都表示即使该节点执行任务失败了,也会执行已经触发的 handler 任务。

但要注意,只有因任务执行失败的情况才能强制执行 Handler 任务,如果是因为 unreachable 而导致的失败,显然是没有办法的。

--force-handlersforce_handlers: true 是对 play 全局生效的,如果想针对单个任务,也可以使用 rescue 或 always 的方式来 flush handler。例如:

tasks: 
  - block:
      - template: 
          src: nginx.conf.j2
          dest: /etc/nginx/nginx.conf
        notify: restart nginx
    rescue: 
      - meta: flush_handlers
标签云