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

(Просмотрено 32 раз, 1 раз за сегодня)
Вы можете оставить комментарий, или Трекбэк с вашего сайта.

Оставить комментарий