Действие происходит в следующей вселенной: лаборатория тестирования 2ГИС; gitlab CI, тесты всех команд запускаются на общих раннерах, над которыми властвует команда IO; e2e-тесты на различные BE-сервисы — python и vedro. Однажды инженер Василий (собирательный образ, все совпадения случайны) проснулся и понял, что больше не может ждать эти бесконечные пайплайны. Чтобы отделить ощущения от реальности, он начал собирать статистику — сколько ходят пайпланы, сколько выполняются сами тесты в сервисе фото, а сколько собираются образы. Какая была картина. От старта пайплайна до непосредственного запуска тестов в сервисе проходило в среднем 7,5 минут. Допустим, за рабочий день каждый член команды (разработчик/тестировщик) запускает 3 пайплайна, а людей в команде — 14. Тогда на сборку образа уходит 5 часов 15 минут.  Собрав статистику и проделав некоторую работу по ускорению самих тестов, Василий заметил, что подозрительно много времени мы тратим на ожидание запуска этих тестов. Он понял: нужно что-то делать непосредственно с самой сборкой — от пайплайна к пайплайну зависимости меняются редко, а пересобирается всё каждый раз, как в первый. Первое, о чём подумал Василий: можно же собирать образ с зависимостями заранее (локально), а в CI собраться на его основе. Как это выглядит: Сам алгоритм: Фиксируем новую версию (CODE_VERSION) Локально собираем образ, основываясь на Dockerfile.base Пушим его в docker-hub Тогда в CI долгая часть сборки просто не происходит. Действительно, в CI образ собирался ~30 секунд. Однако в этом случае нужно пересобирать базовый образ на тачке Василия (а это те же 7,5 минут) и руками изменять версию базового образа в коде. И если кто-то в команде внесёт изменения в зависимости образа, то отслеживать всё предстоит вручную. Василию не подходит.  А что если собирать тот же базовый образ, но автоматически на мастере, помечая его как latest? Звучало неплохо, и Василий решил попробовать. Выпив чаю, он вспомнил, что сам образ собирается по слоям, поэтому можно использовать --cache-from в docker build — с ним можно пересобирать образ, используя уже собранный образ как источник кэшированных слоёв.  И вроде всё хорошо: образ собирается за те же 30 секунд. Если зависимости не меняются, руками делать ничего не надо. Но если зависимости изменены, то образ собирается очевидно дольше — с самого длинного этапа установки зависимостей. Причём, если разработка тестов подразумевает несколько запусков пайплайнов, то в каждом запуске на ветке будем собирать образ заново. Василий решил попросить «помощь зала», и те отметили, что способ не работает для multi-staged билдов. Хоть и в текущем сервисе multi-staged отсутствовал, решение-то хотелось найти универсальное! А значит, побрёл Василий в поисках нового пути, не оглядываясь. Поразмыслив немного ещё, Василий решил двигаться в сторону кэширования в момент установки зависимостей — pip3 install --cache-dir .cache_pip/ Алгоритм выглядел так: Нулевая сборка — собираем без кэширования, но полученный кэш будем использовать в дальнейшем. Последующая сборка:2.1. Стягиваем образ (нулевой) с кэшем.2.2. Поднимаем контейнер по этому образу и копируем из него папку с кэшем.2.3. Начинаем сборку.2.3.1. Копируем папку с кэшем в контейнер.2.3.2. Собираем зависимости с учетом кэша. Как стал выглядеть Dockerfile: Локальная сборка полного образа с --cache-dir взлетела до 30 секунд! «Вот это скорость», — подумал Василий и начал затаскивать в CI. Чтобы все заработало в gitlab CI, необходимо завести cache: Как это выглядело в CI: При таком способе скорость сборки варьировалась от 2 до 3,5 минут. Казалось бы, прекрасный способ! Но есть нюансы. Добавив кэш, Василий увеличил вес самого образа почти на 30%! Соответственно, время стягивания образа в джобы для запусков тестов тоже увеличилось.  Василий приуныл немного, но пошел гуглить дальше. Новая надежда — Buildx — docker-плагин, расширяющий возможности сборки с Buildkit (полезная статья). Закатав рукава и попробовав локально, Василий преисполнился: Первый запуск локально — 4m 15,975s: Повторная сборка — меньше секунды: Дело за малым — засунуть в CI. Беда пришла откуда не ждали: permission denied А всё потому, что директория .docker была скрыта властелинами раннеров — однажды они столкнулись с багом использования buildkit и прикрыли эту возможность. Тогда Василий решил поднять собственный раннер и использовать его. Сложности возникали на каждом шагу — так, Василий узнал, что: для каждой группы в gitlab нужно создавать свой групповой раннер; поднять раннер не так просто, как описано в https://docs.gitlab.com/runner/install/kubernetes.html, нужно выполнить ещё много приседаний. А ещё поднятные раннеры предстоит поддерживать. Так что Василий, поборовшись какое-то время, бросил эту идею с мыслью вернуться к buildx, когда властелины раннеров излечат баг (а они обещали сделать это вскоре). В одно прекрасное солнечное утро Василий пришел к осознанию, что python-то — скриптовый язык! А значит, образ с зависимостями можно собрать только в случае изменения этих самых зависимостей, а тесты просто прокидывать через volume. Идея, простая как дважды два! Хоть и работающая только для скриптовых языков. Что ж, пробуем: В gitlab-ci.yml добавляем джобу со сборкой образа с зависимостями (базовый) Пользуясь магией документации, находим способ** триггерить джобу при изменении определенных файлов — https://docs.gitlab.com/ee/ci/yaml/#ruleschanges** В некоторых случаях может стрелять баг (файлы не изменялись, а джоба стриггерилась), который можно закостылить предварительной проверкой на существование базового образа в docker-hub перед его сборкой. Добавляем зависимость между сборкой обычного образа: не должен стартовать раньше, чем соберётся база — https://docs.gitlab.com/ee/ci/yaml/#needs Пишем внутренности для сборки4.1. Чтобы по состоянию зависимостей можно было идентифицировать образ, в название образа зашиваем кэш:export REQ_HASH ?= $$(shasum tests/e2e/requirements.txt tests/e2e/Dockerfile | shasum | cut -d ' ' -f 1;)4.2. Пишем команды для сборки базового образа (с зависимостями)4.3. Тогда для того, чтобы собрать обычный образ с тестами, нужно стянуть базовый и сделать (= тегнуть) его как обычный образ Прокидываем папку с тестами через volume Получаем gitlab-ci.yml: В итоге получаем ощутимый буст при сборке, когда зависимости/способ сборки не меняется — образ собирается за 5−10 cекунд. При изменении зависимостей или Dockerfile время остается тем же. Чтобы не мотать вверх-вниз, сопоставим все варианты: Кстати, ещё один из вариантов, который Василий не стал рассматривать, — ускорение через https://github.com/uber-archive/makisu. Deprecated — значит, всегда что-то может пойти не так. Да! Можно посмотреть на цифры выше (или на Василия, который перестал засыпать в ожидании сборки). В целом на все эксперименты ушло меньше недели (а чтобы написать эту статью — вечность). Если у вас этап сборки стал узким горлышком, то эти способы помогут быстро сдвинуться с места.  В дальнейшем мы планируем попробовать buildx и оптимизировать сборку не только е2е-образов, но и образов приложения.