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

в 17:15, , рубрики: continuous integration, devops, gitlab, jsonnet, Программирование

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

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

Дано

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

  • 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, который включает посветку синтаксиса и автоматически выполняет jesonnet 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.

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

Сборка:

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(env, name): {
    stage: 'deploy',
    script: [
      'qbec apply ' + env + ' --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

Автор: kvaps

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js