Привет. Я Сергей Истомин, DevOps-инженер в KTS.

Это статья о том, как сделать так, чтобы заказчику было легко пользоваться Ансиблом. Сразу оговорюсь: речь пойдет о кейсе, когда на вашей ферме 100 голов, и всех вы знаете по именам (да, так тоже бывает).

Ниже будет спорный материал. Результат описанного рефакторинга — компромисс между практикой IAC и людьми. Последняя составляющая этого компромисса требует тонкой настройки и деликатного подхода, поскольку люди в любой момент могут неосознанно начать отдаляться от IAC. Причина всегда одна — когнитивная нагрузка. Как только она начнет доставлять дискомфорт, IAC потихоньку двигают в сторону.

Оглавление

Немного предыстории

Наш заказчик сделал осознанный выбор в пользу использования виртуальных машин и отказа от контейнеризации своего продакшена. Главные факторы — это очень ёмкие по ресурсам стейтфул-приложения, которые должны очень быстро производить вычисления. Latency — один из важнейших отслеживаемых показателей, и все усилия команды заказчика направлены на сокращение этого показателя. В общем тайминге ответа каждые 10 мс на счету. А всё, что может оказать сильное взаимное влияние приложений друг на друга (включая общие компоненты цепочки обработки трафика), проходит через скрупулезную оценку баланса пользы и вреда.

Но чтобы было что подвергать сомнению, сначала этот объем «сомнительных» технологий нужно было накопить. Да, были площадки в разных регионах мира, был Кубернетес ещё на заре становления его популярности, были динамические инвентори для деплоя различных компонентов продакшена, и решения, впоследствие потерявшие актуальность. И вот за несколько лет накопился огромный и неповоротливый репозиторий с ансибл-кодом, пользоваться которым становилось всё труднее. Сотрудники менялись, одни задачи вытесняли из памяти другие, документация велась местами на скорую руку, а местами — и так понятно. И пришло понимание, что с этим надо что-то делать.

Следы древних цивилизаций

Начали мы, как и полагается, с инвентаризации. И обнаружили:

  • три десятка плейбуков;

  • переменные в этих плейбуках;

  • переменные в host_vars;

  • переменные в group_vars;

  • переменные в разнообразных папках, вставляемые повсеместно;

  • переменные в ad-hoc-скриптах;

  • переменные в README, описанные для использования в командной строке (ведь вы же читаете README перед использованием, правда?);

  • переменные в ролях в дефолтах;

  • переменные в ролях в vars;

  • переменные в ролях в задачах (вроде и захардкожены, а вроде и переменные в loop — мой любимый вид хардкода);

  • переменные в инвентори;

  • инвентори в ini и в yaml, плоские и сгруппированные на любой вкус.

Если допустить, что вы и есть Ансибл, то для вас такая путаница не будет проблемой. Вы прекрасно знаете, где и в каком порядке прочитать свои переменные — у вас всего-то 22 места. И опять же, с вас какой спрос? Вам инженер команды даёт, вы выполняете.

Вот только инженер — не Ансибл, и для него простая задача что-то накатить может превратиться в увлекательный квест. 

В какой-то момент команда заказчика начала потихоньку уходить от этого веселья, нарастающего с каждым изменением. У них уже появился отдельный ансибл-репозиторий под раскатку продакшена, и в этом репозитории стали появляться инфровые роли. Основной ансибл-репозиторий рисковал превратиться в чулан Моники (привет, олды).

Баста!

И мы решили это хозяйство отрефакторить. Стояла цель сделать эту репу great again, несколько сузить широкие возможности Ансибла. Всех мух свести в одно место, все котлеты — в другое, чтобы сходу можно было понять, где что лежит и как оно влияет на результат.

Начали с нуля. Создали простую структуру:

├── inventories/
├── roles/
└── play_by_inventory.yml

Реализовали следующие принципы:

  • на логическую группу приходится один инвентори, в котором описаны:

    • сами хосты;

    • все переменные к группам и хостам;

    • применяемые к группам и хостам роли;

  • в ролях содержатся только дефолты и что-то базовое (позже поясню);

  • один универсальный плейбук play_by_inventory.yml, в который мы никогда не лезем что-то править, т.к. он выполняет чисто техническую функцию — женитьбу инвентори на ролях.

Неизбежно появился четвертый компонент — самые общие переменные группы all

Там живут штуки вроде 

ansible_python_interpreter: "/usr/bin/python3"

Итоговая базовая структура:

├── group_vars/
│   └── all.yml
├── inventories/
├── roles/
└── play_by_inventory.yml

Но group_vars/all.yml — это единственный случай переменных вне инвентори.

В самой директории inventories было решено не хранить директорий вроде group_vars, чтобы осталось пространство под простую структуру логических группировок инвентори-файлов.

Инвентори

Инвентори пишем в yaml. Помним, что при проигрывании можно в качестве инвентори указать директорию, и Ансибл возьмет в работу все находящиеся в этой директории файлы, объединив хосты и переменные в указанные вами группы. Если группировки не использовать, то всё окажется в группе all.

Пример простого инвентори

all:
 hosts:
   host_1:
     host_roles:
       - role_a
     basic_docker_enabled: true

Здесь host_1 получит роль role_a, и будет заявлен параметр basic_docker_enabled: true.

Пример сложного инвентори

big_group:
 vars:
   group_roles:
     - role_a
     - role_b
 children:
   small_group_1:
     hosts:
       host_1:
         host_roles:
           - role_c
       host_2:
       host_3:
         group_roles:
           - role_b

Здесь произойдут такие назначения:

  • host_1 получит role_a, role_b, role_c (объединены списки group_roles и host_roles);

  • host_2 получит role_a, role_b (задействован только список group_roles);

  • host_3 получит role_b (переопределен список group_roles).

Плейбук

Состав плейбука

Внутри play_by_inventory.yml такой код:

- hosts: all
  become: true
  tasks:
    - name: Discovering roles in inventory files
      include_role:
        name: "{{ discovered_role }}"
      loop: "{{ ( hostvars[inventory_hostname].group_roles | default([]) + hostvars[inventory_hostname].host_roles | default([]) ) | unique }}"
      loop_control:
        loop_var: discovered_role
      tags: always

Как это работает

Запускаются только роли, если они поименованы в списке в переменных group_roles и/или host_roles в инвентори. При этом:

  • списки group_roles  и host_roles объединяются для каждого хоста;

  • переменные можно переопределить в дочерних группах и на уровне хостов.

Теги дополнительно ограничивают запускаемые задачи из объема поименованых в инвентори ролей. Если же предварительно подготовить роль и повесить на нее тег, то через теги можно указать на запуск только определенных ролей, что удобно, например, при обновлении. Для этого механизма важно наличие в плейбуке в таске магической директивы tags: always — она позволяет тегам протранслироваться на уровень поиска тегов в ролях.

Роли

В идеале стоит привести каждую роль к двум простым принципам.

  1. Роль имеет свой общий тег, совпадающий с ее названием. Это позволит запускать частичную накатку на инвентори.

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

Как просто реализовать первый принцип? Создайте в роли в директори tasks отдельный yaml-файл, например, myrole.yml , куда перенесите содержимое из tasks\main.yml, а тело самого tasks\main.yml превратите в: 

- import_tasks: "myrole.yml"
  tags:
    - myrole

Второй принцип важен больше для удобства чтения и ориентирования.

Про «что-то базовое»

Выше я писал, что в ролях мы держим «только дефолты и что-то базовое». Дело в том, что в этом репозитории есть 2 вида ролей — универсальные и кастомизированные. Пример кастомизированной роли — базовая роль (basic) для раскатки на новую машину. В этой роли в директории files лежат публичные ssh-ключи, а в vars/main.yml лежит список пользователей и их параметры. И чтобы на машине (или на группе машин) при прокатке роли появились эти пользователи, в инвентори этой машины (или группы) в group_roles или в host_roles нужно прописать только:

     - basic

Как запускать

Вот простые примеры:

  • Прокатить 1 инвентори-файл
    ansible-playbook play_by_inventory.yml -i inventories/yandex/monitoring.yml

  • Прокатить все инвентори-файлы из директории
    ansible-playbook play_by_inventory.yml -i inventories/yandex/

  • Прокатить все инвентори-файлы из директории, ограничиться ролью basic (роль подготовлена и имеет общий тег)
    ansible-playbook play_by_inventory.yml -i inventories/yandex/ --tags=basic

  • Прокатить все инвентори-файлы из директории, ограничиться задачей обновления ssh-ключей роли basic
    ansible-playbook play_by_inventory.yml -i inventories/yandex/ --tags=basic_users

С этим, разумеется, можно миксовать прочие полезные ключи, такие как:

--limit — сужает перечень хостов/групп до перечисленных;

--user — переопределяет пользователя, от лица которого пойдет проигрывание плейбука;

--private-key — переопределяет приватный ключ; пользователя, от лица которого пойдет проигрывание плейбука;

–-diff — вывод в протокол изменений в файлах на целевых хостах;

--check — холостой прогон без реальных изменений.

Результаты рефакторинга

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

До

Для наглядности посмотрим, как могло выглядеть наполнение машины с Нексусом до рефакторинга.

├── ad-hoc/ — находим директории с названиями датацентров
│   └── yandex/ — наш ДЦ. Идем глубже
│       └── nexus.corp/ — наша машина. Идем глубже
│           └── ... — несколько shell-скриптов на ansible-playbook на разные инвентори, разные роли. Ок, разбираемся.
├── global_vars/ — тут смотрим, нет ли чего для нашей задачи (а то вдруг пропустим?)
│   ├── logging.yml — находим пресеты для filebeat (но будут ли они задействованы?)
│   ├── sysctl.yml — находим директивы sysctl (но все ли они нам нужны?)
│   ├── time.yml — находим переменные под разные датацентры (теперь где-то нужно найти логику выбора)
│   └── users.yml — находим список пользователей (берем и надеемся, что он актуальный)
├── group_vars/ — тут смотрим обязательно, чтобы избежать сюрпризов
│   ├── ... — пропускаем файлы с названиями групп вне нашего контекста
│   └── all — находим список consul_hosts (хмурим брови, удивляемся — у нас в инфре кто-то видел Consul?..)
├── host_vars/ — видим только десяток машин, пытаемся понять, чем они особенны на фоне остальных
│   ├── ... — пропускаем файлы с названиями других машин
│   └── nexus.corp.yml — находим малоинформативные переменные, но что-то про SSL-сертификаты (берем?)
├── inventories/ — видим каталоги по датацентрам, раскрываем
│   └── yandex/ — наш ДЦ, идем глубже
│       └── nexus.yml — похоже, что это наш инвентори. Есть переменные. О! Одна из переменных упоминает filebeat.
├── .../ — видим еще каталоги с названиями других сервисов
├── nginx_vars/ — стоп, а здесь нам что-то понадобится? Ведь у нас nginx проксирует запросы на машине…
│   └── ... — раскрываем и видим несколько файлов с названиями по категориям, топонимам, ролям. Вроде всё мимо нас.
├── corp_vars/ — так, ну а здесь-то что может быть? Внутри что-то про elasticsearch, пропускам.
├── ... — видим три десятка плейбуков вперемешку с дополнительными инвентори-файлами.

Это простой случай с относительно свежей машиной. Добавим на вход квеста переменные и логические условия в плейбуках, и получим кофе-брейк, отрицание и гнев.

После

А теперь взглянем, как это выглядит сейчас (как и выше, исключаем роли и плейбук).

├── group_vars/
│   └── all.yml — файл номер 1
├── inventories/
│   └── yandex/
│       └── nexus.yml — файл номер 2

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

Итог

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

Стоило ли так упрощать, сводить всё в одному плейбуку и к переменным только в инвентори? Ведь Ансибл описывает свои возможности гораздо шире. Стоит ли продавливать канонический подход с теми же host_vars и group_vars

На мой взгляд, результат важнее. Нужно убирать решения, конфликтующие с планами заказчика на развитие и на что заказчик хотел бы тратить время. Нам удалось добиться отличного баланса между концепцией IAC и простотой реализации. Репозиторий сейчас действительно актуален и позволяет в считанные минуты получить предсказуемый результат.

Кстати, это далеко не первая статья в нашем блоге, где мы делимся опытом работы с инфраструктурой. Если у вас руки чешутся оптимизировать все, до чего эти руки дотягиваются, предлагаю вашему вниманию:

Комментарии (0)