С распространением практики доставки непрерывных обновлений время сборки приложений стало критически важным параметром как для разработчиков, так и для бизнеса компании в целом. В данной статье описан мой опыт ускорения Frontend пайплайна Jenkins в Kubernetes на базе yarn и nx Disclaimer В данной статье делается упор на один из вариантов ускорения сборки UI без описания причин выбранной инфраструктуры, а также без детального описания настройки всей сборки Для сборки всех приложений (ui, backend) используется Jenkins, развернутый внутри Kubernetes кластера (данный подход позволяет лучше утилизировать используемые ресурсы). Таким образом каждая сборка стартует в индивидуальном поде внутри одного или нескольких контейнерах.  В день на конкретно нашем Jenkins может проходить до 5000 сборок. Не так уж и много если бы не одно но - некоторые сборки занимают непропорционально много времени.  На Frontend сборки приходится 1/20 часть ~ 250 в день. Средняя продолжительность сборки ~ 26 минут, однако до четверти из них может занимать больше часа. Практически все ресурсы UI у нас находятся в одном репозитории (monorepo), а те что не находятся, активно туда подтягиваются Все необходимые шаги сборки запускаются с помощью yarn и nx Чертовски медленная сборка Frontend для большой части пайплайнов, которая продолжает замедляться по мере добавления новых модулей в monorepo. Пример полной пересборки проекта с прогоном всех тестов и проверок (некоторая часть шагов опущена ввиду нерепрезентативности) Из описания проблемы самым очевидным решением является распараллеливание сборки. Распараллеливание может быть как на уровне сборщика nx и yarn внутри контейнера, так и на уровне модулей параллельно собирающихся в разных контейнерах, но по-прежнему в том же поде. Первый способ уже был настроен и использовался при сборке. Пример одного из шагов сборки: Второй же способ не принесет ожидаемого результата, так как почти бессмысленно запускать сборки в разных контейнерах того же пода, если можно все распараллелить в одном контейнере, т.к. все контейнеры запускаются на одной и той же машине.  Отсюда следует решение, что вся сборка должна иметь возможность быть максимальна распределена по разным машинам, в случае с Kubernetes -  подам. Благо nx позволяет легко получить список affected модулей: чтобы потом легко раскидать их по подам:   При переходе к такому подходу необходимо учитывать, что мы теряем преимущества использования общего локального кэша, так как сборка будет разнесена по различным машинам. Для того, чтобы не терять данную возможность необходимо предусмотреть использование общего хранилища между подами. В случае с Kubernetes это может быть persistent volume, поддерживающий режим использования ReadWriteMany или же использование third-party хранилища по аналогии с Nx Cloud (рассматривались хранилища поддерживающие s3 api). В данном случае провайдером инфраструктуры выступал AWS, поэтому в качестве поставщика для shared volume подов был выбран EFS. EFS поддерживает протокол NFS. При использовании сетевых файловых систем необходимо помнить что наилучшая производительность достигается при записи больших файлов.  После распараллеливания сборки по подам и использования shared volume на EFS время сборки … увеличилось  (yarn install стал занимать 37 минут вместо 2-3) Почему? Ответ находится чуть выше - а именно, низкая производительность сетевой файловой системы (EFS) при записи маленьких файлов, которые хранятся в кэше nx и node_modules. Первой идеей решения данной проблемы было включение Provisioned Throughput режима для EFS c тарификацией по выделенным мегабайтам. Однако данное решение обладало 2 существенными недостатками: предварительный счет за необходимый throughput впечатлял (в нашем случае сборки запускаются достаточно часто и практически без перерыва); даже отбросив материальный аспект мы уперлись в лимиты EFS (по запросу AWS может их увеличить). Совокупность данных факторов сделало использование EFS безумным, особенно учитывая то, что объем используемого дискового пространства весьма небольшой (~ 10 Гб). Принимая во внимание невозможность использования EFS. а также просчитав цену использования s3 для хранения и чтения (в данном случае цена базируется на занимаемом место  и количестве запросов; в нашем случае данный подход оказался существенно дешевле подхода с EFS) было принято решение отказаться от стратегии кэширования для сборки. Т.е. каждый раз при старте нового пода требовалось вызывать команду  yarn install, которая загружала все зависимости из внешнего мира. В итоге без использования общего кэша в EFS сборка была намного быстрее, но зависимости загружались из вне в каждом новом поде, что вызывало определенный дискомфорт Казалось бы, что задача выполнена и сборка ускорена, однако невозможность использования общего кэша не давала мне покоя, так как скачивание и пересборка одних и тех же библиотек внутри каждого пода отнимали время и впустую использовали внешний траффик. На этом моменте снова вспомнился не бесплатный aws s3 в который можно было бы складывать кэш и в каждом новом поде его доставать. В целях экономии трафика я даже стал рассматривать использование minio внутри kubernetes кластера конкретно для этой задачи. При использовании minio мы бы смогли использовать s3 api и не платить за входящий/исходящий трафик, так как он бы циркулировал внутри кластера (но никто не отменял оплату дополнительных ресурсов необходимых для minio). Оценивая преимущества/недостатки использования minio, меня посетила идея: “А что если просто копировать кэш с одного пода на другой во время его старта?”. Jenkins всегда запускает так называемый базовый под для всей сборки и по мере необходимости, для распараллеливания, создает дополнительные поды. Потенциальным бутылочным горлышком данного решения могла оказаться пропускная способность сети kubernetes кластера при копировании файлов, однако, мы запускали Frontend пайплайны в отдельной Instance Group, пропускная способность которой имела хороший запас. Данная концепция выглядит следующим образом:   В данном случае у нас есть parent-pod, на котором выполняется  клонирование репозитория - git clone первоначальная сборка - yarn install создание снэпшота сборки - tar --exclude=".git" -czf \$CACHE_DIR/project.tar.gz ./   Внутри этого пода запущен nginx-контейнер, выполняющий роль файлового сервера. Пример декларативного описания parent pod'a: Шаг сборки в Jenkins: Остальные поды пайплайна при своем старте скачивают кэш parent-poda и запускают необходимые задачи: Внедрение такого подхода не меняет времени сборки по сравнению с вариантом без использования общего кэша, однако позволяет свести к минимуму взаимодействие с внешним миром при скачивании зависимостей.  Итоговый пайплайн для полной пересборки и проверки выглядит теперь так: Выглядит лучше, не правда ли? Однако здесь нужно быть осторожным и не перегружать машины данными задачами, иначе весь профит от распараллеливания сойдет на нет. Конкретно этот пайплайн запускался всего лишь на 2-х машинах, имеющихся в пуле. В более продуктивное время у нас в пуле запущено около 5-7 машин, что может в теории дать неплохой прирост к ускорению сборки. Также в данном случае распределение задач по подам происходит не эффективно (некоторые задачи завершаются намного раньше и параллельный stage прекращает работу). Помимо этого требовалось не навредить небольшим сборкам, когда распараллеливание не требуется. Для этого просто был введен лимит на минимальное количество модулей, запускаемое на одном поде. Если количество не превышает этот лимит, то все тяжеловесные шаги запускаются последовательно. Маленькие сборки до рефакторинга: И после: И все таки неравномерное распределение сборки модулей по параллельным шагам не давало мне покоя. Некоторые шаги выполнялись довольно долго, а некоторые завершались достаточно быстро. Разница могла достигать двукратного размера. Появилась идея внедрить программную очередь для равномерного распределения задач. Получилось довольно интересно и каждый параллельный шаг одинакового типа теперь занимает +- одинаковое время. Общее время сборки также заметно упало (33 минуты против 56).  Ниже приведен код реализации такого поведения А также как это выглядит в Jenkins пайплайне Итоговое решение имеет как плюсы:  быстрая сборка отсутствие трат на сторонние сервисы, внешний трафик или ресурсы необходимые, для развертывания внутренних сервисов)  данное решение применимо к большинству kubernetes кластерам так и минусы: достаточно большая сетевая нагрузка на родительский под, как следствие машину на которой он расположен, что может негативно влиять на другие поды расположенные на этой машине Таким образом, в моем случае оптимальным оказалось решение не использующие сторонние сервисы, но дающее дополнительную нагрузку на throughput кластера и повышающее утилизацию ресурсов данного кластера. C финансовой точки зрения данное решение оказалось самым выгодным, так как расходы на внешние сервисы отсутствуют..  Очевидно, что данное решение и подход является идеальным конкретно для одного конкретного случая, надеюсь, что некоторые идеи могут быть полезными для кого-то еще. Однако, не стоит забывать, что каждое решение должно быть просчитано и протестировано с точки зрения ресурсов и финансов.