Как описать 100 Gitlab джоб в 100 строк

от автора

В продолжение предыдущей статьи про инструменты деплоя в Kubernetes, хочу рассказать вам про то как можно использовать Jsonnet для упрощения описания джоб в вашем .gitlab-ci.yml

Дано

Есть монорепа, в которой:

  • 10 Dockerfiles
  • 30 описанных деплоев
  • 3 окружения: devel, staging и production

Задача

Настроить пайплайн:

  • Сборка Docker-образов должна производиться по добавлении git-тэга с версией.
  • Каждая операция деплоя должна выполняться при пуше в ветку окружения и только по изменении файлов в конкретной директории
  • В каждом окружении установлен свой gitlab-runner с отдельным тэгом, который выполняет деплой только в своём окружении.
  • Не все приложения должны быть задеплоены в каждое из окружений, мы должны описать пайплайн так, чтобы иметь возможность делать исключения.
  • Некоторые деплойменты используют git submodule и должны запускаться с установленной переменной GIT_SUBMODULE_STRATEGY=normal

Описать это всё может показаться настоящим адом, но мы не отчаиваемся и вооружившись Jsonnet сделаем это легко и непринуждённо.

Решение

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

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

При работе с jsonnet очень советую установить вам плагин для вашего редактора

К примеру для vim есть плагин vim-jsonnet, который включает подсветку синтаксиса и автоматически выполняет jsonnet fmt при каждом сохранении (требует наличия установленно jsonnet).

Посмотрим на структуру нашего репозитория:

. ├── deploy │   ├── analyse │   ├── basin │   ├── brush │   ├── copper │   ├── dinner │   ├── dirty │   ├── drab │   ├── drunk │   ├── education │   ├── fanatical │   ├── faulty │   ├── guarantee │   ├── guitar │   ├── hall │   ├── harmonious │   ├── history │   ├── iron │   ├── maniacal │   ├── mist │   ├── nine │   ├── pleasant │   ├── polish │   ├── receipt │   ├── shop │   ├── smelly │   ├── solid │   ├── stroke │   ├── thunder │   ├── ultra │   └── yarn └── dockerfiles     ├── dinner     ├── drunk     ├── fanatical     ├── guarantee     ├── guitar     ├── harmonious     ├── shop     ├── smelly     ├── thunder     └── yarn

Сборка docker-образов будет производиться с помощью Kaniko

Деплой приложений в кластер будет производится с помощью qbec. Каждое приложение описано для трёх разных окружений, чтобы применить изменения в кластер достаточно выполнить:

qbec apply <environment> --root deploy/<app> --yes

где:

  • <app> — название нашего приложения
  • <environment> — одно из наших окружений: devel, stage или prod.

В конечном итоге наши джобы должны выглядeть так:

Сборка:

build:{{ image }}:   stage: build   tags:     - build   image:     name: gcr.io/kaniko-project/executor:debug     entrypoint: [""]   script:     - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json     - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dockerfiles/{{ image }}/Dockerfile --destination $CI_REGISTRY_IMAGE/{{ image }}:$CI_COMMIT_TAG   only:     refs:       - tags

Где вместо {{ image }}, будет подставляться имя директории из dockerfiles

Деплой:

deploy:{{ environment }}:{{ app }}:   stage: deploy   tags:     - {{ environment }}   script:     - qbec apply {{ environment }} --root deploy/{{ app }} --force:k8s-context __incluster__ --wait --yes   only:     changes:       - deploy/{{ app }}/**/*     refs:       - {{ environment }}

Где вместо {{ app }}, будет подставляться имя директории из deploy,
а вместо {{ environment }} — имя окружения в которое нужно произвести деплой.

Давайте опишем прототипы наших джоб в виде объектов в отдельной либе lib/jobs.jsonnet

{   // Задание на сборку docker-образа   dockerImage(name):: {     tags: ['build'],     stage: 'build',     image: {       name: 'gcr.io/kaniko-project/executor:debug-v0.15.0',       entrypoint: [''],     },     script: [       'echo "{\\"auths\\":{\\"$CI_REGISTRY\\":{\\"username\\":\\"$CI_REGISTRY_USER\\",\\"password\\":\\"$CI_REGISTRY_PASSWORD\\"}}}" > /kaniko/.docker/config.json',       '/kaniko/executor --cache --context $CI_PROJECT_DIR/dockerfiles/' + name + ' --dockerfile $CI_PROJECT_DIR/dockerfiles/' + name + '/Dockerfile --destination $CI_REGISTRY_IMAGE/' + name + ':$CI_COMMIT_TAG --build-arg VERSION=$CI_COMMIT_TAG',     ],   },   // Задание на деплой qbec-приложения   qbecApp(name): {     stage: 'deploy',     script: [       'qbec apply $CI_COMMIT_REF_NAME --root deploy/' + name + ' --force:k8s-context __incluster__ --wait --yes',     ],     only: {       changes: [         'deploy/' + name + '/**/*',       ],     },   }, }

Обратите внимание я намеренно не стал указывать refs и tags чтобы сделать нашу либу более гибкой и в полной мере продемонстрировать вам возможности jsonnet, их мы добавим позже уже из основного файла.

Теперь опишем наш .gitlab-ci.jsonnet:

// Импортируем нашу либу local jobs = import 'lib/jobs.libsonnet';  // Определяем функции модификаторы local ref(x) = { only+: { refs: [x] } }; local tag(x) = { tags: [x] }; local submodule(x) = { variables+: { GIT_SUBMODULE_STRATEGY: x } };  {   // Cборка docker-образов:   ['build:' + x]: jobs.dockerImage(x) + tag('build') + ref('tags')   for x in [     'dinner',     'drunk',     'fanatical',     'guarantee',     'guitar',     'harmonious',     'shop',     'smelly',     'thunder',     'yarn',   ] } + {   // Деплой приложений которые должны быть развёрнуты только в 'prod'   ['deploy:prod:' + x]: jobs.qbecApp(x) + tag('prod') + ref('prod')   for x in [     'dinner',     'hall',   ] } + {   // Деплой с git-submodule   ['deploy:' + env + ':' + app]: jobs.qbecApp(app) + tag(env) + ref(env) + submodule('normal')   for env in ['devel', 'stage', 'prod']   for app in [     'brush',     'fanatical',     'history',     'shop',   ] } + {   // Деплой всего остального   ['deploy:' + env + ':' + app]: jobs.qbecApp(app) + tag(env) + ref(env)   for env in ['devel', 'stage', 'prod']   for app in [     'analyse',     'basin',     'copper',     'dirty',     'drab',     'drunk',     'education',     'faulty',     'guarantee',     'guitar',     'harmonious',     'iron',     'maniacal',     'mist',     'nine',     'pleasant',     'polish',     'receipt',     'smelly',     'solid',     'stroke',     'thunder',     'ultra',     'yarn',   ] }

Обратите внимание на функции ref, tag и submodule вначале файла, они позволяют сформировать переопределяющий объект.

Небольшое пояснение: использование «+:« вместо «:« для override-объектов позволяет добавить значение к уже существующему объекту или списку.

Например «:« для refs:

local job = {   script: ['echo 123'],   only: { refs: ['tags'] }, }; local ref(x) = { only+: { refs: [x] } };  job + ref('prod')

вернёт:

{    "only": { "refs": [ "prod" ] },    "script": [ "echo 123" ] }

А вот «+:« для refs:

local job = {   script: ['echo 123'],   only: { refs: ['tags'] }, }; local ref(x) = { only+: { refs+: [x] } };  job + ref('prod')

вернёт:

{    "only": { "refs": [ "prod", "tags" ] },    "script": [ "echo 123" ] }

Как видите, использование Jsonnet позволяет очень эффективно описывать и проводить слияние ваших объектов, на выходе вы всегда получаете готовый JSON, который мы сразу же можем записать в наш .gitlab-ci.yml файл:

jsonnet .gitlab-ci.jsonnet > .gitlab-ci.yml

Проверим количество строк:

# wc -l .gitlab-ci.jsonnet lib/jobs.libsonnet .gitlab-ci.yml    77 .gitlab-ci.jsonnet    24 lib/jobs.libsonnet  1710 .gitlab-ci.yml

На мой взгляд очень неплохо!

Посмотреть больше примеров и пощупать Jsonnet можно прямо на официальном сайте: jsonnet.org
Если вам, как и мне, нравится Jsonnet то вступайте в нашу группу в телеграме t.me/jsonnet_ru


ссылка на оригинал статьи https://habr.com/ru/post/483626/


Комментарии

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

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