SSD Nodes Learn 🎉 VPS от $5.50/мес
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-07

Ansible playbook или role: что выбрать для проекта

Узнайте, когда достаточно одного файла playbook, а когда пора переходить к структуре role. Разбираем критерии сложности, порядок вызова задач и приоритеты переменных в Ansible.

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 — это каталог с фиксированной структурой, содержащий задачи, шаблоны, обработчики и переменные по умолчанию; playbook вызывает его по имени. Синтаксис задач в обоих случаях идентичен, поэтому вопрос не в том, что можно реализовать, а в возможности повторного использования.

Начните с плоского playbook. Один site.yml, содержащий список tasks:, — это подходящая форма для вашей первой автоматизации, и она остается эффективной дольше, чем ожидает большинство пользователей. Переходите к использованию role, когда один и тот же блок задач должен выполняться для второй группы хостов или когда файл вырастает примерно до 100 строк и вы перестаете быстро находить нужную задачу при прокрутке.

Если вы еще не написали свой первый 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 содержит переменные, которые вызывающий код не должен переопределять. По приоритету они стоят выше инвентаря, что является серьезным утверждением. Используйте их редко.
  • handlers/main.yml содержит задачи, запускаемые через notify. Обработчик (handler) выполняется один раз в конце плейбука, независимо от того, сколько задач отправили ему уведомление.
  • 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, который интерпретирует значение 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 называется ssh, а в системах семейства RHEL — sshd. Обработчик (handler), указывающий на неверное имя, выдаст ошибку только тогда, когда шаблон будет изменен, поэтому такие проблемы часто обнаруживаются спустя недели.

Строка 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 вызывает роль

# 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 является динамическим. Ничего не считывается до момента выполнения задачи, что позволяет задавать имя роли через переменную или цикл. Платой за это является невидимость таких задач для --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.

Какой приоритет у переменных: defaults, group_vars, vars, extra vars

Ansible определяет более двадцати уровней приоритета переменных. Четыре из них разрешают почти любой практический спор; ниже они приведены от самого слабого к самому сильному.

  • roles/<name>/defaults/main.yml находится почти в самом низу. Почти всё, что вы зададите в другом месте, перекроет это значение, поэтому здесь лучше всего хранить параметры настройки роли.
  • group_vars/ и host_vars/ находятся посередине. Здесь должны находиться значения, специфичные для вашей инфраструктуры, они корректно переопределяют значения по умолчанию из роли.
  • roles/<name>/vars/main.yml стоит выше, чем host_vars. Значение, заданное здесь, нельзя переопределить из инвентаря. Оставьте это место для параметров, которые должны оставаться неизменными внутри роли, например, для имени пакета, которое должно совпадать с именем службы.
  • Параметр роли, переданный при вызове, перекрывает 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. Инвентарь перекрыл значение по умолчанию, но уступил переменной роли. Второй запуск выведет internal=from-cli, так как extra vars находятся на самом верху, и ничто ниже не может их изменить. Именно поэтому -e подходит для разового запуска, но является ошибкой в постоянном скрипте: этот способ незаметно перекрывает все продуманные решения в вашем репозитории.

Рабочее правило: если вы хотите, чтобы значение можно было изменять, помещайте его в defaults/. Размещение значения в vars/ сообщает каждому будущему пользователю роли, что инвентарь не может его изменить. Иногда это именно то, что требуется, но чаще всего это происходит случайно.

Проверка идемпотентности роли: повторный запуск

Надежный playbook 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 пропускаются в режиме проверки, поэтому план, который выглядит корректным, может скрывать выполнение определенных действий.

Почему 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 в текущем каталоге, если у этого каталога установлены права на запись для всех (world writable). Любой пользователь на сервере мог бы разместить там конфигурацию и изменить поведение вашего запуска.

[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-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-записи у вашего провайдера, и попытки заставить её это делать превращают плейбуки в нечто, что никто не захочет поддерживать. Разделение задач между Ansible и Terraform стоит прочитать перед началом работы. Роль также не заменяет проектирование инвентаря: как только количество машин превышает несколько штук, то, как вы группируете серверы и обращаетесь к ним, становится важнее, чем то, как разложены задачи.

Укрепление безопасности, которое устанавливает эта роль common, также требует отдельных решений. Приведённый выше фрагмент конфигурации задаёт только две директивы, поэтому ознакомьтесь с тем, какие настройки 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, поэтому инвентарь не может её переопределить. Перенесите переменную в defaults/main.yml, которая находится ближе к концу списка приоритетов и является правильным местом для всего, что должен иметь возможность изменить вызывающий код. Чтобы убедиться, что причина именно в приоритетах, а не в опечатке, запустите playbook с флагом -e name=value, который имеет высший приоритет среди всех источников.

Почему Ansible сообщает, что роль не найдена?

Поиск начинается рядом с файлом playbook, поэтому site.yml и roles/ должны находиться в одной директории. В сообщении об ошибке выводятся пути, по которым выполнялся поиск, например 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