Настройка CI/CD для GitLab-репозитория: работа с микросервисами

Рассказываем о полезных практиках конфигурации пайплайна с помощью GitLab CI/CD в контексте микросервисной архитектуры. Рассмотрим: добавление заданий в пайплайн путём проверки изменений в определённых микросервисах, концепцию модульных пайплайнов и явное указание зависимостей между заданиями для их выполнения не по этапам.

46e1f2d9e50e04be04169cc67d4a6356.png

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

Эту статью можно считать логическим продолжением прошлой, где речь шла о настройке CI/CD для GitLab-репозитория с наглядным объяснением и пошаговым гайдом.

Небольшое отступление: Регистрация GitLab Runner через Authentication token

В прошлой статье, в практической части на шаге 3 говорилось про регистрацию GitLab Runner через Registration token. Как указано в официальной документации GitLab, c версии 17.0 данный способ отключён, а с версии 18.0 — удалён. Старые runner-ы, созданные с использованием Registration token продолжат работать, но создать новые уже будет нельзя. Сейчас для регистрации GitLab Runner следует использовать Runner authentication token.

ea8d6fb8710734fabb1d81150a8c3602.png

Чтобы узнать параметр Authentication token, перейдите в репозиторий проекта, в левой панели откройте меню Settings > CI/CD и разверните секцию «Runners». В этой секции, в разделе «Project runners», нажмите на кнопку «New project runner». В открывшемся окне напишите необходимые теги для раннера или сделаете активным чекбокс «Run untagged jobs», чтобы он мог выполнять задания без тегов. Параметры конфигурации заполнять не обязательно, если они не нужны по требованиям. После нажатия на кнопку «Create runner», откроется новое окно, в котором отображены шаги регистрации. На первом шаге можно видеть сразу два необходимых значения:

1) --url  — URL-адрес GitLab, к которому будет подключаться runner.

2) --token  — токен для аутентификации runner-а с сервером GitLab. Токены аутентификации имеют префикс glrt-.

Теперь скопированные значения «GitLab server URL» и «Authentication token» подставляем в команду для регистрации раннера. В качестве платформы будем использовать Docker.

docker run --rm -v /srv/gitlab-runner/config:/etc/gitlab-runner gitlab/gitlab-runner register \
  --non-interactive \
  --url  \
  --token  \
  --executor "docker" \
  --docker-image alpine:latest \
  --description "Brief description for the project"

Выполнили команду, затем переходим во вкладку «Runners» в настройках CI/CD. Там появится, зарегистрированный с помощью Authentication token, раннер проекта, готовый к работе.

С профессиональным долгом разобрались, переходим непосредственно к теме данной статьи :)

Запуск заданий (job) только тех микросервисов, в которых произошли изменения

С архитектурной точки зрения есть несколько подходов к хранению микросервисов в системах управления git-репозиториями. Наиболее популярными из них являются монорепозиторий и полирепозиторий. 

При первом подходе вся кодовая база микросервисов разделена на отдельные директории внутри одного общего репозитория. Все сервисы разрабатываются синхронно, что позволяет быстро вносить изменения во взаимозависимые компоненты и проводить централизованное тестирование. Этот подход удобен, если проект небольшой и нет необходимости сильно изолировать разработку.

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

Для тех, кому интересно сравнение данных подходов, вот ссылка на хороший источник для ознакомления. В этой статье сосредоточимся на практическом аспекте работы с GitLab CI/CD. 

При подходе полирепозитория, каждый микросервис требует настройки собственного CI/CD процесса. Задания по сборке, тестированию и развёртыванию запускаются только для конкретного микросервиса, независимо от других. При подходе монорепозитория, напротив, настраивается один CI/CD процесс. Даже если изменения касаются только одного микросервиса, в пайплайне будет происходить запуск сборки, тестирования и развёртывания всех микросервисов, что увеличивает нагрузку на ресурсы в CI/CD. Следовательно, время на выполнение этих заданий может значительно возрасти. В качестве решения данной проблемы, в GitLab CI/CD есть директива changes.

Она используется для определения файлов и директорий в репозитории, изменения в которых должны вызвать выполнение конкретного задания. Благодаря этому можно оптимизировать использование ресурсов в CI/CD. Не перезапускать сразу все сервисы, а только те, над которыми велась работа, или которые имеют прямые зависимости.

Поддерживаемые шаблоны для директивы changes представлены в таблице.

476f4514bb7cd2def8e174b20014bae8.png

Использование различных шаблонов обеспечивает гибкость и точность настройки условий выполнения заданий. Рассмотрим использование директивы changes на примере из официальной документации GitLab:

docker build:
  script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
  only:
    refs:
      - branches
    changes:
      - Dockerfile
      - docker/scripts/*
      - dockerfiles/**/*
      - more_scripts/*.{rb,py,sh}
      - "**/*.json"

4154e487dbd7f9be36bb82856a4b37df.jpg

Инструмент Gitlab CI/CD предоставляет два способа конфигурации заданий с директивой changes:

— с помощью директив only/except;  

— с помощью директивы rules

Оба подхода позволяют запускать задания (job-ы) только при изменении в определённых директориях/файлах. Но они имеют различия в синтаксисе и предоставляемых возможностях. 

1) Директивы only/except.

Директива only позволяет задавать простые условия, при которых определённое задание будет выполняться в пайплайне. Эти условия могут включать в себя отдельные ключи: refs, changes и другие. Здесь важно понимать, как эти ключи логически соединяются, чтобы правильно построить условия для выполнение задания.

С помощью директивы only условия, заданные в отдельных ключах, связаны логическим оператором AND. Это означает, что задание будет добавлено в пайплайн, если все условия возвращали истину. Если указано несколько значений внутри ключей, они соединяются логическим оператором OR. Это означает, что достаточно выполнения одного из указанных значений, чтобы условие в ключе возвращало истину.

Ниже представлен пример использования директивы only:

auth-service-build:
  stage: build
  ...
  only:
    refs:
      - merge_requests
    changes:
      - auth-service/**/*
    variables:
      - $DEV_MODE == "true"

Условие для ключа refs будет в значении true только если прошла проверка, является ли текущее событие Merge request.

Условие для ключа changes будет в значении true, если произошло изменение в любом файле в директории auth-service (по сути, корне микросервиса auth).

Условие для ключа variables будет в значении true, если переменная окружения DEV_MODE равна «true». Это устаревший подход и в документации GitLab рекомендуют использовать для этого rules: if.

Итоговое условие для выполнения задания в пайплайн, прописанное в директиве only, выполнится только при выполнении всех следующих условий одновременно.

Директива except позволяет задавать простые условия, чтобы исключить выполнение определённого задания. Это противоположность директиве only и её использование помогает избежать выполнение задания. К примеру, если изменения в проекте произошли в директориях/файлах, которые должны исключать сборку определённого микросервиса.

С помощью директивы except условия, заданные в отдельных ключах, связаны логическим оператором OR. Это означает, что задание будет исключено из пайплайна, если выполняется любое из условий. Если указано несколько значений внутри ключей, они соединяются логическим оператором OR. Это означает, что достаточно выполнения одного из указанных значений, чтобы условие в ключе возвращало истину.

Ниже представлен пример использования директивы except:

deploy-prod:
  stage: deploy
  image: docker:20.10-git
  variables:
    DOCKER_HOST: "ssh://${SERVER_USER}@${SERVER_HOST}"
  ...
  except:
    refs:
      - tags
    changes:
      - config/**/*
      - deployment/**/*.yaml
  when: manual

Условие для ключа refs будет в значении true только если прошла проверка, является ли текущее событие тегом. Если событие является тегом, задание не будет выполнено.

Условие для ключа changes будет в значении true, если изменения затрагивают файлы в директории config или файлы с расширением ».yaml» в директории deployment и её поддиректориях. Если любые изменения произошли в этих файлах или директориях, задание не будет выполнено.

Итоговое условие для исключения задания из пайплайна, прописанное в директиве except, выполнится, если выполняется хотя бы одно из описанных выше условий для ключей.

Более подробно про директивы only и except можно почитать в официальной документации GitLab.

2) Директива rules.

Директива rules позволяетзадать более сложные условия для выполнения задания по сравнению с директивами only и except. Она представляет собой список, где каждый элемент является отдельным правилом, определяющим условия для выполнения задания с помощью ключей: if, changes, exists, variables, allow_failure, when, start_in и других.

С помощью директивы rules условия, заданные внутри одного правила, связаны логическим оператором AND. Это означает, что задание будет добавлено в пайплайн, если все условия внутри одного правила будут возвращать истину. Если хотя бы одно из условий не выполняется, правило считается ложным. Между правилами действует логический оператор OR. Это означает, если любое из правил в директиве rules истинно, задание будет выполнена.

Более подробно про директиву rules и все её вариации можно почитать в официальной документации GitLab. Для решения текущей проблемы, остановимся на условиях с ключами if и changes.

Ниже представлен пример использования директивы rules:

auth-service-build-only-MR:
  stage: build
  image:
    name: executor:v1.16.0-debug
    entrypoint: [ "" ]
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "dev"
      changes:
        - auth-service/**/*
        - common-module/**/*
    - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"
      changes:
        - auth-service/**/*
        - common-module/**/*
        - keycloak/**/*.yaml
  script:
    - /kaniko/executor
      --context "${CI_PROJECT_DIR}"
      --dockerfile "${CI_PROJECT_DIR}/auth-service/Dockerfile"
      --no-push

В данном примере блок rules содержит два правила, каждое из которых состоит из ключей if и changes.

# Первое правило
- if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "dev"
  changes:
    - auth-service/**/*
    - common-module/**/*

Условие для ключа if будет в значении true, если тип события «merge request» и целевая ветка для МР-а — dev. Используется оператор »&&», т.е. оба условия должны быть истинными.

Условие для ключа changes будет в значении true, если изменились любые файлы в директории auth-service и её поддиректориях или изменились любые файлы в директории common-module и её поддиректориях.

# Второе правило
- if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"
  changes:
    - auth-service/**/*
    - common-module/**/*
    - keycloak/**/*.yaml

Условие для ключа if будет в значении true, если тип события «merge request» и целевая ветка для МР-а — main. Используется оператор »&&», т.е. оба условия должны быть истинными.

Условие для ключа changes будет в значении true, если изменились любые файлы в директории auth-service и её поддиректориях или изменились любые файлы в директории common-module и её поддиректориях или любые YAML-файлы в директории keycloak и её поддиректориях.

Если любое из двух правил истинно, задание будет выполнено. Правила обрабатываются последовательно, и если одно из них выполняется, остальные игнорируются.

Директиву rules: changes стоит использовать, когда нужны более сложные и комбинированные условия, такие как определённые ветки, события, или переменные окружения наряду с изменениями в файлах. Это позволяет точно настраивать выполнение заданий для различных сценариев. Директивы only/ except: changes лучше подходят для более простых случаев, когда требуется запустить или пропустить задания на основе конкретных изменений в файлах, без необходимости учитывать дополнительные условия.

Модульная структура пайплайнов

Создание пайплайна для микросервисной архитектуры, так или иначе, может привести к дублированию конфигурации в .gitlab-ci.yml файле. Особенно когда микросервисы написаны на одном языке программирования и используют один и тот же технологический стек. 

Возьмём, к примеру, репозиторий, в котором расположено несколько микросервисов и есть один общий конфигурационный файл. При появлении новых микросервисов в проекте под них пишутся новые задания (job-ы), что со временем сильно усложняет «читаемость» файла и поддержку любых изменений в правилах или логике. Если же под каждый микросервис будет выделен отдельный репозиторий и написан свой файл конфигурации, снова возникает сложность в поддержке таких конфигураций. Любое изменение в правилах или логике потребует обновления конфигурации чуть ли не в каждом репозитории. Хочется получить достаточно гибкое и масштабируемое решение, чтобы можно было быстро подключать новые микросервисы к CI/CD процессу. 

В такой ситуации использование модульных пайплайнов помогает выделить общие блоки конфигурации, например, в отдельную директорию или в отдельный репозиторий и использовать их в CI/CD процессах для всех микросервисов. Таким образом мы можем применять уже готовые модули для строительства нужного нам пайплайна (по аналогии с кубиками «Лего») и менять/дополнять конфигурацию в одном месте, вместо каждого задания.

GitLab CI/CD предоставляет следующие средства для создания модульных пайплайнов:

1) Директива include позволяет подключать свои собственные конфигурационные файлы CI/CD процесса внутрь других конфигурационных файлов по типу .gitlab-ci.yml тремя возможными способами:  

— подключение файла из того же репозитория (include: local);

— подключение файла из другого проекта в GitLab (include: project);

— подключения шаблонов из внешних источников по URL (include: remote).

2) Директива extends позволяет расширять один блок конфигурации процессов CI/CD посредством добавления к нему других (повторное использования выделенных блоков конфигурации).

3) ! reference tags позволяют ссылаться на существующие блоки конфигурации, такие как скрипты, правила, переменные или другие параметры, и использовать их повторно в разных частях конфигурационного файла. В отличие от якорей (anchors), ссылаться можно на блоки конфигурации из внешних файлов.

4) anchors — якоря, определяющие блоки конфигурации для дублирования или наследования свойств в рамках одного конфигурационного файла.

Рассмотрим пример, цель которого показать, как можно использовать на практике модульные пайплайны, чтобы получить управляемую и масштабируемую конфигурацию:

«В одном репозитории лежит программный код двух микросервисов auth и core, написанных на языке Java с применением Spring фреймворка. Вся конфигурация пайплайна находится в корневом файле .gitlab-ci.yml. Проект растёт, появляются новые сервисы. Их уже порядка восьми и все задания записываются в тот же файл .gitlab-ci.yml. Количество строк порядком выросло и в нём уже с трудом можно ориентироваться. При этом сами задания, относящиеся к одному этапу, имеют идентичную конфигурацию, отличаясь только указанием директории сервиса. Из-за этого приходится «копипастить» целиком задание и редактировать его в паре мест. А когда требования меняются, приходится исправлять конфигурацию не в одном месте, а проходить по каждому заданию отдельно.»

Итак, необходимо выделить общие блоки, которые будут часто переиспользовать при добавлении новых сервисов. В ходе анализа выяснилось, что можно выделить отдельно блоки переменных (variables), правил (rules) и кэширование зависимостей Maven (cache). Остальные блоки конфигурации были вынесены в шаблоны (templates). 

Все общие блоки запишем в отдельные YAML-файлы и создадим диаграмму структуры папок в проекте.

Диаграмма структуры папок в проекте

Диаграмма структуры папок в проекте

Теперь перейдём непосредственно к заполнению всех файлов и посмотрим зависимости между ними.

Определим общие переменные в файле .job-vars.gitlab-ci.yml

variables:
  # Environment names for branches
  DEV_ENV_NAME: "dev"
  PRE_PROD_ENV_NAME: "pre_prod"
  # Container registry
  CONTAINER_REGISTRY_PATH: gitlab.com:5050/microservices/backend

Определим набор правил для выполнения заданий в файле .job-rules.gitlab-ci.yml

include:
  - local: '/cicd/variables/.job-vars.gitlab-ci.yml'


.if-pipeline-source-merge-request-and-target-branch-name-dev: &if-pipeline-source-merge-request-and-target-branch-name-dev
  if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "dev"

.if-pipeline-source-merge-request-and-target-branch-name-pre_prod: &if-pipeline-source-merge-request-and-target-branch-name-pre_prod
  if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "pre_prod"

.if-commit-branch-dev-and-merge-request-source-branch-name-is-not-dev: &if-commit-branch-dev-and-merge-request-source-branch-name-is-not-dev
  if: $CI_COMMIT_BRANCH == "dev" && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME != "dev"

.if-commit-branch-pre_prod-and-merge-request-source-branch-name-is-not-pre_prod: &if-commit-branch-pre_prod-and-merge-request-source-branch-name-is-not-pre_prod
  if: $CI_COMMIT_BRANCH == "pre_prod" && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME != "pre_prod"


.rules:test:
  rules:
    - <<: *if-pipeline-source-merge-request-and-target-branch-name-dev
      variables:
        ENVIRONMENT_NAME: $DEV_ENV_NAME
    - <<: *if-pipeline-source-merge-request-and-target-branch-name-pre_prod
      variables:
        ENVIRONMENT_NAME: $PREPROD_ENV_NAME

.rules:build:
  rules:
    - <<: *if-commit-branch-dev-and-merge-request-source-branch-name-is-not-dev
      variables:
        ENVIRONMENT_NAME: $DEV_ENV_NAME
    - <<: *if-commit-branch-pre_prod-and-merge-request-source-branch-name-is-not-pre_prod
      variables:
        ENVIRONMENT_NAME: $PREPROD_ENV_NAME

Файл .job-rules.gitlab-ci.yml содержит набор правил, которые определяют, при каких условиях запускаются те или иные задания. Правила включают условия с помощью якорей для различных веток и источников событий, таких как создание merge request. Эти правила легко могут быть использованы в других файлах конфигурации CI/CD-процесса, обеспечивая единообразие в управлении пайплайнами.

Определим общую конфигурацию (шаблоны) для заданий тестирования, сборки и развертывания. При добавлении новых микросервисов, шаблоны вместе с правилами можно будет переиспользовать.

Шаблон для заданий тестирования в файле .test-template.gitlab-ci.yml

.template:test:
  stage: test
  tags:
    - docker-dev
  services:
    - name: docker:26.1.2-dind
      command: [ "--tls=false" ]
  image: ${CONTAINER_REGISTRY_PATH}/maven-3.8-openjdk-17-slim:latest
  environment:
    name: $ENVIRONMENT_NAME
  script:
    - source ${TEST_RUN_PATH}
  artifacts:
    when: always
    reports:
      junit:
        - ${SERVICE_DIR}/target/surefire-reports/TEST-*.xml
    expire_in: 3 days

Шаблон для заданий сборки в файле .build-template.gitlab-ci.yml

.template:build:
  stage: build
  image:
    name: ${CONTAINER_REGISTRY_PATH}/kaniko-project/executor:v1.16.0-debug
    entrypoint: [ "" ]
  script:
    - /kaniko/executor
      --context "${CI_PROJECT_DIR}"
      --build-arg MAVEN_IMAGE=${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/maven:3.8.6
      --build-arg JAVA_IMAGE=${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/openjdk:17-jdk-slim
      --build-arg MAVEN_OPTS="${MAVEN_OPTS}"
      --build-arg MAVEN_CLI_OPTS="${MAVEN_CLI_OPTS}"
      --dockerfile "${CI_PROJECT_DIR}/${SERVICE_DIR}/Dockerfile"
      --destination "${CI_REGISTRY_IMAGE}/${SERVICE_DIR}:$ENVIRONMENT_NAME"

Шаблон для заданий развертывания в файле .deploy-template.gitlab-ci.yml

.template:deploy:
  stage: deploy
  image: docker:24.0.9-git
  variables:
    DOCKER_HOST: "ssh://${SERVER_USER}@${SERVER_HOST}"
  environment:
    name: $ENVIRONMENT_NAME
  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
    - apk add --no-cache python3 git
    - chmod +x ./scripts/gitlab_ci/collect_deployed_services.sh
  script:
    - eval "$(./scripts/gitlab_ci/collect_deployed_services.sh pull ${STAND_NAME})"
    - eval "$(./scripts/gitlab_ci/collect_deployed_services.sh remove ${STAND_NAME})"
    - eval "$(./scripts/gitlab_ci/collect_deployed_services.sh up ${STAND_NAME})"
    - docker image prune -f || true

Определим настройки кэширования maven-зависимостей в локальный m2 репозиторий в файл .maven-cache.gitlab-ci.yml. Это ускоряет сборку проектов на Java.

variables:
  MAVEN_OPTS: >-
    -Dhttps.protocols=TLSv1.2
    -Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository
    -Dorg.slf4j.simpleLogger.showDateTime=true
    -Djava.awt.headless=true
  MAVEN_CLI_OPTS: >-
    --batch-mode
    --errors
    --fail-at-end
    --show-version
    -DdeployAtEnd=true

cache:
  paths:
    - .m2/repository

Поскольку в репозитории хранится код более чем одного приложения, определим для каждого сервиса свой файл .gitlab-ci.yml, где будут сами задания (job-ы). Этот подход особенно хорош, когда у нас гетерогенные микросервисы, то есть написанные на разных языках программирования. У каждого приложения могут быть разные задания на сборку и тестирование для выполнения. Каждый файл конфигурации процессов CI/CD будет содержать вначале приписку, которая указывает на принадлежность к сервису. Для стадии деплоя также определим свой .gitlab-ci.yml файл с припиской «deploy», где будут храниться задания для развёртывания на разные среды.

Конфигурационный файл для auth сервиса .auth.gitlab-ci.yml будет выглядеть следующим образом.

include:
  # rules
  - local: '/cicd/rules/.job-rules.gitlab-ci.yml'
  # templates
  - local: '/cicd/templates/.test-template.gitlab-ci.yml'
  - local: '/cicd/templates/.build-template.gitlab-ci.yml'
  # variables
  - local: '/cicd/variables/.job-vars.gitlab-ci.yml'

auth-service-test:
  extends:
    - .template:test
  rules:
    - !reference [ .rules:test, rules ]
  variables:
    SERVICE_DIR: "auth-service"
    TEST_RUN_PATH: "./scripts/gitlab_ci/auth_test.sh"

auth-service-build:
  extends:
    - .template:build
  rules:
    - !reference [ .rules:build, rules ]
  variables:
    SERVICE_DIR: "auth-service"

Конфигурационный файл для core сервиса .core.gitlab-ci.yml.

include:
  # rules
  - local: '/cicd/rules/.job-rules.gitlab-ci.yml'
  # templates
  - local: '/cicd/templates/.test-template.gitlab-ci.yml'
  - local: '/cicd/templates/.build-template.gitlab-ci.yml'
  # variables
  - local: '/cicd/variables/.job-vars.gitlab-ci.yml'

core-service-test:
  extends:
    - .template:test
  rules:
    - !reference [ .rules:test, rules ]
  variables:
    SERVICE_DIR: "core-service"
    TEST_RUN_PATH: "./scripts/gitlab_ci/core_test.sh"

core-service-build:
  extends:
    - .template:build
  rules:
    - !reference [ .rules:build, rules ]
  variables:
    SERVICE_DIR: "core-service"

Файлы .auth.gitlab-ci.yml и .core.gitlab-ci.yml содержат задания для тестирования и сборки программного кода. С помощью директивы exstends мы объединили нужные нам шаблоны, специфичные для каждого сервиса. Правила указаны через ! reference теги. Таким образом можно добавить новые правила для конкретного задания на другой строчке, не изменяя текущие. Через директиву variables передаются нужные переменные, которые затем будут переданы в шаблон.

Определим конфигурационный файл для задания развёртывания .deploy.gitlab-ci.yml.

include:
  # templates
  - local: '/cicd/templates/.deploy-template.gitlab-ci.yml'
  # variables
  - local: '/cicd/variables/.job-vars.gitlab-ci.yml'

deploy-dev:
  tags:
    - deploy-dev
  extends: .template:deploy
  variables:
    ENVIRONMENT_NAME: $DEV_ENV_NAME
    STAND_NAME: dev
  only:
    - dev

deploy-pre_prod:
  tags:
    - deploy-pre_prod
  extends: .template:deploy
  variables:
    ENVIRONMENT_NAME: $PREPROD_ENV_NAME
    STAND_NAME: pre_prod
  only:
    - pre_prod

Файл .deploy.gitlab-ci.yml содержит задания развёртывания программного кода для dev и pre_prod (staging) окружений. Эти задания расширяют шаблон деплоя и используют соответствующие переменные из файла .job-vars.gitlab-ci.yml.

Конфигурационный файл .gitlab-ci.yml в корне репозитория выглядит следующим образом.

stages:
  - test
  - build
  - deploy

variables:
  DOCKER_HOST: "tcp://docker:2375"
  DOCKER_TLS_CERTDIR: ""
  DOCKER_DRIVER: overlay2

include:

  #
  #   MAVEN CACHE 
  #
  - local: '/cicd/cache/.maven-cache.gitlab-ci.yml'

  #
  #   DEPLOY JOB 
  #
  - local: '/cicd/.deploy.gitlab-ci.yml'

  #
  #   SERVICES JOBS 
  #
  - local: '/auth-service/.auth.gitlab-ci.yml'
    rules:
      - changes:
          - auth-service/**/*
          - cicd/**/*

  - local: '/core-service/.core.gitlab-ci.yml'
    rules:
      - changes:
          - core-service/**/*
          - cicd/**/*

В корневом файле .gitlab-ci.yml используется директива include с rules: changes. Это нововведение, которое появилось в GitLab 16.4, для определения правил включения других конфигурационных файлов на основе директивы changes.

Такой подход позволяет использовать задания только тогда, когда изменяются файлы, относящиеся к определённому микросервису. Например, если обновляются файлы в папке auth-service, запустится только пайплайн для этого сервиса, а пайплайн для core-service пропускается. Таким образом, задания не будут запускаться лишний раз при изменениях, которые не касаются текущего микросервиса, что значительно снижает нагрузку на CI/CD и сокращает время выполнения пайплайнов.

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

Явное указание зависимостей для выполнения заданий

В базовом пайплайне по умолчанию новый этап не начинается, пока все задания из предыдущего этапа не будут успешно завершены. Надо сказать, что в контексте монорепозитория с несколькими микросервисами, это может быть не самым эффективным способом. 

Представим, что у нас репозиторий, состоящий из трёх микросервисов: auth, core и notification. Пайплайн состоит из этапов сборки, тестирования и развёртывания. На этапе сборки auth и notification сервисы собираются довольно быстро, но core сервис может собираться дольше, и в это время задания для двух других сервисов на этапе тестирования запуститься не могут, что увеличивает общее время работы CI/CD.

Архитектура базового пайплайна

Архитектура базового пайплайна

Для повышения эффективности и быстроты работы пайплайна в Gitlab CI/CD была добавлена директива needs. Она используется для управления зависимостями между заданиями в пайплайне в рамках одного этапа или нескольких. Когда в конфигурационном файле чётко обозначено, какие задания от каких зависят, процесс CI/CD может игнорировать порядок этапов, описанных в блоке stages и запускать некоторые задания, не дожидаясь завершения других.

Использование needs позволит запустить задания для каждого сервиса параллельно, вместо того чтобы ждать завершения всех предыдущих заданий на одном этапе. Это особенно полезно, если микросервисы не зависят друг от друга. 

Кроме того, мы можем явно указать, какие задания должны завершиться перед началом других. Например, у нас есть этап развёртывания, в котором происходит деплой для двух окружений: dev и prod. С помощью needs мы можем указать в пайплайне, что соответствующие задания должны выполняться только после успешного завершения тестов для каждого микросервиса. Таким образом мы получаем чёткую иерархию зависимостей, что позволит лучше понимать процессы внутри пайплайна. 

Рекомендую ознакомиться с разделом «Архитектура пайплайнов»‎ в официальной документации GitLab.

Архитектура пайплайна c использованием директивы needs

Архитектура пайплайна c использованием директивы needs

Определим конфигурацию пайплайна, соответствующую схеме выше. На ней видно, что есть зависимости, обозначенные как сплошной, так и прерывистой линией. Дело в том, что задания, использующие rules, only или except и добавляемые с помощью include, не всегда могут быть добавлены в пайплайн. Чтобы избежать ошибок, связанных с этим, надо использовать needs с optional: true. Таким образом, если зависимые задания не попадут в пайплайн, задание, которое зависит от них, всё-равно будет выполнено. 

По умолчанию в GitLab для всех заданий указывается опция when: on_success, поэтому явно её можно не указывать. Она означает, что задание выполнится только в случае успешного завершения всех предыдущих заданий в пайплайне. Но иногда явное указание такой опции помогает разработчикам, читающим файл конфигурации, легче понять, при каких условиях выполняется определённое задание. Для задания deploy_prod укажем опцию when: manual. Она означает, что задание запустится только при ручном подтверждении. 

Конфигурационные файлы микросервисов будут выглядеть следующим образом.

/auth-service/.auth.gitlab-ci.yml

build_auth:
  stage: build
  tags:
    - gitlab-org-docker
  script:
    - echo "Building auth service"

test_auth:
  stage: test
  tags:
    - gitlab-org-docker
  script:
    - echo "Testing auth service"
  needs:
    - job: build_auth

/core-service/.core.gitlab-ci.yml

build_core:
  stage: build
  tags:
    - gitlab-org-docker
  script:
    - echo "Building core service"
    - sleep 30 # Имитация долгой сборки

test_core:
  stage: test
  tags:
    - gitlab-org-docker
  script:
    - echo "Testing core service"
  needs:
    - job: build_core

/notification-service/.notification.gitlab-ci.yml

build_notification:
  stage: build
  tags:
    - gitlab-org-docker
  script:
    - echo "Building notification service"

test_notification:
  stage: test
  tags:
    - gitlab-org-docker
  script:
    - echo "Testing notification service"
  needs:
    - job: build_notification

Конфигурационный файл для заданий развертывания будет выглядеть следующим образом. 

/deploy/.deploy.gitlab-ci.yml

.needs:services:
  needs:
    - job: test_auth
      optional: true
    - job: test_core
      optional: true
    - job: test_notification
      optional: true


deploy_dev:
  stage: deploy
  tags:
    - gitlab-org-docker
  extends:
    - .needs:services
  script:
    - echo "Deploying to dev"
  when: on_success

deploy_prod:
  stage: deploy
  tags:
    - gitlab-org-docker
  extends:
    - .needs:services
  script:
    - echo "Deploying to prod"
  when: manual

Конфигурационный файл в корне репозитория будет выглядеть следующим образом.

.gitlab-ci.yml

stages:
  - build
  - test
  - deploy

include:

  - local: '/auth-service/.auth.gitlab-ci.yml'
    rules:
      - changes:
          - auth-service/**/*

  - local: '/core-service/.core.gitlab-ci.yml'
    rules:
      - changes:
          - core-service/**/*

  - local: '/notification-service/.notification.gitlab-ci.yml'
    rules:
      - changes:
          - notification-service/**/*

  - local: '/deploy/.deploy.gitlab-ci.yml'

В итоге получаем пайплайн, на котором видны все зависимости. При наведении на конкретное задание GitLab подсвечивает всю историю зависимостей от начальных заданий к выбранному.

beaa3510d1d006fe0148ff2fcb0fa812.png

При работе с директивой needs важно понимать, что несмотря на все описанные преимущества, существуют также определённые риски и минусы, которые стоит учитывать. При неверной настройке зависимостей задания будут выполняться не в том порядке, который вы планировали. Это может привести к некорректным результатам самого приложения, когда некоторые сервисы будут уже с новой версией, а другие ещё не развёрнуты. Также чрезмерное применение директивы needs может усложнить конфигурацию пайплайна и сделать его трудным для понимания, особенно в больших проектах. Без хорошей документации разобраться будет сложно. Поэтому важно тщательно планировать структуру пайплайна и устанавливать зависимости между заданиями максимально рационально.

Краткие выводы

В этой статье мне хотелось поделиться частью своего опыта и теми практиками, которые помогли сделать процесс CI/CD более масштабируемым и понятным. Я не говорю: «Используйте это так и никак иначе».  Вариантов конфигурации может быть множество. Всё зависит от особенностей проекта и поставленных задач. Но само понимание работы данных инструментов GitLab CI/CD может пригодиться в будущем.

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

© Habrahabr.ru