Перейти до змісту

Ansible. Середній рівень

У цьому розділі ви продовжите вивчати, як працювати з Ansible.


Цілі: В цьому розділі ви дізнаєтеся як:

✔ працювати зі змінними;
✔ використовувати цикли;
✔ керувати змінами стану та реагувати на них;
✔ керувати асинхронними задачами.

🏁 ansible, module, playbook

Знання: ⭐ ⭐ ⭐
Складність: ⭐ ⭐

Час читання: 30 хвилин


У попередньому розділі ви дізналися, як інсталювати Ansible, використовувати його в командному рядку або як писати playbooks, щоб сприяти повторному використанню вашого коду.

У цьому розділі ми можемо розпочати знайомство з деякими більш просунутими уявленнями про те, як використовувати Ansible, а також відкриємо кілька цікавих завдань, які ви будете регулярно використовувати.

Змінні

Примітка

Більше інформації можна знайти тут.

В Ansible існують різні типи примітивних змінних:

  • strings,
  • integers,
  • booleans.

Ці змінні можна організувати як:

  • словники,
  • списки.

Змінну можна визначити в різних місцях, наприклад, у підручнику, у ролі або, наприклад, з командного рядка.

Наприклад, із playbook:

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

або з командного рядка:

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

Після визначення змінну можна використовувати, викликавши її між подвійними дужками:

  • {{ port_http }} for a simple value,
  • {{ service['rhel'] }} or {{ service.rhel }} for a dictionary.

Наприклад:

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

Звичайно, також можна отримати доступ до глобальних змінних (facts) Ansible (тип ОС, 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 — це список змінних типу string, спосіб організації змінних, з яким ми ще не стикалися.

Доступ до рядків, які складають збережену змінну, можна отримати за допомогою значення 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 з попередньої вправи за допомогою циклу.

Примітка

Вам доведеться перетворити вашу змінну service, яка є словником, на список за допомогою фільтра jinja list, а саме:

{{ 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

Вправи:

  • Роздрукувати значення service.web лише тоді, коли type дорівнює web.

Керування змінами: handlers

Примітка

Додаткову інформацію можна знайти тут.

Handlers дозволяють запускати операції, наприклад перезапуск служби, коли відбуваються зміни.

Модуль, будучи ідемпотентним, може виявити, що у віддаленій системі відбулася значна зміна, і таким чином запустити операцію у відповідь на цю зміну. Сповіщення надсилається в кінці блоку завдань з 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 — це завдання, на яке посилається унікальне глобальне ім’я:

  • Він активується одним або декількома нотифікаторами.
  • Він не запускається відразу, а чекає, поки всі завдання будуть виконані, щоб запуститися.

Приклад handlers:

handlers:

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

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

Починаючи з версії 2.2 Ansible, обробники також можуть безпосередньо слухати:

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-з’єднання з хостами залишаються відкритими під час виконання різноманітних завдань на всіх вузлах.

Це може спричинити деякі проблеми, зокрема:

  • якщо час виконання завдання перевищує тайм-аут підключення SSH
  • якщо з'єднання перервано під час дії (наприклад, перезавантаження сервера)

У цьому випадку вам доведеться перейти в асинхронний режим і вказати максимальний час виконання, а також частоту (за замовчуванням 10 секунд), з якою ви будете перевіряти стан хоста.

Якщо вказати значення опитування 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

Ви також можете вирішити запустити довгострокове завдання та забути його (запустити й забути), оскільки виконання не має значення в 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 з попередньої вправи за допомогою циклу.

Примітка

Вам доведеться перетворити вашу змінну service, яка є словником, на елемент або список за допомогою фільтрів jinja dict2items або list як це:

{{ 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   
  • Роздрукувати значення service.web лише тоді, коли type дорівнює 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