21 мар. 2016 г.

Классный бесплатный сервис Continuous Integration

Коллега посоветовал классный бесплатный сервис http://semaphoreci.com/ в связке с bitbucket для простой и удобной организации непрерывной интеграции и автоматического деплоя. 

До этого, если мне приходилось организовать что-то подобное, я просто поднимал TeamCity на том же сервере, где расположено приложение. По мержу в мастер происходила автоматическая сборка и деплой на сервер в случае удачи. Существенный минус этого способа - это то, что сборка тратит ресурсы боевого сервера, и вообще, в принципе может его положить. Если выносить все это на отдельный сервер, то это увеличивает расходы. 

Если проект приносит деньги, то разумеется, оплатить дополнительные ресурсы это не проблема. Но если это не так, то приятно иметь бесплатную систему.


Бесплатный аккаунт Semaphoreci позволяет делать до 100 сборок в месяц. Он имеет встроенную интеграцию с bitbucket. Как только произошел мерж в master или просто настало время, он автоматически загружает актуальную версию кода к себе в рабочий диалог. Далее пользователю необходимо с нуля настроить окружение и затем сделать сборку.  

На момент написания поста мне CI был не нужен, было интересно освоить сам механизм. Поэтому я пристроил CI к проекту Akademo с недавнего хакатона. Мне нужно было, чтобы при мерже в master выполнялись следующие операции:
  1. Сборка проекта в jar
  2. Загрузка jar на сервер
  3. Остановка предыдущей instance
  4. Старт новой intance
У semaphoreCI есть одна особенность: для каждой сборки выделяется чистая виртуальная машина, которая уничтожается после завершения сборки. Таким образом, необходимо не только с нуля настраивать машину при каждой сборке, но и надо думать где хранить артефакты после окончания сборки. Для всего этого была выбрана такая схема:
Для реализации этой схемы в корневой директории проекта я расположил скрипты, которые все это выполняет. 

Данные скрипты выполняются на сервере semaphoreCI:

  • Подготовка окружения
  • Сборка проекта
  • Загрузка jar
  • Загрузка скриптов на боевой сервер и запуск одного из них.
А эти скрипты выполняются на боевой машине:
  • Удаление предыдущего приложения
  • Скачивание jar и его запуск. 

Таким образом, инструкция сборки у меня состоит из нескольких последовательных вызовов этих скриптов.

Теперь по шагам:

Подготовка окружения:

Нужно установить gradle, npm, bower, gulp. В проекте применяется система сборки похожая на ту, которую я описал в этом посте.
#!/bin/bash

sudo apt-get install -y gradle npm
npm install gulp -g
npm install bower -g


Сборка проекта:

В моем приложении это делается одной строкой. Я прокидываю дополнительный параметр чтобы в названии итогового билда было слово RELEASE. 
#!/bin/bash

gradle -PbuildType=RELEASE clean build

Загрузка jar:

Для этого скрипту нужно сделать следующее:
  1. Добавить к итоговому имени файла номер сборки, чтобы хоть как-то отличать сборки внутри одной версии.
  2. Загрузить полученный файл на bitbucket с помощью bitbucket API. Потом этот файл можно будет скачать в разделе downloads.
Чтобы понять, с каким файлом работать, необходимо подгрузить gradle.properties файл, который у меня выглядит вот так:

baseVersion=1.2
baseProjectName=akademo

А это сам скрипт:
#!/bin/bash
. ./gradle.properties
OLD_BUILD_FILE=build/libs/$baseProjectName-$baseVersion-RELEASE.jar
NEW_BUILD_FILE=build/libs/$baseProjectName-$baseVersion-RELEASE-build-$SEMAPHORE_BUILD_NUMBER.jar
mv $OLD_BUILD_FILE $NEW_BUILD_FILE
echo Build is uploading...
curl -s -u $BB_USER:$BB_PASS -X POST $BB_API -F files=@$NEW_BUILD_FILE
echo Build is uploaded

Загрузка скриптов на сервер и запуск:

У меня было написано два скрипта: deploy.sh и kill.sh. Оба эти скрипта должны выполнятся на сервере. Первый скрипт отвечает за deploy, второй за остановку и удаление приложения. Как и в предыдущем случае мне нужен gradle.properties чтобы определить имя билда. Прежде чем скопировать новые скрипты нужно удалить старые. 

#!/bin/bash

. ./gradle.properties

SCRIPT_DIR="~/akademo"
BUILD_FILE=$baseProjectName-$baseVersion-RELEASE-build-$SEMAPHORE_BUILD_NUMBER.jar
echo Removing old deploy.sh ...
ssh -o StrictHostKeychecking=no $SERVER_USER@$SERVER_HOST rm $SCRIPT_DIR'/'deploy.sh
echo Removing old kill.sh ...
ssh -o StrictHostKeychecking=no $SERVER_USER@$SERVER_HOST rm $SCRIPT_DIR'/'kill.sh
echo Copy new deploy.sh to server...
scp -o StrictHostKeychecking=no deploy.sh $SERVER_USER@$SERVER_HOST:$SCRIPT_DIR
echo Copy new deploy.sh to server...
scp -o StrictHostKeychecking=no kill.sh $SERVER_USER@$SERVER_HOST:$SCRIPT_DIR
echo Run delpoy.sh ...
ssh -o StrictHostKeychecking=no $SERVER_USER@$SERVER_HOST bash $SCRIPT_DIR'/'deploy.sh -u $BB_LOGIN -p $BB_PASSWORD -h $BB_DOWNLOADS -b $BUILD_FILE -q 8082
echo Finish deploying

Выполнение скриптов на стороне сервера:

Скрипт deploy.sh принимает на вход параметры:
  • Логин bitbucket
  • Пароль bitbucket
  • URL bitbucket для загрузки
  • Имя билда
  • Номер порта для запуска
Это позволяет с помощью него деплоить не только последнюю сборку, но и любую сборку - достаточно указать имя. 
В начале работы скрипт запускает скрипт удаления предыдущего билда, затем скачивает актуальный и запускает. После запуска создается файл kill.properties. В нем храниться pid актуального запущенного процесса, имя и путь запущенного jar. Эти данные нужны будут скрипту, который будет этот билд удалять.
deploy.sh (убрал парсинг параметров для лучшего просмотра):
#!/bin/bash

bash $SCRIPT_DIR'/'kill.sh

echo "Loading build..."

wget --user $BB_USER --password $BB_PASSWORD $BB_URL'/'$BUILD_FILE -O $DEPLOY_DIR'/'$BUILD_FILE

echo "Running new build..."

nohup java -jar $DEPLOY_DIR'/'$BUILD_FILE --server.port=$APP_PORT > $DEPLOY_DIR/ak.log 2>&1&

echo 'OLD_PID='$! > $SCRIPT_DIR'/'kill.properties
echo 'OLD_BUILD_FILE='$BUILD_FILE >> $SCRIPT_DIR'/'kill.properties
echo 'DEPLOY_DIR='$DEPLOY_DIR >> $SCRIPT_DIR'/'kill.properties

exit 0

kill.sh

#!/bin/bash

SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

if [ ! -f $SCRIPT_DIR'/kill.properties' ]
then
   echo "Nothing to kill"
   exit 0
fi


. $SCRIPT_DIR'/kill.properties'


echo 'kill pid ' $OLD_PID
kill $OLD_PID
echo "Waiting for stop..."
while [ -d /proc/$OLD_PID ]; do
sleep 1
done
echo 'Deleting old build...'
rm $DEPLOY_DIR'/'$OLD_BUILD_FILE
rm $DEPLOY_DIR'/ak.log'
echo 'Old build was deleted'

Что получилось в итоге:

Теперь достаточно сделать merge в мастер ветку и через некоторое время новая сборка будет поднята на сервере. Как видно, все настраивается самостоятельно, что придает гибкость системе и реализовать можно практически все что угодно.



Для интереса я подобрал диаграмму распределения времени двух разных билдов. Левый примерно самый худший, правый - лучший. Время показано в секундах. Основное время сборки уходит на deploy приложения, большую часть этого времени мой сервер качает актуальную сборку с bitbucket. Т.е. в моих силах уменьшить этот промежуток. (билд занимает около 40 мегабайт)

По тому, как сильно может меняться время загрузки билда (зеленый) можно понять, как сильно скачет скорость интернет соединения у semaphoreCI.

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

Также благодаря этому кэшу билд выполняется за довольно стабильное время. (время загрузки зависимостей константное) Для сравнения, на машине Intel Pentium CPU G2120 3.1 GHz 8Gb ОЗУ  он выполняется за 35 секунд. 


Комментариев нет :

Отправить комментарий