Настройка 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