Gitlab-ci модульные pipelines для вашего enterprise

от автора

Привет, меня зовут Панов Михаил, я DevOps‑инженер МТС Digital. Хочу поделиться с вами опытом построения «модульных» pipelines на основе gitlab-ci. В этой статье я расскажу, что такое модульный CI/CD, из чего он состоит, для чего нужен и как поможет командам, поддерживающим большой список нетиповых репозиториев.

Часть 1: Проблема класcического gitlab-ci

Предположим, что вы участник команды, которая сопровождает крупный стрим, в котором есть несколько команд со своими сервисами:

  • Frontend;

  • Backend;

  • API. 

Одна команда использует Golang, другая часть сервисов – на основе Gradle, а еще Frontend есть. Нужно не забывать и о том, что существуют нетиповые репозитории, например, для AWX или Terraform.

В таких условиях возникает вопрос: «А как лучше все это автоматизировать?». Варианты такие:

  • можно создать типовые для каждого языка gitlab-ci.yml и складывать их в репозитории. Но тут мы столкнемся с трудностями версионирования и обновления;

  • можно создать один инфраструктурный репозиторий и переложить все туда, затем с помощью include подключать в нужный репозиторий. Но и тут возникнут трудности. Файлы будут предназначены для конкретной задачи (test, build, deploy, etc).

Хотелось бы получить что-то настолько гибкое и быстрое, что-то, что даст возможность уменьшить время внедрения новых проектов в наши pipelines.

Посмотрим, что можно сделать и перейдем к примерам.

Часть 2: Типовые gitlab-ci – что можно сделать лучше?

Типовой gitlab-ci.yml, как правило, располагается в самом репозитории и содержит несколько stage и job. Например:

#Пример 1 stages:     - test     - build     - deploy  test:job:     stage: test     image:         name: $IMAGE         entrypoint: []     script:         - echo "test"     rules:         if: $CI_COMMIT_BRANCH == "master"     varuables:         ENABLE: "true"     tags:         - prod-runner  build:job:     stage: build     image:         name: $IMAGE         entrypoint: []     dependencies: []     needs:         - test-job     script:         - echo "build"     rules:         if: $CI_COMMIT_BRANCH == "master"     varuables:         ENABLE: "true"     tags:         - prod-runner  deploy:job:     stage: deploy     image:         name: $IMAGE         entrypoint: []     dependencies:         - build-job     needs:         - test-job         - build-job     script:         - echo "deploy"     rules:         if: $CI_COMMIT_BRANCH == "master"     varuables:         ENABLE: "true"     tags:         - prod-runner 

В свою очередь, каждая job несет в себе информацию о том, что и как нужно сделать с кодом. Это рабочий вариант, но с каждой новой строчкой кода будет усложняться «читабельность» файла.

Пункт 2.1: Теория модульности

Чтобы достичь гибкости и читабельности кода, разобьем Пример 1 на логические части. 

Gitlab предоставляет возможности для разделения кода с помощью:

  • include – добавление одного файла в другой;

  • extends – объединение схожих блоков кода между собой или расширение одного блока кода за счет другого;

  • anchors – якоря, которые позволяют переиспользовать блоки кода в разных сценариях;

  • reference tags – ссылки, которые дают возможность сослаться на блок кода, а также использовать и переиспользовать этот код.

Эти инструменты позволят собирать пайплайны (pipelines) как «конструктор».

Пункт 2.2: Структура и теория

Взглянув еще раз на Пример 1, выделим части, которые занимают много места и могут быть переиспользованы. Например:

  • блок правил (rules);

  • блок переменных (variables);

  • блок скриптов(before_script, script, after_script) 

Вынесем эти блоки в отдельные файлы и определим структуру проекта:

templates-repo/ |--variables |   |--test-vars |   |  └──test-vars.gitlab-ci.yml |   |--build-vars |   |  └──build-vars.gitlab-ci.yml |   └──deploy-vars |      └──deploy-vars.gitlab-ci.yml |--rules |   |--test-rules |   |  └──test-rules.gitlab-ci.yml |   |--build-rules |   |  └──build-rules.gitlab-ci.yml |   └──deploy-rules |      └──deploy-rules.gitlab-ci.yml |--scripts |   |--test-scripts |   |  └──test-scripts.gitlab-ci.yml |   |--build-scripts |   |  └──build-scripts.gitlab-ci.yml |   └──deploy-scripts |      └──deploy-scripts.gitlab-ci.yml |--templates |   |--build-bin |   |  └──build-bin-template.gitlab-ci.yml |   |--build-image |   |  └──build-image-template.gitlab-ci.yml |   └──deploy |      └──deploy-template.gitlab-ci.yml |--entrypoints |   |--java |   |  |--mvn.gitlab-ci.yml |   |  └──gradle.gitlab-ci.yml |   |--golang |   |  └──golang.gitlab-ci.yml |   └──deploy |      └──deploy-template.gitlab-ci.yml

Структура проекта определена, теперь нам нужно собрать первую job. Для примера соберем job kaniko, остальные jobs будут собираться по такому же принципу.

Пункт 2.3: Примеры

Чтобы получить полноценную job, необходимо объединить вынесенные элементы между собой. Ниже примеры объединяемых шаблонов:

build-rules.gitlab-ci.yml

############# ## Changes ## #############  .changes-exclude-files-devops: &changes-exclude-files-devops   - ".gitlab-ci.yml"   - "Dockerfile"  ########### ## Rules ## ###########  .if-enable-build-kaniko-env: &if-enable-build-kaniko-env   if: $ENABLE_KANIKO_BUILD == "false"  .if-enable-build-kaniko-tag-env: &if-enable-build-kaniko-tag-env   if: $ENABLE_KANIKO_BUILD_TAG == "false"  .if-ci-branch-and-env-exist: &if-ci-branch-and-env-exist   if: $CI_COMMIT_BRANCH =~ /master|feature\/.*/ && $ENABLE_KANIKO_BUILD == "true"  .if-ci-tag-and-env-exist: &if-ci-tag-and-env-exist   if: $CI_COMMIT_TAG =~ /^(v[0-9]\d*)\.([0-9]\d*)\.([0-9]\d*)(-([a-zA-Z-]*))?/ && $ENABLE_KANIKO_BUILD_TAG == "true"  .if-piplince-surce-push-or-merge-request: &if-piplince-surce-push-or-merge-request   if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event"  .if-ci-commit-tag: &if-ci-commit-tag   if: $CI_COMMIT_TAG  ################# ## Build rules ## #################  .rules:build-image:job:   rules:     - <<: *if-enable-build-kaniko-env       when: never     - <<: *if-piplince-surce-push-or-merge-request       changes: *changes-exclude-files-devops       when: never     - <<: *if-ci-commit-tag       when: never     - <<: *if-ci-branch-and-env-exist     - when: never 

build-vars.gitlab-ci.yml

.variables:k8s:deploy:kaniko:   variables:     ENVIRONMENT: prod     IMAGE: kaniko:latest 

build-scripts.gitlab-ci.yml

.script:infra-build-script:   script:     - 'echo "Building docker image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA"'     - /kaniko/executor         --context "$CI_PROJECT_DIR"         --dockerfile "$CI_PROJECT_DIR/.docker/Dockerfile"         --destination "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA"         --cache=true --cache-ttl 336h         --label ru.mts.git.project="$CI_PROJECT_URL"         --label ru.mts.git.ref="$CI_COMMIT_REF_NAME"         --label ru.mts.git.revision="$CI_COMMIT_SHORT_SHA"         --skip-tls-verify         --use-new-run         --registry-mirror=your.registry.com         --build-arg http_proxy=$http_proxy         --build-arg https_proxy=$https_proxy         --build-arg no_proxy=$no_proxy 

Файл /templates/build-image/build-image-template.gitlab-ci.yml будет выглядеть следующим образом:

include:     - local: '/scripts/build-scripts/build-scripts.gitlab-ci.yml'     - local: '/rules/build-rules/build-rules.gitlab-ci.yml'     - local: '/variables/build-vars/build-vars.gitlab-ci.yml'  build:kaniko:job:     stage: build-image     image:         name: $IMAGE         entrypoint: []     needs:         - job: test-job     dependencies: []     extends:         - .script:infra-build-script         - .rules:build-image:job         - .variables:k8s:deploy:kaniko     tags:         - prod-runner 

С помощью extends мы объединили в шаблон scripts, rules и variables. Это дает возможность собрать полноценный job.

Как видно из примера выше (Пункт 2.2 — структура), появилась еще одна директория entrypoints/, которая объединяет в себе шаблоны (templates) для формирования подключаемого файла для группы проектов.

Он будет выглядеть следующим образом:

include:     - local: '/templates/build-bin/build-bin-template.gitlab-ci.yml'     - local: '/templates/build-image/build-image-template.gitlab-ci.yml'     - local: '/templates/deploy/deploy-template.gitlab-ci.yml'  stages:     - test     - build     - deploy  # Global variables variables:     ENABLE_LINT: "true"     ENABLE_BUILD: "true"     ENABLE_DEPLOY_PROD: "true"     ENABLE_DEPLOY_TEST: "true"     ENABLE_DEPLOY_DEV: "true" 

Тут мы перечисляем stages, includes, variables. То есть те элементы которые могут находиться только в «верхнеуровневом» или подключаемом файле.

Пункт 2.4: Нюансы

Пройдемся по нюансам этой реализации. Так как Gitlab у всех может быть разной степени владения — «коммунальный» или полностью подконтрольный и закрытый, реализация шаблонов будет немного меняться в части include. Документация говорит, что мы можем воспользоваться четырьмя возможными «инпутами»:

Possible inputs: The include subkeys. 

include:local – используется для подключения файлов, находящихся в репозитории локально;

include:project – вариант, подходящий в большинстве случаев, так как позволяет тонко настраивать подключение файла в репозиторий назначения; 

include:remote – используется для подключения шаблонов из внешних источников, потребуется авторизация (подробнее в документации);

include:template – отметаем, так как он используется для подключения встроенных шаблонов самого gitlab.

Также хотелось бы обратить внимание на несколько моментов: 

Если репозиторий у вас открыт и все хранится в main (master) ветке — можно воспользоваться include:remote, так как это будет ссылка на конкретный файл и она займет меньше места (хотя, казалось бы, куда еще меньше?). Если репозиторий закрытый(internal) — используйте include:project.

Include:project можно использовать двумя разными способами в зависимости от того, как вы управляете инфраструктурными файлами в своих репозиториях.

Вариант 1. Если изменения производятся с помощью ansible, terraform, etc — файл будет следующего вида, с явным указанием полей:

include:   - project: 'my-group/my-project'     ref: v1.0.0     file: '/templates/.gitlab-ci-template.yml'  variables:     ENABLE_LINT: "true"     ENABLE_BUILD: "true"     ENABLE_DEPLOY_PROD: "false"     ENABLE_DEPLOY_TEST: "true"     ENABLE_DEPLOY_DEV: "true" 

Вариант 2. Если управление производится на основе переменных окружения:

include:   - project: '$CI_PROJECT_PATH'     ref: '$CI_PROJECT_REF'     file: '$CI_PROJECT_FILE'  variables:     ENABLE_LINT: "true"     ENABLE_BUILD: "true"     ENABLE_DEPLOY_PROD: "false"     ENABLE_DEPLOY_TEST: "true"     ENABLE_DEPLOY_DEV: "true" 

У вас может быть наоборот.

Заключение

Такой способ объединения шаблонов позволяет гибко настроить CI/CD без заявки на ультиматизм. Вы можете комбинировать шаблоны(templates) любым удобным для вас способом, один из примеров – это репозиторий шаблонов самого gitlab. Мне же хотелось в этой статье раскрыть вопрос модульности и гибкости, а также результаты, которые можно получить, используя такой подход. Буду рад Вашим вопросам и комментариям.


ссылка на оригинал статьи https://habr.com/ru/companies/ru_mts/articles/747978/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *