Привет, %username%! Я уже не первый раз вспоминаю про такой классный инструмент, как Ansible. Давай в этот раз посмотрим на такую штуку как Ansible Roles – роли в Ansible.

Что такое Ansible?

О том, что такое Ansible написано уже множество статей в сети. Про основные понятия ansible я уже писал – там все просто. Но на всякий случай быстренько пробежимся по основным моментам.

Ansible – это система управления конфигурациями. Если перевести на простейший язык, то ansible – это такая штука, которая позволяет воспроизводить конфигурацию на множестве схожих систем.

Давай разберем на примере. Представь, что у тебя один твой любимый серверочек и ты его долго настраивал: устанавливал пакеты руками, правил конфиги, настраивал красивенький motd, чтоб при логине в систему тебе показывалось всякое разное и полезное. И вот в один прекрасный (или не очень) день, по не зависящей от тебя причине твой “огородик” сдох, а вместе с ним и его бэкапы (свежа еще история в памяти о пожаре в ДЦ). В обычной ситуации ты просто найдешь другой сервер и будешь снова все настраивать руками. Будешь делать все в точности как и было, но немного другое – некоторые вещи становятся не актуальными, а некоторые ты просто забываешь со временем. По итогу ты получаешь хоть и похожий, но немного другой “огородик”.

В альтернативной ситуации, с точно таким же пожаром, но используя Ansible, ты развернешь “идентичный огородик” – даже “сорняки” будут на тех же местах. И, что не менее важно, ты это сделаешь в разы быстрее, чем руками.

Состав роли

И так, давай посмотрим из чего должна состоять хорошая роль. Для того чтобы посмотреть на “дефолтную роль”, достаточно выполнить инициализацию роли с помощью утилиты ansible-galaxy:

ansible-galaxy init jtprogru.packages
- Role jtprogru.packages was created successfully

В данном примере создается роль под названием jtprogru.packages – директория с именем jtprogru.packages будет создана в текущей директории откуда запускалась ansible-galaxy. Вот так выглядит структура файлов и директорий:

tree -a jtprogru.packages
jtprogru.packages
├── defaults
├── files
├── handlers
├── meta
├── tasks
├── templates
├── tests
└── vars

8 directories, 9 files

Немного подробнее по некоторым пунктам:

  • defaults – Директория с дефолтными значениями некоторых переменных;
  • files – Тут положено складывать все файлики, которые необходимы (бинари, готовые конфиги, публичные сертификаты и ключи);
  • handlers – Тут хранятся хендлеры, которые должны реагировать на выполнение задач;
  • meta – Метаинформация о роли (автор, организация, etc);
  • tasks – Тут хранятся задачи из которых состоит роль;
  • templates – Тут хранятся шаблоны в формате Jinja2;
  • tests – Тесты! Да, роль – это “код”, а любой код должен быть покрыт тестами;
  • vars – Общие переменные для роли, например OS-specific переменные;

Почти в каждой директории создается файл main.yml – это своего рода index.html – входная точка для каждого компонента роли.

Давай рассмотрим чуть подробнее некоторые моменты.

defaults

Тут складываются те переменные, которыми можно и нужно управлять снаружи – из групповых перменных. В этой директории размещаются те переменные, которые очень желательно переопределять на уровне инвентаря, но значения указанные для них являются валидными и роль может отработать корректно.

vars

Сюда складываешь все OS-specific переменные и/или те переменные, которые не требуется показывать внешнему пользователю твоей роли. К примеру: у тебя есть большой шаблон, который внутри использует мешок путей из файловой системы в рамках домашней директории. Ты точно знаешь, что иногда меняется пользователь и соответственно его “хомяк”. В defaults указываешь просто my_role_base_dir: "{{ my_role_user }}" и все. А в vars уже составляешь пути на основании my_role_base_dir – таким образом ты точно будешь уверен в том, что случайно не поменяются директории и они всегда будут там, где они должны быть.

handlers

Хэндлеры - это такие же таски, но запускаются только в случае если произошли изменения и их вызвали. Простой пример: после настройки Nginx’a тебе надо бы проверить валидность нового конфига через sudo nginx -t и если все “ОК”, то корректно применить его через sudo nginx -s reload. Вот для таких ситуаций и нужны хендлеры – выполнять какие-либо задачи в случае изменения конфигураций на управляемом хосте. По сути, хендлеры - это такие же задачи, а главное преимущество хендлеров в том, что они запускаются только в случае наличия изменений. Т.е. если у тебя конфиг не изменился – твой Nginx не будет перезапускаться.

tasks

Основное место, где описываются все задачи, из которых состоит роль. Все шаги, которые ansible должен выполнить для описываются именно тут. Описывать можно как одном файле в виде гигантской простыни, так и в десятке файлов. Главное чтобы тебе и твоим коллегам было логично и понятно.

templates

Шаблоны любых файлов в формате Jinja2 складываешь в этой директории и встроенный модуль template будет искать эти шаблоны в этой директории если не указать иное.

files

Самое простое место для хранения какого-либо файла. Например какая-то сложно-собираемая и трудно-доставаемая зависимость строго определенной версии в виде бинарника может спокойно лежать тут и не отсвечивать. Ты просто будешь копировать этот файлик туда, куда это требуется.

meta

Nребуется использовать в том случае, когда ты собираешься распростарнять свою роль через Ansible Galaxy или просто начинаешь вести управление ролями через “внутренний galaxy” (заводишь отдельный репозиторий под каждую роль и подключаешь строго определенные версии ролей в инфра-репозиторий). Считается хорошим тоном заполнять данный раздел и указывать в нем зависимости от других ролей. При установке такой роли через ansible-galaxy install -r requirements.yaml ее зависимость будет так же установлена без явного указания в файле requirements.yaml.

test

Лично я не использую данную директорию для хранения тестов, потому что привык к отдельному инструменту тестирования ролей – Molecule и его тесты хранятся рядом в директории molecule.

Практика

Немного о том, что и за что отвечает я тебе рассказал. Теперь я тебе расскажу свой подход к разработке ролей, как я стараюсь делать и как я не делаю, а главное поясню “почему именно так, а не иначе”.

Инициализация роли

Если речь идет о не публичной роли (которую я не буду публиковать в Ansible Galaxy или хоть как-то показывать кому-то кроме себя), то тут все просто:

cd roles
mkdir jtprogru.my_new_role/{defaults,handlers,tasks,templates}
vim .
git init/add/commit/push

Если речь идет о публичной роли (которую я буду публиковать в Ansible Galaxy), то тут можно воспользоваться утилитой ansible-galaxy:

ansible-galaxy init my_new_role
cd my_new_role
vim .
git init/add/commit/push

Я же пользуюсь альтернативным вариантом - инициализация роли через Molecule (сразу генерируется шаблон для тестирования роли с использованием Molecule):

molecule init role jtprogru.my_new_role --driver-name docker
cd my_new_role
vim .
git init/add/commit/push

У меня была попытка сделать шаблон роли – sample-role. Чтоб можно было форкнуть/скачать архив/скопипастить и иметь уже готовый скелет с тестами в виде Molecule и прочим, а в идеале собрать из нее нормальный шаблон для Cookiecutter – если будет желание, кидай PR.

Написание кода

В чем писать - вообще до фонаря, главное чтобы твой редактор умел корректно работать с отступами. Я в зависимости от настроения использую VIM, VS Code, GoLand/PyCharm - все с кучей разных плагинов.

На что стоит обращать внимание при написании кода роли:

именование тасок

Простой пример:

- name: my_new_role | install pkg
  ansible.builtin.apt:
    name: "{{ base_soft_list }}"
    update_cache: false

Каждый раз, когда имя задачи начинается с имени роли я мысленно говорю себе спасибо за то, что я смогу отличить эту задачу в общем выхлопе плейбука. Ведь задача с именем - name: install pkg может быть где угодно, так ведь? А вот с конкретным имененем - name: my_new_role | install pkg будет явно только в моей роли.

хендлеры

На конференции DevOpsConf 2022 я совершенно случайно узнал про “топики”. Топики это такая штука, благодаря которой тебе в своих задачах не надо перечислять нотифаи. Вот так ты делаешь обычно:

- name: "jtprogru.install_atop | generate default config for atop"
  ansible.builin.template:
    src: "atop.j2"
    dest: "{{ atop_config_path }}"
    owner: root
    group: root
    mode: "0644"
  notify: 
    - restart atop
    - restart atopacct

А теперь ты можешь просто написать один нотифай, а ansible сам все поймет и сделает правильно. Например вот так будет выглядеть задача:

# tasks/main.yaml
- name: "jtprogru.install_atop | generate default config for atop"
  ansible.builin.template:
    src: "atop.j2"
    dest: "{{ atop_config_path }}"
    owner: root
    group: root
    mode: "0644"
  notify: atop listner

# handlers/main.yaml
- name: restart atop
  ansible.builtin.service:
    name: atop.service
    state: restarted
  listen: atop listner

- name: restart atopacct
  ansible.builtin.service:
    name: atopacct.service
    state: restarted
  listen: atop listner

И оба этих сервиса будут перезапущены. Более того, ты в своих ролях можешь слать нотифай в топики других своих ролей.

линтеры

Обязательно используй линтеры! Их не так много, но их достаточно:

  • ansible-lint
  • yamllint

Каждый из них настраиваешь и прогоняешь перед коммитом. В идеале добавить их в pre-commit-hooks и тогда система тебе просто не даст закоммитить в репозиторий кривой код.

Тестирование

Про тестирование можно говорить бесконечно и это в целом отдельная тема для холиваров, но стоит упомянуть про некоторые вещи. Например, уже упомянутая мною molecule. Однако, в работе с molecule на текущий момент есть огромный нюанс. Текущая версия поддерживает исключительно два типа драйверов (того, на базе чего будут запускаться ваши роли), это Delegated и Docker.

Как именно инициализировать роль с тестами от molecule я уже показал выше, но повторюсь – вот такой командой:

molecule init role jtprogru.my_new_role --driver-name docker

После чего, можно смело идти в файл molecule/default/molecule.yml и добавлять различные варианты Docker images для различных платформ. Например вот так:

# molecule/default/molecule.yml
---
dependency:
  name: galaxy

driver:
  name: docker

platforms:
  - name: instance-cs8
    image: quay.io/centos/centos:stream8
    pre_build_image: true
  - name: instance-ub22
    image: ubuntu:22.04
    pre_build_image: true

provisioner:
  name: ansible

verifier:
  name: ansible

В тестовом инвентаре у тебя будет два хоста:

  • instance-cs8 - будет запущен из образа quay.io/centos/centos:stream8 с docker registry quay.io;
  • instance-ub22 - будет запущен из образа ubuntu:22.04 с docker registry hub.docker.com;

Важно лишь помнить, что далеко не все варианты можно протестировать в Docker’e. Например, если у тебя в роли выполняется управление сервисами systemd или настройка параметров ядра через sysctl, то вариант тестирования в Docker тебе совсем не подойдет. Можно посмотреть в сторону Vagrant’a (как именно обойти санкции использую VPN – инструкций предостаточно в сети, но если сложно, то заходи в чат и спросить) и через синхронизацию директорий (vagrant будет просто монтировать указанную директорию в нужный каталог внутри гостевой системы) катать внутри нужной гостевой ОС.

В остальном, все что касается тестирования – это в большинстве случаев краеугольный камень. Ты либо тестируешь свои роли, либо не тестируешь. Выбор как обычно за тобой.

Где и как хранить

Начну с того, что есть два варианта хранения всех ролей (про наркоманские упоминать не буду – не та ситуация). Первый вариант – публичный – отправлять все свои роли в Ansible Galaxy. Этот вариант стоит использовать только в том случае, если написанные роли можно публиковать в интернет (тебе не стыдно их показать и компания не возбраняет это). Второй вариант – приватный – хранить свои роли у себя на приватном git-сервере. Это самый частый вариант хранения, потому что “низя показывать нашу интеллектуальную собственность” (и еще тысяча вариантов фразы “нам стыдно”).

Если публичное Ansible Galaxy не подходит по различным причинам, то просто не парься и создавай отдельный репозиторий под каждую роль. А в инфраструктурном репозитории просто подключаешь все через специальный файлик requirements.yaml – пример:

roles:
  # Установка роли из конкретной ветки из Github
  - name: jtprogru.install_atop
    src: https://github.com/jtprogru/ansible-role-install-atop
    version: origin/master

  # Установка роли по конкретному тегу из Github
  - name: jtprogru.install_atop
    src: https://github.com/jtprogru/ansible-role-install-atop
    version: 2022.9.0

  # Установка роли ко конкретному коммиту из Github
  - name: jtprogru.install_atop
    src: https://github.com/jtprogru/ansible-role-install-atop
    version: 752b9cf7ed13261736b257317e41d1bc03261d61

  # Установка роли из Ansible Galaxy
  - name: jtprogru.install_atop

Вообще, я категорически рекомендую использовать данный файлик для фиксации всех зависимостей – внутренних/внешних ролей и коллекций. Логика тут простейшая: ты можешь переписывать свою роль сколько угодно и совершенно не бояться чего-то сломать в инфраструктуре. Этот же подход позволит коллегам из смежных команд спокойно пользоваться твоими ролями, а когда что-то новое привносишь в свою роль – оповещаешь коллег об этом.

Зависимости ролей

Не редки случаи, когда мне попадались странные решения в этом плане. Представь картину: есть роль install_nginx, которая ставит Nginx с дефолтными конфигами, и есть роль configure_nginx, которая настраивает Nginx – просто генерит шаблон из Jinja2 и кладет его куда надо. Далее ты добавляешь новую группу проксей в инвентарь и забываешь навесить роль install_nginx или, что чаще, путаешь порядок – сначала “настраиваешь”, а потом “устанавливаешь”. Теперь вопрос: что обычно происходит когда ты пытаешь прогнать все роли по новой группе хостов в инвентаре?

Пока ты размышляешь над ответом, я не буду напоминать про “внимательность” и “ответственность” при написании плейбуков. Я хочу напомнить про маленькую директорию meta, о которой часто забывают (что не есть хорошо). Ранее по тексту я упоминал, что в этой директории хранится мета-информация о данной роли – например такая:

---
galaxy_info:
  role_name: install_atop
  namespace: jtprogru
  author: Mihael Savin
  description: Ansible role for install atop
  company: Owl Legion

  license: WTFPL
  min_ansible_version: 2.9

  platforms:
    - name: Ubuntu
      versions:
        - bionic
        - focal
    - name: Debian
      versions:
        - stretch
        - buster
    - name: EL
      versions:
        - 7

  galaxy_tags:
    - atop
    - monitoring
    - ubuntu
    - install

dependencies: []

Кроме информации, которая нужна для публикации такой роли в Ansible Galaxy, тут есть замечательная директива: dependencies: []. В данной директиве ты можешь описать роли от которых эта роль зависит. Это значит, что Ansible при установке твоей роли через утилиту ansible-galaxy автоматически обнаружит эти зависимости и установит их. А главное, что при запуске плейбука Ansible прогонит сначала те роли, от которых зависит твоя, а потом уже прогонит твою роль.

Чтобы никогда не путаться в том, какую роль накатить сначала, а какую потом, очень удобно пользоваться зависимостями самих ролей. Просто указывай в виде списка названия ролей, от которых зависит твоя.

Именование ролей

Самая страшная роль, которую можно представить – роль под названием common. В ней можно увидеть абсолютно всё – от раскладывания ssh-ключей и установки софта, до настройки sshd и установки docker. Старайся избегать слова common когда придумываешь имя для роли. Исходи из того, какую задачу решает конкретная роль. Если роль решает слишком много задач, то стоит попробовать декомпозировать ее на более атомарные объекты.

Многие кубероводы слышали про такую штуку как kubespray для разворачивания кластеров Kubernetes. Если заглянуть под капот в официальный репозиторий, то можно обнаружить, что kubespray не что иное, как набор плейбуков и ролей Ansible, которые решают одну большую задачу – развернуть Kubernete-кластер.

Заглянув в директорию roles, ты сможешь там увидеть роль adduser. В этой роли есть defaults, в которых определены универсальные дефолтные значения для того, чтобы роль гарантированно отработала. Есть vars для OS-specific переменных – в разных семействах ОСей некоторые пути могут отличаться. Есть tasks состоящий из двух задач – “создать группу” и “создать пользователя”. Все! Ничего лишнего! Даже если бы это были не системные пользователи, а обычные – роль с именем adduser должна только добавлять пользователей и группы для них. Роль с именем adduser не должна выполнять настройку окружения для этих пользователей, не должна закидывать ключи доступа от этих пользователей.

Вообще по части именования, kubespray, на мой взгляд, самый хороший пример. Ты открываешь директорию с ролями и по наименованиям понимаешь, что именно делает та или иная роль. Рекомендую делать так же – просто и понятно, не пытаясь усложнить то, что не требует усложнения.

Вредные советы

site.yaml

Вредный совет 1: Чем больше в твоей роли вызовов ansible.builtin.command, ansible.builtin.raw или ansible.builtin.shell, тем веселее тебе будет дебажить. Особенно есть ты подкладываешь всякие script.sh.

Вредный совет 2: Чтобы обезопасить сервера от несанкционированного запуска твоих ролей, в первую очередь выполни ansible.builtin.raw: chmod a-x $(which python) и ansible.builtin.raw: chmod a-x $(which python3) – безопасность гарантируется.

Вредный совет 3: Именовать задачи не обязательно, это всего лишь прерогатива тех, у кого на работе много свободного времени. А когда надо срочно сделать, то не до красивостей и правильных неймингов.

Вредный совет 4: Флаг no_log: true никому не нужен, потому что кроме тебя никто не смотрит в логи, а пароли и токены ты и так хранишь на стикерах вокруг монитора.

Вредный совет 5: Запускать плейбуки со своими ролями можно прямо со своего ведроида – зря что ли настраивал какой-нибудь termux, а всякие gitlab-runner’ы и AWX’ы для слабаков.

Вредный совет 6: Если у тебя в какой-то группе есть отличающийся параметр от остальных, то не надо править group_vars, надо просто скопипастить роль полностью и назначить ее на нужную группу – будет гарантия результата.

Вопросы и ответы

Перед написанием данной статьи я кинул клич в своем канале и просил накидать разных странных вопросов на тему ролей. А изначальная идея статьи вообще была предложена одним Senior Golang Developer у которого я учусь писать на Golang.

Теперь попробую внятно ответить только на часть вопросов, потому что некоторые из пропущенных вопросов тянут на отдельную статью (а то и целую серию).

Вопрос 1: Что лучше – универсальная роль на несколько ОСей или под каждую ОСь своя?

Ответ: Зависит от задачи которую ты хочешь решить. Если ты хочешь без головной боли управлять “одним и тем же конфигом” в разных ОСях (RHEL/Debian/BSD), то логичнее сделать универсальную роль. Универсальная роль тебе будет гарантированно выполнять одно и то же вне зависимости от целевой ОСи.

Вопрос 2: Что делать с копипастой в Ansible репозитории?

Ответ: Самый простой и одновременно сложный вопрос. Ответ тут максимально лаконичный – вычищать. Все, что повторяется более чем один раз – стопроцентно можно оформить в виде роли. Остальное по мере возможностей и необходимостей так же выделять в роль.

Вопрос 3: Где граница, за которой таски из плейбука выделяются в роли и что делать с тасками в плейбуке?

Ответ: По хорошему, эту границу каждый определяет для себя сам. Но я дам тебе пищу для размышления. В комьюнити Ansible культивируется простое правило: “в рамках одного play (и даже целого playbook) должны быть либо только роли, либо только таски”. Лично я использую более жесткую форму этого правила. В рамках основного плейбука – условный site.yaml – у меня должны быть только инклюды с другими плейбуками, в которых уже будут описаны используемые роли или только необходимые таски. Исходя из вышесказанного – мой подход: никаких тасок в плейбуках.

Вопрос 4: Как правильно жить с vars в плейбуке?

Ответ: Тут все очень просто – в идеальном мире у тебя не должно быть директивы vars_files в твоих плейбуках. Начни с того, что правильно составь инвентарь – составь список всех хостов, побей его на группы, посмотри чего не хватает и что можно объединить. После составления правильного инвентаря начни раскладывать по group_vars/ все переменные которые требуются. Если есть необходимость, то выделяй host_vars/ – но не увлекайся, а старайся осознанно подходить к этому и проверяй в первую очередь возможность создания новой группы (пусть и под один хост).

Вопрос 5: Какая разница между vars и defaults?

Ответ: Глобально – никакой. Я довольно часто вижу, что vars в роли не используются – не создается директория даже, потому что “зачем создавать переменную ради переменно если ее не надо переопределять снаружи”. Один из примеров использования я уже описал выше в соответствующем разделе. Если ты пишешь роль, которая может быть исполнена на нескольких разных ОСях, то vars обязаны присутствовать. И на основании того, что указано в group_vars (а это переопределенные переменные из defaults) или того, где запускается роль подключаются соответствующие переменные из vars.

Вопрос 6: Откуда запускать свои плейбуки – выделенный хост или локально со своей ПеКарни?

Ответ: Если удобно запускать локально со своей рабочей станции – запускай с нее. Просто мне лично видится более логичным и правильным исполнение всех плейбуков с выделенного хоста (или группы хостов). Смотри, если ты задался таким вопросом, то у тебя явно есть своя инфраструктура, в которой наверняка есть git-сервер в лице того же Gitlab. Как известно у Gitlab есть свои раннеры – по сути бинарь, который может исполнять все что ему скажу. Использовать Gitlab-раннеры для запуска инфраструктурного кода – выглядит более чем логично. К тому же запуск можно делать “по кнопке”, а не на каждый коммит и все будет отлично. Различные вариации на тему Ansible Tower/AWX и прочее – исключительно вкусовщина. Если тебе или твоим разработчикам не нравится смотреть на выхлопы Gitlab-раннера, то можно попробовать что-то из “запускаторов для Ansible”.

Вопрос 7: Что делать, если в ИБ сидят звери и каждый пробел в плейбуке надо согласовывать?

Ответ: Простой ответ – сменить работу. Сложный ответ – найти с ИБ общий язык. В одном из рабочих кейсов прошлых лет была необходимость разворачивать “штуки” в закрытом контуре (интернета нету никакого – совсем). Новые версии “штук” передавались на DVD-R дисках, а на CD-R дисках передавались zip-архивы с Docker-образами, которые имели в себе все плейбуки. Лучше всего в таких ситуация стараться найти общий язык. Выяснять, почему те или иные вещи запрещены и какие есть альтернативные варианты. Ты ж будущий девупс – прокачивай коммуникативные навыки.

Вопрос 8: Как тестировать роли?

Ответ: Тестирование ролей это достаточно широкая тема, которую раскрыть в рамках данной статьи будет проблематично. Поэтому я сделаю отдельную статью чуть позже с различными вариантами тестирования. Но если совсем не терпится, то можешь посмотреть на живые примеры в репозитории kubespray. Там используется Molecule и Vagrant для тестирования ролей.

Итоги

В целом роли в Ansible не так уж и сложно. Некоторые вопросы я не стал включать, потому что они тянули на отдельную статью и более глубокое разбирательство. Я их точно не потеряю, потому что вынес в отдельный issue.

Посмотреть мои кривенькие и простенькие роли можно на странице Github. Там все далеко от идеала, что дает тебе шанс проверить себя и сделать лучше.

Чтобы написать роль, достаточно ответить на несколько вопросов:

  • Что должна делать роль (список задач)?
  • Где должна работать роль (на одной ОС или на нескольких)?
  • Какими параметрами можно управлять из инвентаря, а какими не стоит?
  • Требуются ли какие-то дополнительные зависимости для работы моей роли?
  • Каким образом я хочу тестировать роль?
  • Будет ли ей пользоваться кто-то кроме меня?

Сформировав ответы на все эти вопросы, ты сможешь написать роль, которой будет пользовать удобно, а главное, результат работы которой будет всегда ожидаемый.


Если у тебя есть вопросы, комментарии и/или замечания – заходи в чат, а так же подписывайся на канал.