Рассказываем, как работать с CI/CD. Сравнение инструментов и подробный гайд по сборке и развертыванию через Docker на удаленный сервер с помощью Gitlab CI/CD на примере Spring Boot-приложения.

Привет! Меня зовут Николай, я Backend-разработчик в РЕЛЭКС. В статье ты найдешь полезный теоретический материал и сравнение инструментов CI/CD. Покажу, как настроить сервер и поделюсь полезными командами, которые помогут в работе.
О чём внутри
- Начнём с теории: коротко о CI/CD — это база. 
- Инструменты работы с CI/CD: выбираем свой. 
- Как устроен процесс CI/CD: схема работы. 
- Переходим к практике: настройка виртуального сервера. 
- Настройка конфига .gitlab-ci.yml. 
Начнём с теории: коротко о CI/CD — это база
CI/CD (Continuous Integration / Continuous Deployment) — это непрерывная интеграция и развертывание, предназначенные для повышения удобства, частоты и надежности публикации изменений программного обеспечения или продукта, где:
CI — это практика разработки ПО, при которой изменения в коде автоматически собираются, тестируются и интегрируются в целевую ветку репозитория. Основная идея — минимизация разрыва между компонентами проекта и быстрая обратная связь о качестве кода, благодаря автоматической сборке и тестированию.
CD — это продолжение CI, которое позволяет автоматически разворачивать успешно собранный и протестированный код на сервере или другой среде реального применения. Цель — автоматизация процесса разработки и развертывания приложения или программного продукта после всех этапов проверки и тестирования. Развертывание в продакшн должно выполняться после ручного подтверждения деплоя, чтобы предоставить дополнительный уровень контроля и безопасности.
Инструментов работы с CI/CD огромное множество — самыми популярными считаются:
- Gitlab CI/CD — полностью интегрированная в GitLab система для автоматизации сборки, тестирования и развертывания программного кода. GitLab CI/CD использует файл конфигурации YAML в репозитории проекта для определения правил работы на каждом этапе в пайплайне. Поддерживает использование Docker-образов для определения окружения сборки — отсюда большая гибкость и повторное использование кода. 
- Jenkins — система с открытым исходным кодом для внедрения CI/CD для автоматизации процесса разработки. Jenkins — самостоятельное приложение, которое требует настройки на сервере, зато предлагает обширный набор плагинов. Это расширяет его функциональность и интеграцию с другими системами и сервисами. 
- Azure DevOps — отдельное комплексное решение от Microsoft с набором инструментов разработки. Оно позволяет командам планировать работу, совместно создавать код и доставлять приложения. Для автоматизации сборки, тестирования и развертывания приложений используется инструмент Azure Pipelines. 
- TeamCity — сервер непрерывной интеграции от JetBrains. У TeamCity открытая архитектура, которая позволяет разработчикам создавать плагины для расширения функционала. Некоторые функции бесплатны, но для больших команд и проектов может потребоваться покупка коммерческой лицензии, с чем сейчас возникают сложности. 

Выбор инструмента зависит от ваших целей, задач и сложности реализации.
Задача — развернуть Spring Boot-приложение, расположенное в Gitlab-репозитории на продакшн стенде. Сделать это надо с помощью удобного и простого инструмента, чтобы начать использовать его функционал сразу, «из коробки» — Gitlab CI/CD отлично подходит. Он полностью интегрирован со средой Gitlab. Не требует дополнительной установки, а также имеет поддержку и подробную документацию.
С выбором инструмента разобрались. Перейдем к главному: как устроен процесс CI/CD. Ниже — схема его работы.
Как устроен процесс CI/CD: схема работы

Алгоритм схемы следующий:
- Разработчик пишет код и заливает его в GitLab-репозиторий проекта. 
- GitLab ищет в корне репозитория конфиг .gitlab-ci.yml и, когда находит, запускает пайплайн согласно описанной в конфиге логике. 
Пайплайн (pipeline) представляет собой целиковый процесс из этапов или стадий (stage), которые состоят из задач (job). Каждая задача выполняется в изолированном процессе (используется GitLab Runner).
Что за термины мы описали выше?
- Раннер (gitlab runner) — приложение, в рамках которого выполняются задачи и которое можно развернуть на разных типах систем: Linux, macOS, Windows, Docker, Kubernetes и так далее. 
- Задачи (jobs) — «кирпичики», из которых строится процесс CI/CD. Это может быть сборка проекта (компиляция, подтягивание зависимостей) или прогон автотестов, или публикация собранного кода в Docker-репозиторий. По умолчанию задачи выполняются изолированно, но их можно связать между собой при помощи артефактов. 
- Артефакты (artifacts) — исполняемые файлы или пакеты для передачи результатов выполнения одной задачи на вход другой. Это позволяет управлять жизненным циклом программного продукта. Пример артефактов — скомпилированные бинарные файлы, архивы, образы контейнеров. 
- Этапы (stages) — служат для группировки задач и определения порядка их выполнения. Задачи, принадлежащие одному этапу, выполняются параллельно, если доступно достаточное количество раннеров. Этапы будут выполняться в порядке, указанном в конфиге. 
- Пайплайн (pipeline) — верхнеуровневый элемент процесса CI/CD, включающий в себя этапы и задачи. 
Переходим от теории к практике. Теперь только пайплайны! Только хардкор!

Переходим к практике: настройка виртуального сервера
Переходим к настройке сервера. Процесс займет не одно действие, поэтому пойдем последовательно, шаг за шагом:
Шаг 1: Установка Docker
Официальный сайт руководства по Docker предоставляет удобный скрипт для неинтерактивной установки Docker. Такой вариант, скорее, подходит для рабочего окружения, а не для продакшена.
С помощью curl-команды скачаем sh-файл для запуска и настройки докера.
curl -fsSL https://get.docker.com -o get-docker.shЗатем запустим его для установки необходимых зависимостей.
sudo sh ./get-docker.shШаг 2: Скачивание и запуск контейнера GitLab Runner в Docker
В официальном руководстве Gitlab есть описание нескольких вариантов GitLab Runner. В этой статье рассмотрим установку через Docker — более простую для развертывания и версионирования. Также меньше нагружает сервер скачиванием дополнительных пакетов. Для наших целей будем использовать облегченный образ GitLab Runner последней версии:
docker run -d --name gitlab-runner --restart always \
  -v /srv/gitlab-runner/config:/etc/gitlab-runner \
  -v /var/run/docker.sock:/var/run/docker.sock \
  gitlab/gitlab-runner:alpineПосле установки можно выполнить команду docker-ps и посмотреть, что появился контейнер с GitLab Runner.
Помимо создания собственных раннеров, которые привязаны к конкретному проекту или группе, в GitLab есть так называемые “Shared runners”, которые доступны для всех проектов в пределах организации или всего инстанса GitLab. Такие раннеры обычно настраиваются администраторами и имеют общую конфигурацию, которую нельзя изменить на уровне отдельного проекта.
Облачная версия gitlab.com предоставляет несколько таких раннеров, которые можно использовать для ваших проектов. Они располагаются в блоке “Shared runners”. Проверьте, чтобы был активен чекбокс “Enable shared runners for this project”.

Шаг 3: Регистрация нового GitLab Runner
Команда с официального руководства Gitlab, где:
— <Runner registration URL> — URL GitLab-сервера, с которым должен связаться Runner.
— <Registration token> — токен для установления связи между Runner и сервером.
Если вы используете свою установку GitLab, то в качестве параметра Runner registration URL, указываете ее URL-адрес. Если используется облачная версия, то https://gitlab.com/.
Чтобы узнать параметр Registration token перейдите в репозиторий проекта, в левой панели откройте меню Settings > CI/CD и разверните секцию Runners. В этой секции, в разделе Project runners, нажмите на троеточие справа от кнопки New project runner, где находится токен.
docker run --rm -it -v /srv/gitlab-runner/config:/etc/gitlab-runner gitlab/gitlab-runner:alpine register \
  --non-interactive \
  --url <Runner registration URL> \
  --registration-token <Registration token> \
  --executor docker \
  --description "Brief description for the project" \
  --tag-list "docker" \
  --docker-image alpine:latest \
  --docker-privileged \
  --docker-volumes "/certs/client"Выполнили команду — переходим во вкладку Runners в настройках CI/CD проекта. Там появится зарегистрированный раннер проекта, готовый к работе.

Шаг 4: Использование Container Registry для сохранения образа приложения
У gitlab есть интегрированный реестр докер-контейнеров (Container Registry). Так, у проекта появляется собственное пространство для хранения докер-образов, которые получили на этапе сборки. Перед использованием реестра контейнеров проверьте, что эта функция включена для вашего проекта. Для это откройте вкладку Visibility, project features, permissions в общих настройках проекта и сделаете чекбокс активным.

Чтобы сохранять и отправлять изображения, нужно пройти аутентификацию в реестре контейнеров. Для аутентификации нужно использовать один из представленных типов:
— Личный токен доступа (Personal access token).
— Токен доступа для деплоя (Deploy tokens).
В примере использовали личный токен доступа. Перейдём в настройки профиля, во вкладку Access token, где нужно выбрать Add new token.
Для всех типов токенов требуется минимальные правила:
- read_registry — доступ на чтение (pull). 
- write_registry — доступа на запись (push). 


Сохраните в буфер обмена получившийся токен и вставьте его в Docker команду для аутентификации в реестре контейнеров, где:
— <username> — ваше реальное имя пользователя.
— <token> — токен доступа.
Команда выполняется на сервере, где ранее установили Docker, чтобы там взаимодействовать с реестром контейнера
docker login registry.example.com -u <username> -p <token>Шаг 5: Настройка SSH-ключа
SSH-ключ используется GitLab CI/CD для входа на сервер и выполнения процедуры развертывания. Сгенерируем 4096-разрядный SSH-ключ алгоритмом RSA (пара из открытого и закрытого ключей).
Флаг -C добавляет комментарий для идентификации ключа. В качестве комментария можно указать, например, имя пользователя. подтвердите оба вопроса с помощью Enter.
ssh-keygen -t rsa -b 4096 -C <username>Чтобы авторизовать публичную часть SSH-ключа, осуществляющего развертывание, добавим её к authorized_keys файлу.
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keysЗатем сохраним приватный ключ в GitLab как CI/CD переменную, чтобы сделать его доступным в процессе работы раннера. Для этого выведем содержимое приватного ключа.
cat ~/.ssh/id_rsaКопируем содержимое ключа в буфер обмена, переходим в настройки CI/CD проекта во вкладку Variable и нажимаем Add variable.

Шаг 6: Определяем оставшиеся переменные окружения CI/CD
Добавим оставшиеся переменные для развертывания приложения на сервере:
SERVER_USER — имя пользователя для подключения к удаленному серверу через SSH. Можете использовать действующего пользователя или создать отдельного в системе.
SERVER_HOST — IP-адрес удаленного сервера для подключения через SSH. Для переменной сделать активной опцию Mask variable
ENV_FILE — путь к файлу с переменными окружения, который будет использован при выполнении команд Docker Compose. Для этого поля поменяете тип с variable на file. В поле для ввода переменные указываются через знак равенства с новой строки.

Таким образом в списке должны отображаться четыре переменные окружения.

На этом все подготовка закончилась. Переходим к настройке конфига .gitlab-ci.yml.
Настройка конфига .gitlab-ci.yml
Файл .gitlab-ci.yml располагается в корне репозитория и определяет структуру пайплайна и логику его работы. Gitlab при каждом поддерживаемом событии (например, push-изменений или создание merge request разработчиком) ищет его и проверяет, есть ли в нем описание для обработки наступившего события.
Ниже — файл .gitlab-ci.yml для сборки и развертывания Spring Boot-приложения в docker-контейнере.
 stages:
 - build
 - deploy
build-only-MR:
 stage: build
 tags:
   - docker
 image:
   name: gcr.io/kaniko-project/executor:v1.9.2-debug
   entrypoint: [ "" ]
 script:
   - /kaniko/executor
     --context "${CI_PROJECT_DIR}"
     --dockerfile "${CI_PROJECT_DIR}/Dockerfile"
     --no-push
 only:
   - merge_requests
build:
 stage: build
 tags:
   - docker
 image:
   name: gcr.io/kaniko-project/executor:v1.9.2-debug
   entrypoint: [ "" ]
 script:
   - /kaniko/executor
     --context "${CI_PROJECT_DIR}"
     --dockerfile "${CI_PROJECT_DIR}/Dockerfile"
     --destination "${CI_REGISTRY_IMAGE}:latest"
 only:
   - main
deploy:
 stage: deploy
 image: docker:20.10-git
 tags:
   - gitlab-org-docker
 variables:
   DOCKER_HOST: "ssh://${SERVER_USER}@${SERVER_HOST}"
 before_script:
   - mkdir -p ~/.ssh
   - chmod 700 ~/.ssh
   - eval $(ssh-agent -s)
   - echo "${SSH_PRIVATE_KEY}" | tr -d '\r' | ssh-add -
   - '[[ -f /.dockerenv || -d /run/secrets/kubernetes.io/serviceaccount ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
   - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
 script:
   - docker compose --env-file $ENV_FILE pull
   - docker compose --env-file $ENV_FILE down --timeout=60 --remove-orphans
   - docker compose --env-file $ENV_FILE up --build --detach
   - docker image prune -f || true
 only:
   - main
 when: manualПайплайн состоит из двух этапов: сборка и развертывание.
Для этапа сборки написаны две задачи: build-only-MR и build. Обе выполняются на gitlab-runner с тегом Docker, который ранее мы зарегистрировали.
Первая — для сборки проекта только при создании merge request-а. При этом получившийся Docker-образ не сохраняется в реестре контейнеров GitLab.
Вторая задача для сборки проекта при вливании кода в ветку main. Получившийся Docker-образ пушится в реестр контейнеров GitLab с тегом latest.
Для сборки Docker-образов из Dockerfile в директории проекта используется образ Kaniko. Это мощный инструмент для сборки образов Docker, который не требует наличия Docker-демона. Например, внутри своего контейнера или кластера Kubernetes. Использование Kaniko считается более быстрым и безопасным подходом, чем Docker-in-Docker.
Этап развертывания включает в себя одну задачу, которая выполняется на общем раннере с тэгом gitlab-org-docker. Это один из Shared runner, которые предоставляет облачная версия gitlab.com.
Задача использует стандартный Docker-образ версии 20.10-git. В переменных задан путь для подключения к Docker на удаленном сервере через SSH. Переменные окружения SERVER_USER и SERVER_HOST определили ранее, как переменные CI/CD.
В блоке before_script выполняется настройка SSH для безопасного взаимодействия со стендом: создание директории, установка прав доступа для хранения SSH-ключа, инициализация SSH-агента, к которому добавляется приватный ключ из переменной окружения. После чего производится настройка параметров SSH. Затем авторизация в реестре контейнеров с использованием логина и пароля, хранящихся в переменных CI_REGISTRY_USER и CI_REGISTRY_PASSWORD — для получения доступа к собранным Docker-образам.
В блоке scripts подтягиваются обновления для образов с реестра. Затем останавливаются и удаляются текущие контейнеры — начинается запуск контейнеров в фоновом режиме с новыми скачанными образами из реестра. Крайняя команда удаляет все неиспользуемые образы.
Эта задача также выполняется только при вливании кода в ветку main и требует ручного подтверждения для запуска (опция when: manual).
Разворачивать будем REST-приложение на Java. Напишем контроллер, в котором будет один GET-запрос для теста, что приложение развернуто на удаленном сервере, и мы можем к нему обратиться.
Ниже — ключевые файлы, которые мы написали.
Тестовый GET-запрос:
@CrossOrigin
@RequestMapping("/test")
@RestController
public class TestController {
   @GetMapping("/hello")
   public ResponseEntity<String> getTest() {
       return ResponseEntity.ok("Hello world");
   }
}Dockerfile
FROM maven:3.8-openjdk-17-slim
RUN mkdir -p /home/app
WORKDIR /home/app
ADD pom.xml /home/app
ADD src /home/app/src
RUN mvn clean package
CMD ["java", "-jar", "/home/app/target/test-0.0.1-SNAPSHOT.jar"]docker-compose.yml
version: '3.9'
services:
 core:
   container_name: spring-application
   image: registry.gitlab.com/my-test-project6/test-cicd-project:latest
   environment:
     TEST_SERVICE_PORT: ${TEST_SERVICE_PORT}
     OPEN_API_TITLE: ${OPEN_API_TITLE}
   ports:
     - "8081:${TEST_SERVICE_PORT}"
   logging:
     driver: 'json-file'
     options:
       max-size: '100m'
       max-file: '3'Такой момент: мы используем переменные TEST_SERVICE_PORT - порт, на котором будет запущено приложение в Docker-контейнере и OPEN_API_TITLE — заголовок swagger-а. Прописав переменные в docker-compose-файле, мы указали нашему Java-приложению, что ее можно использовать как значение переменной в конфигурационном файле application.yml.
application.yml
server:
 port: ${TEST_SERVICE_PORT}
 servlet:
   context-path: /api
api:
 title: ${OPEN_API_TITLE}
springdoc:
 api-docs:
   path: /doc/api-docs
 swagger-ui:
   path: /doc/swagger-ui.htmlКогда изменения кода зальются в main-ветку, начнется выполнение задачи build. Как только сборка успешно завершилась, можно деплоить на прод, нажав на значок запуска.

Через пару минут увидим, что приложение успешно прошло сборку и развертывание в Docker-контейнере на удаленном сервере.

Приложение доступно на порту 8081. Мы можем обратиться по API /api/test/hello и увидеть заветную надпись «Hello world». Готово!

Краткие выводы
CI/CD — важная практика разработки программного обеспечения для автоматизации процесса интеграции, тестирования и развертывания кода. Благодаря CI/CD команды разработчиков могут улучшать качество ПО и доставлять новые функции эффективнее и в более короткие сроки.
Приятных «разворачиваний»! Не бойтесь сложностей — это только начало!
Комментарии (3)
 - w1ldy0uth30.09.2023 14:51- Статья крутая, было бы ещё интересно почитать про разграничение стадий сборки для случаев, когда микросервисное приложение собирается из одного репозитория (чтобы не билдить разом все сервисы, а только те, в которых есть изменения) 
 
           
 
trabl
"Чтобы сохранять и отправлять изображения, нужно пройти аутентификацию в реестре контейнеров."
Не изображения а docker images наверное?
relex_ru Автор
Согласны, верное уточнение! :)