Рассказываем, как работать с 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 команды разработчиков могут улучшать качество ПО и доставлять новые функции эффективнее и в более короткие сроки.
Приятных «разворачиваний»! Не бойтесь сложностей — это только начало!