Ansible. Середній рівень¶
У цьому розділі ви продовжите вивчати, як працювати з Ansible.
Цілі: у цьому розділі ви дізнаєтеся, як:
працювати зі змінними;
використовувати цикли;
керувати змінами стану та реагувати на них;
керувати асинхронними задачами.
ansible, module, playbook
Знання:
Складність:
Час читання: 30 хвилин
У попередньому розділі ви дізналися, як інсталювати Ansible, використовувати його в командному рядку або як писати playbooks, щоб сприяти повторному використанню вашого коду.
У цьому розділі ми можемо розпочати знайомство з деякими більш просунутими уявленнями про те, як використовувати Ansible, а також відкриємо кілька цікавих завдань, які ви будете регулярно використовувати.
Змінні¶
Примітка
Більше інформації можна [знайти тут](https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html).
В 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
Керування циклом¶
За допомогою циклу ви можете повторити завдання по списку, хешу або словнику, наприклад.
Примітка
Більше інформації можна [знайти тут](https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html).
Простий приклад використання, створення 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 }}
```
Умови¶
Примітка
Більше інформації можна знайти [тут](https://docs.ansible.com/ansible/latest/user_guide/playbooks_conditionals.html).
Оператор 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¶
Примітка
Додаткову інформацію можна [знайти тут](https://docs.ansible.com/ansible/latest/user_guide/playbooks_handlers.html).
Handlers дозволяють запускати операції, наприклад перезапуск служби, коли відбуваються зміни.
Модуль, будучи ідемпотентним, може виявити, що у віддаленій системі відбулася значна зміна, і таким чином запустити операцію у відповідь на цю зміну. Сповіщення надсилається в кінці блоку завдань з playbook, і операція реакції буде запущена лише один раз, навіть якщо кілька завдань надсилають одне й те саме сповіщення.

Наприклад, кілька завдань можуть вказувати на необхідність перезапуску служби 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"
Асинхронні завдання¶
Примітка
Більше інформації можна знайти [тут](https://docs.ansible.com/ansible/latest/user_guide/playbooks_async.html).
За замовчуванням 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