GitLab CI: cache vs artifacts на примере Node.js проекта
Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.
Привет! Меня зовут Антон Якутович. Занимаюсь тестированием более восьми лет. Специализируюсь на тестировании веб-сервисов, в основном бэкенда. Организую митапы по тестированию и автоматизации Ministry of Testing Abu Dhabi. Помимо тестирования, занимаюсь поддержкой
В этой статье хочу на примерах объяснить разницу между кешем и артефактами. И показать, как оптимально настроить пайплайн для Node.js-приложения.
Можно вечно смотреть на три вещи: как горит огонь, льётся вода и как билд проходит пайплайн после очередного коммита. Чтобы ожидание не было утомительным, лучше позаботиться о настройке CI сразу. Кеш и артефакты помогают сократить время прогона пайплайна.
Многие путают эти абстракции и не всегда используют по назначению. У GitLab богатая документация, но примеры использования кеша для Node.js-проекта и пример шаблона пайплайна для Node.js противоречат друг другу.
Pipeline внутри GitLab представляет собой набор стейджей (стадий). В каждом стейдже может быть одна или несколько джоб. Каждая джоба может запускаться на отдельном раннере. GitLab-runner — это агент, который занимается запуском джоб. Для простоты считаем, что все раннеры запускаются в докере.
Когда запускаем пайплайн, каждая джоба будет выполняться на раннере со свободными ресурсами. Заранее неизвестно, где физически расположен этот раннер. Система распределенная.
Каждая джоба стартует с «чистого листа» и не знает результаты предыдущей. Если не использовать кеш и артефакты, при установке зависимостей проекта придётся сходить в интернет или локальный registry и скачать необходимые пакеты.
Что такое кеш?
Набор файлов, которые могут скачиваться джобой перед запуском и выгружаться после исполнения. Кеш хранится там же, где установлен GitLab Runner. Если настроен распределённый кеш, в качестве хранилища выступает S3.
Предположим, запустили пайплайн первый раз с локальным кешем. Для первой джобы Build кеш не будет найден. Джоба выполнится и сохранит кеш на runner01. Вторая джоба выполнится на runner02, на нём тоже не найдёт кеш и отработает без него. Результаты кеша сохранит на runner02. Третья джоба Lint обнаружит кеш на runner01 и будет использовать его (pull). А после выполнения обновит кеш актуальными файлами (push).
Что такое артефакты?
Набор файлов, которые после выполнения джобы сохраняются на сервере GitLab. Последующие джобы скачают артефакт перед выполнением основных команд.
Build джоба создаёт артефакт DEF и сохраняет его на сервере. Вторая джоба Test скачивает артефакт с сервера перед запуском основных команд. Третья джоба Lint аналогично скачивает артефакт с сервера. То есть артефакт создаётся в первой джобе и используется в следующих. А кеш создаётся внутри каждой джобы.
Рассмотрим пример шаблона CI для Node.js, рекомендуемый GitLab:
image: node:latest # (1) # This folder is cached between builds cache: paths: - node_modules/ # (2) test_async: script: - npm install # (3) - node ./specs/start.js ./specs/async.spec.js test_db: script: - npm install # (4) - node ./specs/start.js ./specs/db-postgres.spec.js
В строке 1 указан докер-образ, который будет использоваться во всех джобах. Первая проблема — использование тега latest. Это приводит к тому, что у нас нет воспроизводимости сборок. Тег всегда указывает на последний релиз Node.js. Если в GitLab-раннере настроен кеш для докер-образов, при первом запуске будет скачан образ, а все последующие запуски будут обращаться к локально доступному образу. Получается, даже если нода обновиться с версии XX на YY, наш пайплайн об этом не узнает. Поэтому всегда указываем версию образа. И не просто релизную ветку (node:14), а версию целиком (node:14.2.5).
Строку 2 нужно рассматривать вместе с 3 и 4. Для кеширования указан каталог node_modules, и в каждой джобе выполняется установка пакетов (npm install). Установка должна происходить быстрее, потому что пакеты будут доступны внутри node_modules. Так как для кеша не указан ключ (ключевое слово key), будет использовано слово default. Это значит, что кеш будет постоянный, общий для всех веток.
Напомню, наша задача — сохранить воспроизводимость работы пайплайна. Пайплайн, запущенный сегодня, должен одинаково отработать и через год.
NPM хранит зависимости в двух файлах — package.json и package-lock.json. Если использовать package.json, принцип воспроизводимости нарушается, потому что при запуске `npm install`, пакетный менеджер будет ставить последние минорные апдейты при обнаружении нестрогих зависимостей. Чтобы зафиксировать дерево зависимостей, используем файл package-lock.json. Там строго указаны все версии пакетов.
Но есть еще одна проблема, `npm install` переписывает package-lock.json, а это совсем не то поведение, которое мы ожидаем. Поэтому используем специальную команду npm ci. Её принцип такой:
- удалить каталог node_modules;
- установить пакеты из package-lock.json.
А как быть с кешем, если node_modules каждый раз будет удаляться? Для корректной работы npm, указываем путь к кешу через переменную окружения npm_config_cache
.
И последнее, в конфиге нет явного указания стейджей, внутри которых выполняются джобы. По умолчанию джоба выполняется внутри стадии test. Получается, две джобы будут выполняться параллельно. Отлично! Добавим указание стейджей и исправим все недочеты.
Что получилось после первой итерации правок:
image: node: 16.3.0 # (1) stages: - test variables: npm_config_cache: "$CI_PROJECT_DIR/.npm" (5) # This folder is cached between builds cache: key: files: - package-lock.json (6) paths: - .npm # (2) test_async: stage: test script: - npm ci # (3) - node ./specs/start.js ./specs/async.spec.js test_db: stage: test script: - npm ci # (4) - node ./specs/start.js ./specs/db-postgres.spec.js
Получилось построить воспроизводимый пайплайн. Осталось два минуса. Первый — кеш объявлен общим. Значит внутри каждой джобы будет скачиваться кеш и выгружаться новая версия кеша после выполнения джобы (pull-push). Хорошей практикой считается обновление кеша только один раз внутри пайплайна. Второй минус — установка зависимостей происходит внутри каждой джобы, это лишняя трата времени.
Чтобы исправить первый минус, описываем управление кешем в явном виде. Для этого описываем «скрытую» джобу и по умолчанию оставляем только скачивание кеша без обновления:
# Define a hidden job to be used with extends # Better than default to avoid activating cache for all jobs .dependencies_cache: cache: key: files: - package-lock.json paths: - .npm policy: pull
Теперь, чтобы подключить кеш, нужно явно унаследовать джобу через ключевое слово extends.
… extends: .dependencies_cache …
Чтобы исправить второй минус, воспользуемся артефактами. Создаем джобу, которая устанавливает зависимости проекта и передает артефакт с node_modules дальше. Последующие джобы запустят тесты без подготовительных шагов.
setup: stage: setup script: - npm ci extends: .dependencies_cache cache: policy: pull-push artifacts: expire_in: 1h paths: - node_modules
Здесь устанавливаем зависимости npm и используем кеш, описанный в скрытой джобе dependencies_cache. Явным образом указываем, что хотим обновить кеш через pull-push policy. Для артефакта указываем короткое время жизни (1 час). Сам по себе артефакт никакой ценности не несёт, и нет смысла хранить его на сервере GitLab.
Теперь пайплайн будет выглядеть так:
image: node: 16.3.0 # (1) stages: - setup - test variables: npm_config_cache: "$CI_PROJECT_DIR/.npm" (5) # Define a hidden job to be used with extends # Better than default to avoid activating cache for all jobs .dependencies_cache: cache: key: files: - package-lock.json paths: - .npm policy: pull setup: stage: setup script: - npm ci extends: .dependencies_cache cache: policy: pull-push artifacts: expire_in: 1h paths: - node_modules test_async: stage: test script: - node ./specs/start.js ./specs/async.spec.js test_db: stage: test script: - node ./specs/start.js ./specs/db-postgres.spec.js
Итог
Мы научились различать области применения кеша и артефактов и смогли построить воспроизводимый пайплайн, который работает предсказуемо и эффективно использует все ресурсы. Эта статья разбирает большой пласт реальных ошибок, которые допускают при настройке CI в GitLab.
Ссылки:
5 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів