Привет, Хабр! Мы системные инженеры X5 Tech — Алексей Кузнецов и Борис Мурашин. У нас за плечами больше 15 лет опыта, в том числе поддержка сервисов Rapida, CyberPlat, TeleTrade, сопровождение стека BigData и внедрение кластеров Hadoop.
В этой статье мы расскажем, как выбирали систему управления конфигурациями, какими критериями руководствовались, что в итоге выбрали, с какими проблемами столкнулись и как их решали.
Рассматривать вопрос, зачем вообще нужна система управления конфигурацией, не будем. Потому что считаем, что если у вас больше одного сервера, она уже необходима. Перейдём сразу к тому, почему мы выбрали именно Ansible.
Особенности Ansible
У Ansible есть особенности, которые и определили наш выбор:
Отсутствует агент. На тот момент мы не занимались установкой и ПНР серверов. По сути, у нас был только SSH-доступ на сервера, поэтому этот критерий стал ключевым.
Большое сообщество и хорошая документация. Это позволяет быстро найти ответы на вопросы и решить проблемы.
Лёгкость входа — важна, чтоб не затягивать процессы.
Большое количество модулей и возможность писать собственные позволяет найти и реализовать практически любое решение.
Infrastructure as Code. Мы хотели, чтобы весь проект лежал в Git, чтобы там была единая точка правды и документация.
Периодический запуск. Это необходимо, чтобы исключить возможность ручного изменения, и все изменения проходили только через git commit.
Простая и понятная структура. Благодаря этому даже новичок в команде может скачать проект и разобраться, как выглядит инфраструктура.
Автоматическое применение при merge. Это позволяет не ждать, когда Ansible запустится по таймауту.
Конечно, у Ansible есть и недостатки:
Отсутствует агент. Это не только преимущество, но и недостаток. Ведь из-за этого приходится выделять ресурсы на запуск playbook-ов.
Нужна оптимизация playbooks. Из коробки Ansible не подходит для большой инфраструктуры.
Но для нас они не перевесили преимущества Ansible.
Реализации для Gitlab
Первая реализация на GitLab CI/CD
В GitLab Ansible настраивается легко. Мы дополнительно получаем визуализацию и встроенный scheduler, но скорость работы высокая только при небольшом количестве хостов.
Пример gitlab-ci.yml:
![](https://habrastorage.org/getpro/habr/upload_files/ad6/d05/b02/ad6d05b02665a91a4a30f53feac524f7.jpg)
Количество хостов постоянно увеличивалось, скорость уменьшалась, и нас это не устраивало. Поэтому мы решили автоматизировать процесс.
У Ansible есть свои крутилки. Например, можно увеличить количество форков, изменить стратегии прогона и сбора фактов. Мы честно это пробовали, но должного результата не получили. Поэтому решили при помощи тегов распараллелить задачи.
Вторая реализация GitLab CI/CD с тегами
Чтобы ускорить процесс, мы добавили теги в site.yaml и стали запускать их параллельно в Gitlab CI. В результате скорость работы увеличилась, но теги усложнили проект. Большие группы пришлось дробить. Появилась непрогнозируемая нагрузка на Kubernetes и возникла дополнительная задержка при старте контейнеров.
![](https://habrastorage.org/getpro/habr/upload_files/b1b/aac/b5e/b1baacb5e07a7f5f97bbe2b32cd721bf.jpg)
Также первым стейджом шла сборка фактов. Мы запускали их в пайплайне единожды в самом начале, сохраняя в Redis, и все последующие стейджи брали факты оттуда.
![](https://habrastorage.org/getpro/habr/upload_files/9d2/faf/d0b/9d2fafd0b5e9f2ac460824450dee2f7c.jpg)
За счёт того, что мы распараллелили процессы — увеличилась скорость, но сами по себе теги слишком усложнили проект. Он стал менее читабельным. А чтобы задеплоить новую роль, стало необходимо придумывать новый тег и добавлять его в gitlab-ci.yml.
Чтобы распараллелить большие группы, мы поделили их на меньшие. И это тоже было нечитабельно.
Как итог, при работе с GitLab возникала не совсем прогнозируемая нагрузка на Kubernetes. Могло выстроиться сразу несколько коммитов, из-за этого Kubernetes съедал большое количество ресурсов. Могли «поехать» пользовательские поды.
Да и в целом у GitLab есть хоть и небольшая, но определённая задержка на старт самих контейнеров. Поэтому решили попробовать более нативное средство — бесплатный аналог Ansible Tower — AWX.
AWX
У этого решения были свои преимущества.
Отсутствовала задержка при старте контейнеров в Kubernetes. Ведь ещё на стадии установки мы подняли какое-то количество контейнеров (воркеров).
Нагрузку на Kubernetes можно было прогнозировать, потому что количество контейнеров было статичным — сколько ресурсов «съели», с таким количеством и «едем».
Нет нужды дробить группы. Благодаря возможности AWX-слайсов, мы перестали дробить большие группы на более мелкие.
![](https://habrastorage.org/getpro/habr/upload_files/84d/308/9bc/84d3089bcdd899907828a066a5316c40.jpg)
Мы внедрили это решение буквально за вечер и решили протестировать.
F*ckup со слайсами и patroni
Мы быстро запустили AWX без большого количества тегов в большом количестве слайсов. В итоге получили наш первый провал с внедрением.
В шаблоне конфигурации для ZooKeeper использовался шаблон:
{% for i in ansible_play_hosts_all %}
server.{{ loop.index }}={{ i }}:2888:3888
{% endfor %}
Запустив в большом количестве слайсов без тегов, мы столкнулись с тем, что patroni-кластер превратился в несколько stand-alone PostgreSQL серверов, а наш ZooKeeper получил splitbrain. Появилось сразу несколько лидеров, это было грустно и печально, особенно для наших DBA.
![](https://habrastorage.org/getpro/habr/upload_files/32d/913/05c/32d91305c1e2abdd23bce6dbe57ac3ff.jpg)
Первые реализации AWX и возникшие проблемы
У первых решений было несколько преимуществ: слайсы решили проблему с большими группами. Визуализация стала лучше, а поиск failed tasks — удобнее. Есть встроенный scheduler.
Но есть и нюансы: большой и сложный workflow, потребность в template на каждый tag, проблема с одновременными коммитами и то, что один тормозящий хост портит всё.
Переехав, мы сделали первый workflow, который выглядел так:
![](https://habrastorage.org/getpro/habr/upload_files/4b5/01b/78f/4b501b78f9ca50df2ccbf4f5a2301621.jpg)
Мы решили оставить теги, посчитав, что так будет удобнее, но сам workflow был большой и сложный. Если раньше мы добавляли в GitLab CI какой-то тег, то теперь приходилось заходить напрямую в AWX и ещё для каждого тега создавать отдельный template.
Ещё одна проблема возникла с одновременными коммитами. Мы не запускали их параллельно. Это происходило последовательно: коммиты выстраивались в очередь. Если за пять минут три человека закоммитились, можно было прождать своего коммита довольно долго. Единственная альтернатива – просто удалить из очереди все предыдущие коммиты.
В целом прогон ускорился не сильно, потому что количество хостов выросло. Один тормозящий хост мог заставить ждать, пока изменения заезжали на сервер. Получалось довольно долго.
Gitlab CI/CD vs AWX
В результате мы переписали проект, оптимизировали всё, что было возможно, использовали очевидные способы ускорения работы Ansible, но возникли новые нюансы.
У Gitlab CI/CD прогон в среднем составлял около 30 минут до таймаута. Проект был трудночитаемым из-за тегов и групп, а команда DevOps — недовольна нагрузкой на Kubernetes.
AWX со слайсами работал быстрее, но всё равно было нужно около 25 минут. А 90 рабочих контейнеров AWX по 6 Gb требовали полтерабайта RAM (чтобы уменьшить число хостов в слайсах).
В итоге мы потеряли изначальное преимущество лёгкой читаемости проекта. В AWX было лучше за счёт слайсов, но чтобы заставить его отрабатывать хотя бы за 25 минут, нам приходилось забирать больше полтерабайта памяти в Kubernetes.
При этом при локальном прогоне ansible-playbook отдельные таски выполнялись примерно за 0,5-1 секунд, а 100-200 тасков (средний объём плейбуков на хост) проходили за 2-2,5 минуты.
При запуске же в AWX прогон ожидает, пока каждая таска отработает везде – т. е. работает со скоростью самого медленного хоста. А всегда будут хосты, ограниченные и/или перегруженные по CPU/RAM/IOPS, могут быть задержки при доступе к репозиториям, плохо написанные роли тоже никто не отменял.
Мы переписали всё 10 раз, но локальный прогон на любом хосте требовал 2-3 минуты, а централизованный запуск в GitLab и в AWX длился 20-30 минут. Ведь когда инфраструктура большая — тысячи хостов — это, скорее всего, высоконагруженные кластера, хосты в которых 24х7 дымятся от нагрузки. Достаточно поймать хотя бы один хост, на котором таска задержится, скажем, на 10 секунд вместо секунды и в итоге 2-3 минуты превратятся в 20-30.
Решаем, что делать
Мы рассмотрели несколько реалистичных и не очень вариантов и выбрали Puppet. То есть систему с агентом, потому что система без агента рано или поздно столкнётся с трудно решаемыми или вовсе не решаемыми проблемами производительности.
На тот момент у нас была команда из 10 человек и за несколько лет мы накоммитили 4 Мб кода в ролях. Для понимания: все тома «Войны и мира» занимают меньше 3 Мб. Страшно даже подумать о том, чтобы переписать такой проект.
![](https://habrastorage.org/getpro/habr/upload_files/c0f/a05/cda/c0fa05cda1790af8bde5252af7c49b6f.jpeg)
Если перейти на другую систему невозможно, а агента нет, значит, нужно написать собственный.
Натягиваем агент на Ansible
Собственным агентом стало максимально экономичное решение. Простое приложение на Python, буквально в 200 строк.
![](https://habrastorage.org/getpro/habr/upload_files/b78/92a/043/b7892a043a97d018551a5755dc08f7ec.jpg)
Два бесконечных цикла позволяют запускать приложение как systemd unit. Один цикл проверяет наличие новых коммитов в Git, а обнаружив, запускает локальный прогон. Это ультимативное решение, быстрее локального прогона ничего уже не будет.
Всё работало очень быстро по сравнению с многочасовыми очередями в GitLab CI, потом в AWX. Скорость обновления была настолько высокой, что изменения применялись на сервере быстрее, чем успеваешь подключиться к нему через SSH и перейти в нужную директорию.
Мы выбрали Infrastructure-as-Code. Любая наша активность должна была заканчиваться коммитом в проект Ansible. Это новая степень свободы — если что-то недотестировал, а это случается с каждым, коммит может что-то поломать на хостах. На больших кластерах — таких, как Hadoop — расчёты не попадают сразу. Проходит 10-15 минут, прежде чем ноды начнут помечаться как нерабочие. Скорость доставки в 2-3 минуты на хосте — это реальная возможность быстрого отката. Если быстро понимаешь, что что-то сломал, это можно поправить и избежать аварии. Для сравнения, когда прогон составлял 30 минут, можно было сразу делать рассылку об аварии.
Второй цикл каждые 25 минут делал безусловный прогон. Это наша защита от ручного изменения конфигурации серверов, например, новенькими в команде.
F*ckup при обновлении GitLab
Чтобы не перегружать наш основной GitLab, мы сделали три реплики встроенными в GitLab средствами Repository Mirroring. Казалось бы, GitLab — это зрелый продукт, что может пойти не так? Но когда мы обновились, одно зеркало начало отставать.
![](https://habrastorage.org/getpro/habr/upload_files/093/941/e3a/093941e3ae126a7f1ad62c3f1085f7d7.jpg)
В результате агенты через балансировщик попадали то на актуальное зеркало, то на отставшее и перезаписывали то актуальные, то отставшие конфигурации, а потом перезапускали соответствующий сервис вне регламентных окон, без объявления работ.
В тот день мы вынесли урок, что репликацию иногда тоже надо мониторить. Хотя, казалось бы, интуитивно воспринимается, что если репликация отстала, то должна догнать. Но это не всегда так.
Расскажем немного про агент. Мы собираем его со всеми зависимостями в единственный бинарный файл с помощью pyarmor. Так безопаснее и удобнее для деплоя. Также нет возможности увидеть vault-password в списке процессов: он отображается в ps axu просто как ansible-agent. Его необходимо собирать для каждой версии OS. А ещё это усиливает безопасность, потому что в агенте зашиты vault-password для расшифрования секретов в проекте, и pyarmor не даёт возможности его оттуда просто взять. Pyarmor привязывается к мажорной версии ОС. В итоге надо собирать 4-5 бинарей, в зависимости от того, сколько у вас используется операционных систем. Это удлиняет пайплайн, но особых сложностей не создаёт. Агент работал довольно быстро, но его нужно было как-то затащить на все сервера, и мы решили использовать AWX.
![](https://habrastorage.org/getpro/habr/upload_files/496/49d/e0e/49649de0ee5e6c2d60d39cb7772ddf3c.jpg)
Мы создали для AWX новый template по установке агента, но полностью от него (AWX) не отказывались. Он работал на некоторых хостах для прогона. Например, на таких, где у пользователей был sudo, или на хостах, которые не полностью на нашей поддержке. Если нужно просто добавить туда, например, hadoop-клиент, то агент туда не ставим.
Установка выглядела следующим образом:
![](https://habrastorage.org/getpro/habr/upload_files/301/6ab/f9e/3016abf9e74ecc1e6a783ab20884e9f8.jpg)
У нас появился отдельный inventory для управления агентами и отдельный template для их настройки. Каждый раз, когда мы коммитили в этот inventory, запускался template и устанавливал нам агент. Роль ansible_agent устанавливала ansible-agent, ansible-collections, записывала файл конфигурации ansible-agent.conf, создавала и запускала systemd unit ansible-agent.service.
После внедрения Ansible-агента наш workflow сильно уменьшился, стал более читабельным и понятным. По сути, у нас остались template для установки агента и template на те хосты, на которые мы не тащим агент. Также мы решили оставить создание виртуальных машин на гипервизоре в AWX, потому что процесс состоит из нескольких шагов, а в AWX хорошая визуализация workflow.
Скорость выполнения сильно увеличилась, потому что нам уже было всё равно, как долго будет работать workflow. Агенты работали независимо от самого AWX.
![](https://habrastorage.org/getpro/habr/upload_files/41a/13a/b2e/41a13ab2e2c9684ef7830d80dfa9388a.jpg)
Утилита ansiblectl
Ansiblectl — утилита повторного запуска.
![](https://habrastorage.org/getpro/habr/upload_files/a81/4a0/802/a814a08024d27c5ff76b1fba3d328a7e.jpg)
Если у вас что-то с первого раза не получилось, то при помощи ansiblectl можно ещё раз запустить все playbooks на хосте.
Утилита имеет 4 ключа:
[-run] — для запуска.
[-b branch] — выбрать ветку, например, из которой вы хотите получить свои роли.
[-c check]
[-d diff]
Два последних ключа, думаю, понятны из названия.
Чем удобна ansiblectl:
Не надо создавать окружение.
Можно тестировать изменения.
Использовать как инструмент повторного прогона.
Без ansiblectl коммит в master осуществляется быстро, но страдает безопасность. Помимо того, что это инструмент повторного прогона, это также инструмент тестирования изменений. Если вы ведёте разработку новой роли, то, наверное, делаете это в отдельной ветке, и с этой утилитой вам не нужно устанавливать Ansible и заморачиваться с версиями. Вы используете инструмент с ключом [-b branch] и проверяете, что получается.
Безопасность агента
В нашем быстром агенте не хватало некоторых важных деталей. Вас могло смутить, что в одном предложении упоминается pyarmor и безопасность. Мы сделали быстрый агент, но на каждом хосте была полная копия проекта из git, описывающая всю его инфраструктуру, и vault password в памяти агента. Вытащить его оттуда любым отладчиком — 2 минуты. Поэтому надо было добавить компонент между агентами и git, который бы подтягивал режим работы нашего агента к другим системам с агентом, таким, как Puppet, и выдавал бы для каждого хоста минимально достаточный кусок проекта для работы. Тогда скомпрометированный хост выдал бы атакующему только конфигурации и секреты, которые и так, скорее всего, есть в /etc и никак ему не помогут. Мы этот компонент назвали Ansible-server.
Ansible-server
Компонент выдаёт агентам предназначенную им часть проекта. На скомпрометированном хосте можно увидеть только его роли и относящиеся к нему секреты. Секреты дополнительно перешифровываются с другим паролем – чтобы пароль из памяти агента не подходил к проекту git, на случай если кто-то по неосторожности оставит копию проекта на каком-нибудь хосте. Для нашего немаленького проекта ansible-server формирует статический контент в веб-сервере (nginx) за <5 секунд – т. е. практически не вносит дополнительной задержки.
Мы немного переживали за производительность, потому что для начала взяли Python, как для агента, и надо было готовить статический контент для тысячи хостов. Но в итоге наш Ansible-server отрабатывал, формируя контент, буквально за 5 секунд. Это на обычном механическом жёстком диске 7200 оборотов. Мы даже не брали SSD. Если мы будем расти до десятков тысяч хостов, просто поставим SSD, которые на операциях с мелкими файлами как раз сильно обходят жёсткие диски.
Как ни удивительно, мы даже получили прирост производительности, потому что заставили наш Ansible-server чаще опрашивать GitLab, а агентов — чаще ходить в Nginx.
Также мы делали дедупликацию: для хостов, в которых совпадает набор ролей, мы делали копирование только один раз, для остальных проставляли симлинки.
В нашей инфраструктуре Ansible-server заменил реплики GitLab.
![](https://habrastorage.org/getpro/habr/upload_files/456/310/198/4563101983879b38ae1a161b96d6c46f.jpg)
Мы научили агент работать в зависимости от конфигурации либо с git напрямую, либо с Ansible-сервером.
Ещё буквально два слова про Ansible-server.
![](https://habrastorage.org/getpro/habr/upload_files/b11/9a9/269/b119a9269911c0ef7732dcf2969af256.jpg)
Это приложение на Python чуть больше 400 строк. Идея, стоящая за ним, очень простая. Мы берём inventory, парсим, составляем словарь всех хостов и групп. Имея этот словарь, мы можем из playbooks из site.yaml создать словарь всех хостов и всех назначенных им ролей. Этих двух словарей достаточно, чтобы для каждого агента создать индивидуальный кусок проекта — скопировать нужные роли и group_vars, сделать минимально достаточный inventory и минимально достаточный кусок site.yaml.
Также Ansible-server создаёт конфигурацию для Nginx, директивы location {allow: <host_ips>; deny: all} для того, чтобы только соответствующий хост мог забрать свой контент.
Утилита ansiblectl тоже теперь работает с Ansible-server, и она тоже безопасна.
Теперь рассмотрим, с какой структурой проекта работает Ansible-server.
Структура проекта для работы Ansible-server
У нас в проекте стандартная структура:
![](https://habrastorage.org/getpro/habr/upload_files/e3e/f31/c89/e3ef31c8953a034e01ebad4600a37302.jpg)
Есть каталоги для групповых переменных, для переменных хоста и каталог с самими ролями. Мы использовали несколько inventory — основной и один для установки самого агента. А также site.yaml, который выглядел, как на картинке выше (пример справа). Группе хостов мы назначали все роли, которые должны пройти на этой группе, и обязательно тег, которых у нас несколько. Это работало либо через AWX, либо через Ansible-агент.
У нас сформировались определённые требования, как лучше что-то добавлять:
Группы должны называться по типу сервиса, так удобно и понятно.
Хост по возможности должен быть в одной группе, потому что так inventory более читабельный. Мы нашли единожды свой хост и понимаем, в какой он группе.
Используем только два тега ansible-agent-run или not-ansible-agent-run.
Все коммиты обязательно приходят через merge request и ревью у коллег.
Также есть определённые требования со стороны ansible-server:
Нельзя include_role в ролях.
Нельзя meta dependencies.
Ansible-server просто не поймёт, если у вас используется include_role в самих ролях. Он это не распарсит, и вы не получите изменения на хосте. Так специально сделано для читаемости. Вы смотрите site.yaml и сразу видите, какие в этой группе хостов выделены роли. По этой же причине мы не используем meta dependencies. Мы хотим, чтобы новый (или старый) сотрудник открыл site.yaml и сразу понял, какие роли для какого хоста назначены.
Мы любим AWX за возможность прогнать команду на всю инфраструктуру или её кусок. Написав агента, мы ни в коем случае не собирались от этого отказываться.
AWX для второй линии поддержки
Сценарий для того, чтобы нас меньше будили ночью.
![](https://habrastorage.org/getpro/habr/upload_files/45f/477/754/45f477754c220ebce8cd7a13e801c700.jpg)
Отдельный сценарий для второй линии поддержки нужен, чтобы решать типовые проблемы, а также собирать статистику для L3.
Для этого мы выносим рутинные задачи в отдельные playbooks в отдельном проекте в git. Даём дежурной смене доступ только на запуск этих playbooks, и они сразу видят в алерте, какой template запустить в AWX. Они могут без нашего участия почистить кэши или остановить какое-то агрессивное приложение. А если всё-таки придётся нас будить, заранее соберут статистику.
AWX для команд разработчиков
Второй сценарий похож на первый, но он уже для команд разработки.
![](https://habrastorage.org/getpro/habr/upload_files/c28/f77/1b8/c28f771b893f6e859e795a9555d26698.jpg)
Он нужен для внесения изменений без L3. Это свой проект в git без прав merge в master. При этом доступ в AWX предоставляется только на запуск, а inventory в AWX — только на чтение.
Мы даём команде разработки доступ в git с правом коммитить, но не мержить кластер. Когда им надо внести в инфраструктуру изменения, которые требуют прав root, они просто скидывают merge request. Мы его проверяем и, если ничего не смущает, мержим. Они получают свои изменения на инфраструктуре очень быстро — без бюрократии и переписок. В лучших традициях DevOps, буквально за 10 минут.
Мониторинг
Естественно, у нас есть мониторинг.
![](https://habrastorage.org/getpro/habr/upload_files/e9f/d9f/17e/e9fd9f17e25af3cff4b792c4e280edcb.jpg)
Мы уже научены горьким опытом с GitLab, поэтому мониторим, насколько хорошо он ходит в GitLab и обновляет роли. Если сервер не смог что-то распарсить, нам тоже придёт alert.
Чего мы добились
Самая большая боль — это долгое исполнение. На данный момент мы это решили, и прогон занимает лишь 2-3 минуты. С учётом того, что одна общая роль common насчитывает 200-300 тасок.
Делегировали всю нагрузку на хосты. Так мы смогли освободить больше, чем полтерабайта ресурсов из Kubernetes.
Возможность раскатки на тысячи узлов. Думаю, мы спокойно раскатаем проект на 2-3-10 тысяч хостов.
Тестирование с dev-веток. Ansiblectl — отличная утилита, чтобы вести разработку, тестировать в GitLab и сразу всё быстро проверять.
Безопасность не хуже, чем у других систем с агентом, включая Puppet или Chef.
Все свои наработки мы выложили в GitLab: t.ly/MK_3. Сделали всё максимально дружелюбно. Там два playbooks: один устанавливает агент на хосты, второй устанавливает сервер. А Bash-скрипт делает сборку бинаря Ansible-агента в Docker. Также опубликовали пример проекта, с которым работает наш Ansible-сервер.
Планы на будущее:
прикрутить ssl авторизацию для ansible-agent на ansible-servers;
прикрутить prometheus-exporter сбора большего количества метрик для ansible-agent.
Сообщество в Telegram:
Если возникнут вопросы или вы захотите просто пообщаться, заходите в нашу группу в Telegram — познакомимся, обсудим, поможем.