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

Привет! Меня зовут Николай, я Backend-разработчик в РЕЛЭКС. В статье ты найдешь полезный теоретический материал и сравнение инструментов CI/CD. Покажу, как настроить сервер и поделюсь полезными командами, которые помогут в работе.

О чём внутри
  1. Начнём с теории: коротко о CI/CD — это база.

  2. Инструменты работы с CI/CD: выбираем свой.

  3. Как устроен процесс CI/CD: схема работы.

  4. Переходим к практике: настройка виртуального сервера.

  5. Настройка конфига .gitlab-ci.yml.

Начнём с теории: коротко о CI/CD — это база

CI/CD (Continuous Integration / Continuous Deployment) — это непрерывная интеграция и развертывание, предназначенные для повышения удобства, частоты и надежности публикации изменений программного обеспечения или продукта, где:

CI — это практика разработки ПО, при которой изменения в коде автоматически собираются, тестируются и интегрируются в целевую ветку репозитория. Основная идея — минимизация разрыва между компонентами проекта и быстрая обратная связь о качестве кода, благодаря автоматической сборке и тестированию.

CD — это продолжение CI, которое позволяет автоматически разворачивать успешно собранный и протестированный код на сервере или другой среде реального применения. Цель — автоматизация процесса разработки и развертывания приложения или программного продукта после всех этапов проверки и тестирования. Развертывание в продакшн должно выполняться после ручного подтверждения деплоя, чтобы предоставить дополнительный уровень контроля и безопасности.

Инструментов работы с CI/CD огромное множество — самыми популярными считаются:

  1. Gitlab CI/CD — полностью интегрированная в GitLab система для автоматизации сборки, тестирования и развертывания программного кода. GitLab CI/CD использует файл конфигурации YAML в репозитории проекта для определения правил работы на каждом этапе в пайплайне. Поддерживает использование Docker-образов для определения окружения сборки — отсюда большая гибкость и повторное использование кода.

  2. Jenkins — система с открытым исходным кодом для внедрения CI/CD для автоматизации процесса разработки. Jenkins — самостоятельное приложение, которое требует настройки на сервере, зато предлагает обширный набор плагинов. Это расширяет его функциональность и интеграцию с другими системами и сервисами.

  3. Azure DevOps — отдельное комплексное решение от Microsoft с набором инструментов разработки. Оно позволяет командам планировать работу, совместно создавать код и доставлять приложения. Для автоматизации сборки, тестирования и развертывания приложений используется инструмент Azure Pipelines.

  4. TeamCity — сервер непрерывной интеграции от JetBrains. У TeamCity открытая архитектура, которая позволяет разработчикам создавать плагины для расширения функционала. Некоторые функции бесплатны, но для больших команд и проектов может потребоваться покупка коммерческой лицензии, с чем сейчас возникают сложности.

Сравнительный анализ по наиболее показательным характеристикам
Сравнительный анализ по наиболее показательным характеристикам

Выбор инструмента зависит от ваших целей, задач и сложности реализации.

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

С выбором инструмента разобрались. Перейдем к главному: как устроен процесс CI/CD. Ниже — схема его работы.

Как устроен процесс CI/CD: схема работы

Схема работы CI/CD
Схема работы CI/CD

Алгоритм схемы следующий:

  1. Разработчик пишет код и заливает его в GitLab-репозиторий проекта. 

  2. 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.

Для всех типов токенов требуется минимальные правила:

  1. read_registry — доступ на чтение (pull).

  2. 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)


  1. trabl
    30.09.2023 14:51
    +4

    "Чтобы сохранять и отправлять изображения, нужно пройти аутентификацию в реестре контейнеров."

    Не изображения а docker images наверное?


    1. relex_ru Автор
      30.09.2023 14:51

      Согласны, верное уточнение! :)


  1. w1ldy0uth
    30.09.2023 14:51

    Статья крутая, было бы ещё интересно почитать про разграничение стадий сборки для случаев, когда микросервисное приложение собирается из одного репозитория (чтобы не билдить разом все сервисы, а только те, в которых есть изменения)