SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-25

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

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

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

Теперь заполните файлы, которые выполняют работу. Сначала укажите значения по умолчанию, так как они являются публичным интерфейсом роли.

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

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

Ansible ищет каталог roles/ рядом с файлом плейбука, а затем в roles_path. Поиск привязан к расположению плейбука, а не к текущему каталогу вашей оболочки.

ERROR! the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely

Это сообщение означает, что site.yml и roles/ разошлись, и Ansible выводит пути, по которым он пытался выполнить поиск. Храните их в одном каталоге. Запуск из родительского каталога допустим, так как учитывается именно путь к плейбуку:

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