SSD Nodes Learn 🎉 VPS від $5.50/міс
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-08-07

Ansible playbook чи role: що обрати і коли

Дізнайтеся, коли достатньо плоского playbook, а коли потрібна role: структура каталогів, ansible-galaxy init, виклики та пріоритет змінних.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

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.yml
  • tasks/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=local
ansible-playbook -i inventory.ini site.yml

Play має завершуватися 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=0

changed=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.0
ansible-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 фактично виконують роботу.

#ansible#roles#playbook#structure#automation