Ansible playbook чи role: що обрати і коли
Дізнайтеся, коли достатньо плоского playbook, а коли потрібна role: структура каталогів, ansible-galaxy init, виклики та пріоритет змінних.
Ansible playbook і role: у чому різниця
Ansible playbook — це файл, який ви запускаєте за допомогою ansible-playbook. Він зіставляє групу хостів із потрібними для них операціями. Ansible role — це каталог із фіксованою структурою, у якому зберігаються tasks, templates, handlers і default variables, а playbook викликає його за іменем. Синтаксис tasks в обох випадках однаковий, тому питання не в тому, що можна описати. Питання в повторному використанні.
Почніть із плоского playbook. Один site.yml зі списком tasks: — правильна структура для першої автоматизації, і вона залишається правильною довше, ніж очікує більшість користувачів. Перетворюйте його на role, коли той самий блок tasks потрібно запускати для другої групи хостів або коли файл перевищує приблизно 100 рядків і потрібну task уже не можна знайти прокручуванням.
Якщо ви ще не створили playbook, почніть із першого playbook для одного VPS і поверніться до цього матеріалу, коли він почне розростатися.
Коли плоский playbook є правильним рішенням
Плоский playbook доречний, якщо роботу виконують одноразово, на одному хості або його ніхто інший не читатиме. Підготовка одного сервера застосунку чи встановлення патчів перед вікном обслуговування не потребують дерева каталогів. Role додає сім каталогів і ще один рівень опосередкування. Якщо 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, але приховує, які файли ролі справді потрібні.
Тепер заповніть файли, які виконують потрібні дії. Спочатку налаштуйте значення за замовчуванням, оскільки вони є публічним інтерфейсом ролі.
# 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. Обробник із неправильним іменем дає збій лише тоді, коли щось справді змінює шаблон. Саме тому проблема зазвичай виявляється через кілька тижнів.
Рядок validate є найкориснішим у цьому завданні. Ansible візуалізує шаблон у тимчасовий файл, підставляє шлях до цього файлу замість %s і виконує команду. Файл призначення замінюється лише тоді, коли команда завершується з кодом 0. Додайте до шаблону неправильну директиву й запустіть завдання ще раз: воно завершиться помилкою з failed to validate, справжній /etc/ssh/sshd_config.d/99-hardening.conf залишиться без змін, і ви й надалі зможете підключитися до сервера. Врахуйте, що ця перевірка тестує не лише синтаксис. Якщо sshd -t не може прочитати ключі хоста, команда завершується з sshd: no hostkeys available -- exiting., а Ansible повідомляє про той самий failed to validate. Тому перед тим, як звинувачувати шаблон, прочитайте msg модуля.
Як playbook викликає role
# 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 у підсумковому звіті. Передавайте параметри в місці виклику в розгорнутій формі. Так одна role обслуговує дві групи хостів:
roles:
- role: common
common_admin_group: ops
common_permit_root_login: prohibit-passwordЄ одне правило порядку, яке дивує майже всіх. Play може містити pre_tasks, roles, tasks і post_tasks, а Ansible виконує їх саме в такому порядку, незалежно від порядку в файлі. Розмістіть tasks: вище за roles: — і role все одно виконуватимуться першими. Тому якщо щось має відбутися до role, це потрібно розмістити в 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Щоб викликати role всередині списку завдань, а не через ключ 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 читає role під час розбору, а її завдання стають частиною play. Тому ansible-playbook --list-tasks site.yml перелічує їх, а тег для імпорту застосовується до кожного завдання всередині. include_role є динамічним. Його вміст читається лише під час виконання завдання, що дає змогу визначати ім’я role зі змінної або циклу. Недоліком є те, що ці завдання невидимі для --list-tasks і --start-at-task.
Тут є одна типова пастка. when: для завдання include_role обчислюється до того, як defaults/main.yml включеної role стає доступною. Додайте when: common_packages | length > 0 до include — і виконання завершиться помилкою 'common_packages' is undefined, навіть якщо цю змінну визначено в самій role, яку ви включаєте. Виправлення полягає в тому, щоб винести перемикач за межі role: розмістіть його в group_vars/all.yml, де він доступний всюди, а в defaults role залиште значення, які використовує сама role.
Яка змінна має перевагу: defaults, group_vars, vars, extra vars
Ansible документує понад двадцять рівнів пріоритету змінних. Чотири з них визначають майже всі практичні випадки. Нижче їх наведено від найнижчого до найвищого пріоритету.
roles/<name>/defaults/main.ymlрозташований майже внизу. Майже будь-яке значення, задане в іншому місці, має над ним перевагу. Саме тому тут слід зберігати змінні параметри ролі.group_vars/іhost_vars/розташовані посередині. Тут мають бути значення, визначені для вашого сайту. Вони коректно перевизначають значення за замовчуванням ролі.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 мало перевагу над значенням ролі за замовчуванням, але поступилося змінній ролі. Другий запуск виводить 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 повідомляє, що роль не знайдена
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. Тому власні ролі залишаються в репозиторії та проходять перевірку, а сторонні ролі завантажуються відтворювано із зафіксованим тегом.
Де роль уже не є правильним рішенням
Роль — це одиниця повторного використання в межах одного запуску Ansible. Вона не створює сервери або записи DNS у вашого провайдера. Спроба змусити її виконувати ці завдання перетворює playbook на код, який ніхто не хоче супроводжувати. Перед початком варто прочитати про розподіл завдань між Ansible і Terraform. Роль також не замінює проєктування inventory: коли серверів стає більше кількох, спосіб групування серверів і доступу до них важливіший за спосіб розподілу завдань між файлами.
Параметри hardening, які встановлює ця роль common, також потребують окремих рішень. Наведений drop-in задає лише дві директиви. Тому перед визначенням параметрів для ролі на всіх ваших хостах прочитайте які параметри SSH справді варто змінювати та як налаштувати автоматичне застосування Ubuntu оновлень безпеки.
FAQ
Коли слід перетворити Ansible playbook на role?
Коли той самий блок завдань має виконуватися в іншому play або для іншої групи хостів. Копіювання завдань між playbook є сигналом проблеми, оскільки від цього моменту кожне виправлення потрібно застосовувати двічі, а одного дня його буде застосовано лише один раз. Один playbook обсягом приблизно до 100 рядків, який завжди працює лише з однією групою, не отримує переваг від role, а додаткові каталоги ускладнюють читання.
Чи виконуються roles перед завданнями в тому самому play?
Так. Ansible виконує pre_tasks, потім усе, що перелічено в roles:, далі tasks:, а потім post_tasks:. Порядок появи цих ключів у файлі не має значення. Розміщення tasks: вище за roles: не змусить ці завдання виконуватися першими. Якщо щось має відбутися перед role, розмістіть це в pre_tasks:.
Чому значення з group_vars не перевизначає role?
Перевірте, чи не задано змінну в vars/main.yml role замість defaults/main.yml. vars/ має вищий пріоритет, ніж group_vars і host_vars, у порядку пріоритетів Ansible, тому inventory не може його перевизначити. Перемістіть змінну до defaults/main.yml. Цей рівень розташований ближче донизу порядку пріоритетів і є правильним місцем для всього, що має змінювати викликач. Щоб підтвердити, що причина саме в пріоритеті, а не в помилці друку, один раз запустіть із -e name=value. Він має вищий пріоритет за будь-яке інше джерело.
Чому Ansible повідомляє, що role не знайдено?
Пошук починається поруч із файлом 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 для створення role?
Ні. Role — це лише каталоги з очікуваними назвами, тому mkdir -p roles/common/tasks разом із tasks/main.yml уже є робочою role. ansible-galaxy init --init-path roles common заощаджує час на введенні та створює повний каркас, зокрема meta/main.yml і заготовку README. Видаляйте каталоги, які залишаються порожніми, оскільки порожній vars/main.yml приховує, які файли в role фактично виконують роботу.