Настройка Gitlab CI/CD для java приложения
1. Создание раннера
Для начала нам нужно организовать постоянно работающий процесс (runner), который будет выполнять все задачи по нашему CICD (т.е. задания билдинга, проверки, закрузки на сервер и выполнения в нём каких-то команд). Кстати, у гитлаба есть много разных публичных runner’ов, но, во-первых — я бы не хотел чтобы код моего закрытого репозитория улетал на какие-то непонятные раннеры, во-вторых — раннер надо настроить под конкретную задачу, чтобы адекватно кешировались промежуточные результаты и не тормозил весь процесс снова и снова проделывая одни и те же операции.
В общем, про установку гитлаб раннеров написано здесь. Я буду поднимать на имеющемся ubuntu сервере докер контейнер с раннером.
Установленный раннер регистрируется на gitlab и постоянно спрашивает у него новые задачи. Как только задача будет получена, то раннер создаёт ещё один докер-контейнер рядом с собой и задача уже выполняется в том контейнере. После выполнения контейнер удаляется. Для того чтобы это работало, раннеру, который сам будет запущен в докер-контейнере, будет нужен доступ к докеру хостовой машины, чтобы он сам мог создавать и запускать в нём другие контейнеры.
Для нормальной работы раннера нужно сохранять на жестком диске:
— конфигурацию раннера
— кэш раннера (кэш билдов + кэш maven)
Подготовлю директорию для хранения этих файлов:
mkdir /home/gitlab-runner mkdir /home/gitlab-runner/cache mkdir /home/gitlab-runner/cache/builds mkdir /home/gitlab-runner/cache/maven mkdir /home/gitlab-runner/config |
Теперь создам именованные volumes, которые понадабятся для настройки раннера (а именно — кэширования):
docker volume create --driver local -o o=bind -o type =none -o device= /home/gitlab-runner/cache/maven gitlab-runner-cache docker volume create --driver local -o o=bind -o type =none -o device= /home/gitlab-runner/cache/builds gitlab-runner-builds |
Теперь нужно инициировать раннер, т.е. сгенерировать необходимые конфигурационные файлы, которые будут постоянно находиться в примаунтенном volume.
Для этого согласно документации выполняем команду:
docker run -- rm -it --name gitlab_runner -- mount type =bind,src= /home/gitlab-runner/config ,destination= /etc/gitlab-runner -- mount type =bind,src= /var/run/docker .sock,destination= /var/run/docker .sock gitlab /gitlab-runner register |
В консоль будет выведен запрос Enter the GitLab instance URL (for example, https://gitlab.com/):. Чтобы на него ответить, нужно зайти на свой гитлаб и перейти в репозиторий, для которого этот раннер настраивается. Далее Settings->CICD->Runners->Expand.

В разделе Specific runners указаны url и token. Вводим их в консоле при соответствующих запросах.
Далее в следующих запросах я вводил это:
- description: ubuntu_runner_1 #будет выводиться в списке раннеров как название раннер
- tags: javamaven #будет брать задачи только помеченные этим тэком (чтобы не брать, например, задачи сборки фронтенда)
- maintenance: knastnt #это можно не указывать
- executor: docker #х.з. что это, но в инструкции рекомендуют указывать docker
- default docker image: openjdk:11 #образ, на основе которого будут выполняться задачи
В результате регистрации в папке конфигов должен появиться файл config.toml.
Для того, чтобы все задачи могли использовать общий кэш, меняем в нём секцию volumes с:
volumes = ["/cache"] |
на:
volumes = ["gitlab-runner-builds:/builds", "gitlab-runner-cache:/cache"] |
в которой указываем ранее созданные volume’s.
В итоге получится файл следующего содержания cat /home/gitlab-runner/config/config.toml:
concurrent = 1 check_interval = 0 [session_server] session_timeout = 1800 [[runners]] name = "ubuntu_runner_1" url = "https://gitlab.com/" token = "qwertyqwertyqwerty" executor = "docker" [runners.custom_build_dir] [runners.cache] [runners.cache.s3] [runners.cache.gcs] [runners.cache.azure] [runners.docker] tls_verify = false image = "openjdk:11" 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 run -- rm -d --name gitlab_runner -- mount type =bind,src= /home/gitlab-runner/config ,destination= /etc/gitlab-runner -- mount type =bind,src= /var/run/docker .sock,destination= /var/run/docker .sock gitlab /gitlab-runner |
А вот и он:

Ещё нужно отключить использование публичных раннеров, чтобы они не лезли и не мешали жить (Enable shared runners for this project = false):

Особо не примудрствуя, я написал bash скрипт, который будет использоваться для старта раннера и для его перезагрузки:
#/bin/sh echo "(Re)Start gitlab runner:" docker rm -f gitlab_runner docker run -- rm -d --name gitlab_runner -- mount type =bind,src= /home/gitlab-runner/config ,destination= /etc/gitlab-runner -- mount type =bind,src= /var/run/docker .sock,destination= /var/run/docker .sock gitlab /gitlab-runner echo "Done." |
2. Настройка CI/CD в репозитории
Для этого нужно создать файл .gitlab-ci.yml, который будет содержать информацию о шагах (stages) и описании действий в каждом шаге.
В моём скрипте будут следующие шаги:
- build. Шаг, в котором будет собираться проект, т.е. выполнятся команда mvn compile. В этом шаге будет выкачан из интернета набор maven зависимостей и помещён в кэш, а также выполнится проверка возможности компиляции проекта.
- test. Шаг, на котором будут выполняться тесты. Этот шаг будет использовать заранее подготовленный кэш + докачаются зависимости необходимые для тестов.
- package. В результате этого шага будет сгенерирован jar и помещён в артифакты. Кстати, сгенерированные артифакты можно выкачать с самого гитлаба со страницы job’а.
- deploy. Самый трудный шаг, в котором нужно будет подключиться к удалённому серверу, закинуть туда jar и выполнить все команды по его инициализации. Этот шаг будет с речным запуском (не будет вызываться автоматически), кроме одной ветки — production.
В общем, вот мой рабочий .gitlab-ci.yml:
image: maven:3.8.6-jdk-11 stages: - build - test - package - deploy build: stage: build tags: - javamaven script: - 'mvn compile -Dmaven.repo.local=./.m2/repository' cache: paths: - . /target - ./.m2 test : stage: test tags: - javamaven script: - 'mvn test -Dmaven.repo.local=./.m2/repository' cache: paths: - . /target - ./.m2 package: stage: package tags: - javamaven script: - 'mvn package -Dmaven.repo.local=./.m2/repository -Dmaven.test.skip=true' artifacts: paths: - target/*.jar cache: policy: pull paths: - . /target - ./.m2 deploy: stage: deploy tags: - javamaven rules: - if : $CI_COMMIT_BRANCH == "production" when: on_success - when: manual script: # Start update: - apt-get update # Update done. Start create authentication key: - echo -n "$DEPLOY_KEY" > key - chmod 600 key # Creating authentication key done. Start creating script for adding this key into ssh-agent: - touch r.sh - echo '#!/usr/bin/expect -f' >> r.sh - echo "spawn ssh-add key" >> r.sh - echo "expect \"Enter passphrase for key:\"" >> r.sh - echo "send $DEPLOY_KEY_PASS\r" >> r.sh # duplicate this because it's not work from first time. I dont know - echo "spawn ssh-add key" >> r.sh - echo "expect \"Enter passphrase for key:\"" >> r.sh - echo "send $DEPLOY_KEY_PASS\r" >> r.sh - echo "interact" >> r.sh - chmod +x r.sh # Creating script done. Install Expect - apt-get install expect -y # Installing Expect done. Start ssh-agent - eval ` ssh -agent` # Ssh-agent started. Run script and remove it with key - . /r .sh - rm r.sh - rm key # Running script done. Start other native commands: - ssh -o UserKnownHostsFile= /dev/null -o StrictHostKeyChecking=no root@$DEPLOY_DEST_HOST -p$DEPLOY_DEST_PORT "cd $DEPLOY_DEST_APP_BACK_PATH; pkill java; rm app.jar" - scp -o UserKnownHostsFile= /dev/null -o StrictHostKeyChecking=no -P $DEPLOY_DEST_PORT target /app .jar root@$DEPLOY_DEST_HOST:$DEPLOY_DEST_APP_BACK_PATH /app .jar - ssh -o UserKnownHostsFile= /dev/null -o StrictHostKeyChecking=no root@$DEPLOY_DEST_HOST -p$DEPLOY_DEST_PORT "cd $DEPLOY_DEST_APP_BACK_PATH; nohup java -verbose:gc -Xloggc:gc.log -XX:+PrintGCDetails -jar $DEPLOY_DEST_APP_BACK_PATH/app.jar --spring.config.location=file://$DEPLOY_DEST_APP_BACK_PATH/app.properties >/dev/null 2>&1 &" |
Ситуацию значительно усложнол тот факт, что для подключения к удалённому серверу у меня есть единственный ключ, который защищён паролем, но у ssh нет такой команды, которая позводяет передать этот пароль. Поэтому пришлось извращаться. Но по хорошему, Вам нужно сгенерировать отдельный ключ для деплоя и не защищать его паролем. Это будет гораздо проще.
Здесь же используется ssh-agent, в который закидывается ключ. При закидывании он спрашивает пароль, который с пишу при помощи библиотеки expect, которую тоже нужно установить. Процесс закидования ключа в ssh-agent организовывается внутри скрипта r.sh. После выполнения которого всё идёт нормальным чередом. Если у вас ключ не защищён паролем, то Вы просто используете директиву ssh -i yourkey и радуетесь, ведь заморочка с ssh-agent, expect и r.sh не понадобится.
Чтобы это всё заработало, нужно создать переменные окружения в gitlab (Settings->CICD->Variables->Expand):
- DEPLOY_KEY = ваш приватный ключ в формате OpenSSH. (начнётся c ——BEGIN RSA PRIVATE KEY——). Можно сгенерировать или сконвертировать при помощи PuTTYgen
- DEPLOY_KEY_PASS = у меня это пароль к ключу, Вам может быть он и не нужен, если Ваш ключ без пароля
- DEPLOY_DEST_HOST = ip сервера куда будем грузить jar
- DEPLOY_DEST_PORT = порт подключения ssh
- DEPLOY_DEST_APP_BACK_PATH = директория, куда будем грузить jar

Заранее нужно на сервере подготовить эту директорию и положить в неё app.properties для старта задеплоенного приложения.
Готово. Теперь можно наблюдать Ваши пайплайны в CI/CD->Pipelines