Как ускорить сборку Docker-образов в GitLab: стратегии кэширования с Docker Buildx

Введение
Скорость сборки Docker-образов играет важную роль в CI/CD, особенно для микросервисов, где частые обновления и тестирования требуют быстрой доставки изменений.
Одним из решений для оптимизации сборок является Docker Buildx — расширение к стандартной команде `docker build`. Docker Buildx предлагает дополнительные возможности, такие как кэширование слоев образов, что помогает значительно сократить время сборки за счет повторного использования неизменных слоев. В отличие от стандартного процесса сборки, Docker Buildx предоставляет более гибкое управление кэшем, поддерживает мультиархитектурные сборки и работу с несколькими платформами.
В этой статье мы сосредоточимся на том, как эффективно настроить и использовать кэширование с Docker Buildx в CI/CD пайплайнах на GitLab. Мы рассмотрим примеры, когда кэширование позволяет ускорить сборку, и ситуации, когда его лучше отключить для гарантии корректности итогового образа.
Особенно остро проблема долгих сборок ощущается при работе с крупными проектами или легаси-системами, где CI процессы могут занимать до 20 минут. Когда разработчику приходится ждать завершения всех этапов, включая юнит-тесты и FastApi-тесты, производительность команды падает. Нередко ошибки выявляются только на последних этапах, что приводит к необходимости повторной сборки, тратя ценное время и усилия. Подобные ситуации вызывают разочарование и стресс у разработчиков, когда сборка завершает неудачно на 19-й минуте.
Одной из приоритетных задач DevOps-инженера является оптимизация пайплайнов таким образом, чтобы сборки занимали минимальное время. Идеальная ситуация — это возможность выявить проблемы с кодом в течение нескольких минут, что существенно увеличивает время, которое разработчик может посвятить эффективной работе.
В этой статье мы не будем углубляться в архитектурные проблемы легаси-кода или потенциальные улучшения со стороны разработки. Мы сосредоточимся на текущем процессе и рассмотрим, как можно ускорить пайплайны, в частности, сборку бэкенда. Эта часть обычно является наиболее длительной в нашем пайплайне, и проблема ясна — пора перейти к ее решению.
Альтернативы Docker Buildx
Прежде чем обсудить Docker Buildx, давайте рассмотрим следующие альтернативы: :
Kaniko — безопасная альтернатива сборки Docker-образов в Kubernetes без привилегированных контейнеров. Отлично подходит для работы в облачных средах с ограниченными правами.
BuildKit — сборочная среда, поддерживающая параллельные сборки и кэширование. Используется внутри Docker Buildx, но может применяться отдельно для расширенного управления сборками. Используется для распределённых систем и глубокого управления кэшем, особенно полезен в микросервисных архитектурах.
Bazel — система сборки от Google, оптимизированная для крупных проектов с множеством зависимостей. Поддерживает кэширование артефактов и эффективное управление зависимостями. Применяется для крупных проектов, требующих высокой скорости сборки и масштабируемого кэширования на уровне исходного кода.
GitHub Actions с Docker Layer Caching — встроенное решение в GitHub CI/CD, которое поддерживает кэширование слоев Docker-образов. Это решение идеально подходит для тех, кто уже использует GitHub Actions и ищет простую интеграцию с кэшированием Docker для ускорения сборок.
Каждое из этих решений имеет свои преимущества и может быть эффективно использовано в зависимости от специфики проекта. Например, Kaniko идеально подходит для Kubernetes. Выбор инструмента следует делать на основе требований к безопасности, производительности и интеграции с существующей инфраструктурой.
Docker buildx и отличия от docker build
Далее разберем ключевые отличия команды docker build от docker buildx:
Мультиархитектурные сборки:
Одно из главных преимуществ Docker Buildx — возможность создавать образы для нескольких архитектур (например, x86 и ARM) в рамках одной команды. Это особенно важно в мире современных микросервисов, где приложения должны работать на разных платформах, от серверов до мобильных устройств и IoT.
В стандартной команде docker build такие сборки поддерживаются ограниченно и требуют дополнительных шагов.
Расширенное кэширование:
Docker Buildx предоставляет улучшенные механизмы кэширования, которые могут значительно ускорить процесс сборки. С помощью Docker Buildx можно сохранять и использовать кэш на удаленных хранилищах (например, в облаке или на удаленных серверах), что позволяет ускорить сборки в CI/CD пайплайнах, особенно в тех случаях, когда сборки выполняются на разных машинах или в распределенных системах.
Стандартная команда docker build также поддерживает кэш, но она ограничена локальным использованием, что может замедлить сборки в сложных инфраструктурах.
Инкрементальные сборки:
Docker Buildx позволяет выполнять инкрементальные сборки — то есть пересобирать только измененные части образа, а не весь образ целиком. Это уменьшает время сборки и делает процесс более эффективным, особенно при частых, но небольших изменениях кода.
В отличие от стандартной команды docker build, где слои кэшируются на основе не измененных файлов, Docker Buildx предоставляет более гибкие инструменты для управления этим процессом.
Поддержка новых форматов образов:
Docker Buildx поддерживает создание образов в новых форматах, таких как OCI (Open Container Initiative), что делает его более совместимым с другими инструментами и экосистемами контейнеризации.
Стандартная команда docker build в основном поддерживает формат Docker.
Сборка в распределённых средах:
Docker Buildx поддерживает так называемую «дистрибутивную» сборку, когда процесс сборки может выполняться параллельно на нескольких узлах, что ускоряет создание больших образов или образов для нескольких архитектур. Это особенно полезно в крупных CI/CD пайплайнах, где сборка может быть распределена по нескольким машинам для ускорения процесса.
Лучшее управление флагами и параметрами:
Docker Buildx предлагает более гибкое управление параметрами сборки. Например, можно задавать различные платформы, использовать кастомные драйверы для сборки, а также определять, где и как сохранять кэш.
Эти возможности значительно превосходят стандартные параметры docker build.
Приведу рабочий пример использование команды docker buildx:

Команда сборки Docker Buildx с использованием аргументов и меток
Команда выполняет сборку Docker-образа с использованием расширенного инструментария Docker Buildx, добавляя метки, управляя кэшированием, аргументами и тегами. Вот детальное описание каждого элемента команды:
docker buildx build ${PUSH_FLAG} — запускает сборку Docker-образа с использованием Docker Buildx;
PUSH_FLAG — указывает, будет ли образ автоматически отправлен в удаленный реестр (если присутствует флаг --push). Если этот флаг не указан, образ останется локальным;
--progress=plain — Устанавливает формат вывода прогресса сборки в текстовом формате («plain»). Это делает лог сборки более читаемым и удобным для отладки, особенно в CI/CD системах;
--label «bimeister.url_project=${CI_PROJECT_URL}» — добавляет метку (label) к образу. В данном случае метка содержит URL проекта, который задается переменной;
CACHE_STRING — переменная содержит параметры для управления кэшированием во время сборки.
ARGS_STRING — переменная содержит аргументы сборки, передаваемые в процессе сборки Docker-образа. Аргументы могут включать параметры, такие как версии зависимостей, параметры среды или другие пользовательские переменные, необходимые для настройки билда;
TAGS_STRING — это список тегов для образа, которые будут добавлены к собранному Docker-образу;
LABELS — переменная содержит дополнительные метки (labels), которые описывают Docker-образ. Эти метки могут включать в себя информацию о версии приложения, времени сборки, разработчиках и других характеристиках, что помогает в дальнейшем управлении и отслеживании образов;
-f ${CONTEXT}/${DOCKERFILE} — указывает путь к файлу Dockerfile, который будет использован для сборки. Переменная CONTEXT задает корневую директорию контекста сборки, а DOCKERFILE — это имя файла Dockerfile, описывающего шаги сборки.
А какой Dockerfile используется?
Одним из ключевых преимуществ Docker Buildx является продвинутое кэширование, позволяющее повторно использовать промежуточные слои как на этапе сборки, так и при запуске CI/CD пайплайнов. Это снижает общее время билда и уменьшает нагрузку на систему, так как неизменные слои не пересобираются.
Наш подход к сборке также включает заранее подготовленные шаблоны для CI/CD, что позволяет разработчикам быстро начать работу над микросервисом. Шаблон предоставляет все необходимые инструменты для запуска проекта и значительно ускоряет процесс разработки. В планах — отдельная статья про GitLab-шаблоны которые помогают управлять всеми кодовыми-проектам в компании.
Для упрощения и эффективности мы используем шаблонизированный Dockerfile, который обогащается определенными переменными в каждом микросервисе, что помогает оптимально использовать кэш без лишних пересборок.

Переменные окружения для сборки приложения
Переменные позволяют адаптировать процесс билда под нужды каждого микросервиса, и на базе шаблона Dockerfile собираются контейнеры с помощью Docker Buildx.
Разберем базовую структуру Dockerfile, который применяется для всех наших бэкенд-микросервисов.
Первая часть, restore-env, сканирует проект на наличие файлов .csproj. Если файлы не менялись, этот слой может быть закэширован, что ускоряет сборку. Инструмент dotnet-subset копирует необходимые файлы в указанную директорию, оптимизируя команду dotnet restore за счёт эффективного использования кэша.

Dockerfile: использование dotnet-subset для восстановления зависимостей
Вторая часть builder, тут также применяются множество ключей для ускорения билда эта часть Dockerfile выполняет восстановление зависимостей, копирование исходных файлов, и сборку приложения с публикацией в определенный каталог. Оптимизации можно добиться распараллелив сборку за счет дополнительных ключей команды dotnet.

Dockerfile: Настройка публикации и управления версиями
На этом этапе кэш применить не можем, кэшируется лишь повторный перезапуск джобы.
Для примера, первый билд занял 3 минуты:

Повторный билд той же джобы, проходит моментально за счет того что у нас есть кэш в registry, проверяем что ничего не поменялось и используем весь кэш. Это быстро, но на сколько это может пригодится для реальных задач? Максимум если кто-то удалил нужный image и пересборка займет не 3 минуты, а 6 секунд.
Чаще всего, при изменениях в коде, именно часть Dockerfile с builder пересобирается дольше всего.

Информация о сборке в GitLab CI: длительность
И третья часть final, эта часть Dockerfile создает финальный образ, копируя артефакты сборки, устанавливая нужные метаданные, открывая порты, и задавая окружение для корректной работы ASP.NET Core приложения.

Dockerfile: финальная сборка с лейблами и настройками
В итоге Dockerfile состоит из трех частей, каждая из которых может быть закэширована и оптимизирована для ускорения сборки. Некоторые детали, такие как аргументы и инструменты для отладки, опущены из соображений безопасности
Как работает кэширование в Docker?
Кэширование позволяет повторно использовать ранее созданные слои образов, что существенно сокращает время сборки, особенно при частых повторных сборках, когда значительная часть этапов остается неизменной.
Каждый Docker-образ состоит из последовательности слоёв, которые создаются на основе шагов, описанных в Dockerfile. Например, команды `COPY`, `RUN`, `ADD` и другие генерируют новые слои. Если один из шагов изменяется, Docker пересобирает только изменённый шаг и все последующие, но при этом повторно использует те слои, которые не изменились.
При изменении одного из этапов сборки, например, копировании файлов с исходным кодом или установки зависимостей, Docker способен использовать кэш для всех предыдущих шагов, тем самым избегая полной пересборки образа. Это значительно ускоряет процесс и снижает нагрузку на ресурсы, особенно в больших проектах или CI/CD пайплайнах, где сборки выполняются часто.
Пример кэширования в Docker
Допустим, у вас есть проект с несколькими шагами сборки. Первый шаг устанавливает зависимости, второй копирует исходный код, а третий компилирует приложение. Если в новом коммите изменяется только исходный код, то нет необходимости повторно устанавливать зависимости — Docker может использовать уже закэшированные слои.
Рассмотрим пример использования ключевых флагов для управления кэшем в Docker Buildx:
Давайте разберем CACHE_STRING, выглядит она следующим образом:

Контроль кэширования Docker в зависимости от веток и переменных окружения
Как видно мы управляем использованием кэша при сборке Docker-образов в зависимости от ветки и тегов коммита. Если сборка происходит в релизной ветке или для тегированного коммита, кэш отключается для создания чистого образа. Если сборка выполняется в основной ветке и кэширование включено, проверяются обязательные переменные окружения, такие как Docker Registry и имя контейнера. При их наличии кэширование включается, и слои образа сохраняются и загружаются из Registry для ускорения последующих сборок. В противном случае кэширование также отключается.
--cache-from — этот флаг указывает на то, откуда загружать кэшированные слои. Он полезен в CI/CD пайплайнах, где кэш может храниться в удалённом Docker-Registry или на предыдущих сборочных этапах. При сборке Docker пытается загрузить кэшированные слои с этого источника, чтобы использовать их повторно.
--cache-to — этот флаг определяет, куда сохранять кэшированные слои после завершения сборки. Это особенно важно при работе с распределенными системами, где различные этапы сборки могут происходить на разных машинах или в разных средах. кэширование позволяет не пересобирать неизменные слои, что ускоряет процесс на всех последующих этапах.
Вытаскиваем кэш при сборке, пример:

Пример импорта кэша
Кэш мы храним в Harbor

Статистика и управление репозиториями для кэширования
Соответственно при новой сборке кэш выкладывается в Harbor и в дальнейшем используется --cache-from. Пример команды использования кэша:
docker buildx build --push --progress=plain --label bimeister.url_project=https://git/platform/journal --cache-to type=registry,ref=dockerhub/cache/journal:cache,mode=min,image-manifest=true --cache-from type=registry,ref=dockerhub/cache/journal:cache …

Детали артефактов в проекте journal на Harbor
В CI/CD пайплайнах кэширование особенно полезно, когда сборки происходят на разных машинах или в разных окружениях, где нет общего локального кэша.
При изменении одного из этапов сборки, например, копировании файлов с исходным кодом или установки зависимостей, Docker способен использовать кэш для всех предыдущих шагов, тем самым избегая полной пересборки образа. Это значительно ускоряет процесс и снижает нагрузку на ресурсы, особенно в больших проектах или CI/CD пайплайнах, где сборки выполняются часто.
Настройка пайплайна GitLab CI для сборки контейнеров с Docker Buildx
Конфигурация файла gitlab-ci.yml с параметрами для Docker Buildx и кэширования позволяет сократить общее время сборки, эффективно распределяя ресурсы между задачами.
Мы используем GitLab, у нас есть шаблонизированный template проект со всей CI/CD оболочкой, что позволяет нам централизованно управлять и исправлять ошибки, возникающие с CI/CD и соответственно управлять изменениями и новинками.
Для билдов у нас есть свой шаблон, который легко можно переиспользовать у себя в проекте и на основе Dockerfile собирать образ.

Шаблон GitLab CI: конфигурация сборки Docker образов с переменными окружения
Как видно из скрина, что есть вложенности ! reference, c помощью которых определяем выполнение того или иного этапа сборки. Задача build from dockerfile в нашем случае состоит из 4 этапов, пробежимся по ним. Первый этап — это авторизация в Registry для того чтобы отправлять готовые образы в него или же забирать кэш:

Шаг аутентификации в Docker Registry для GitLab CI/CD
Дальше идет определение переменных, таких как тег готового образа в зависимости от ветки или переопределения тега:

Скрипт создания тегов в зависимости от ветки и условий сборки
Третий шаг, проверка на наличие контейнера с тегом : stable в Docker-репозитории и, если такой контейнер существует, выполняем его копирование (репуш) с помощью инструмента Skopeo.

Репуш стабильного контейнера с проверкой наличия в Harbor
В случае отсутствия контейнера с тегом : stable, выводится предупреждение о необходимости повторной сборки.
Четвертый этап — это уже этап самой сборки.

Сборка Docker образа с использованием Buildx и динамическими флагами
Шаблон автоматизирует процесс сборки Docker-образа с использованием Docker Buildx, управляя созданием и удалением временного Buildx builder, а также корректно обрабатывая возможные ошибки.
Создание Docker Buildx builder: Перед сборкой создается временный Docker Buildx builder с уникальным именем, который будет использоваться для выполнения всех операций сборки.
Запуск Builder: Builder запускается с нужной конфигурацией, и проверяется его готовность к работе.
Решение по отправке образа: В зависимости от настроек среды определяется, будет ли готовый образ отправлен в реестр или останется только на локальной машине.
Процесс сборки: Сборка Docker-образа выполняется с выводом подробных логов. Если возникает ошибка, Builder автоматически останавливается и удаляется, чтобы избежать утечек ресурсов.
Завершение: По окончании сборки, независимо от её результата, Builder останавливается и удаляется для освобождения всех задействованных ресурсов.
Обработка ошибок: Если на этапе проверки контейнера с тегом : stable возникают проблемы, выводится предупреждение, и сборка завершает работу с ошибкой.
Таким образом, шаблон позволяет полностью автоматизировать сборку Docker-образов, эффективно управлять ресурсами и гарантировать стабильное завершение процесса даже в случае сбоев.
Проблемы и пути их решения
Несмотря на преимущества кэширования в Docker, его использование может привести к некоторым сложностям. Одной из распространённых проблем является применение устаревших слоёв образов, что может привести к некорректной работе приложения, особенно при изменении зависимостей. Некорректная работа кэша может вызвать неожиданные результаты, замедлить процесс разработки и привести к дополнительным усилиям по отладке.
Для предотвращения подобных ситуаций важно правильно настраивать срок жизни кэша и эффективно управлять им. Регулярное обновление базовых образов и зависимостей гарантирует актуальность используемых компонентов. Настройка политики очистки кэша поможет избежать накопления ненужных данных и обеспечивает стабильность процесса сборки. Эффективное управление кэшированием включает в себя установление оптимальных сроков хранения и регулярную очистку устаревших слоёв. Инструменты, предоставляемые GitLab CI, позволяют гибко настроить хранение артефактов и кэша, обеспечивая контроль над их актуальностью. Это способствует повышению производительности сборок и снижает риски, связанные с использованием неактуальных данных.
Для более эффективного управления сроком жизни кэша можно использовать файл конфигурации buildkitd.toml для Buildx. Этот файл позволяет тонко настраивать различные аспекты процесса сборки, включая политики кэширования и сборки мусора (garbage collection), что дает больше контроля над тем, как кэш хранится и обслуживается.

Конфигурация Buildkit для работы с зеркалами и политиками очистки
Режим отладки: debug = true включает подробное логирование, что помогает в мониторинге и отладке процесса сборки;
Конфигурация прокси если это необходимо, у нас свой прокси и все общедоступные image мы забираем с него;
Настройка воркера:
[worker.oci] включает OCI-воркер и активирует сборку мусора (gc = true).
Блоки [[worker.oci.gcpolicy]] задают политики сборки мусора:
Первый блок нацелен на определенные типы кэша (локальные источники, монтирование кэша, Git-чекауты) и устанавливает лимит в 35 ГБ. Это гарантирует, что эти типы кэша не займут слишком много дискового пространства.
Второй блок применяется ко всем элементам кэша (all = true) с общим лимитом в 60 ГБ. Это помогает предотвратить бесконтрольный рост кэша, который мог бы привести к проблемам с хранением данных.
[worker.containerd] включает сборку мусора для воркера containerd, что дополнительно помогает управлять дисковым пространством, используемым кэшем сборки.
Используя buildkitd.toml, мы получаем более тонкий контроль над размером и сроком хранения кэша. Политики сборки мусора автоматически очищают устаревшие или менее важные элементы кэша, предотвращая его переполнение и сохраняя актуальность данных. Это особенно важно в среде CI/CD, где частые сборки могут быстро заполнить дисковое пространство устаревшими данными.
Кэширование в Docker Buildx: когда и почему его следует отключить.
Есть определенные случаи, когда кэширование Docker-образов не применяется или его необходимо отключить. В нашем случае я выделил моменты, когда мы можем отключать использование кэша, чтобы быть уверенными, что не произойдет ошибок при сборке. В таких ситуациях мы соглашаемся с тем, что сборка может занять больше времени, гарантируя, что финальный Docker-образ будет содержать все актуальные изменения и обновления, а также предотвратит возможные проблемы, связанные с использованием устаревших закэшированных данных.
Сборка в релизной ветке или при наличии тега коммита:
Если текущая ветка сборки соответствует шаблону release/* или если коммит помечен тегом (то есть переменная CI_COMMIT_TAG не пуста), кэширование отключается с помощью флага --no-cache.
Почему отключаем кэширование: При подготовке релизных версий важно обеспечить чистую сборку, которая не зависит от ранее сохраненных слоев. Это гарантирует, что все изменения, включая обновления зависимостей и базовых образов, будут учтены, а финальный образ будет максимально стабильным и предсказуемым.
Отсутствие необходимых переменных окружения при сборке в основной ветке:
Если сборка происходит в основной ветке (обычно main или master), и кэширование включено (переменная ENABLE_DOCKER_CACHE установлена в «true»), но при этом не заданы необходимые переменные окружения (DOCKER_REGISTRY или CONTAINER_NAME), кэширование также отключается.
Почему отключаем кэширование: Без указания Docker-реестра или имени контейнера невозможно корректно сохранить или загрузить кэш. Попытка использовать кэширование в таких условиях может привести к ошибкам сборки или непредсказуемому поведению. Отключение кэширования в этом случае предотвращает возможные сбои.
Другие случаи, когда кэширование не применяется:
Если сборка выполняется в ветках, отличных от основной или релизной, и кэширование не было явно включено, то по умолчанию оно может быть отключено. Это обеспечивает более предсказуемый процесс сборки в средах разработки.
Примеры ситуаций, когда кэширование не срабатывает или его нужно отключить:
Обновление базовых образов или зависимостей: Если были обновлены базовые Docker-образы или ключевые зависимости приложения, использование старого кэша может привести к тому, что обновления не будут учтены. В таких случаях необходимо отключить кэширование, чтобы гарантировать использование последних версий компонентов.
Изменения в конфигурации или среде: При изменении переменных окружения, конфигурационных файлов или установке новых пакетов кэшированные слои могут содержать устаревшие данные. Отключение кэша обеспечивает сборку с нуля, учитывающую все изменения.
Решение проблем с нестабильной сборкой: Если возникают непредсказуемые ошибки во время сборки, возможно, они связаны с поврежденными или несовместимыми кэшированными слоями. Выполнение сборки без кэша помогает выявить и устранить такие проблемы.
Безопасность и соответствие требованиям: В некоторых случаях политики безопасности или нормативные требования требуют выполнения сборки без использования кэша, чтобы гарантировать отсутствие устаревших или уязвимых компонентов в финальном образе.
Смена окружения сборки: При переносе сборочного процесса на новый сервер или в новую среду кэш может быть недоступен или несовместим. Отключение кэширования в таких случаях предотвращает возможные сбои.
Мониторинг и анализ эффективность кэширования.
Регулярный мониторинг и анализ эффективности кэширования помогут выявить узкие места в пайплайне и оптимизировать процесс сборки:
Анализ логов сборки: Логи предоставляют информацию о том, какие слои были использованы из кэша, а какие пересобраны. Это поможет определить, где кэширование не работает должным образом.
Метрики времени сборки: Сравнивайте время сборки до и после внедрения кэширования. Сокращение времени укажет на эффективность стратегии кэширования. У себя мы используем NiFi для сборки таких метрик. Этому можно посвятить отдельный цикл статей, найти бы время.
Инструменты для мониторинга CI/CD: Используйте встроенные средства GitLab CI или сторонние инструменты для отслеживания производительности сборок и использования ресурсов. Также стоит рассмотреть использование опенсорсных решений, таких как gitlab-ci-pipelines-exporter, для мониторинга метрик в GitLab. Этот инструмент позволяет собирать данные о времени выполнения пайплайнов и отдельных джоб. Однако при внедрении подобных решений рекомендуется осторожно подходить к конфигурации экспорта метрик. Например, изначально стоит настроить экспорт данных только для конкретного проекта и отслеживать метрики лишь по ключевым задачам, постепенно расширяя область мониторинга. Это поможет избежать излишней нагрузки на Gitlab и позволит собирать наиболее релевантные данные для анализа.
Настройка оповещений: Настройте уведомления при превышении определенного времени сборки или при сбоях, связанных с кэшированием, чтобы своевременно реагировать на проблемы.
Заключение
Внедрение стратегий кэширования с Docker Buildx позволяет значительно сократить время сборки, повысить эффективность CI/CD и уменьшить нагрузку на разработчиков. Рекомендуемые шаги для оптимизации процесса:
Анализ сборки: Определите ресурсоемкие этапы, подходящие для кэширования, такие как установка зависимостей и базовые шаги.
Настройка Dockerfile и CI/CD: Используйте флаги --cache-from и --cache-to для управления кэшем. Внедряйте кэширование постепенно, начиная с наиболее критичных проектов.
Обучение команды: Введите стандарты эффективного кэширования и обучите команду их применению для повышения стабильности и скорости разработки.
Мониторинг и корректировка: Анализируйте метрики времени сборки и ресурсоемкость для корректировки стратегий кэширования.
Постепенное применение этих шагов улучшит стабильность и производительность CI/CD пайплайнов, что положительно повлияет на качество и скорость разработки.
