SSD Nodes Learn Hosting plans →
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-08-28

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

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

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 викликає 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.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, але приховує, які файли ролі справді важливі.

Тепер заповніть файли, які виконують потрібну роботу. Спочатку налаштуйте 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=local
ansible-playbook -i inventory.ini site.yml

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

#ansible#roles#playbook#structure#automation