Автоматическое развёртывание проекта при обновлении кода в репозитории

Я уже не представляю себе процесс разработки без системы контроля версий. Но помимо явного полезного действия, такие системы при определённых событиях могу вызывать различные скрипты (например, запрос по URL или отправка уведомлений на электронную почту). Этим можно воспользоваться для автоматического обновления кода проекта на сервере при наличии изменений в определённой ветке.

Коммит в репозиторий, сборка, копирование артефакта

В Интернете можно найти некоторое количество как платных, так и бесплатных сервисов и программ. У меня нет сложных задач, для которых требуются такие системы. Поэтому я пишу свои несложные скрипты для этого.

Для хостинга приватных Git-репозиториев я использую Bitbucket. Там есть настройка «Webhooks». Webhook — это URL, на который Bitbucket делает POST-запрос. Нам нужно написать скрипт, который бы вызывался бы на каждый запрос, загружал изменения из Git-запозитория и выполнял другие полезные действия.

Все команды и настройки были произведены на чистом сервере Ubuntu 17.04

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

В файл /etc/apt/sources.list.d/nginx.list добавляем адрес репозитория с официальными пакетами nginx:

deb http://nginx.org/packages/ubuntu/ zesty nginx
deb-src http://nginx.org/packages/ubuntu/ zesty nginx

Теперь загрузим ключ, которым подписаны пакеты и установим nginx и fcgiwrap:

curl -L https://nginx.ru/keys/nginx_signing.key | apt-key add -
apt-get update
apt-get install nginx fcgiwrap

Добавим конфигурацию сервера, который будет принимать запросы от Git-репозитория. Создадим файл /etc/nginx/conf.d/deploy.conf со следующим содержимым:

server {
  listen 80;
  listen [::]:80;

  server_name deploy.my-cool-project.com;

  location /push-hook {
    gzip off;
    fastcgi_pass unix:/var/run/fcgiwrap.socket;
    include /etc/nginx/fastcgi_params;
    fastcgi_param DOCUMENT_ROOT /var/www/deploy;
    fastcgi_param SCRIPT_FILENAME /var/www/deploy/deploy.sh;
  }
}

Каждый раз, когда nginx получить запрос по адресу http://deploy.my-cool-project.com/push-hook, на сервере будет выполнен скрипт /var/www/deploy/deploy.sh. Давайте создадим его.

mkdir -p /var/www/deploy
nano deploy.sh

Содержимое файла deploy.sh:

#!/bin/sh

echo "Content-type:text/plain\n"
echo "OK!"

Обязательно установим флаг «eXecute» у этого скрипта:

chmod 755 /var/www/deploy/deploy.sh

Теперь можно проверить как он работает:

curl http://ci.my-cool-project.com/push-hook

Клонирование репозитория

Перейдём к настройке git и первоначальному клонированию проекта на сервере.

apt-get install git

Генерируем ключ для доступа:

ssh-keygen -t rsa -f /var/www/deploy/access-key -N ""

Добавляем этот ключ в панели управления репозиторием. Ключ для доступа имеет права «только чтение» и специально создаётся для того, чтобы система CI/CD могла получать изменения.

Сохраняем публичный ключ удаленного сервера:

ssh-keyscan -t rsa bitbucket.org 2>&1 >> /etc/ssh/ssh_known_hosts

Команда ssh делает это автоматически при первом подключении к неизвестному хосту во время процедуры обмена ключами. Но сохраняет она этот публичный ключ в файл для пользователя, выполнившего команду, и только после положительного ответа на запрос. Чтобы команда git pull нормально выполнялась в фоновом процессе под пользователем www-data, публичный ключ нужно добавить вручную в общий файл.

Установим владельца папки /var/www/deploy и её содержимого:

chown -R www-data:www-data /var/www/deploy

Теперь можно клонировать репозиторий:

cd /var/www/deploy
GIT_SSH_COMMAND='ssh -i /var/www/deploy/access-key -o UserKnownHostsFile=/dev/null'
git clone git@bitbucket.org:mistakster/test-project.git ./repo
chown -R www-data:www-data ./repo

Так как мы используем специальный ключ для доступа к репозитории, то мы явно должны сообщить команде git о нём. Однако она не предоставляет соответствующей опции. Данная проблема решается через переменную окружения GIT_SSH_COMMAND. В ней можно указать любые настройки, которые нам нужны.

Подтягивание изменений

Когда мы запушим в репозиторий какие-то изменения, Git-сервер вызовет webhook и передаст информацию о них в виде JSON-объекта. В самом простом варианте, деплой можно запускать при любых изменениях. Но корректнее было бы отслеживать только изменения в нужной ветке, например, master.

Для парсинга JSON в bash-скрипте я буду использовать команду jq.

apt-get install jq

Обновим файл deploy.sh:

#!/bin/sh

echo "Content-type:text/plain\n"

if
  [ -n "$HTTP_X_HOOK_UUID" ] &&
  [ -n "$HTTP_X_REQUEST_UUID" ] &&
  [ "$HTTP_X_EVENT_KEY" = "repo:push" ] &&
  [ "$HTTP_CONTENT_TYPE" = "application/json" ] &&
  [ "$REQUEST_METHOD" = "POST" ]
then
  MASTER_COMMITS=`jq '[.push.changes[].new | select(.name == "master" and .type == "branch")] | length'`

  if [ "$MASTER_COMMITS" -gt 0 ]
  then
    cd ./repo

    GIT_SSH_COMMAND='ssh -i /var/www/deploy/access-key -o UserKnownHostsFile=/dev/null'
    git fetch --all 2>&1
    git reset --hard origin/master
    ./build.sh

    echo "Build complete"
  else
    echo "Build skipped"
  fi
else
  echo "Wrong request"
fi

Теперь мы ожидаем только POST-запрос с соответствующим заголовками и данными. Если в среди всех коммитов встретились изменения в ветке master, то запускается загрузка этих изменений из репозитория и вызов скрпита build.sh в корне проекта.

Фактические действия, необходимые для сборки, например, загрузка пакетов из npm-репозитория, запуск webpack, копирование артефактов и т.п., удобнее вынести именно в скрипт build.sh. Это вполне логично, так как для разных проектов требуются разные действия. К тому же он будет обновляться вместе с проектом и загружаться автоматически.

Теперь можно настроить webhook в Bitbucket.

Настройка webhook в Bitbucket

Заключение

Данное решение покрывает лишь минимально необходимые требования. Оно не является заменой популярных систем CI/CD. Тем не менее, оно стабильно, прекрасно подходит для экспериментальных проектов и заметно сокращает количество рутинных операций для небольшой команды разработчиков.