跳至内容

Ansible 中级

在本章中,您将继续学习如何使用 Ansible。


目标:在本章中,您将学会如何

✔ 使用变量;
✔ 使用循环;
✔ 管理状态变更并对其做出反应;
✔ 管理异步任务。

🏁 ansible, module, playbook

知识: ⭐ ⭐ ⭐
复杂度⭐ ⭐

阅读时间: 30 分钟


在前一章中,您学习了如何安装 Ansible,如何在命令行中使用它,以及如何编写 playbook 来提高代码的重用性。

在本章中,我们将开始探索更多高级的 Ansible 使用技巧以及一些您将经常使用的有趣任务。

变量

注意

更多信息可在此处找到。

在 Ansible 中,存在不同类型的原始变量:

  • 字符串 (strings),
  • 整数 (integers),
  • 布尔值 (booleans)。

这些变量可以组织成:

  • 字典 (dictionaries),
  • 列表 (lists)。

变量可以在不同位置定义,例如 playbook、role 或命令行。

例如,从 playbook 中

---
- hosts: apache1
  vars:
    port_http: 80
    service:
      debian: apache2
      rhel: httpd

或从命令行中

ansible-playbook deploy-http.yml --extra-vars "service=httpd"

定义后,可以通过双大括号调用变量来使用它们:

  • {{ port_http }} 用于简单值,
  • {{ service['rhel'] }}{{ service.rhel }} 用于字典。

例如

- name: make sure apache is started
  ansible.builtin.systemd:
    name: "{{ service['rhel'] }}"
    state: started

当然,也可以访问 Ansible 的全局变量(facts)(操作系统类型、IP 地址、虚拟机名称等)。

外部化变量

变量可以包含在 playbook 外部的文件中,在这种情况下,该文件必须在 playbook 中使用 vars_files 指令进行定义。

---
- hosts: apache1
  vars_files:
    - myvariables.yml

myvariables.yml 文件

---
port_http: 80
ansible.builtin.systemd::
  debian: apache2
  rhel: httpd

也可以使用 include_vars 模块动态添加。

- name: Include secrets.
  ansible.builtin.include_vars:
    file: vault.yml

显示变量

要显示变量,您必须像这样激活 debug 模块:

- ansible.builtin.debug:
    var: service['debian']

您也可以在文本中使用变量:

- ansible.builtin.debug:
    msg: "Print a variable in a message : {{ service['debian'] }}"

保存任务的返回结果

要保存任务的返回结果并稍后访问它,您必须在任务本身中使用 register 关键字。

使用已保存的变量

- name: /home content
  shell: ls /home
  register: homes

- name: Print the first directory name
  ansible.builtin.debug:
    var: homes.stdout_lines[0]

- name: Print the first directory name
  ansible.builtin.debug:
    var: homes.stdout_lines[1]

注意

变量 homes.stdout_lines 是一个字符串变量列表,这是一种我们尚未遇到的组织变量的方式。

组成已保存变量的字符串可以通过 stdout 值访问(这允许您执行类似 homes.stdout.find("core") != -1 的操作),使用循环(请参阅 loop)来利用它们,或者像上一个示例中那样简单地通过它们的索引来访问。

练习:

  • 编写一个 playbook,命名为 play-vars.yml,使用全局变量打印目标系统的发行版名称和主版本号。

  • 编写一个 playbook,使用以下字典来显示将要安装的服务:

service:
  web:
    name: apache
    rpm: httpd
  db:
    name: mariadb
    rpm: mariadb-server

默认类型应为“web”。

  • 使用命令行覆盖 type 变量。

  • 将变量外部化到一个 vars.yml 文件中。

循环管理

循环允许您对列表、哈希或字典等进行任务迭代,例如。

注意

更多信息可在此处找到。

一个简单的使用示例,创建 4 个用户:

- name: add users
  user:
    name: "{{ item }}"
    state: present
    groups: "users"
  loop:
     - antoine
     - patrick
     - steven
     - xavier

在每次循环迭代中,列表的值将被存储在 item 变量中,可以在循环代码中访问。

当然,列表也可以定义在外部文件中:

users:
  - antoine
  - patrick
  - steven
  - xavier

并在任务中像这样使用(在包含 vars 文件之后):

- name: add users
  user:
    name: "{{ item }}"
    state: present
    groups: "users"
  loop: "{{ users }}"

我们可以使用在学习已保存变量时看到的示例来改进它。使用已保存的变量:

- name: /home content
  shell: ls /home
  register: homes

- name: Print the directories name
  ansible.builtin.debug:
    msg: "Directory => {{ item }}"
  loop: "{{ homes.stdout_lines }}"

字典也可以在循环中使用。

在这种情况下,您必须使用 **jinja 过滤器** (jinja 是 Ansible 使用的模板引擎) 将字典转换为一个项:| dict2items

在循环中,可以使用 item.key(对应字典的键)和 item.value(对应键的值)。

让我们通过一个具体的例子来看一下,展示系统用户的管理:

---
- hosts: rocky8
  become: true
  become_user: root
  vars:
    users:
      antoine:
        group: users
        state: present
      steven:
        group: users
        state: absent

  tasks:

  - name: Manage users
    user:
      name: "{{ item.key }}"
      group: "{{ item.value.group }}"
      state: "{{ item.value.state }}"
    loop: "{{ users | dict2items }}"

注意

循环可以用于许多事情。当您使用 Ansible 的需求变得更复杂时,您将发现它们提供的可能性。

练习:

  • 使用循环显示上一练习中 service 变量的内容。

注意

您需要使用 jinja 过滤器 list 将您的 service 变量(一个字典)转换为一个项或列表,如下所示:

{{ service.values() | list }}

条件语句

注意

更多信息可在此处找到。

when 语句在很多情况下非常有用,例如不在某些类型的服务器上执行某些操作,如果文件或用户不存在等。

注意

when 语句后面,变量不需要双大括号(它们实际上是 Jinja2 表达式...)。

- name: "Reboot only Debian servers"
  reboot:
  when: ansible_os_family == "Debian"

条件可以用括号组合:

- name: "Reboot only CentOS version 6 and Debian version 7"
  reboot:
  when: (ansible_distribution == "CentOS" and ansible_distribution_major_version == "6") or
        (ansible_distribution == "Debian" and ansible_distribution_major_version == "7")

逻辑 AND 的条件可以作为列表提供:

- name: "Reboot only CentOS version 6"
  reboot:
  when:
    - ansible_distribution == "CentOS"
    - ansible_distribution_major_version == "6"

您可以测试布尔值并验证它是否为真:

- name: check if directory exists
  stat:
    path: /home/ansible
  register: directory

- ansible.builtin.debug:
    var: directory

- ansible.builtin.debug:
    msg: The directory exists
  when:
    - directory.stat.exists
    - directory.stat.isdir

您也可以测试它是否不为真:

when:
  - file.stat.exists
  - not file.stat.isdir

您可能需要测试一个变量是否存在,以避免执行错误。

when: myboolean is defined and myboolean

练习:

  • 仅当 type 等于 web 时,打印 service.web 的值。

管理变更:handlers

注意

更多信息可在此处找到。

当发生变更时,handlers 可以启动操作,例如重新启动服务。

由于模块是幂等的,playbook 可以检测到远程系统上发生了重大变更,从而触发操作以响应此变更。在 playbook 任务块结束时会发送一个通知,即使有多个任务发送相同的通知,也只会触发一次响应操作。

Handlers

例如,多个任务可能指示由于其配置文件发生更改,需要重新启动 httpd 服务。但是,该服务只会重启一次,以避免多次不必要的启动。

- name: template configuration file
  template:
    src: template-site.j2
    dest: /etc/httpd/sites-availables/test-site.conf
  notify:
     - restart memcached
     - restart httpd

Handler 是一种任务,由一个唯一的全局名称引用:

  • 一个或多个 notifiers 激活它。
  • 它不会立即启动,而是等到所有任务完成后再运行。

Handlers 示例:

handlers:

  - name: restart memcached
    systemd:
      name: memcached
      state: restarted

  - name: restart httpd
    systemd:
      name: httpd
      state: restarted

从 Ansible 2.2 版本开始,handlers 也可以直接监听:

handlers:

  - name: restart memcached
    systemd:
      name: memcached
      state: restarted
    listen: "web services restart"

  - name: restart apache
    systemd:
      name: apache
      state: restarted
    listen: "web services restart"

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

异步任务

注意

更多信息可在此处找到。

默认情况下,SSH 连接在所有节点上执行各种 playbook 任务时保持打开状态。

这可能会导致一些问题,尤其是:

  • 如果任务的执行时间超过 SSH 连接超时时间;
  • 如果在操作期间连接中断(例如服务器重启);

在这种情况下,您必须切换到异步模式,并指定最大执行时间和检查主机状态的频率(默认为 10 秒)。

通过指定 poll 值为 0,Ansible 将执行任务并继续,而无需担心结果。

这是一个使用异步任务的示例,它允许您重启服务器并等待端口 22 再次可达:

# Wait 2s and launch the reboot
- name: Reboot system
  shell: sleep 2 && shutdown -r now "Ansible reboot triggered"
  async: 1
  poll: 0
  ignore_errors: true
  become: true
  changed_when: False

  # Wait the server is available
  - name: Waiting for server to restart (10 mins max)
    wait_for:
      host: "{{ inventory_hostname }}"
      port: 22
      delay: 30
      state: started
      timeout: 600
    delegate_to: localhost

您还可以决定启动一个长期运行的任务然后不管它(fire and forget),因为其执行在 playbook 中无关紧要。

练习结果

  • 编写一个 playbook,名为 `play-vars.yml`,使用全局变量,打印目标系统的发行版名称和主版本号。
---
- hosts: ansible_clients

  tasks:

    - name: Print globales variables
      debug:
        msg: "The distribution is {{ ansible_distribution }} version {{ ansible_distribution_major_version }}"
$ ansible-playbook play-vars.yml

PLAY [ansible_clients] *********************************************************************************

TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]

TASK [Print globales variables] ************************************************************************
ok: [192.168.1.11] => {
    "msg": "The distribution is Rocky version 8"
}

PLAY RECAP *********************************************************************************************
192.168.1.11               : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
  • 编写一个 playbook,使用以下字典来显示将要安装的服务:
service:
  web:
    name: apache
    rpm: httpd
  db:
    name: mariadb
    rpm: mariadb-server

默认类型应为“web”。

---
- hosts: ansible_clients
  vars:
    type: web
    service:
      web:
        name: apache
        rpm: httpd
      db:
        name: mariadb
        rpm: mariadb-server

  tasks:

    - name: Print a specific entry of a dictionary
      debug:
        msg: "The {{ service[type]['name'] }} will be installed with the packages {{ service[type].rpm }}"
$ ansible-playbook display-dict.yml

PLAY [ansible_clients] *********************************************************************************

TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]

TASK [Print a specific entry of a dictionnaire] ********************************************************
ok: [192.168.1.11] => {
    "msg": "The apache will be installed with the packages httpd"
}

PLAY RECAP *********************************************************************************************
192.168.1.11               : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
  • 使用命令行覆盖 type 变量。
ansible-playbook --extra-vars "type=db" display-dict.yml

PLAY [ansible_clients] *********************************************************************************

TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]

TASK [Print a specific entry of a dictionary] ********************************************************
ok: [192.168.1.11] => {
    "msg": "The mariadb will be installed with the packages mariadb-server"
}

PLAY RECAP *********************************************************************************************
192.168.1.11               : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
  • 将变量外部化到一个 vars.yml 文件中。
type: web
service:
  web:
    name: apache
    rpm: httpd
  db:
    name: mariadb
    rpm: mariadb-server
---
- hosts: ansible_clients
  vars_files:
    - vars.yml

  tasks:

    - name: Print a specific entry of a dictionary
      debug:
        msg: "The {{ service[type]['name'] }} will be installed with the packages {{ service[type].rpm }}"
  • 使用循环显示上一练习中 service 变量的内容。

注意

您需要使用 jinja 过滤器 dict2itemslist 将您的 service 变量(一个字典)转换为一个项或列表,如下所示:

{{ service | dict2items }}
{{ service.values() | list }}

使用 dict2items

---
- hosts: ansible_clients
  vars_files:
    - vars.yml

  tasks:

    - name: Print a dictionary variable with a loop
      debug:
        msg: "{{item.key }} | The {{ item.value.name }} will be installed with the packages {{ item.value.rpm }}"
      loop: "{{ service | dict2items }}"              
$ ansible-playbook display-dict.yml

PLAY [ansible_clients] *********************************************************************************

TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]

TASK [Print a dictionary variable with a loop] ********************************************************
ok: [192.168.1.11] => (item={'key': 'web', 'value': {'name': 'apache', 'rpm': 'httpd'}}) => {
    "msg": "web | The apache will be installed with the packages httpd"
}
ok: [192.168.1.11] => (item={'key': 'db', 'value': {'name': 'mariadb', 'rpm': 'mariadb-server'}}) => {
    "msg": "db | The mariadb will be installed with the packages mariadb-server"
}

PLAY RECAP *********************************************************************************************
192.168.1.11               : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

使用 list

---
- hosts: ansible_clients
  vars_files:
    - vars.yml

  tasks:

    - name: Print a dictionary variable with a loop
      debug:
        msg: "The {{ item.name }} will be installed with the packages {{ item.rpm }}"
      loop: "{{ service.values() | list}}"
~                                                 
$ ansible-playbook display-dict.yml

PLAY [ansible_clients] *********************************************************************************

TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]

TASK [Print a dictionary variable with a loop] ********************************************************
ok: [192.168.1.11] => (item={'name': 'apache', 'rpm': 'httpd'}) => {
    "msg": "The apache will be installed with the packages httpd"
}
ok: [192.168.1.11] => (item={'name': 'mariadb', 'rpm': 'mariadb-server'}) => {
    "msg": "The mariadb will be installed with the packages mariadb-server"
}

PLAY RECAP *********************************************************************************************
192.168.1.11               : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
  • 仅当 type 等于 web 时,打印 service.web 的值。
---
- hosts: ansible_clients
  vars_files:
    - vars.yml

  tasks:

    - name: Print a dictionary variable
      debug:
        msg: "The {{ service.web.name }} will be installed with the packages {{ service.web.rpm }}"
      when: type == "web"


    - name: Print a dictionary variable
      debug:
        msg: "The {{ service.db.name }} will be installed with the packages {{ service.db.rpm }}"
      when: type == "db"
$ ansible-playbook display-dict.yml

PLAY [ansible_clients] *********************************************************************************

TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]

TASK [Print a dictionary variable] ********************************************************************
ok: [192.168.1.11] => {
    "msg": "The apache will be installed with the packages httpd"
}

TASK [Print a dictionary variable] ********************************************************************
skipping: [192.168.1.11]

PLAY RECAP *********************************************************************************************
192.168.1.11               : ok=2    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   

$ ansible-playbook --extra-vars "type=db" display-dict.yml

PLAY [ansible_clients] *********************************************************************************

TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]

TASK [Print a dictionary variable] ********************************************************************
skipping: [192.168.1.11]

TASK [Print a dictionary variable] ********************************************************************
ok: [192.168.1.11] => {
    "msg": "The mariadb will be installed with the packages mariadb-server"
}

PLAY RECAP *********************************************************************************************
192.168.1.11               : ok=2    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0