Дочитав статью до самого конца, вы догадаетесь, почему в качестве КДПВ выбран бобренок в коробке

Всем здоровья, товарищи хаброжители. Совсем недавно столкнулся с необходимостью поднять и настроить сервис «Непрерывной интеграции» (далее CI) на одном очень небольшом проекте, очень косвенно связанном с моей работой. Время не поджимало, потому решил попробовать что-то новенькое (ранее использовал только Travis и Jenkins). Главным критерием выбора была: «простота и скорость развертывания системы на интеграционном сервере».

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

Идея


Возможно я плохо искал, возможно мне хотелось принести жертву богу велосипедов, но мне не попался на глаза ни один сервис CI, который можно было просто «закинуть» на сервер, прописать в конфигурацию Github пару webhooks и забыть о DevOps. Потому я решил больше не тратить время на поиск (а потратил я около 1-2 часов), а написать что-то простое на Bash.

Идея заключалась в следующем: написать простейший CI-trigger на Bash, который будет проходить по цепочке (конвееру, pipe) функций, настраиваемых из другого (конфигурационного) скрипта.

Плюсы данного решения очевидны:

  • Написать его можно за десять минут
  • Оно будет очень гибким
  • Ничего для развертывания CI не потребуется, кроме переноса CI-trigger файла и конфигурации на сервер интеграции

Думаю о минусах этого решения вы уже и сами догадались. Я приведу лишь несколько:

  • Нет удобного для использования и настройки GUI
  • Нет готовых решений и плагинов, все придется писать на Bash или доступных ему для вызова
  • Требуются некоторые знания в области подготовки, развертывания и тестирования проекта

Реализация


Сам CI-trigger элементарен:

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, на пример так:

crontab
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:

.git/hooks/post-commit
curl -X POST http://ci-server.com:8000


Ссылки и все такое


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

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


  1. SLASH_CyberPunk
    07.11.2017 13:32

    GitLab?


    1. Delphinum Автор
      07.11.2017 13:37

      А еще Jenkins, Trevis, TeamCity, Drone и Bamboo ) Я думал этот вопрос я закрыл еще в начале статьи. Ну если вам интересно, я повторюсь — у меня было свободное время и желание найти (или написать, если потребуется) CI, не требующей развертывания большего, чем

      scp file ci-server.com:/home/user/ci-trigger


      1. SLASH_CyberPunk
        07.11.2017 13:39

        И ручное управление хуками, разграничение прав разрабов на серверах…
        Что может быть сложнее, чем подключить репу, развернуть gitlab (обновлять так же), на деплой сервере развернуть gitlab-runner и прописать конфиги в репах?


        1. Delphinum Автор
          07.11.2017 13:42

          Эмм… а что такого страшного в ручном управлении хуками?


      1. kahi4
        07.11.2017 14:54
        +1

        Окей. Билд упал, как об этом узнать? Как узнать что упало? Смотреть ручками файл лога? А какие-то нибудь junit и прочее? А конфигруация разных параметров, инкремент номера сборки, а хочется две+ машины для сборщиков? И опять же, "не требующий развертывания большего", но требующий настройки почты, например.


        А где хранить пароли к бд? А если хочется посмотреть как меняется динамика каких-то показателей, например растет или уменьшается количество warning? И, заодно зафейлить билд, если оно выросло?


        А если нужно пересобрать, что делать, фейковый пустой коммит? А как указать параметры окружения куда конкретно деплоить в данный момент? Например, на stage, rc или прод?


        1. Delphinum Автор
          07.11.2017 15:01

          Окей. Билд упал, как об этом узнать?

          Вся соль кроется тут:
          ...
          ci_archive &&ci_report || ci_error
          

          Либо все функции вернут 0 и выполнится ci_report, уведомляя юзера об успешном билде, либо выполнится ci_error, уведомляя того же юзера об ошибке билда.

          А какие-то нибудь junit и прочее?

          Не понял вопроса, что именно не так с junit? Если речь о том, как его запустить и обработать выхлоп, то через консоль с выводом в stdout.

          А конфигруация разных параметров, инкремент номера сборки, а хочется две+ машины для сборщиков?

          Так это чистый Bash, там можно все что вы сможете придумать. Про несколько машин-сборщиков тоже не совсем понял, в чем вы видите проблему?

          А где хранить пароли к бд?

          Ну это уже вам решать, можно просто создать файлик по типу passwd с доступом только от имени юзера CI-trigger.

          А если хочется посмотреть как меняется динамика каких-то показателей, например растет или уменьшается количество warning? И, заодно зафейлить билд, если оно выросло?

          Мониторинг это уже немного из другой оперы, данное решения его не подразумевает.


          1. kahi4
            07.11.2017 15:51
            +1

            Понятно что все можно сделать на чистом bash, вопрос в удобстве и количеству телодвижений. Так же как и стандартный вывод junit несколько не удобен для чтения в явном виде, особенно без привязки к коду.


            Про несколько машин-сборщиков тоже не совсем понял, в чем вы видите проблему?

            У вас есть две машины со сборщиками (вашим CI), вы делаете пуш, какая из этих машин заберет эту задачу? Обе сразу?


            Так это чистый Bash, там можно все что вы сможете придумать

            Как мне в вашем сборщике выкатить на прод/следующий-стейдж/qa — в общем, один и тот же коммист на разные окружения. Если для прода еще можно придумать, что на прод едут только ветки master, например, то ручной редеплой на разные окружения вызывает вопросы.


            1. Delphinum Автор
              07.11.2017 15:59

              Возможно вы не совсем поняли цели юзания данного решения. Представьте что у вас есть Raspberry Pi, который 4 раза в час опрашивает гисметео и показывает вам на небольшом экранчике прогноз погоды. Вы не хотите заниматься деплоем в этом проекте, а просто пушить правки в ваш репозиторий (на той же расбери или в github) и чтоб он релизился автоматически, или сообщал вам об ошибке.

              Я надеюсь, для таких задач вы не будете поднимать GitLab или Jenkins, а просто закините на сервер (расбери) bash-скрипт и пропишите хук для git. Вот это примерно то, где я советую юзать данное решение. Если вы хотите чего то большего, то естественно вам нужно что-то серьезнее.

              У вас есть две машины со сборщиками (вашим CI), вы делаете пуш, какая из этих машин заберет эту задачу? Обе сразу?

              Зависит от того, как вы сконфигурируете хук и вызов триггера.

              Как мне в вашем сборщике выкатить на прод/следующий-стейдж/qa — в общем, один и тот же коммист на разные окружения

              Я бы предложил для тестирования в различных окружениях использовать Docker-контейнеры. Повторюсь — решение очень упрощенное, это не конкурент какого нибудь Travis.


              1. SLASH_CyberPunk
                07.11.2017 16:07

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


                1. Delphinum Автор
                  07.11.2017 16:10

                  Сначала чекаем гисметео, потом понимает, что расбери может большее и пошло-поехало. Обычно так оно и бывает у программистов )


                  1. SLASH_CyberPunk
                    07.11.2017 16:34

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


                    1. Delphinum Автор
                      07.11.2017 16:37

                      Не проще ли было бы сделать на устройстве просто удаленный репозиторий, на который пушим, а в удаленном репозитории хук на деплой?

                      Так это оно и есть ) Я писал чуть выше об этом:
                      Вы не хотите заниматься деплоем в этом проекте, а просто пушить правки в ваш репозиторий (на той же расбери или в github) и чтоб он релизился автоматически

                      Одна из реализацией: вы размещаете репозиторий на CI-сервере, вы прописываете в hook вызов CI-trigger, вы добавляете в конфигурацию триггера git pull и все что нужно для развертывания.


                      1. SLASH_CyberPunk
                        07.11.2017 16:40

                        Что-то из статьи я не уловил это, и не понял, зачем тогда слушать порт…


                        1. Delphinum Автор
                          07.11.2017 16:47

                          Возможно вы просто прочитали статью по диагонали ) Про порт, это в том случае, если у вас исходники хостятся на каком нибудь github и вам нужно при пуше в него, интегрировать правки на своем сервере. В этом случае вы на CI-сервере слушаете порт, а github заставляете дергать этот порт при push commits.

                          Если же у вас исходники хотятся на той же машине, что и CI-сервер, то вы можете добавить hook в git, который будет вызывать триггер CI как то так: /home/user/prj/trigger — и будет вам счастье.


              1. kahi4
                07.11.2017 16:34

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


                И вы описали кейс CD, а не CI, что достаточно другая категория продуктов и решений тоже хватает. Гораздо более простых, причем встроенных в практически любой сборщик типа мавена, фабрики и подобного.


                1. Delphinum Автор
                  07.11.2017 16:42

                  В случае с расбери лучше замарочаться (это не долго) и пакетики собирать и доверить автоматически обновлять обновлялке пакетов

                  Для просмотра погоды собирать пакеты под текущую ОСь? Серьезно? ) Я вас понял, спасибо за совет ))
                  И вы описали кейс CD, а не CI, что достаточно другая категория продуктов и решений тоже хватает.

                  Если я добавлю тест перед деплоем и отсылку мне результатов на почту, то это дотянет до CI?


              1. MasMaX
                08.11.2017 12:27

                Для одиночных сборок хорошо подходит ansible. Вместо деления на функции там можно как вариант каждому шагу давать отдельный тег (или группу тегов) и вызывать потом только эти шаги.


  1. resetme
    07.11.2017 16:01

    Чем обычный Makefile хуже этого решения?


    1. Delphinum Автор
      07.11.2017 16:04

      Сборка проекта и CI это немного разные вещи. Данное решение, на пример, может использовать Makefile в ci_build функции, для сборки исходных кодов проекта.


      1. resetme
        07.11.2017 16:48

        Makefile может не только собирать проект. С его помощью можно легко организовать зависимости между шагами которых нет в вашем скрипте. К примеру, не нужно запускать проект на сборку, если не изменились исходники. Тем более можно начать с любого шага и все необходимые шаги автоматом выполнятся.


        1. Delphinum Автор
          07.11.2017 16:53

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


          1. resetme
            07.11.2017 17:04

            Да, ну? Из хука git запускается make, который из описания в Makefile проделывает эту работу: и протестирует, и по почте уведомит, и в Slack настучит, и еще кучу разных дел сделает, типа сборки дистрибутива программы. Не один раз встречал такое у разных команд и в разных проектах. Поэтому у меня возник вопрос о Makefile.


            1. Delphinum Автор
              07.11.2017 17:06

              Я немного не о том. Make это сборщик проекта, конечно можно с его помощью сделать и больше, так как он позволяет вызывать внешние тулзы, но стоит ли использовать инструмент не по назначению?


              1. resetme
                07.11.2017 17:16

                По моему как раз он под это и заточен. Отслеживает изменения в файлах и выполняет последовательность действий.

                В вашем случае тоже используется bash не по назначению, так как можно легко использовать любой готовый CI.


                1. Delphinum Автор
                  07.11.2017 17:20

                  Отслеживает изменения в файлах и выполняет последовательность действий

                  Проблема в том, что частенько в CI отслеживаются не изменения в файлах, а изменения в версии или коммиты.

                  В вашем случае тоже используется bash не по назначению

                  Но ведь у баша назначение — объединять готовые тулзы для выполнения задачи, что я и сделал )


                  1. resetme
                    07.11.2017 17:41

                    Какая разница. Изменения версии или коммита не обходится без изменения файла на диске. Этим и занимается make.

                    Хоть bash и есть универсальное средство, но в этой задаче он не очень подходит, так как нужно отслеживать изменения файлов и выполнять шаг только тот который необходим. А отслеживать изменения файлов прекрасно умеет make.


                    1. Delphinum Автор
                      07.11.2017 17:43

                      Ну если вас устраивает CI через Makefile, то я не нисколько не против. Вполне рабочее решение, правда немного странное, мне кажется.


                      1. resetme
                        07.11.2017 18:07

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