Масштабирование CI/CD монорепозитория

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Lerna


Дано


  1. Монорепозиторий на базе Lerna и Yarn workspaces.
  2. Десяток приложений, и десятки общих пакетов на TypeScript, Angular, NodeJS.
  3. Высокое покрытие тестами самых разных мастей (модульные, интеграционные, e2e).
  4. и Atlassian Bamboo CI/CD.

Задача


Ускорить имеющиеся пайплайны в 2 раза (до, хотя бы, получаса). Попутно повысив стабильность до 90%.


Забегая вперед, скажу что требуемые показатели были достигнуты.


Было



Для инкрементальной сборки lerna filter options:


lerna run build:packages --since --include-merged-tags --include-dependencies

Чтобы попасть в инкремент пакеты должны проходить фазу lerna publish в артифакторий (JFrog):


# Master
lerna publish patch --yes
# Feature
lerna publish prepatch --yes --no-push --preid "${PREID}"

При такой организации pipeline, возможно только вертикальное масштабирование путём увеличения мощностей elastic агентов.


Этот подход крайне ограничен. И с ростом числа пакетов средняя длительность постепенно росла (~1ч).


Надо заметить, что в силу короткого релизного цикла (сутки), стабильность JFrog и, как следствие, всего pipeline была низка (~70%).


Идея


Собирать каждое приложение независимо от остальных.
На входе — монорепозиторий
На выходе — production image приложения.


Тестировать тоже независимо от остальных.
На входе — production image (зависимости устанвлены, все пакеты собраны)
На выходе — отчеты о тестировании и покрытии.


Это позволит собирать и тестировать в параллельном режиме, масштабируясь горизонтально по мере развития линейки продуктов.


Но в таком случае размер node_modules составил бы ~1.5Gb (суммарные зависимости всех пакетов монорепозитория). Что негативно отразилось бы на размере image, времени его загрузки в AWS ECR, и времени развертывания.


Фокусировка


Чтобы "урезать" ("сфокусировать") монорепозиторий для сборки, тестирования и развертывания одного конкретного приложения, достаточно найти подмножество пакетов в общем графе пакетов и переписать декларацию workspaces в корневом package.json непосредственно перед сборкой на CI.


#!/usr/bin/env node

const { spawnSync } = require('child_process');
const { existsSync, promises: { readFile, writeFile } } = require('fs');
const { join, dirname, relative, normalize } = require('path');

const PACK_JSON_PATH = './package.json';

(async (apps) => {
    const packJSON = JSON.parse((await readFile(PACK_JSON_PATH)).toString());

    await spawnSync('yarn', ['global', 'add', 'lerna'], { shell: true });

    const locations = await listPackages(apps);

    const [someLocation] = locations;
    const basePath = findBasePath(someLocation);

    // All paths should be relative to monorepo root
    const workspaces = locations.map((loc) => normalize(relative(basePath, loc)));

    packJSON.workspaces.packages = workspaces;

    const packJSONStr = JSON.stringify(packJSON, undefined, 2);
    await writeFile(PACK_JSON_PATH, `${packJSONStr}\n`);
})(
    process.argv.slice(2),
);

async function listPackages(apps = []) {
    const filterOptions = apps.flatMap((app) => ['--scope', app]);
    const { stdout } = await spawnSync(
        'lerna',
        ['ls', '-pa', '--include-dependencies', ...filterOptions],
        { shell: true },
    );
    return String(stdout).split(/[\r\n]+/).filter(Boolean);
}

function findBasePath(packageLocation) {
    return existsSync(join(packageLocation, 'lerna.json'))
        ? packageLocation
        : findBasePath(dirname(packageLocation));
}

После "фокусировки" все команды (в том числе и changed) будут относится лишь к подмножетсву пакетов конкретного приложения.


Размер node_modules удалось снизить в среднем в 3 раза.


Fixed mode


Lerna fixed mode, отказ от lerna publish и артифактория позволили повысить стабильность и упростить логику pipeline.


Но как же быть с инкрементальностью сборок?


Инкремент


Для инкремента достаточно отслеживать изменения через команду lerna changed


lerna changed -a --include-merged-tags

Если изменений не обнаружено, то можно переиспользовать latest image приложения для развертывания и тестирования:


#!/usr/bin/env bash

APP=$1

lerna-focus.js "${APP}"

function nothing_changed() {
    [[ -z "$(lerna changed -a --include-merged-tags || true)" ]]
}

function pull_latest_image() {...}
function push_latest_image() {...}
function tag_latest_with_version() {...}

pull_latest_image

if nothing_changed; then
    tag_latest_with_version
    exit
fi

build-app.sh "${APP}"

if is-master.sh; then
    push_latest_image
fi

Стало



Что дальше?


Сейчас активно набирают обороты такие решения как Nx: Extensible Dev Tools for Monorepos. Это предмет следующих разборов.


Если эта статья окажется полезной, то в следующей расскажу о горизонтальном масштабировании "на коленке" модульных тестов (Angular, Jest, ElasticSearch, Bamboo CI).

Источник: https://habr.com/ru/post/511614/


Интересные статьи

Интересные статьи

Привет, Хабр!TeamCity — пожалуй, один из самых популярных CI/CD-серверов, который используют разработчики по всему миру. В этой статье я расскажу про базовые концепции, н...
Предыстория Когда-то у меня возникла необходимость проверять наличие неотправленных сообщений в «1С-Битрикс: Управление сайтом» (далее Битрикс) и получать уведомления об этом. Пробле...
Несмотря на то, что “в коробке” с Битриксом уже идут модули как для SOAP (модуль “Веб сервисы” в редакции “Бизнес” и старше), так и для REST (модуль “Rest API” во всех редакциях, начиная с...
Этот пост будет из серии, об инструментах безопасности, которые доступны в Битриксе сразу «из коробки». Перечислю их все, скажу какой инструмент в какой редакции Битрикса доступен, кратко и не очень р...
Реализация ORM в ядре D7 — очередная интересная, перспективная, но как обычно плохо документированная разработка от 1с-Битрикс :) Призвана она абстрагировать разработчика от механики работы с табл...