Из-за прекращения поддержи Bitbucket Server пришлось переехать на GitLab.
В Bitbucket Server не было встроенного CI/CD, поэтому использовали Teamcity. Из-за проблемы интеграции Teamcity с GitLab, мы попробовали GitLab Pipline. И остались довольны.
Коротко о Gitlab Pipline
Если говорить простыми словами: сборка делится на задачи. Какие-то задачи выполняются последовательно и передают результаты следующим задачам, какие-то задачи распаралеливаются: например можно одновременно деплоить на сервер и в nexus.
Не обязательно один раннер отвечает за все задачи в сборке. Если на проекте два раннера, то первую задачу может взять один ранер, после ее выполнения вторую задачу возьмет другой раннер.
Раннеры бывают разных типов. Мы рассмотрим executor docker. Для каждой задачи создается новый чистый контейнер. Но между контейнерами можно передавать промежуточные результаты – это называется кэширование.
Кэширование
Механизм кэширования разрабатывал какой-то одаренный человек. Поэтому сразу разобраться, как оно работает, будет не просто. В отдельной статье можно прочитать о кэшировании подробнее.
Каждый раннер хранит кэш в папке /cache
. Для каждого проекта в этой папке создается еще папка. Сам кэш хранится в виде zip архива.
Из-за наличия у каждого раннера своей папки для кэша, возникает проблема. Если один раннер кэшировал папку в первой задаче, а второй раннер взялся за выполнение второй задачи, то у него не окажется кэша из первой задачи. Чуть далее рассмотрим как это исправить.
Можно определить только один набор папок для кэша. Также только для всего набора можно выбрать политику кэширования. Например если вы уверены, что задача не изменит кэш, то можно его не загружать. Но нельзя запретить загружать например две папки из четырех.
Нет возможности установить глобальный кэш и добавить к нему дополнительные папки для конкретных задач.
Так же стоит знать, что кэш не удаляется после окончания сборки. При следующей такой же сборке старый кэш будет доступен.
Эти особенности усложняют создание инструкций для CI CD.
Артефакты
Помимо кэша между сборками можно передавать артефакты.
В отличие от кэша, артефакт передается между раннерами. Из-за этого многие злоупотребляют использованием артефактов там, где стоит использовать кэш.
Следуйте следующим правилам:
- Если текущее задание может выполняться самостоятельно, но в присутствии контента работа будет идти быстрее, используйте кэш.
- Если текущее задание зависит от результатов предыдущего, то есть не может выполняться само по себе, используйте артефакты и зависимости.
Установка Gitlab Runner
Перед установкой ранера создадим папку с конфигурацией, чтобы не приходилось для изменений лезть в контейнер.
mkdir ~/runner_name
Само создание ранера выполняется в одну команду. Можно создать сколько угодно ранеров.
docker run -d --name gitlab-runner-name \
--restart always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /home/user/runner_name:/etc/gitlab-runner:z \
gitlab/gitlab-runner:latest
Мало создать раннер, теперь его нужно зарегистрировать в GitLab. Зарегистрировать можно на уровне всего GitLab, тогда сборки будут выполняться для любого проекта; на уровне группы – выполнятся только для группы, и на уровне проекта.
Заходим в контейнер.
sudo docker exec -ti gitlab-runner-name bash
Внутри контейнера выполним команду регистрации. Регистрация происходит в интерактивном режиме.
gitlab-runner register
Отвечаем на вопросы:
Runtime platform arch=amd64 os=linux pid=27 revision=888ff53t version=13.8.0
Running in system-mode.
Enter the GitLab instance URL (for example, https://gitlab.com/):
http://git.company.name/
Enter the registration token:
vuQ6bcjuEPqc8dVRRhgY
Enter a description for the runner:
[c6558hyonbri]: runner_two
Enter tags for the runner (comma-separated):
Registering runner... succeeded runner=YJt3v3Qg
Enter an executor: parallels, shell, virtualbox, docker+machine, kubernetes, custom, docker, docker-ssh+machine, docker-ssh, ssh:
docker
Enter the default Docker image (for example, ruby:2.6):
maven:3.3.9-jdk-8
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded
Тут все просто:
- Адрес вашего gitlab.
- Токен авторизации. Посмотреть его можно в настройках гурппы/проекта в разделе CI/CD Runners.
- Название раннера.
- Теги ранера, можно пропустить нажав Enter.
- Исполнитель сборки. Вводим docker.
- Образ, который будет использоваться по умолчанию, если не установлен другой.
После этого в настройках проекта можно посмотреть доступные раннеры.
После регистрации, в папке /home/user/runner_name
появится файл с настройками конфигурации config.toml
. Нам нужно добавить docker volume для кэширования промежуточных результатов.
volumes = ["gitlab-runner-builds:/builds", "gitlab-runner-cache:/cache"]
В начале статьи я рассказал о проблеме кеширования. Ее можно решить с помощью монтирования одного volume к разным раннерам. То есть во втором своем раннере так же укажите volumes = ["gitlab-runner-cache:/cache"]
.
Таким образом разные раннеры будут иметь единый кэш.
В итоге файл конфигурации выглядит так:
concurrent = 1
check_interval = 0
[session_server]
session_timeout = 1800
[[runners]]
name = "runner_name"
url = "gitlab_url"
token = "token_value"
executor = "docker"
[runners.custom_build_dir]
[runners.cache]
[runners.cache.s3]
[runners.cache.gcs]
[runners.cache.azure]
[runners.docker]
tls_verify = false
image = "maven:3.3.9-jdk-8"
privileged = false
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["gitlab-runner-builds:/builds", "gitlab-runner-cache:/cache"]
shm_size = 0
После изменения перезапускаем раннер.
docker restart gitlab-runner-name
Что хотим получить?
У нас на проекте было 3 контура:
- dev-сервер, на него все попадает сразу после MR;
- пре-прод-сервер, на него все попадает перед попаданием на прод, там проходит полное регресс тестирование;
- прод-сервер, собственно сама прод среда.
Что нам было необходимо от нашего CI/CD:
- Запуск unit-тестов для всех MergeRequest
- При мерже в
dev
ветку повторный запуск тестов и автоматический деплой на dev-сервер. - Автоматическая сборка, тестирвоание и деплой веток формата
release/*
на пре-прод-сервер. - При мерже в
master
ничего не происходит. Релиз собирается при обнаружении тега форматаrelease-*
. Одновременно с деплоем на прод-сервер будет происходить загрузка в корпоративный nexus. - Бонусом настроим уведомления о статусе деплоя в Telegram.
🗜 Настройка GitLab CI для Maven
Создайте в корне проекта файл .gitlab-ci.yml
.
Вы можете указать нужный образ, вместо дефолтного образа у раннера.
image: maven:latest
Устанавливаем место хранения папки .m2
через переменные среды variables
. Это понадобится, когда будем настраивать кэширование.
// ... ... ... ... ...
variables:
MAVEN_OPTS: "-Dmaven.repo.local=./.m2/repository"
// ... ... ... ... ...
Далее указываются этапы сборки. Они позволяют группировать задачи для одновременного выполнения.
// ... ... ... ... ...
stages:
- build
- test
- package
- deploy
- notify
// ... ... ... ... ...
- build – стадия сборки.
- test – стадия тестирования.
- package – стадия упаковки в jar.
- deploy – стадия деплоя на сервер.
- notify – стадия уведомления о провале.
Указываем непосредственно задачи для каждой стадии.
Сборка - Build
// ... ... ... ... ...
build:
stage: build
only:
- dev
- merge_requests
- /^release\/.*$/
except:
- tags
script:
- 'mvn --settings $MAVEN_SETTINGS compile'
cache:
paths:
- ./target
- ./.m2
// ... ... ... ... ...
Раздел script
выполняет linux команды.
Переменная GitLab CI/CD $MAVEN_SETTINGS
необходима для передачи файла settings.xml
, если вы используете нестандартные настройки, например корпоративные репозитории.
Переменная создается в настройках CI/CD для группы/проекта. Тип переменной File.
Раздел only
указывает для каких веток и тегов выполнять задачу. Чтобы не собирать каждую запушенную ветку устанавливаем: dev
, merge_requests
и ветки формата /release/*
.
Раздел only
не отличает ветки от тегов. Поэтому мы указываем параметр except
, который исключает теги. Из-за этого поведения за сборку на прод отвечают отдельные задачи. В противном случае, если бы кто-то создал тег формата release/
, то он бы запустил сборку.
Для защиты от случайного деплоя в прод джуном, рекомендую установить защиту на ветки dev
, master
, /release/*
, а так же на теги release-*
. Делается это в настройках проекта GitLab.
Раздел cache
отвечает за кэширование. Чтобы каждый раз не выкачивать зависимости добавляем в кэш папку ./target
и ./.m2
.
Запуск unit тестов – test
Создаем задачу для запуска тестов.
// ... ... ... ... ...
test:
stage: test
only:
- dev
- merge_requests
- /^release\/.*$/
except:
- tags
script:
- 'mvn --settings $MAVEN_SETTINGS test'
cache:
paths:
- ./target
- ./.m2
// ... ... ... ... ...
Ничем не отличается от стадии сборки, только команда мавена test
. Кэш необходимо указать и в этой задаче, чтобы получить его и отправить дальше.
Упаковка – package
Следом добавляем задачу упаковки с выключенными тестами. Она уже выполняется только для веток dev
и release/*
. Упаковывать Merge Request смысла нет.
// ... ... ... ... ...
package:
stage: package
only:
- dev
- /^release\/.*$/
except:
- tags
script:
- 'mvn --settings $MAVEN_SETTINGS package -Dmaven.test.skip=true'
artifacts:
paths:
- target/*.jar
cache:
policy: pull
paths:
- ./target
- ./.m2
// ... ... ... ... ...
Обратите внимание на policy: pull
и artifacts
. В следующих задачах не нужны исходники и зависимости. Параметр policy: pull
загрузит кэш для этой задачи, но не отправит его для следующей.
Для сохранения результатов сборки используем artifacts
, который сохраняет .jar
файлы и передает их следующим задачам.
Деплой – deploy
Рассмотрим два варианта деплоя. С помощью обычного переноса .jar
файла на сервер и упаковка в докер.
Перенос jar на сервер
Осуществим это с помощью стандартных ssh
и scp
.
// ... ... ... ... ...
deploy_dev_server:
stage: deploy
only:
- dev
except:
- tags
before_script:
- which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan $DEV_HOST >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- ssh $DEV_USER@$DEV_HOST "[ ! -f $DEV_APP_PATH/app_name.jar ] || mv $DEV_APP_PATH/app_name.jar $DEV_APP_PATH/app_name-build-$CI_PIPELINE_ID.jar"
- scp target/app_name.jar $DEV_USER@$DEV_HOST:$DEV_APP_PATH/
- ssh $DEV_USER@$DEV_HOST "sudo systemctl stop app_name_service && sudo systemctl start app_name_service"
// ... ... ... ... ...
Не забудьте создать переменные в GitLab CI CD:
$DEV_USER
– пользователь системы, от чьего имени будет происходить деплой.$DEV_HOST
– ip адрес сервера.$DEV_APP_PATH
– путь до папки приложения.$SSH_PRIVATE_KEY
– приватный ключ.
Раздел before_script
отвечает за выполнение настроек перед основным скриптом. Мы проверяем наличие ssh-agent, устанавливаем его при отсутствии. После чего добавляем приватный ключ, устанавливаем правильные права на папки.
В разделе script
происходит деплой на сервер:
- Проверяем наличие старого jar и переименовываем его.
$CI_PIPELINE_ID
- это глобальный номер сборки Pipeline. - Копируем новый jar на сервер.
- Останавливаем и запускаем службу, отвечающую за приложение.
- Отправляем уведомление об успехе в телеграм. Об этом ниже.
На пре-прод делаем по аналогии, только меняем переменные на $PRE_PROD_*
.
// ... ... ... ... ...
deploy_pre_prod:
stage: deploy
only:
- /^release\/.*$/
except:
- tags
before_script:
- which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan $PRE_PROD_HOST >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- ssh $PRE_PROD_USER@$PRE_PROD_HOST "[ ! -f $PRE_PROD_APP_PATH/app_name.jar ] || mv $PRE_PROD_APP_PATH/app_name.jar $PRE_PROD_APP_PATH/app_name-build-$CI_PIPELINE_ID.jar"
- scp target/app_name.jar $DEV_USER@$PRE_PROD_HOST:$PRE_PROD_APP_PATH/
- ssh $PRE_PROD_USER@$PRE_PROD_HOST "sudo systemctl stop app_name_service && sudo systemctl start app_name_service"
// ... ... ... ... ...
Деплой в docker
Предполагается, что у вас уже есть свой DockerFile. Мы добавим новую стадию сборки save_nexus
, и разделим задачи доставки образа и его запуска на севрере.
Сборка и доставка образа:
docker_push_dev_nexus:
image: docker:20.10.7
stage: save_nexus
only:
- dev
except:
- tags
before_script:
- echo "$DOCKER_DEV_PASS" | docker login localhost:8183 --username user --password-stdin
script:
- docker build --no-cache -f Dockerfile-dev -t localhost:8183/company/app-name:latest .
- docker push localhost:8183/company/app-name:latest
- docker image rm localhost:8183/company/app-name:latest
В разделе before_script
мы авторизуемся в нашем nexus, после чего собираем наш образ и пушим его. Ничего сложного.
Теперь следующий шаг. Подключиться к серверу и перезапустить контейнер.
deploy_dev_server:
stage: deploy
only:
- dev
except:
- tags
before_script:
- which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan $DEV_HOST >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- ssh $DEV_USER@$DEV_HOST -t 'bash -ci "update-app"'
Раздел script запускает alias на сервере, который и должен выполнить все команды для перезапуска контейнера. Вы можете перезапустить один контейнер, или перезапустить контейнер в docker-compose. Здесь все индивидуально.
Настройка деплоя на прод
Для прод-деплоя можно объединить сборку, тестирование и упаковку в одну задачу.
// ... ... ... ... ...
package_prod:
stage: package
only:
- /^release-.*$/
except:
- branches
script:
- 'mvn --settings $MAVEN_SETTINGS package'
artifacts:
paths:
- target/*.jar
// ... ... ... ... ...
Мы защищаемся от срабатывания на ветки формата release-*
, нам нужно срабатывание только по тегу.
Деплой аналогичен пре-проду, изменяется только only
и переменные.
Дополнительно появляется задача с отправкой артефакта в корпоративный nexus. Деплой на сервер и в нексус работает параллельно.
// ... ... ... ... ...
deploy_prod:
stage: deploy
only:
- /^release-.*$/
except:
- branches
...
deploy_nexus_server:
stage: deploy
only:
- /^release-.*$/
except:
- branches
script:
- 'mvn --settings $MAVEN_SETTINGS deploy -Dmaven.test.skip=true'
// ... ... ... ... ...
Итоговый .gitlab-ci.yml
:
image: maven:latest
variables:
MAVEN_OPTS: "-Dmaven.repo.local=./.m2/repository"
stages:
- build
- test
- package
- deploy
- notify
build:
stage: build
only:
- dev
- merge_requests
- /^release\/.*$/
except:
- tags
script:
- 'mvn --settings $MAVEN_SETTINGS compile'
cache:
paths:
- ./target
- ./.m2
test:
stage: test
only:
- dev
- merge_requests
- /^release\/.*$/
except:
- tags
script:
- 'mvn --settings $MAVEN_SETTINGS test'
cache:
paths:
- ./target
- ./.m2
package:
stage: package
only:
- dev
- /^release\/.*$/
except:
- tags
script:
- 'mvn --settings $MAVEN_SETTINGS package -Dmaven.test.skip=true'
artifacts:
paths:
- target/*.jar
cache:
policy: pull
paths:
- ./target
- ./.m2
deploy_dev_server:
stage: deploy
only:
- dev
except:
- tags
before_script:
- which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan $DEV_HOST >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- ssh $DEV_USER@$DEV_HOST "[ ! -f $DEV_APP_PATH/app_name.jar ] || mv $DEV_APP_PATH/app_name.jar $DEV_APP_PATH/app_name-build-$CI_PIPELINE_ID.jar"
- scp target/app_name.jar $DEV_USER@$DEV_HOST:$DEV_APP_PATH/
- ssh $DEV_USER@$DEV_HOST "sudo systemctl stop app_name_service && sudo systemctl start app_name_service"
deploy_pre_prod:
stage: deploy
only:
- /^release\/.*$/
except:
- tags
before_script:
- which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan $PRE_PROD_HOST >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- ssh $PRE_PROD_USER@$PRE_PROD_HOST "[ ! -f $PRE_PROD_APP_PATH/app_name.jar ] || mv $PRE_PROD_APP_PATH/app_name.jar $PRE_PROD_APP_PATH/app_name-build-$CI_PIPELINE_ID.jar"
- scp target/app_name.jar $DEV_USER@$DEV_HOST:$DEV_APP_PATH/
- ssh $PRE_PROD_USER@$PRE_PROD_HOST "sudo systemctl stop app_name_service && sudo systemctl start app_name_service"
package_prod:
stage: package
only:
- /^release-.*$/
except:
- branches
script:
- 'mvn --settings $MAVEN_SETTINGS package'
artifacts:
paths:
- target/*.jar
deploy_prod:
stage: deploy
only:
- /^release-.*$/
except:
- branches
before_script:
- which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan $PROD_HOST >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- ssh $PROD_USER@$PROD_HOST "[ ! -f $PROD_APP_PATH/app_name.jar ] || mv $PROD_APP_PATH/app_name.jar $PROD_APP_PATH/app_name-build-$CI_PIPELINE_ID.jar"
- scp target/app_name.jar $PROD_USER@$PROD_HOST:$PROD_APP_PATH/
- ssh $PROD_USER@$PROD_HOST "sudo systemctl stop app_name_service && sudo systemctl start app_name_service"
Удобно, что теперь и код и сборка находятся в одном месте. Несмотря на недостатки GitLab CI, пользоваться им в целом удобно.
🌟 Бонус: уведомления о деплое в Telegram
Полезная фишка для уведомления менеджеров. После сборки в телеграм отправляется статус сборки, название проекта и ветка сборки.
В задачах деплоя последней командой пропишите:
// ... ... ... ... ...
script:
- ...
- sh ci-notify.sh ✅
// ... ... ... ... ...
Сам файл необходимо добавить в корень проекта, рядом с файлом .gitlab-ci.yml
.
Содержимое файла:
#!/bin/bash
TIME="10"
URL="https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage"
TEXT="Deploy status: $1%0A-- -- -- -- --%0ABranch:+$CI_COMMIT_REF_SLUG%0AProject:+$CI_PROJECT_TITLE"
curl -s --max-time $TIME -d "chat_id=$TELEGRAM_CHAT_ID&disable_web_page_preview=1&text=$TEXT" $URL >/dev/null
Скрипт отправляет запрос к API Telegram, через curl. Параметром скрипта передается emoji статуса билда.
Не забудьте добавить новые параметры в CI/CD:
$TELEGRAM_BOT_TOKEN
– токен бота в телеграмм. Получить его можно при создании своего бота.$TELEGRAM_CHAT_ID
– идентификатор чата или беседы, в которую отправляется сообщение. Узнать идентификатор можно с помощью специального бота.
Необходимо создать задачи для уведомления о падении сборки.
// ... ... ... ... ...
notify_error:
stage: notify
only:
- dev
- /^release\/.*$/
script:
- sh ci-notify.sh ❌
when: on_failure
notify_error_release:
stage: notify
only:
- /^release-.*$/
except:
- branches
script:
- sh ci-notify.sh ❌
when: on_failure
// ... ... ... ... ...
По сложившейся традиции у нас отдельная задача для прод-среды. Благодаря параметру when: on_failure
задача отрабатывает только, когда одна из предыдущих завершилась неудачно.