Ansible playbook чи role: що обрати і коли
Дізнайтеся, коли достатньо плоского playbook, а коли потрібен role: структура каталогів, ansible-galaxy init, виклики role та пріоритет змінних.
Ansible playbook і role: у чому різниця
Ansible playbook — це файл, який ви запускаєте за допомогою ansible-playbook. Він зіставляє групу хостів із потрібними для них діями. Ansible role — це каталог із фіксованою структурою, у якому зберігаються tasks, templates, handlers і default variables. Playbook викликає role за її назвою. Синтаксис tasks в обох випадках однаковий, тому питання не в тому, що можна описати. Питання в повторному використанні.
Почніть із простого playbook. Один site.yml із переліком tasks: — правильна структура для першої автоматизації. Вона залишається зручною довше, ніж зазвичай очікують. Перетворюйте playbook на role, коли той самий блок tasks потрібно виконати для другої групи хостів або коли файл перевищує приблизно 100 рядків і потрібну task уже неможливо швидко знайти прокручуванням.
Якщо ви ще не писали playbook, почніть із першого playbook для одного VPS і поверніться до цього питання, коли він почне розростатися.
Коли простий playbook є правильним рішенням
Простий playbook доречний, якщо роботу виконують один раз, на одному хості або цей код більше ніхто не читатиме. Розгортання одного сервера застосунку чи встановлення оновлень на сервері перед вікном технічного обслуговування не потребує дерева каталогів. Роль додає сім каталогів і ще один рівень абстракції. Якщо єдиним викликачем є playbook, що лежить поруч, така абстракція нічого не дає, але щоразу змушує переходити в інший файл, щоб побачити фактичні дії.
Простий playbook перестає бути правильним рішенням у конкретний момент, і цей момент легко помітити. Ви копіюєте блок завдань у другий playbook. Це і є сигналом. Відтепер кожне виправлення потрібно вносити двічі, а одного дня його внесуть лише в одне місце.
Фактичний вміст директорії ролі
roles/common/
defaults/main.yml
vars/main.yml
tasks/main.yml
handlers/main.yml
templates/99-hardening.conf.j2
files/
meta/main.ymltasks/main.ymlє точкою входу. Ansible запускає цей файл, коли викликається роль. Усі інші директорії необов’язкові.defaults/main.ymlмістить змінні, які очікується, що виклична сторона перевизначить. Це джерело з найнижчим пріоритетом в Ansible, тому майже будь-яке інше джерело має вищий пріоритет.vars/main.ymlмістить змінні, які виклична сторона не повинна перевизначати. Ці змінні мають вищий пріоритет, ніж inventory, що є вагомою підставою для такого рішення. Використовуйте цю директорію рідко.handlers/main.ymlмістить задачі, які запускаються черезnotify. Handler виконується наприкінці play один раз, незалежно від кількості задач, які його викликали.files/містить файли, які модульcopyкопіює без змін, аtemplates/— Jinja2-шаблони, які рендерить модульtemplate. Усередині ролі посилайтеся на обидва типи файлів лише за іменем без шляху, оскільки Ansible спочатку шукає їх у власних директоріях ролі.meta/main.ymlоголошує залежності ролі та метадані, які читає Ansible Galaxy.
Структура не є питанням стилю. Ansible шукає файли саме за цими шляхами, тому шаблон, який ви розмістили в roles/common/template/ (в однині), просто не буде знайдено.
Створення спільної ролі за допомогою ansible-galaxy init
mkdir -p ~/infra/roles
cd ~/infra
ansible-galaxy init --init-path roles commonЦя команда створює повний каркас у roles/common, зокрема каталоги, які ви не використовуватимете, і заготовки main.yml, що містять лише ---. Видаліть каталоги, які залишаються порожніми. Порожній vars/main.yml не заважає Ansible, але приховує, які файли ролі справді важливі.
Тепер заповніть файли, які виконують потрібну роботу. Спочатку налаштуйте defaults, оскільки це публічний інтерфейс ролі.
# roles/common/defaults/main.yml
---
common_packages:
- ufw
- fail2ban
- unattended-upgrades
common_admin_group: admins
common_permit_root_login: "no"
common_password_authentication: "no"Візьміть у лапки "no" і "yes". Ansible аналізує YAML за допомогою PyYAML. PyYAML сприймає необрамлене no як логічне значення false, тому візуалізований рядок конфігурації стає PermitRootLogin False, і sshd відхиляє його. Лапки зберігають це значення як рядок.
# roles/common/tasks/main.yml
---
- name: Install the base packages
ansible.builtin.apt:
name: "{{ common_packages }}"
state: present
update_cache: true
cache_valid_time: 3600
- name: Create the admin group
ansible.builtin.group:
name: "{{ common_admin_group }}"
state: present
- name: Install the sshd hardening drop-in
ansible.builtin.template:
src: 99-hardening.conf.j2
dest: /etc/ssh/sshd_config.d/99-hardening.conf
owner: root
group: root
mode: "0644"
validate: /usr/sbin/sshd -t -f %s
notify: Restart sshd# roles/common/handlers/main.yml
---
- name: Restart sshd
ansible.builtin.service:
name: ssh
state: restarted# roles/common/templates/99-hardening.conf.j2
# Managed by Ansible. Local edits are overwritten on the next run.
PermitRootLogin {{ common_permit_root_login }}
PasswordAuthentication {{ common_password_authentication }}У Debian та Ubuntu systemd unit має назву ssh, а в системах сімейства RHEL — sshd. Handler із неправильною назвою завершується помилкою лише тоді, коли шаблон справді змінюється. Тому така проблема зазвичай виявляється через кілька тижнів.
Рядок validate — найважливіша частина цього завдання. Ansible рендерить шаблон у тимчасовий файл, підставляє шлях до цього файлу замість %s і запускає команду. Файл призначення замінюється лише якщо команда завершується з кодом 0. Додайте до шаблону неправильну директиву та запустіть завдання ще раз: завдання завершиться помилкою з failed to validate, справжній /etc/ssh/sshd_config.d/99-hardening.conf залишиться без змін, і ви все ще матимете доступ до сервера. Пам’ятайте, що ця перевірка аналізує не лише синтаксис. Якщо sshd -t не може прочитати host keys, команда завершується з кодом sshd: no hostkeys available -- exiting., а Ansible повідомляє той самий failed to validate. Тому перед тим як звинувачувати шаблон, прочитайте msg модуля.
Як playbook викликає роль
# site.yml
---
- name: Base configuration for every server
hosts: all
become: true
roles:
- common# inventory.ini
[local]
localhost ansible_connection=localansible-playbook -i inventory.ini site.ymlPlay має завершуватися failed=0 у підсумковому звіті. Передавайте параметри в місці виклику в розгорнутій формі. Так одна роль обслуговує дві групи хостів:
roles:
- role: common
common_admin_group: ops
common_permit_root_login: prohibit-passwordЄ одне правило порядку, яке дивує майже всіх. Play може містити pre_tasks, roles, tasks і post_tasks, а Ansible виконує їх саме в такому порядку незалежно від порядку, у якому вони записані у файлі. Розмістіть tasks: перед roles: — ролі все одно виконуватимуться першими. Тому якщо щось має відбутися до ролі, це потрібно додати до pre_tasks:, а не на початок tasks:.
- name: Ordering demonstration
hosts: local
gather_facts: false
pre_tasks:
- name: Runs first
ansible.builtin.debug:
msg: pre
roles:
- common
tasks:
- name: Runs after the role
ansible.builtin.debug:
msg: task
post_tasks:
- name: Runs last
ansible.builtin.debug:
msg: postЩоб викликати роль зі списку завдань, а не через ключ roles:, використовуйте import_role або include_role.
tasks:
- name: Static, read when the playbook is parsed
ansible.builtin.import_role:
name: common
- name: Dynamic, resolved when the task runs
ansible.builtin.include_role:
name: postgres
when: "'db' in group_names"import_role є статичним. Ansible читає роль під час розбору, а її завдання стають частиною play. Тому ansible-playbook --list-tasks site.yml перелічує їх, а тег для імпорту застосовується до кожного завдання всередині. include_role є динамічним. Ansible нічого не читає, доки не буде виконано завдання. Це дає змогу визначати ім’я ролі зі змінної або циклу. Недолік полягає в тому, що такі завдання не відображаються в --list-tasks і --start-at-task.
Тут є одна типова пастка. when: у завданні include_role обчислюється до того, як defaults/main.yml включеної ролі стане доступним у поточній області видимості. Додайте when: common_packages | length > 0 до include — і виконання завершиться з помилкою 'common_packages' is undefined, навіть якщо цю змінну визначено саме в ролі, яку ви включаєте. Виправлення полягає в тому, щоб винести перемикач із ролі: розмістіть його в group_vars/all.yml, де він доступний у всіх потрібних місцях, а значення за замовчуванням ролі залиште для параметрів, які використовує сама роль.
Яка змінна має пріоритет: defaults, group_vars, vars, extra vars
Ansible документує понад двадцять рівнів пріоритету змінних. Чотири з них визначають майже всі практичні випадки. Нижче вони наведені від найнижчого до найвищого пріоритету.
roles/<name>/defaults/main.ymlрозташована майже внизу. Майже будь-яке значення, задане в іншому місці, має вищий пріоритет. Саме тому сюди варто поміщати параметри ролі, які можна налаштовувати.group_vars/іhost_vars/розташовані посередині. Тут мають бути значення, визначені для вашого сайту. Вони коректно перевизначають defaults ролі.roles/<name>/vars/main.ymlмає вищий пріоритет, ніжhost_vars. Значення, задане тут, не можна перевизначити з inventory. Використовуйте цей рівень для значень, які мають залишатися внутрішньо узгодженими в ролі, наприклад для імені пакета, яке має відповідати імені сервісу.- Параметр ролі, переданий під час її виклику, має вищий пріоритет, ніж
vars/main.yml, а-eу командному рядку має найвищий пріоритет, зокрема й над параметрами ролі.
Приблизно за хвилину можна побачити, як це працює. Передайте невеликій ролі одне значення за замовчуванням і одну змінну ролі, а потім задайте ті самі імена в host_vars.
# roles/prec/defaults/main.yml
---
prec_tunable: from-defaults
prec_internal: from-defaults# roles/prec/vars/main.yml
---
prec_internal: from-rolevars# host_vars/localhost.yml
---
prec_tunable: from-hostvars
prec_internal: from-hostvars# roles/prec/tasks/main.yml
---
- name: Show which value survived
ansible.builtin.debug:
msg: "tunable={{ prec_tunable }} internal={{ prec_internal }}"ansible-playbook -i inventory.ini prec.yml
ansible-playbook -i inventory.ini prec.yml -e prec_internal=from-cliПерший запуск виводить tunable=from-hostvars internal=from-rolevars. Значення з inventory мало вищий пріоритет, ніж default ролі, але нижчий, ніж змінна ролі. Другий запуск виводить internal=from-cli, оскільки extra vars мають найвищий пріоритет, і жодне значення з нижчим пріоритетом не може його перевизначити. Саме тому -e підходить для одноразового запуску, але не для скрипта, який ви зберігаєте та використовуєте надалі: ця змінна непомітно має вищий пріоритет, ніж усі відповідні рішення у вашому репозиторії.
Практичне правило таке: якщо значення має можна налаштовувати, розміщуйте його в defaults/. Розміщення значення в vars/ повідомляє кожному майбутньому користувачу ролі, що inventory не може його змінити. Іноді саме цього ви й хотіли, але зазвичай це трапляється випадково.
Перевірте ідемпотентність ролі: запустіть її двічі
Надійний запуск Ansible під час другого виконання дає той самий результат і повідомляє, що нічого не змінилося. Запустіть playbook двічі та перегляньте підсумок.
ansible-playbook -i inventory.ini site.yml
ansible-playbook -i inventory.ini site.ymlДругий підсумок має виглядати так:
PLAY RECAP *********************************************************************
localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0changed=0 означає, що кожен модуль перевірив поточний стан і виявив, що роботу вже виконано. changed=2 під час другого запуску означає, що два завдання не можуть визначити різницю, тому вони безперервно переписуватимуть файли та перезапускатимуть сервіси. Зазвичай причиною є command або shell, оскільки Ansible не може визначити, що саме зробила довільна команда.
# traps.yml
---
- name: Command modules do not know what they changed
hosts: local
gather_facts: false
tasks:
- name: This appends a line on every run
ansible.builtin.shell: "echo run >> /tmp/grow.txt"
- name: This appends a line only once
ansible.builtin.shell: "echo run >> /tmp/guarded.txt"
args:
creates: /tmp/guarded.txtЗапустіть цей playbook двічі, а потім порахуйте рядки з wc -l /tmp/grow.txt /tmp/guarded.txt. /tmp/grow.txt містить два рядки, а /tmp/guarded.txt — один. Під час другого запуску завдання із перевіркою взагалі не виконувалося, а його результат містить повідомлення skipped, since /tmp/guarded.txt exists, оскільки creates дає модулю видимий результат, який можна спочатку перевірити. Якщо команда не створює такого результату, зареєструйте її вивід і самостійно визначте стан за допомогою changed_when.
ansible-playbook --check --diff site.yml прогнозує зміни, не вносячи їх, а --diff виводить точні рядки, які шаблон переписав би. Читайте цей вивід з урахуванням одного застереження: завдання shell і command пропускаються в режимі check mode, тому план, який виглядає чистим, усе одно може приховувати невиконану роботу.
Окремий стовпець у цьому підсумку потребує такої самої уваги: хост, до якого Ansible не зміг підключитися, зараховується до unreachable, а не до failed, і жодне його завдання взагалі не виконувалося. Тому заздалегідь визначте, чи має один недоступний хост зупиняти весь запуск, перш ніж застосовувати цю роль до більш ніж кількох машин.
Чому Ansible повідомляє, що роль не знайдено
Ansible спочатку шукає каталог roles/ поруч із файлом playbook, а потім у roles_path. Пошук залежить від playbook, а не від вашої оболонки.
ERROR! the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonelyЦе повідомлення означає, що site.yml і roles/ більше не узгоджуються. Ansible також виводить шляхи, які він перевірив. Зберігайте ці два елементи в одному каталозі. Запуск із батьківського каталогу допустимий, оскільки визначальним є шлях до playbook:
ansible-playbook -i infra/inventory.ini infra/site.ymlІснує менш очевидний варіант тієї самої проблеми. Ansible ігнорує ansible.cfg у поточному каталозі, якщо цей каталог доступний для запису всім користувачам. Будь-який користувач сервера може додати туди конфігурацію та змінити результат виконання.
[WARNING]: Ansible is being run in a world writable directory (/tmp/infra), ignoring it as an ansible.cfg source.Тоді параметри roles_path і inventory непомітно не застосовуються, а пошук ролі завершується помилкою, яка не пов’язана з ролями. ansible --version виводить config file, яке Ansible фактично завантажив, а ansible-config dump --only-changed — усі параметри, що відрізняються від вбудованих значень за замовчуванням. Перевіряйте обидва параметри, коли виконання поводиться так, ніби конфігурації не існує.
Ролі: requirements.yml і зафіксована версія
Роль, написану кимось іншим, потрібно встановлювати, а не копіювати. Оголосіть її один раз:
# requirements.yml
---
roles:
- name: postgres
src: https://github.com/example/ansible-role-postgres
scm: git
version: v1.4.0ansible-galaxy install -r requirements.yml -p galaxy_rolesЗавжди задавайте version. Без цього ви отримаєте вміст гілки за замовчуванням на день виконання команди. Через це розгортання, яке працювало минулого місяця, може зламатися без жодних змін у вашому репозиторії. Вкажіть для roles_path каталог завантаження та не додавайте цей каталог до git:
# ansible.cfg
[defaults]
inventory = inventory.ini
roles_path = ./galaxy_rolesРолі в roles/ поруч із playbook також знаходяться, оскільки цей шлях завжди перевіряється додатково до roles_path. Тому власні ролі залишаються зафіксованими в репозиторії та проходять перевірку, а сторонні ролі завантажуються відтворювано й фіксуються на tag.
Коли ролі вже не вирішують завдання
Роль — це одиниця повторного використання в межах одного запуску Ansible. Вона не створює сервери або DNS-записи у вашого провайдера. Спроба змусити її робити це перетворює playbook на конфігурацію, яку ніхто не хоче підтримувати. Перед початком варто прочитати про розподіл завдань між Ansible і Terraform. Роль також не замінює проєктування inventory: коли машин стає більше кількох, спосіб групування та підключення до цих серверів має більше значення, ніж спосіб розподілу завдань за файлами.
Посилення безпеки, яке встановлює ця роль common, також потребує окремих рішень. Наведений drop-in задає лише дві директиви. Тому перед визначенням того, що має входити до ролі для кожного вашого хоста, прочитайте які параметри SSH справді варто змінити та як налаштувати автоматичне застосування Ubuntu оновлень безпеки.
FAQ
Коли слід перетворити Ansible playbook на роль?
Коли той самий блок завдань потрібно виконати в іншому play або для іншої групи хостів. Копіювання завдань між playbook є сигналом, що відтепер кожне виправлення доведеться застосовувати двічі, а одного дня його буде застосовано лише один раз. Один playbook обсягом приблизно до 100 рядків, який завжди націлений лише на одну групу, не отримує переваг від ролі, а додаткові каталоги ускладнюють його читання.
Чи виконуються ролі перед завданнями в тому самому play?
Так. Ansible виконує pre_tasks, потім усе, що перелічено в roles:, далі tasks:, а потім post_tasks:; порядок появи цих ключів у файлі не має значення. Розміщення tasks: вище за roles: не змушує ці завдання виконуватися першими. Якщо щось має відбутися до ролі, помістіть це в pre_tasks:.
Чому значення з group_vars не перевизначає роль?
Перевірте, чи не задано змінну в vars/main.yml ролі замість defaults/main.yml. vars/ має вищий пріоритет за group_vars і host_vars у порядку пріоритетів Ansible, тому inventory не може його перевизначити. Перемістіть змінну до defaults/main.yml. Цей рівень розташований ближче донизу порядку пріоритетів і є правильним місцем для всього, що має змінювати caller. Щоб підтвердити, що причина саме в пріоритеті, а не в помилці друку, один раз запустіть із -e name=value, який має вищий пріоритет за будь-яке інше джерело.
Чому Ansible повідомляє, що роль не знайдена?
Пошук починається поруч із файлом playbook, тому site.yml і roles/ мають розташовуватися в одному каталозі. У повідомленні про помилку наведено шляхи, які Ansible перевірив, як у the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely. Запуск playbook із батьківського каталогу не є проблемою, оскільки пошук виконується за шляхом до playbook, а не за поточним робочим каталогом оболонки. Якщо ви використовуєте roles_path з ansible.cfg, переконайтеся, що цей файл завантажено за допомогою ansible --version, оскільки Ansible ігнорує його, якщо робочий каталог доступний для запису всім користувачам.
Чи потрібна ansible-galaxy init для створення ролі?
Ні. Роль — це лише каталоги з очікуваними назвами, тому mkdir -p roles/common/tasks разом із tasks/main.yml уже утворюють робочу роль. ansible-galaxy init --init-path roles common заощаджує час на введенні та створює повний каркас, зокрема meta/main.yml і заготовку README. Видаліть порожні каталоги, оскільки порожній vars/main.yml приховує, які файли в ролі справді виконують роботу.