Jenkins pipeline для инфраструктурного кода: гоняем Ansible playbook на тестовом стенде
Подключили Jenkins 1.x к Ansible-репозиторию: каждый push запускает синтаксис-чек и smoke-тест playbook на тестовом стенде перед применением в prod.
Jenkins 1.x - де-факто стандарт CI/CD; растёт практика тестирования инфраструктурного кода в пайплайне наравне с приложениями
В январе мы разобрали Ansible-роли и структуру репозитория. С тех пор репозиторий инфраструктурного кода вырос, роли множатся, и появилась новая проблема: изменение в одной роли ломает другую, а узнаёшь об этом только когда применяешь в prod. Последний такой случай - правка в common-роли, которая тихо поломала handlers у postgresql. Заметили через два часа, когда начали разбираться почему мониторинг пишет алерты на хосте клиента.
Решение очевидное, но до него как-то не доходили руки: надо гонять playbook на тестовом стенде автоматически, до того как изменения попадают в prod. Заняться этим заставил именно тот инцидент с handlers.
Почему Jenkins
Jenkins 1.x стоит у нас уже давно - сначала для сборки и деплоя Java-приложений клиентов по интеграционным проектам. Поднимать отдельный инструмент под инфраструктурный CI не было смысла. Уже настроен, уже знаем как он работает, плагины нужные есть.
Альтернативы смотрели вскользь. GitLab CI набирает обороты, но у нас код лежит на GitHub, и тащить ещё один сервер под GitLab ради одной задачи - лишнее. Travis CI для приватных репозиториев платный. Jenkins стоит на нашем железе, ничего дополнительно не платим, под рукой.
Как устроен пайплайн
Создали Freestyle job в Jenkins с двумя этапами.
Первый этап - синтаксис-чек. Простейший ansible-playbook --syntax-check для каждого playbook в репозитории. Это не занимает времени и ловит очевидные ошибки: опечатки в именах модулей, несбалансированные блоки, неправильные типы значений. Раньше такие вещи обнаруживались при запуске - теперь сразу при пуше.
ansible-playbook --syntax-check -i inventory/test site.yml
ansible-playbook --syntax-check -i inventory/test base.yml
Запускается на агенте Jenkins, где стоит Ansible. Быстро, без внешних зависимостей.
Второй этап - smoke-тест на тестовом стенде. Здесь интереснее. Есть три виртуалки на KVM - одна под CentOS 7, одна под CentOS 6, одна под Debian 7. Они живут в отдельном VLAN без доступа к продовым ресурсам. Jenkins запускает base.yml и common-роль против этих хостов с отдельным тестовым инвентарём.
ansible-playbook -i inventory/test base.yml
Если playbook прошёл без ошибок - Jenkins помечает сборку зелёной. Если что-то упало - красной, и пуш в ветку master блокируется (ну, не технически блокируется - webhook только уведомляет, но договорились что в master не мержим с красной сборкой).
Тестовый стенд: как поддерживать в нужном состоянии
Это оказалось самым трудоёмким местом. Тестовые виртуалки надо держать в «чистом» состоянии между прогонами, иначе накапливается drift - второй прогон проходит не потому что playbook правильный, а потому что нужные файлы уже там с прошлого раза.
Решили через снапшоты KVM. После каждого прогона - virsh snapshot-revert к базовому образу. Jenkins вызывает это через shell-шаг перед запуском Ansible. Небыстро - минута на откат снапшота, минута на то чтобы VM поднялась и SSH стал доступен. Но зато каждый прогон - чистая система.
Пробовали сначала без отката, просто гонять повторно. Работало, пока не добавили таск, который скачивает rpm из нашего внутреннего репозитория. Второй прогон пропускал таск через кэш пакетного менеджера и показывал зелёный, хотя реальный первый запуск на новом сервере падал с ошибкой сети. Урок: без отката к снапшоту тесты постепенно начинают врать.
Что это поймало за первые недели
Несколько находок, которые без пайплайна дошли бы до prod.
- Синтаксис-чек поймал опечатку в имени переменной в
defaults/main.ymlроли ntp. Переменная былаntp_serverвместоntp_servers- и шаблон Jinja2 рендерился в пустую строку вместо списка серверов. Ansible --syntax-check это не ловит (тут нет синтаксической ошибки), но дальнейший smoke-тест поймал - ntpd отказался стартовать с пустым конфигом. - Smoke-тест поймал проблему с порядком tasks в обновлённой роли
ssh-hardening. Handler на restart sshd срабатывал до того как новый конфиг записывался, и на CentOS 6 (где порядок чуть другой из-за upstart vs systemd) сервис ребутился со старым конфигом. - Разница между CentOS 6 и CentOS 7 в именовании сервисов: в одной роли написали
name: crond, на CentOS 7 сервис называетсяcrond, на Debian -cron. Без тестовой Debian-машины это ушло бы мимо.
Мелочи, но каждая из них в prod - это инцидент.
Что пока не сделали
Хотим добавить ansible-lint - инструмент для статического анализа playbook-ов, проверяет best practices помимо синтаксиса. Пока не включили: он ругается на несколько легитимных конструкций в наших ролях, и настройка исключений заняла бы больше времени, чем хочется тратить сейчас. Отложили на потом.
Тестирование идемпотентности - то есть запуск playbook дважды подряд и проверка что второй прогон не показывает changed - тоже в планах. Пока делаем вручную при написании новой роли, не автоматически.
Сколько это стоило
Настройка заняла примерно день: создание тестовых VM, написание скриптов отката снапшотов, конфигурация Jenkins job, отладка SCM-поллинга с GitHub. Ещё полдня потратили на то чтобы понять почему SSH в VM не поднимается достаточно быстро после отката снапшота - пришлось добавить sleep 10 и retry-цикл в shell-шаге.
Решение не идеальное и местами ручное, но работает. Инфраструктурный код теперь проходит автоматическую проверку на каждый пуш - и последний инцидент с handlers уже не повторился.