Привет! Меня зовут Александр Беседин. Раньше я был сетевым инженером в аэропорту, немного кодил на iOS, создавал простые CMS-сайты, а потом стал техлидом в Wildberries по направлению CI/CD. В этой статье рассказываю, какие проблемы возникали у нас с докер-сборками, как мы их решали и что получилось в итоге. Всё, чтобы вы могли посмотреть на наш опыт и применить его в своих проектах!

Контекст CI/CD в Wildberries

В мобильном приложении Wildberries более 79 млн активных пользователей в месяц и примерно 20 млн заказов в день. У нас есть собственный ЦОД в Электростали, строятся ещё два. Также есть некоторое количество коммерческих площадок по России и ближнему зарубежью.

На всех этих площадках работает наш workload, который обитает в основном в Kubernetes. Около 140 кластеров на более чем 900 машинах. Кластеры по большей части проектные, по 3–5 нод, но есть и коммунальные — по 15–20 нод или чуть больше. Инсталляций по 50–100 нод у нас, к счастью, уже нет.

Каждый кластер живёт в своем ДЦ, мы их не мажем кросс-ДЦ. Чтобы туда задеплоиться, есть прекрасный CI/CD. В день разработчики генерируют в среднем 10 тысяч CI/CD-пайплайнов.

Основной флоу разработки у нас — это GitFlow. Есть dev-ветка, разработчики отводят feature-ветку, там вносят свои изменения, проверяют, а потом вмердживают в dev. В dev накапливается некоторое количество изменений. Потом это готовится к релизу, баги фиксятся, всё мерджится в мастер, навешивается продовый тег — и полетели в продакшн.

Для CI/CD используется, конечно же, GitLab CI. Наш CI/CD шаблонизирован, распространяется централизованно и версионируется. Это значит, что каждый проект подключает CI/CD отдельно в своём проекте и указывает версию, которую хочет использовать. Мы мотивируем их использовать самые последние версии, но даём выбор.

У нас есть множество разных пайплайнов под разные языки программирования и задачи. При этом каждый пайплайн запускается независимо для каждого окружения.

Образы, которые мы собираем, хранятся в registry — у нас это Harbor, одно из самых популярных решений. Для сборки образа мы используем Kaniko, так как почти все наши GitLab-раннеры используют Docker Executor и нам нужно собирать докер-образы внутри других докер-контейнеров.

Для сборки мы используем кэширование, чтобы ускорить процесс. Раннеров очень много, хранить на всех кэши мы не можем. Кэши мы централизованно складываем также в registry.

С учётом кэшей и всех dev-сборок, которые у нас происходят, в Harbor немало мусора, поэтому настраиваются политики очистки. Раз в день мы удаляем всё старое и не особо нужное.

Что было не так с пайплайнами

Как я написал выше, пайплайны у нас независимые. Когда мы собираем образ в прод, есть вероятность, что соберётся не то, что было в Stage-окружении. Происходит это по двум причинам:

  • Первое — кэш в регистре отсутствует, он оттуда удалился.

  • Второе — докер-файл написан таким образом, что кэширование просто-напросто не работает.

Пересборка при этом несёт риски. Если разработчики забыли закрепить версии зависимостей, которые они устанавливают, при очередной пересборке мы подтягиваем последнюю версию зависимости, и она может влиять на работу сервиса. Давайте посмотрим на примере Dockerfile для сервиса на Go.

Что не так с docker-сборками. Пример

ARG GO_VER
ARG ALPINE_VER

FROM golang:${GO_VER}-alpine${ALPINE_VER} as builder
WORKDIR /src
ARG SHORT_SHA

# Первая проблема. Копируются все файлы в контекст сборки, а .dockerignore нету
COPY . .

# Вторая проблема. Не запинена версия зависимости
RUN apk add some_dependency && \
   # Третья проблема. VERSION содержит short SHA коммита. Меняется с каждым коммитом
    go build -o app -ldflags "-X 'main.version=${SHORT_SHA}'" ./app

Команда COPY . . — стандартная, когда мы закидываем все файлы репозитория в контекст сборки. И если у нас некорректно настроен .dockerignore (а такое случается), туда попадает Git-история — которая меняется каждый раз. Кэш инвалидируется, и следующая команда RUN тоже инвалидируется по кэшированию.

Следующее — это установка зависимости. Как я сказал, версия не закреплена. Если установилась последняя, то мы можем получить проблемы.

В сборку также подкидываются короткие SHA коммитов через ARG SHORT_SHA, которые меняются каждый раз. При этом короткие SHA коммитов мы любим использовать — зашиваем их в сервисы для дальнейшего дебага. Таким образом, мы получаем, что в данном Dockerfile команда RUN в принципе никогда не будет закэширована из-за этого аргумента.

Пути решения проблемы

Есть два варианта, как работать с такими вводными: радикальный и более мягкий.

Радикальный — полностью поменять флоу разработки таким образом, чтобы, когда мы деплоимся в прод, шёл также деплой в Stage и чтобы всё это находилось в рамках одного пайплайна. Это называется TBD-флоу, и часть разработчиков Wildberries на нём живут. Но насильно пересаживать почти всю компанию на новый флоу разработки больно. Поэтому мы искали более мягкие подходы.

Каким может быть более мягкий подход? Тут ещё одна развилка:

  • перетегирование образов,

  • анализ Git-истории,

  • вычисление хеша репозитория.

Давайте остановимся на каждом пункте подробнее.

Перетегирование образов. Суть этого подхода в том, что мы деплоимся в Stage, собираем образ, навешиваем на него тег Stage и затем, когда деплоимся в прод, не собираем новый образ, а идём в registry, находим наш образ с тегом Stage, навешиваем туда тег Production и с ним деплоимся.

Тут есть проблема: если вдруг разработчик запустит сборку на более старой версии, тег просто перенавесится на более старый образ. Этот образ потом может случайно задеплоиться в прод, и мы снова получим проблемы. Каким-то адекватным способом обойти эту проблему у нас не получилось, поэтому мы пошли дальше.

Мы подумали: мы знаем флоу наших разработчиков: как они делают коммиты, как ответвляются, мерджатся и т. д. На основе этой информации можно написать систему, где мы идём по Git-истории в обратном направлении, находим коммит с предыдущего окружения, вычисляем, какой там был образ, и затаскиваем этот образ в прод.

Мы долго рассматривали этот вариант, но решили отказаться и от него. Почему? У нас уже два флоу разработки, а в планах — сделать ещё один, чтобы максимально удовлетворить потребности разработчиков. Под каждый флоу нужно написать свою логику поиска коммитов. Если мы будем вносить изменения в флоу, это тоже потребует правок. Всё это приводит к тому, что велик шанс критической ошибки: забыв поменять вместе с флоу систему поиска коммитов, мы снова получим проблемы.

Вдобавок логика этой системы не самая тривиальная. Косты на поддержание решения нас не устроили.

Вычисление хеша репозитория. Есть репозиторий с файлами. Мы берём все файлы, объединяем их в один байтовый поток, вычисляем для него хеш. Хеш сохраняется в лейбл собранного образа. При очередной сборке мы не делаем сразу сборку — мы идем в registry с новым хешем репозитория и пытаемся там найти образ. Если мы его нашли, то мы пропускаем сборку и используем найденный образ.

Третий подход сильно похож на кэширование докера, но с некоторыми отличиями.

Во-первых, мы контролируем влияние аргументов в сборке и можем быть уверены, что короткий SHA коммита не повлияет на эту систему.

Во-вторых, существуют исключения для файлов, которые будут подсчитываться в хеше. Например, это папка Deploy, в которой у нас хранятся Helm Values для деплоя в Kubernetes. Также это всякие .md-файлы, сам .gitlab-ci.yml, .git-история.

В-третьих, что самое важное, мы делаем систему такой, что при деплое в продакшн мы должны обязательно найти образ в registry. Если его там нет, мы просто фейлим пайплайн и говорим: «Извините, мы не нашли образ, тут что-то не так. Давайте разберёмся, почему нет образа, который мы тестировали в Stage-окружении».

Наше финальное решение

В конечном итоге мы остановили свой выбор на вычислении хеша репозитория. Мы его реализовали и здесь хочу показать вам как оно работает верхнеуровнево. Буду отталкиваться от конкретного примера с git–историей и состоянием registry:

Пример итоговой git-истории и состояния образов в registry
Пример итоговой git-истории и состояния образов в registry

В репозитории три ветки: master, dev, feature-some. Разработчик делает коммит в ветку feature-some, хеш-репозиторий у него — 11111, собирает образ с дайджестом аааааа, и туда навесился тег с именем ветки и коротким SHA коммита — feature-some-5db3e22.

Скрытый текст
Первый комит в feature ветку и первый собранный образ
Первый комит в feature ветку и первый собранный образ

Затем разработчик вносит изменения, в данном случае в Helm Values. Как я уже сказал, они у нас исключаются из подсчёта по умолчанию, поэтому на хеш репозитория они не влияют.

Хеш такой же — 111111. Находим его в registry и навешиваем новый тег с новым коротким SHA коммита — feature-some-345489a. Сборка пропускается.

Скрытый текст
Поменяли helm values – пересборка не нужна
Поменяли helm values – пересборка не нужна

Дальше фича проверена и вмердживается в dev. Допустим, что в dev больше не было никаких правок. Создаётся мердж-коммит, и с ним запускается новый пайплайн. Тут ветка dev по состоянию файлов не отличается от самой feature-ветки. Снова находим образ и навешиваем на него новый тег — dev-6795b4c.

Скрытый текст
Вмержили изменения – пересборка тоже не нужна
Вмержили изменения – пересборка тоже не нужна

Решили, что хотим это релизить, — навешиваем ветку release-v1.0.0, деплоимся в Stage — опять состояние файлов не поменялось, находим образ, навешиваем новый тег — release-v1-0-0-6795b4c.

Скрытый текст
Навесили ветку на комит, ситуация та же – пересборка не нужна
Навесили ветку на комит, ситуация та же – пересборка не нужна

Нашли какую-то багу в Stage? Фиксим её в исходном коде. Хеш репозитория меняется — 222222, мы собираем новый образ с дайджестом bbbbbb , тегом release-v1-0-0-16613ff и идём с этим дальше.

Скрытый текст
Пофиксили баг, пересобрать образ нужно
Пофиксили баг, пересобрать образ нужно

Если готовы деплоиться в продакшн, вмердживаемся в master, навешиваем продовый тег. И тут та же ситуация, что с dev: состояние ветки идентично ветке release-v1.0.0. Мы находим наш образ в registry и навешиваем на него продовый тег v1-0-0-45629b0 — и так деплоимся.

Скрытый текст
Влили изменения в мастер – переиспользуем протестированный образ со стейджа
Влили изменения в мастер – переиспользуем протестированный образ со стейджа

Так мы получили, что образ со стейджа стал использоваться в проде. Помимо этого, мы начали переиспользовать образы в рамках одного окружения.

Как мы реализовали переиспользование образов

Инструментарий

Под капотом мы используем следующие инструменты. Это Bash — наш основной скриптовый язык почти для всей логики CI/CD, простой и понятный. Для подсчёта хеша используем xxHash.

Вы спросите: почему не SHA? Лишь потому, что xxHash гораздо быстрее — он работает на скорости оперативной памяти. Да, xxHash, в отличие от SHA, не криптостойкий, но нас это не беспокоит.

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

Дальше — Jsonnet. Это наш инструмент для написания динамических пайплайнов. Мы генерируем n-ное количество джоб на основе входных параметров разработчика. Если у вас не динамические пайплайны, можете не использовать этот инструмент.

Конечно, есть в нашем арсенале и Harbor API. Его мы используем для поиска артефактов и тегирования. Для взаимодействия с Harbor — всем известный Curl.

Общий вид CI/CD пайплайна

Ниже представлен общий вид пайплайна, а именно того что связано со сборкой. Данный кусок пайплайна состоит из 4 джоб, необходимые для сборки:

  • generate build — то, где мы генерируем динамические джобы, а также вычисляем хеш репозитория;

  • build — триггер-джоба запуска динамического пайплайна;

  • hash check <service_name> — поиск хеша в Harbor и перетегирование;

  • build <service_name>— джоба сборки, которая всегда отрабатывает, но если хеш был найден, то сборка пропускается и она сразу завершается.

Под капотом пайплайна

Генерация динамического пайплайна

Итак, джоба generate build генерирует динамический пайплайн.

# /docker/build.yml
# В джобе генерации вычисляем хеш и генерируем наш пайплайн с помощью jsonnet
generate build:
  stage: generate
  artifacts:
    paths:
      - generated-build-pipeline.yml
  script:
    - !reference [.calculate-repo-hash, script]
    - jsonnet --ext-str "RELEASE_HASH=$RELEASE_HASH"
              --ext-str "SERVICE_LIST_SCV=$SERVICE_LIST_CSV"
              --ext-str "TMPL_PROJECT=$TMPL_PROJECT"
              --ext-str "TMPL_VERSION=$TMPL_VERSION"
              ...
              ./docker/build-pipeline.jsonnet > generated-build-pipeline.yml

# Запускаем наш динамический пайплайн
build:
  stage: build
  needs:
    - "generate build"
  trigger:
    include:
      - artifact: generated-build-pipeline.yml
        job: generate build

В триггер-джобе build запускается downstream-пайплайн. Там у нас два джобы-шаблона: hash check <service_name> и build <service_name>.

Хеш репозитория нужно вычислить только один раз, поэтому самое логичное место, куда это можно положить, — джоба generate build. В hash check <service_name> мы общаемся с API Harbor, находим образ с хэшом репозитория для данного пайплайна и в случае необходимости делаем перетегирование.

В джобу build <service_name> мы передаем информацию, найден ли образ. Если найден, процесс сразу же завершается. Если не найден, мы запускаем непосредственно процесс сборки.

Не устали? Пойдём вглубь этих джоб.

В джобе generate build происходит вычисление хеша репозитория. Запускаем Jsonnet, передаём туда вычисленный хеш репозитория, передаём список сервисов и ещё две переменные: TMPL_PROJECT и TMPL_VERSION — это путь нашего шаблонного CI/CD и его версия (дальше расскажу зачем). На выходе у нас YAML-файл, содержащий сгенерированный пайплайн. Сохраняем его в артефакт, а потом триггерим джобу build — тем самым запуская пайплайн.

Генератор пайплайна

Посмотрим Jsonnet конфигурацию, с помощью которой мы можем генерировать независимые джобы сборки для почти любого количества сервисов.

Скрытый текст
# /docker/build-pipeline.jsonnet
# 1. Формируем массив наших сервисов
local services = std.split(std.extVar('SERVICE_LIST_CSV'), ",")
{
# 2. Определяем основные настройки пайплайна
  stages: [ "check hash", "build" ],
  variables: {
    RELEASE_HASH: std.extVar('RELEASE_HASH'),
  }
# 3. Подключаем файл со скрытыми джобами, основную логику джоб
  include: [{
    project:  std.extVar('TMPL_PROJECT'),
    ref: std.extVar('TMPL_VERSION'),
    file: '/docker/build-downstream.yml',
  }],

# 4. Генерируем джобы поиска хеша
  [ "hash check " + svc ]:
  {
    extends: [ ".check_repo_hash" ]
    ...
  }
  for svc in services

# 5. Генерируем джобы сборки
  [ "build " + svc ]:
  {
    needs: [
      {
        job: "hash check " + svc,
        artifacts: true,
        optional: true
      },
    ],
    extends: [ ".build" ]
  },
  for svc in services
}
  1. Определяем переменную services, где со входного параметра генерируем массив сервисов.

  2. Определяем основные настройки пайплайна: stages и переменную, куда мы складываем хеш репозитория – RELEASE_HASH

  3. Делаем подключение CI/CD, берём оттуда файл build.downstream.yaml. Этот файл содержит всю логику работы наших джоб, потому что в Jsonnet не хранится тяжёлая логика — он используется только для генерации видимых джоб.

  4. Это можно увидеть на примере генерации джобы hash check <service_name>. Мы итерируемся по массиву сервисов и для каждого из них генерируем видимое имя джобы hash check + имя сервиса, а также делаем extends от скрытой джобы .check_repo_hash, которая живёт в файле /docker/build-downstream.yml в нашем репозитории с шаблонами. Таким образом .check_repo_hash содержит всю основную логику работы джобы и нет необходимости держать её в Jsonnet.

  5. Аналогичное происходит с джобой build <service_name>: итерируемся, видимое имя джобы build + имя сервиса, extends от скрытой джобы .build. Ещё определяем needs, так как каждая джоба build связана со своей джобой hash check. При этом мы забираем оттуда артефакты и выставляем optional: true, так как джобы hash check может не быть в пайплайне.

Подсчёт хеша

Как мы вычисляем хеш репозитория? Это достаточно длинный Bash-пайплайн, разбитый на этапы, который мы держим в скрытой джобе .calculate-repo-hash.

# /docker/build.yml
.calculate-repo-hash:
  script:
    - |
      # 1. Получаем все файлы для подсчёта хеша c исключением файлов на основе наших настроек
      eval "fd --type f $exclude_flags . -X printf '%s\n'" \
      `# 2. Дополнительно исключаем файлы из .dockerignore` \
      | dockerignore-filter "$DOCKERIGNORE_PATHS" \
      `# 3. Сортируем пути файлов для консистентности` \
      | sort \
      `# 4. Объединяем путь файла с его содержимым` \
      | xargs -I {} sh -c 'printf "### %s ###\n" "{}"; cat "{}"; printf "\n"' \
      `# 5. Добавляем в конец значение важных переменных` \
      | (echo -n "sensitive vars ${SENSITIVE_VARS}\n" && cat) \
      `# 6. Считаем хеш и форматируем вывод` \
      | xxhsum -H128 \
      | awk -F' ' '{print $1}' \
      | tr '[:upper:]'
  1. В первую очередь мы находим все пути файлов, для которых должны посчитать хеш. Мы передаём туда пути исключения для подсчёта хеша и получаем на выходе список путей файлов.

  2. Этот список путей мы отправляем в функцию dockerignore-filter, где дополнительно применяются правила из .dockerignore, которые находятся в текущем репозитории. Мы не смогли реализовать эту логику в рамках Fd, поэтому написали отдельную функцию.

  3. Дальше все эти пути отправляются в sort для консистентности.

  4. Потом мы отправляем в xargs, где объединяем путь файла с его содержимым. Это нужно для случая, если файл был перемещён в репозитории без изменения содержимого; у нас вычислится другой хеш репозитория и, соответственно, триггернётся сборка.

  5. Через echo подмешиваем в конец чувствительные переменные. В нашем случае это аргументы сборки, но без короткого SHA коммита, о котором я говорил. Это обычно какие-то пользовательские аргументы сборки, которые тоже должны влиять на хеш.

  6. Считаем наш хеш и получаем на выходе 128-битную строку, приведённую к нормальному виду.

Проставление хеша

Переместимся в build.downstream.yaml. Здесь в скрытой джобе .build скрипт для джобы build <service_name>.

# /docker/build-downstream.yml
.build:
  stage: build
  script:
    - |
      # Проверяем, был ли уже найден образ
      if [[ "$REPO_HASH_CHECK" == "true" && -n "$MATCHED_ARTIFACT_SHA" ]]; then
        echo "Found image. Skipping"
        exit 0
      # Если мы не нашли образ для прод окружения, то выдаём ошибку
      elif [[ "$ENVIRONMENT_NAME" == "prod" ]]
        echo "ERROR: Did not find image for prod environment"
        exit 1
      else
        echo "Proceeding with build..."
      fi
      # Если не найден, то производим сборку с проставлением лейбла с хешем
      /kaniko/executor \
        --context $CI_PROJECT_DIR \
        --dockerfile $DOCKERFILE_PATH \
        --label "ru.wildberries.ci.release.hash=$RELEASE_HASH" \
        ...

Если образ найден в registry, то просто выходим из джобы с exit 0. Если же образ не найден и при этом мы деплоимся в прод, выводим ошибку, что не нашли образ с предыдущего окружения, и падаем. Во всех остальных случаях запускаем нашу сборку через Kaniko Executor. Туда мы передаём docker-файл, контекст сборки и наш лейбл, который содержит хеш репозитория. Всё это собирается и отправляется в наш registry.

Поиск образа

Теперь переместимся в джобу .check_repo_hash, которая находится в том же файле. Здесь определяются артефакты, которые она сгенерирует и которые потом будут использоваться в джобе .build. В скрипте идёт общение с API Harbor.

# /docker/build-downstream.yml
.check_repo_hash:
  stage: build
  artifacts:
    reports:
      dotenv: $CI_PROJECT_DIR/build$CI_JOB_ID.env
    expire_in: 30 mins
  script:
    - |
      # Делаем запрос в API Harbor на получение информации об образах в репозитории
       curl --write-out "%{http_code}" --output /tmp/response.json \
         --dump-header "./${CI_JOB_ID}-headers" -H "accept: application/json" \
         --user $REGISTRY_USER:$REGISTRY_PASSWORD \
         "$HARBOR_API/projects/$PRJCT_PATH/repositories/$IMAGE_PATH_URL_ENCD/artifacts"

      # Фильтруем список и оставляем образ с нашим хешем
      MATCHED_ARTIFACTS_BY_HASH=$(jq --arg HASH "$RELEASE_HASH" \
      -r 'map(select(.extra_attrs.config.Labels."ru.wildberries.ci.release.hash" == $HASH))'\
      /tmp/response.json)

      # Запоминаем digest найденного образа
      artifact_digest=$(echo $MATCHED_ARTIFACTS_BY_HASH | jq -r '.[0].digest')

      ARTIFACTS_URL="$HARBOR_API/projects/$PRJCT_PATH/repositories/$IMAGE_PATH_URL_ENCD/artifacts"
      # Проставляем тег для найденного образа через Harbor API
      tag_http_status=$(curl --write-out "%{http_code}" \
                             -H "Content-Type: application/json" \
                             --user $REGISTRY_USER:$REGISTRY_PASSWORD \
                             -X POST \
                             -d "{\"name\": \"${new_tag}\"}" \
                             "${ARTIFACTS_URL}/${artifact_digest}/tags")

      # Если такой тег уже есть, то удаляем его
      # Рекурсивно пытаемся установить ещё раз
      if [ "$tag_http_status" == "409" ]; then
        delete_tag_http_status=$(curl --write-out "%{http_code}" \
                                      -H "Content-Type: application/json" \
                                      --user $REGISTRY_USER:$REGISTRY_PASSWORD \
                                      -X DELETE
                                      "${ARTIFACTS_URL}/${new_tag}/tags/${new_tag}")

        # После удаления рекурсивно снова пытаемся установить тег
        tag_artifact
      fi
      ...

Идём на endpoint Harbor через GET с помощью curl. Здесь мы делаем --write-out "%{http_code}" для обработки ошибок. Потом делаем --output, чтобы получить JSON, которую Harbor вернёт. --dump-header мы используем, чтобы получить ответные заголовки, на основе которых будет проходить pagination по API Harbor.

Получаем JSON и затем через jq с помощью фильтра выражения оставляем только те образы, у которых в лейбле содержится хеш репозитория. Там будет либо один образ, либо ничего. Сохраняем эту информацию в переменную artifact_digest и идём с ней дальше.

Если мы нашли образ, то нужно навесить на него новые теги. Идём на endpoint Harbor с артефактами, делаем POST-запрос, просим его навесить новый тег. При этом нам может вернуться 409-я ошибка, которая говорит, что тег уже существует. Это может происходить, когда мы пытаемся установить пользовательские теги, которые могут быть не уникальными. В этом случае мы идём опять же в Harbor, просим его удалить данный тег с текущего образа и рекурсивно просим навесить этот тег ещё раз — уже на целевой образ.

Что оставили за скобками

В снипетах кода в этом посте была вырезана вся логика, не связанная с работой описываемой фичи:

  • пользовательский ввод, который мы обрабатываем;

  • ошибки, граничные состояния;

  • обработка пагинации Harbor;

  • обработка и подготовка путей для исключения подсчёта хеша;

  • дебаг-режим подсчёта хеша, где мы в артефакты отправляем весь сформированный поток данных для хеш функции, чтобы убедиться, что поток корректно сформирован;

  • функция обработки .dockerignore.

Плоды внедрения

После того, как мы сконцентрировались на вычислении хеша репозитория, наш прод стал немного надёжнее. Мы исключили ситуацию, когда образ, протестированный в стейдже, не доезжает до прода. Но это не все плюшки.

Ещё мы оптимизировали процесс сборки: теперь разработчикам нужно меньше ожидать джобы build, так как часть из них пропускается. Они меньше ждут деплоя и могут быстрее разрабатывать свои фичи. А для нас как для инфраструктурной команды, конечно, важно снижать нагрузку на инфраструктуру раннеров.

Сейчас у нас в неделю пропускается примерно 700 сборок. Это примерно 30% от сборок, где включена фича, но пока только 5% от общего числа сборок через наш CI/CD. Всё потому, что довольно долго вычисление хеша было лишь опцией и не включалось по умолчанию. Со временем процент однозначно будет больше, и мы ожидаем, что все сборки приблизятся к 30% пропущенных.

Спасибо, что прочитали о нашем опыте! Готов ответить на ваши вопросы в комментариях.

Комментарии (4)


  1. k3NGuru
    14.10.2025 01:57

    Спасибо за статью.

    В одном "Синем" Банке сделали чуть проще, у них несколько Нексусов. Есть Dev, где заливайте образа и перетегируйте, и есть IFT и выше, где один и тот же коммит нельзя перезаписывать. Раз попал в registry с коммитом, дальнейшие попытки разрабов его перезалить, будут заканчиваться ошибками.


    1. monster1290 Автор
      14.10.2025 01:57

      Вариант вполне хороший и вполне может удовлетворять требованию что образ со стейджа гарантировано попадёт в прод. При условии что никто не сможет руками обойти защиту, например руками в registry залезть. Но всё равно перетегирование нам не сильно заходило по причине того, что нам очень удобно зашивать тег с коротким SHA комита в образ, который потом проникает во все метрики и логи. Это полезная вещь при дебаге, особенно когда образ исчез отовсюду и нету возможности нормально найти ни его digest, ни его лейблы в которых так же есть SHA комита. Ну и конечно нам очень понравилась возможность пропускать сборки, т.к. аргумент содержащий короткий SHA комита нам очень портил возможность переиспользования кеша.


  1. faustoFF
    14.10.2025 01:57

    kaniko с 3 июня 2025 переведен в архив на github, а gitlab удалили статью о сборке образов с kaniko.


    1. monster1290 Автор
      14.10.2025 01:57

      Нашумевашая история. Конечно про неё в курсе. В момент реализации функционала kaniko был ещё живее всех живых актуальный. А так конечно съезжать надо, но это не срочная задача и на содержание статьи никак не влияет :)