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