В продолжение предыдущей статьи про инструменты деплоя в 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
¦ L-- yarn
L-- dockerfiles
+-- dinner
+-- drunk
+-- fanatical
+-- guarantee
+-- guitar
+-- harmonious
+-- shop
+-- smelly
+-- thunder
L-- 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
nbes
Вызов jsonnet как происходит? Руками? Хук при коммите(если git)?
kvaps Автор
Хороший вопрос, на данный момент вручную, но в 12.7 обещают сделать возможность генерировать пайплайн динамически:
https://gitlab.com/gitlab-org/gitlab/issues/16094