Дочитав статью до самого конца, вы догадаетесь, почему в качестве КДПВ выбран бобренок в коробке
Всем здоровья, товарищи хаброжители. Совсем недавно столкнулся с необходимостью поднять и настроить сервис «Непрерывной интеграции» (далее CI) на одном очень небольшом проекте, очень косвенно связанном с моей работой. Время не поджимало, потому решил попробовать что-то новенькое (ранее использовал только Travis и Jenkins). Главным критерием выбора была: «простота и скорость развертывания системы на интеграционном сервере».
Под катом небольшая история и получившийся в ходе нее инструмент для CI, написанный за два вечера на Bash.
Идея
Возможно я плохо искал, возможно мне хотелось принести жертву богу велосипедов, но мне не попался на глаза ни один сервис CI, который можно было просто «закинуть» на сервер, прописать в конфигурацию Github пару webhooks и забыть о DevOps. Потому я решил больше не тратить время на поиск (а потратил я около 1-2 часов), а написать что-то простое на Bash.
Идея заключалась в следующем: написать простейший CI-trigger на Bash, который будет проходить по цепочке (конвееру, pipe) функций, настраиваемых из другого (конфигурационного) скрипта.
Плюсы данного решения очевидны:
- Написать его можно за десять минут
- Оно будет очень гибким
- Ничего для развертывания CI не потребуется, кроме переноса CI-trigger файла и конфигурации на сервер интеграции
Думаю о минусах этого решения вы уже и сами догадались. Я приведу лишь несколько:
- Нет удобного для использования и настройки GUI
- Нет готовых решений и плагинов, все придется писать на Bash или доступных ему для вызова
- Требуются некоторые знания в области подготовки, развертывания и тестирования проекта
Реализация
Сам CI-trigger элементарен:
config=`pwd`/ci.config # Адрес конфигурационного скрипта
log=`pwd`/ci.log # Адрес лог-файла
# Разбор аргументов
# ...
# Дефолтные реализации функций-интеграции
function ci_bootstrap {
return 0
}
function ci_update {
return 0
}
function ci_analyse {
return 0
}
function ci_build {
return 0
}
function ci_unit_test {
return 0
}
function ci_deploy {
return 0
}
function ci_test {
return 0
}
function ci_archive {
return 0
}
function ci_report {
return 0
}
function ci_error {
return 0
}
# Подключение конфигурации и выполнение интеграции
. $config && ( ci_bootstrap && ci_update && ci_analyse && ci_build && ci_unit_test && ci_deploy && ci_test && ci_archive && ci_report || ci_error
) 1>>$log 2>&1
Я не стал приводить полный код скрипта, опустив логику разбора аргументов командной строки, так как это тривиально и не имеет отношения к задаче (в конце статьи будет ссылка на репозиторий проекта, в котором вы сможете лицезреть исходный код без сокращений).
Как видите, все сводится к объявлению 9 функций-интеграции, вызываемых конвейерно для выполнения интеграции при запуске CI-trigger. Оба выходных потока конвейера объединяются в одном файле-лога, который выступает в качестве отчета о результатах интеграции.
Перед выполнением конвейера интеграции вызывается конфигурационный скрипт от имени CI-tirgger (
. $config
), позволяя ему переопределить любые функции. В этом и кроется вся «магия» решения. В связи с тем, что конфигурационный скрипт написан на Bash, мы можем использовать любую логику для выполнения интеграции, просто сгруппировав ее в функции.# Переход в каталог проекта
cd my-project
# Подготовка проекта к сборке
function ci_bootstrap {
mysql -uadmin -pmy_pass -e "DROP DATABASE db; CREATE DATABASE db"
}
# Загрузка изменений исходных кодов
function ci_update {
if test -d .git; then
return git pull
else
return git clone https://github.com/vendor/project ./
fi
}
# Сборка
function ci_build {
return npm install && npm run build
}
# Запуск модульных тестов
function ci_unit_test {
return npm run unit_test
}
# Развертывание проекта
function ci_deploy {
return mysql -uadmin -pmy_pass db < migration/schema.sql && mysql -uadmin -pmy_pass db < migration/data.sql
}
# Уведомление о результатах интеграции
function ci_report {
return mail -s "CI report" my@mail.com < $log
}
# Уведомление об ошибке
function ci_error {
echo "== Error =="
return mail -s "CI report" my@mail.com < $log
}
Теперь нам остается только настроить логику вызова CI-trigger в соответствии с нашими требованиями.
Периодический вызов
Для этого достаточно настроить Cron, на пример так:
0 0 * * * /home/user/ci/trigger
Вызов при изменении
Это решение требует реализации механизма, отвечающего за прослушивание порта интеграционного сервера с вызовом CI-trigger при обращении на него. Я предлагаю использовать для этого netcat и следующий простой Bash-скрипт:
while true; do
{ echo -ne "HTTP/1.0 200 OK\r\n\r\n"; } | nc -v -l -p 8000 && /home/user/ci/trigger
done
Теперь необходимо настроить используемую нами систему контроля версий для выполнения HTTP-запроса к этому порту при каждом push commits, на пример с помощью Curl:
curl -X POST http://ci-server.com:8000
Ссылки и все такое
Естественно данное решение далеко не идеально, вам придется много «поработать руками» для того, чтобы использовать его в большом проекте, но для быстрого запуска сервиса CI оно вполне подойдет (я так думаю!).
Комментарии (28)
resetme
07.11.2017 16:01Чем обычный Makefile хуже этого решения?
Delphinum Автор
07.11.2017 16:04Сборка проекта и CI это немного разные вещи. Данное решение, на пример, может использовать Makefile в ci_build функции, для сборки исходных кодов проекта.
resetme
07.11.2017 16:48Makefile может не только собирать проект. С его помощью можно легко организовать зависимости между шагами которых нет в вашем скрипте. К примеру, не нужно запускать проект на сборку, если не изменились исходники. Тем более можно начать с любого шага и все необходимые шаги автоматом выполнятся.
Delphinum Автор
07.11.2017 16:53Да, только с помощью Makefile нельзя собрать проект, протестировать его и уведомить по почте разработчика о результатах, при коммите изменений в систему контроля версий.
resetme
07.11.2017 17:04Да, ну? Из хука git запускается make, который из описания в Makefile проделывает эту работу: и протестирует, и по почте уведомит, и в Slack настучит, и еще кучу разных дел сделает, типа сборки дистрибутива программы. Не один раз встречал такое у разных команд и в разных проектах. Поэтому у меня возник вопрос о Makefile.
Delphinum Автор
07.11.2017 17:06Я немного не о том. Make это сборщик проекта, конечно можно с его помощью сделать и больше, так как он позволяет вызывать внешние тулзы, но стоит ли использовать инструмент не по назначению?
resetme
07.11.2017 17:16По моему как раз он под это и заточен. Отслеживает изменения в файлах и выполняет последовательность действий.
В вашем случае тоже используется bash не по назначению, так как можно легко использовать любой готовый CI.Delphinum Автор
07.11.2017 17:20Отслеживает изменения в файлах и выполняет последовательность действий
Проблема в том, что частенько в CI отслеживаются не изменения в файлах, а изменения в версии или коммиты.
В вашем случае тоже используется bash не по назначению
Но ведь у баша назначение — объединять готовые тулзы для выполнения задачи, что я и сделал )resetme
07.11.2017 17:41Какая разница. Изменения версии или коммита не обходится без изменения файла на диске. Этим и занимается make.
Хоть bash и есть универсальное средство, но в этой задаче он не очень подходит, так как нужно отслеживать изменения файлов и выполнять шаг только тот который необходим. А отслеживать изменения файлов прекрасно умеет make.Delphinum Автор
07.11.2017 17:43Ну если вас устраивает CI через Makefile, то я не нисколько не против. Вполне рабочее решение, правда немного странное, мне кажется.
resetme
07.11.2017 18:07Было бы странным, если бы им не пользовались. Его как раз используют чтобы не поднимать полноценный CI сервер. Единственный минус такого решения, нужно разбираться как работает make, а это уже зависит от квалификации разработчика.
SLASH_CyberPunk
GitLab?
Delphinum Автор
А еще Jenkins, Trevis, TeamCity, Drone и Bamboo ) Я думал этот вопрос я закрыл еще в начале статьи. Ну если вам интересно, я повторюсь — у меня было свободное время и желание найти (или написать, если потребуется) CI, не требующей развертывания большего, чем
SLASH_CyberPunk
И ручное управление хуками, разграничение прав разрабов на серверах…
Что может быть сложнее, чем подключить репу, развернуть gitlab (обновлять так же), на деплой сервере развернуть gitlab-runner и прописать конфиги в репах?
Delphinum Автор
Эмм… а что такого страшного в ручном управлении хуками?
kahi4
Окей. Билд упал, как об этом узнать? Как узнать что упало? Смотреть ручками файл лога? А какие-то нибудь junit и прочее? А конфигруация разных параметров, инкремент номера сборки, а хочется две+ машины для сборщиков? И опять же, "не требующий развертывания большего", но требующий настройки почты, например.
А где хранить пароли к бд? А если хочется посмотреть как меняется динамика каких-то показателей, например растет или уменьшается количество warning? И, заодно зафейлить билд, если оно выросло?
А если нужно пересобрать, что делать, фейковый пустой коммит? А как указать параметры окружения куда конкретно деплоить в данный момент? Например, на stage, rc или прод?
Delphinum Автор
Вся соль кроется тут:
Либо все функции вернут 0 и выполнится ci_report, уведомляя юзера об успешном билде, либо выполнится ci_error, уведомляя того же юзера об ошибке билда.
Не понял вопроса, что именно не так с junit? Если речь о том, как его запустить и обработать выхлоп, то через консоль с выводом в stdout.
Так это чистый Bash, там можно все что вы сможете придумать. Про несколько машин-сборщиков тоже не совсем понял, в чем вы видите проблему?
Ну это уже вам решать, можно просто создать файлик по типу passwd с доступом только от имени юзера CI-trigger.
Мониторинг это уже немного из другой оперы, данное решения его не подразумевает.
kahi4
Понятно что все можно сделать на чистом bash, вопрос в удобстве и количеству телодвижений. Так же как и стандартный вывод junit несколько не удобен для чтения в явном виде, особенно без привязки к коду.
У вас есть две машины со сборщиками (вашим CI), вы делаете пуш, какая из этих машин заберет эту задачу? Обе сразу?
Как мне в вашем сборщике выкатить на прод/следующий-стейдж/qa — в общем, один и тот же коммист на разные окружения. Если для прода еще можно придумать, что на прод едут только ветки master, например, то ручной редеплой на разные окружения вызывает вопросы.
Delphinum Автор
Возможно вы не совсем поняли цели юзания данного решения. Представьте что у вас есть Raspberry Pi, который 4 раза в час опрашивает гисметео и показывает вам на небольшом экранчике прогноз погоды. Вы не хотите заниматься деплоем в этом проекте, а просто пушить правки в ваш репозиторий (на той же расбери или в github) и чтоб он релизился автоматически, или сообщал вам об ошибке.
Я надеюсь, для таких задач вы не будете поднимать GitLab или Jenkins, а просто закините на сервер (расбери) bash-скрипт и пропишите хук для git. Вот это примерно то, где я советую юзать данное решение. Если вы хотите чего то большего, то естественно вам нужно что-то серьезнее.
Зависит от того, как вы сконфигурируете хук и вызов триггера.
Я бы предложил для тестирования в различных окружениях использовать Docker-контейнеры. Повторюсь — решение очень упрощенное, это не конкурент какого нибудь Travis.
SLASH_CyberPunk
Если вам надо чекать гисметео, то такой код один раз пишется и больше не трогается, пока работает. И такие вещи можно хоть руками закидывать один раз и забыть…
Delphinum Автор
Сначала чекаем гисметео, потом понимает, что расбери может большее и пошло-поехало. Обычно так оно и бывает у программистов )
SLASH_CyberPunk
Т.е. увеличиваем количество репозиториев для деплоя и тем самым увеличиваем баш-скриптов деплоя или допиливаем текущие баш скрипты, чтобы можно было разграничивать деплои?
Не проще ли было бы сделать на устройстве просто удаленный репозиторий, на который пушим, а в удаленном репозитории хук на деплой?
Delphinum Автор
Так это оно и есть ) Я писал чуть выше об этом:
Одна из реализацией: вы размещаете репозиторий на CI-сервере, вы прописываете в hook вызов CI-trigger, вы добавляете в конфигурацию триггера git pull и все что нужно для развертывания.
SLASH_CyberPunk
Что-то из статьи я не уловил это, и не понял, зачем тогда слушать порт…
Delphinum Автор
Возможно вы просто прочитали статью по диагонали ) Про порт, это в том случае, если у вас исходники хостятся на каком нибудь github и вам нужно при пуше в него, интегрировать правки на своем сервере. В этом случае вы на CI-сервере слушаете порт, а github заставляете дергать этот порт при push commits.
Если же у вас исходники хотятся на той же машине, что и CI-сервер, то вы можете добавить hook в git, который будет вызывать триггер CI как то так: /home/user/prj/trigger — и будет вам счастье.
kahi4
Я надеюсь, для таких задач вы не будете использовать свой, мягко говоря, костыль. В случае с расбери лучше замарочаться (это не долго) и пакетики собирать и доверить автоматически обновлять обновлялке пакетов.
И вы описали кейс CD, а не CI, что достаточно другая категория продуктов и решений тоже хватает. Гораздо более простых, причем встроенных в практически любой сборщик типа мавена, фабрики и подобного.
Delphinum Автор
Для просмотра погоды собирать пакеты под текущую ОСь? Серьезно? ) Я вас понял, спасибо за совет ))
Если я добавлю тест перед деплоем и отсылку мне результатов на почту, то это дотянет до CI?
MasMaX
Для одиночных сборок хорошо подходит ansible. Вместо деления на функции там можно как вариант каждому шагу давать отдельный тег (или группу тегов) и вызывать потом только эти шаги.