Введение
Скорость сборки 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-образа с использованием расширенного инструментария 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 за счёт эффективного использования кэша.
Вторая часть builder, тут также применяются множество ключей для ускорения билда эта часть Dockerfile выполняет восстановление зависимостей, копирование исходных файлов, и сборку приложения с публикацией в определенный каталог. Оптимизации можно добиться распараллелив сборку за счет дополнительных ключей команды dotnet.
На этом этапе кэш применить не можем, кэшируется лишь повторный перезапуск джобы.
Для примера, первый билд занял 3 минуты:
Повторный билд той же джобы, проходит моментально за счет того что у нас есть кэш в registry, проверяем что ничего не поменялось и используем весь кэш. Это быстро, но на сколько это может пригодится для реальных задач? Максимум если кто-то удалил нужный image и пересборка займет не 3 минуты, а 6 секунд.
Чаще всего, при изменениях в коде, именно часть Dockerfile с builder пересобирается дольше всего.
И третья часть final, эта часть Dockerfile создает финальный образ, копируя артефакты сборки, устанавливая нужные метаданные, открывая порты, и задавая окружение для корректной работы ASP.NET Core приложения.
В итоге Dockerfile состоит из трех частей, каждая из которых может быть закэширована и оптимизирована для ускорения сборки. Некоторые детали, такие как аргументы и инструменты для отладки, опущены из соображений безопасности
Как работает кэширование в Docker?
Кэширование позволяет повторно использовать ранее созданные слои образов, что существенно сокращает время сборки, особенно при частых повторных сборках, когда значительная часть этапов остается неизменной.
Каждый Docker-образ состоит из последовательности слоёв, которые создаются на основе шагов, описанных в Dockerfile. Например, команды COPY
, RUN
, ADD
и другие генерируют новые слои. Если один из шагов изменяется, Docker пересобирает только изменённый шаг и все последующие, но при этом повторно использует те слои, которые не изменились.
При изменении одного из этапов сборки, например, копировании файлов с исходным кодом или установки зависимостей, Docker способен использовать кэш для всех предыдущих шагов, тем самым избегая полной пересборки образа. Это значительно ускоряет процесс и снижает нагрузку на ресурсы, особенно в больших проектах или CI/CD пайплайнах, где сборки выполняются часто.
Пример кэширования в Docker
Допустим, у вас есть проект с несколькими шагами сборки. Первый шаг устанавливает зависимости, второй копирует исходный код, а третий компилирует приложение. Если в новом коммите изменяется только исходный код, то нет необходимости повторно устанавливать зависимости — Docker может использовать уже закэшированные слои.
Рассмотрим пример использования ключевых флагов для управления кэшем в Docker Buildx:
Давайте разберем CACHE_STRING, выглядит она следующим образом:
Как видно мы управляем использованием кэша при сборке 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 …
В CI/CD пайплайнах кэширование особенно полезно, когда сборки происходят на разных машинах или в разных окружениях, где нет общего локального кэша.
При изменении одного из этапов сборки, например, копировании файлов с исходным кодом или установки зависимостей, Docker способен использовать кэш для всех предыдущих шагов, тем самым избегая полной пересборки образа. Это значительно ускоряет процесс и снижает нагрузку на ресурсы, особенно в больших проектах или CI/CD пайплайнах, где сборки выполняются часто.
Настройка пайплайна GitLab CI для сборки контейнеров с Docker Buildx
Конфигурация файла gitlab-ci.yml с параметрами для Docker Buildx и кэширования позволяет сократить общее время сборки, эффективно распределяя ресурсы между задачами.
Мы используем GitLab , у нас есть шаблонизированный template проект со всей CI/CD оболочкой, что позволяет нам централизованно управлять и исправлять ошибки, возникающие с CI/CD и соответственно управлять изменениями и новинками.
Для билдов у нас есть свой шаблон, который легко можно переиспользовать у себя в проекте и на основе Dockerfile собирать образ.
Как видно из скрина, что есть вложенности !reference, c помощью которых определяем выполнение того или иного этапа сборки. Задача build from dockerfile в нашем случае состоит из 4 этапов, пробежимся по ним. Первый этап — это авторизация в Registry для того чтобы отправлять готовые образы в него или же забирать кэш:
Дальше идет определение переменных, таких как тег готового образа в зависимости от ветки или переопределения тега:
Третий шаг, проверка на наличие контейнера с тегом :stable в Docker-репозитории и, если такой контейнер существует, выполняем его копирование (репуш) с помощью инструмента Skopeo.
В случае отсутствия контейнера с тегом :stable, выводится предупреждение о необходимости повторной сборки.
Четвертый этап — это уже этап самой сборки.
Шаблон автоматизирует процесс сборки 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), что дает больше контроля над тем, как кэш хранится и обслуживается.
Режим отладки: 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 пайплайнов, что положительно повлияет на качество и скорость разработки.
Примеры из статьи доступны для просмотра в репозитории по ссылке.
Комментарии (5)
ganzyukvolodya
29.10.2024 14:37"#НИКОГДА не используйте COPY...
COPY --from"
Ахахах, зачет) Прям настроение подняло)
TwenKey
один из немногих примеров когда dotnet контейнеризируют, да еще и на кубер-рельсах.
очень было бы интересно почитать про это отдельно.