Handlers:在变更时运行操作

有时,您希望仅在机器上进行更改时才运行任务。例如,如果某个任务更新了服务的配置,您可能希望重新启动该服务,但如果配置未更改,则不希望重新启动。Ansible 使用 Handlers 来解决这种情况。Handlers 是仅在收到通知时才运行的任务。

Handler 示例

此 Playbook verify-apache.yml 包含一个带有 handler 的 Play。

---
- name: Verify apache installation
  hosts: webservers
  vars:
    http_port: 80
    max_clients: 200
  remote_user: root
  tasks:
    - name: Ensure apache is at the latest version
      ansible.builtin.yum:
        name: httpd
        state: latest

    - name: Write the apache config file
      ansible.builtin.template:
        src: /srv/httpd.j2
        dest: /etc/httpd.conf
      notify:
        - Restart apache

    - name: Ensure apache is running
      ansible.builtin.service:
        name: httpd
        state: started

  handlers:
    - name: Restart apache
      ansible.builtin.service:
        name: httpd
        state: restarted

在此示例 Playbook 中,在 Play 中的所有任务完成后,handler 会重新启动 Apache 服务器。

通知 Handlers

任务可以使用 notify 关键字指示一个或多个 handler 执行。notify 关键字可以应用于任务,并接受在任务更改时通知的 handler 名称列表。或者,也可以提供包含单个 handler 名称的字符串。以下示例演示了如何通过单个任务通知多个 handler。

tasks:
- name: Template configuration file
  ansible.builtin.template:
    src: template.j2
    dest: /etc/foo.conf
  notify:
    - Restart apache
    - Restart memcached

handlers:
  - name: Restart memcached
    ansible.builtin.service:
      name: memcached
      state: restarted

  - name: Restart apache
    ansible.builtin.service:
      name: apache
      state: restarted

在上面的示例中,handler 在任务更改时按以下顺序执行:Restart memcached, Restart apache。handler 按照它们在 handlers 部分中定义的顺序执行,而不是按照 notify 语句中列出的顺序执行。多次通知同一个 handler 将导致该 handler 只执行一次,而不管有多少任务通知它。例如,如果多个任务更新配置文件并通知一个 handler 来重新启动 Apache,Ansible 只会重启 Apache 一次,以避免不必要的重启。

通知和循环

任务可以使用循环来通知 handler。当与变量结合使用以触发多个动态通知时,这尤其有用。

请注意,如果任务整体发生更改,则会触发 handler。当使用循环时,如果任何循环项发生更改,则会设置已更改状态。也就是说,任何更改都会触发所有 handler。

tasks:
- name: Template services
  ansible.builtin.template:
    src: "{{ item }}.j2"
    dest: /etc/systemd/system/{{ item }}.service
  # Note: if *any* loop iteration triggers a change, *all* handlers are run
  notify: Restart {{ item }}
  loop:
    - memcached
    - apache

handlers:
  - name: Restart memcached
    ansible.builtin.service:
      name: memcached
      state: restarted

  - name: Restart apache
    ansible.builtin.service:
      name: apache
      state: restarted

在上面的示例中,如果任何一个模板文件发生更改,memcached 和 apache 都会被重新启动,如果没有文件更改,则都不会被重新启动。

命名 Handlers

必须命名 Handlers,以便任务能够使用 notify 关键字通知它们。

或者,handler 可以使用 listen 关键字。使用此 handler 关键字,handler 可以监听可以分组多个 handler 的主题,如下所示:

tasks:
  - name: Restart everything
    command: echo "this task will restart the web services"
    notify: "restart web services"

handlers:
  - name: Restart memcached
    service:
      name: memcached
      state: restarted
    listen: "restart web services"

  - name: Restart apache
    service:
      name: apache
      state: restarted
    listen: "restart web services"

通知 restart web services 主题会导致执行所有监听该主题的 handler,而不管这些 handler 如何命名。

这种使用方式使得触发多个 handler 更加容易。它还将 handler 与它们的名称分离,从而更容易在 Playbook 和角色之间共享 handler(尤其是在使用来自共享源(如 Ansible Galaxy)的第三方角色时)。

每个 handler 都应该有一个全局唯一的名称。如果定义了多个具有相同名称的 handler,则只能通知和执行最后一个加载到 Play 中的 handler,有效地屏蔽所有先前具有相同名称的 handler。

无论 handler 在哪里定义,handler(handler 名称和监听主题)都只有一个全局范围。这还包括在角色中定义的 handler。

控制 Handlers 何时运行

默认情况下,handler 在特定 Play 中的所有任务完成后运行。在以下每个部分之后,通知的 handler 会按照以下顺序自动执行:pre_tasksroles/taskspost_tasks。这种方法很有效,因为 handler 只运行一次,而不管有多少任务通知它。例如,如果多个任务更新配置文件并通知一个 handler 来重新启动 Apache,Ansible 只会重启 Apache 一次,以避免不必要的重启。

如果需要 handler 在 Play 结束之前运行,请添加一个任务,使用 meta 模块刷新它们,该模块执行 Ansible 操作

tasks:
  - name: Some tasks go here
    ansible.builtin.shell: ...

  - name: Flush handlers
    meta: flush_handlers

  - name: Some other tasks
    ansible.builtin.shell: ...

meta: flush_handlers 任务会触发在该 Play 中该点已被通知的所有 handler。

一旦 handler 执行,无论是在每个提到的部分之后自动执行,还是通过 flush_handlers meta 任务手动执行,它们都可以在 Play 的后续部分再次被通知和运行。

定义任务何时变更

您可以使用 changed_when 关键字来控制何时通知 handler 关于任务更改。

在以下示例中,handler 在每次复制配置文件时都会重新启动服务

tasks:
  - name: Copy httpd configuration
    ansible.builtin.copy:
      src: ./new_httpd.conf
      dest: /etc/httpd/conf/httpd.conf
    # The task is always reported as changed
    changed_when: True
    notify: Restart apache

有关 changed_when 的更多信息,请参阅定义“已更改”

在 Handlers 中使用变量

您可能希望您的 Ansible handler 使用变量。例如,如果服务的名称因发行版而略有不同,您希望您的输出显示每个目标机器重新启动的服务的确切名称。避免将变量放在 handler 的名称中。由于 handler 名称会提前进行模板化,因此 Ansible 可能无法为像这样的 handler 名称提供值:

handlers:
# This handler name may cause your play to fail!
- name: Restart "{{ web_service_name }}"

如果 handler 名称中使用的变量不可用,则整个 Play 将失败。在 Play 中更改该变量不会导致新创建的 handler。

相反,将变量放置在 handler 的任务参数中。您可以使用 include_vars 加载值,如下所示:

tasks:
  - name: Set host variables based on distribution
    include_vars: "{{ ansible_facts.distribution }}.yml"

handlers:
  - name: Restart web service
    ansible.builtin.service:
      name: "{{ web_service_name | default('httpd') }}"
      state: restarted

虽然 handler 名称可以包含模板,但 listen 主题不能。

角色中的 Handlers

来自角色的 handler 不仅仅包含在其角色中,而是与来自 Play 的所有其他 handler 一起插入到全局范围中。因此,它们可以在定义它们的角色之外使用。这也意味着它们的名称可能会与角色外部的 handler 冲突。为了确保通知来自角色的 handler 而不是来自角色外部的同名 handler,请使用以下形式的名称通知 handler:role_name : handler_name

roles 部分内通知的 handler 会在 tasks 部分的末尾自动刷新,但在任何 tasks handler 之前。

Handlers 中的包含和导入

将诸如 include_task 这样的动态包含作为处理器通知会导致执行该包含中的所有任务。无法通知在动态包含内部定义的处理器。

将诸如 import_task 这样的静态包含作为处理器,会导致在剧本执行之前,该处理器被该导入中的处理器有效重写。静态包含本身无法被通知;另一方面,可以单独通知该包含中的任务。

元任务作为处理器

自 Ansible 2.14 起,允许将 元任务 用作处理器并进行通知。但请注意,flush_handlers 不能用作处理器,以防止出现意外行为。

限制

处理器不能运行 import_roleinclude_role。处理器忽略标签