В продолжение предыдущей статьи про инструменты деплоя в 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/
Добавить комментарий