Конференция по DevOps практикам — DevOps Fest, 20-21 марта. Cпикеры и доклады на сайте >>
×Закрыть

CI/CD для фронтенда: обзор инструментов и практик для автоматизации разработки

Меня зовут Тит Коваленко. Уже почти 6 лет я занимаюсь фронтенд-разработкой, а сейчас работаю со стеком React & TypeScript & Apollo. Вы можете спросить: «Ты ведь не девопс, почему же собираешься рассказывать о CI/CD?» Отвечаю: потому что эта статья в первую очередь ориентирована на других фронтенд-разработчиков, а не девопсов. Но я буду рад прочесть корректировки и комментарии от девопсов, потому что именно общение с ними позволяет лучше разобраться в теме и в итоге получить еще более совершенную систему.

Процессы разработки веб-приложений со временем усложняются, и девопсам труднее разбираться в их нюансах. Кроме того, девопсы, помимо фронта, занимаются и бэкендом, и кучей других задач, которые решать могут только они.

Мне кажется, это хорошая идея — разобраться, как ваше приложение будет автоматически собираться и деплоиться. Тем более сейчас (на самом деле всегда) тренд на T-shaped people — спецов в своей области, которые немного разбираются в смежных.

Что такое CI/CD

Для начала небольшой ликбез. CI/CD расшифровывается как Continuous Integration и Continuous Delivery aka Deployment — то есть непрерывная интеграция и непрерывная доставка. Зачем это нужно?

Чаще всего конечная цель разработки — приложение. Чтобы им пользоваться, люди должны получить к нему доступ: либо скачать из стора и установит. Если это сайт — вбить в адресную строку URL и открыть страничку. Чтобы мобильное приложение попало в стор, его нужно туда загрузить. В случае с сайтами нужно загрузить наши HTML/JS/CSS-файлы.

Вроде бы все просто. Но загружать файлы вручную как минимум неудобно:

  • Нужно находиться за компьютером, на котором эти файлы есть.
  • Когда файлы загружает человек, он может забыть что-то выгрузить или выгрузить что-то не то.

Процесс неплохо бы автоматизировать — это и есть деплоймент, желтая петля на картинке. В статье я упомяну именно деплой- и релиз-шаги. Более продвинутые темы оперирования и мониторинга не трогаю: не хочется совсем уж отнимать хлеб у девопсов :)

Голубая петля, CI, — это то, что мы делаем после того, как доработали новый функционал, и перед тем, как он пойдет в деплой, чтобы стать доступным пользователям.

Что входит в CI

  • линтеры;
  • тесты;
  • подготовка продакшен-билда.

Линтеры

Зачем

Линтеры — это статические анализаторы кода, которые его проверяют, не запуская. Они позволяют сократить время на код-ревью и избавить разработчиков от рутинных задач: проверки стилистики кода (пробелы, точки с запятыми и длина строки); поиска проблем и потенциальных багов: неиспользованные фрагменты кода, заведомо опасные или переусложненные конструкции.

Как

  • ESLlint — де-факто стандартный линтер для JavaScript.
  • TSLint — был основным линтером для TypeScript, однако разработчики отказываются от его поддержки в пользу ESLint.
  • Prettier — не совсем линтер, скорее, форматтер, который следит за единой стилистикой кода; без проблем интегрируется с ESLint и TSLint.
  • stylelint — линтер для CSS и самых популярных его диалектов (SASS, LESS), для которых у него есть плагины.

Тесты

Зачем

Перед тем как деплоить приложение, или даже строже: перед тем как вливать его в мастер, мы хотим убедиться в его стабильности. Мы хотим знать, что приложение не сломается после запуска и что основные сценарии использования работают. И мы хотим делать это автоматически.

Как

  • Jest, Mocha, Jasmine — фреймворки для организации и запуска тестов; в последнее время наиболее популярен Jest, так как он идет из коробки с Create React App.
  • Testing Library, Enzyme — утилиты, в первую очередь нацеленные на тестирование веб-приложений (рендеринг, симуляция кликов и т. п.).
  • selenium-webdriver, Cypress — инструменты для тестирования end-to-end, то есть когда будет действительно запускаться браузер и туда будут отправляться команды, эмулирующие действия пользователя (клики, нажатия клавиш и т. п.).

Подготовка продакшен-сборки

Что и зачем

Сборка — это преобразование исходных файлов так, чтобы их можно было раздавать сервером как веб-сайт (то есть как набор HTML-/JS-/CSS-файлов, которые понимает браузер), публиковать в менеджере пакетов (если вы пишете библиотеку, фреймворк или утилиту), использовать как расширения для браузера, приложение на Electron и др.

В процессе разработки и в продакшене приложение запускается и работает по-разному. И характеристики нам важны разные. В процессе разработки мы хотим, чтобы приложение быстро пересобиралось и мы видели обновленный результат. При этом не важно, сколько оно весит, ведь оно раздается с локальной машины. В продакшене нам не важно (в разумных пределах, конечно), сколько времени приложение собирается, ведь мы его собираем один раз и потом просто раздаем сервером.

Условно продакшен-сборка состоит из таких процессов:

  • Разрешение импортов. Браузеры начали понимать модульность только недавно, и то до сих пор не все. Необходимо разобраться, в каком порядке запускать скрипты и как передавать результаты их исполнения другим скриптам.
  • Минификация и обфускация. Собранный код весит меньше, чем исходники, и его сложнее анализировать. Этим мы усложняем реверс-инжиниринг.
  • Вшивание переменных окружения. Одно и то же приложение может работать в разных средах. Простейший пример — на тестовом сервере и на продакшене: в этом случае необходимо сбилдить приложения два раза, один раз — когда в окружении задан адрес апи тестового сервера, второй раз — продакшен-сервера.

Как

  • webpack, Parcel, Rollup, SystemJS, gulp, Grunt — основные сборщики приложений, которые решают большинство упомянутых задач.
  • Dotenv, dotenv-cli — npm-пакеты, которые упрощают работу с переменными окружения, особенно при разработке.

Дополнительно

Очень полезно после билда и перед деплоем создавать файл version.json. Этот файл будет содержать информацию о версии приложения, о времени билда, фрагмент хеша коммита, из которого приложение было собрано.

Храните этот файл таким образом, чтобы он был легко доступен рядом с веб-приложением. Например, по адресу: https://your-site.com/version.json.

Такое простое действие поможет вам и другим членам команды (в первую очередь тестировщикам) быстро определять версию приложения, которая запущена в данном окружении, и отслеживать деплои.

Итог. Npm Scripts

Все эти процессы — это integration из CI, но пока что не очень continuous. Чтобы их автоматизировать, необходимо потратить время (один раз) и сконфигурировать их так, чтобы они запускались одной командой в командной строке.

Для этого отлично подходят npm-скрипты. В итоге все 3 предыдущих процесса можно свести к запуску трех команд, который будет похож на что-то вроде:

npm run lint
npm run test
npm run build

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

Бонус. Git Hooks

Зачем

Чтобы не забывать запускать линтеры и тесты. Их запуск можно автоматизировать с помощью Git Hooks, то есть линтеры и тесты будут запускаться, например, перед каждым коммитом.

Как

  • Husky — позволяет привязывать npm-скрипты к Git Hooks внутри package.json.
  • lint-staged — позволяет запускать линтеры только для тех файлов, которые подготовлены для коммита.

Что входит в CD

  • версионирование и релиз;
  • деплоймент.

Версионирование и релиз

Зачем

Версионирование решает большое количество проблем: и при разработке библиотек и фреймворков, и связанных с совместимостями. Сфокусируемся на проблемах, возникающих при разработке приложений для конечных пользователей. Их помогают решить:

  1. Маркеры стабильных ревизий. Упрощают поиск последней стабильной ревизии при необходимости откатить версию приложения (если, например, критический баг попал в продакшен).
  2. Именования для коммуникации. У вас появляется возможность обсуждать заливки, не называя их «то, где мы добавили профиль» или «то, где мы пофиксили регистрацию», а используя номера версий — емкие и однозначные, писать более точные ченжлоги, более эффективно исследовать и воспроизводить баги.

Как

  • Semantic Versioning — методология для формирования номера версии. Одна из многих, но именно эта используется для версионирования npm-пакетов (ее удобно совмещать с версией в package.json).
  • Npm version, yarn version — команды, которые увеличивают версию вашего приложения. Они автоматически меняют версию в package.json, делают коммит с соответствующим сообщением и ставят тег, в котором будет имя новой версии.

Деплоймент

Деплоймент — это доставка и выгрузка файлов в место, откуда они будут раздаваться. То, как происходит деплой в значительной мере зависит от того, как именно хостится ваше приложение. Это может быть один из многих вариантов, например: AWS S3 Bucket / AWS CloudFront / другой сервис AWS, коих множество, Heroku/Dokku, VPS/VPH.

Зачем

Очевидно, что если мы не выгрузим наше приложение на сервера, откуда оно будет хоститься, люди не смогут им пользоваться.
Процесс нужно автоматизировать: один раз потратить время на написание скрипта, чтобы сэкономить большое количество времени и нервов, а также снизить риск, связанный с человеческим фактором.

Как

Деплоймент — это просто выгрузка файлов на другой сервер. Разница лишь в протоколе, по которому она будет происходить:

  • SSH — с некоторыми оговорками можно представить как пуш в некий удаленный (в смысле находящийся далеко) репозиторий.
  • HTTP — простой и знакомый фронтендерам способ, когда каждый файл отправляется в теле соответствующего HTTP-запроса.
  • FTP — самый старый из перечисленных протоколов, для которого можно найти клиент на Node.js, но, возможно, придется попотеть, настраивая его.

Операция выгрузки файлов может быть свернута до единого npm script, который будет запускать файл Node.js. Большинство API работают на Node.js (к примеру AWS).

Итог

По аналогии с CI мы получим несколько простых npm-скриптов, которые позволят запускать более сложные и ответственные процессы.

Пайплайны

Если переводить слово pipeline с английского в контексте computer science, одним из переводов будет «конвейер». И это слово хорошо описывает ситуацию.

Зачем

Если взять упрощенную аналогию с машиной, то сначала нам нужно собрать двигатель, ходовую из колес и осей. После соединить их вместе, чтобы двигатель крутил колеса. Затем на все это сверху повесить корпус, чтобы водитель не мог под дождем. А в конце покрасить, чтобы было красиво :)

Существуют взаимозависимости и порядок процессов. Например, нет смысла деплоить приложение, если тесты упали. Упрощенно конвейер для нашего приложения выглядит так: линтинг и тесты — версионирование — билд — деплой.

Именно здесь вступают в дело пайплайны — как инструмент, который описывает и запускает конвейер для процессов CI/CD.

Как

Почти все, что я привел в списке, — это хостинги репозиториев, кроме Jenkins’а (который я добавил для полноты картины, чтобы было ясно, что такие инструменты не обязательно являются частью хостинга репозиториев).

Далее я приведу несколько примеров, как это выглядит в GitLab Pipelines. Для примеров я взял именно GitLab по нескольким причинам. У меня есть опыт плотной работы с этим сервисом. Бесплатный аккаунт на GitLab предоставляет хороший пакет, связанный с пайплайнами, которого с головой хватит, чтобы потренироваться на пет-проекте. То же относится к standalone GitLab-серверу. Также он дает общее понимание, как настраиваются пайплайны. Лично мне было нетрудно по аналогии с GitLab разобраться с тем, что предлагали Bitbucket Pipelines.

GitLab CI/CD

Как это выглядит. Для каждого запушенного коммита запускается пайплайн. Ниже можно увидеть список пайплайнов, которые запускались для разных коммитов.

Рис. 1. Успешно завершенные пайплайны

Пайплайн состоит из шагов (steps). Степы, в свою очередь, состоят из задач (jobs). Ниже можно увидеть развернутую структуру пайплайна. Колонки Setup, Code_quality и далее — это steps. Каждый блок с зеленой иконкой — это отдельная job.

Рис. 2. Декомпозиция пайплайна

Если одна из джоб падает, пайплайн останавливается. В этот момент ясно видна выгода от связки хостинг-репозитория и пайплайна: если для последнего коммита в мерж-реквесте упал пайплайн, смержить такой реквест не удастся. Это не допустит попадания в стабильные ветки кода, который, например, не проходит проверку линтеров или тестов.

Рис. 3. Пайплайн, завершившийся неудачей, так как линтеры упали

.gitlab-ci.yml

Как это настраивать. Пайплайн описывается в файле .gitlab-ci.yml, который должен лежать в корневой папке репозитория.

Я остановлюсь лишь на базовых примерах, а полную документацию вы сможете найти здесь.

image: node:8

variables:
 REACT_APP_ENV_NAME: $CI_ENVIRONMENT_NAME

stages:
 - setup
 - code_quality
 - testing
 - semver
 - deployment

Строки 1-11 .gitlab-ci.yaml

image — указывает, в каком докер-контейнере должен запускаться пайплайн. Если очень коротко, докер — это технология, позволяющая получить предсказуемую среду выполнения. В данном случае мы хотим запускаться в условном Linux, на котором установлена 8-я версия Node.js.

variables — позволяет явно определить переменные окружения во время работы пайплайна. В нашем примере берем встроенную переменную, которая содержит имя энвайронмента, для которого работает пайплайн, и переприсвает его в переменную, которая будет доступна внутри упакованного приложения. В данном случае это делалось для интеграции с системой трекинга ошибок — Sentry.

stages — описывает очередность выполнения задач. Ставим зависимости, линтим скрипты и стили, потом тестируем, после чего уже можем деплоить. Выглядит это как массив строчных значений, которые используются для маркировки задач. Эти же стадии изображены на рис. 2.

Jobs & Scripts

dependencies:installation:
 stage: setup
 cache:
   paths:
     - node_modules/
 script:
   - yarn --prefer-offline --no-progress --non-interactive --frozen-lockfile
 tags:
   - web-ci

lint:scripts:
 stage: code_quality
 cache:
   paths:
     - node_modules/
 script:
   - yarn run lint:scripts:check --max-warnings 0
 only:
   changes:
     - src/**/*.{ts,tsx}
 tags:
   - web-ci

lint:styles:
 stage: code_quality
 cache:
   paths:
     - node_modules/
 script:
   - yarn run lint:styles:check
 only:
   changes:
     - src/**/*.{css,scss}
 tags:
   - web-ci


unit:testing:
 stage: testing
 cache:
   paths:
     - node_modules/
 only:
   changes:
     - src/**/*.{ts,tsx}
 script:
   - yarn test
 tags:
   - web-ci

Строки 13-60 .gitlab-ci.yaml

jobs — далее от корня указываются названия задач и потом вглубь — их описание. Ключевыми параметрами джобы выступают стейджи, то есть привязки конкретной джобы к стейджу. Это определяет, после каких джоб она будет исполнена.

script — набор команд, которые будут выполнены в процессе работы джобы. Для dependencies installation мы видим, что это всего одна команда — yarn — c аргументами, которые говорят не качать лишнего, если оно есть в кеше.

Подобным образом это работает с линтом скриптов и стилей. Обратите внимание, что и скрипты, и стили привязаны к одному стейджу. Это значит, что по возможности они будут идти параллельно.

only и exlude позволяют определить, когда джоба должна работать, а когда нет. Например, мы видим, что линтинг скриптов происходит только при изменениях в рамках .ts- и .tsx-файлов, CSS- и SCSS-стилей.

Таким же образом можно сделать джобу деплоя доступной только для мастер-ветки.

Версионирование

Версионирование — одна из путающих задач при построении пайплайна. Пайплайн запускается на одном из коммитов, а версионирование само по себе провоцирует создание нового коммита, в котором будет изменена версия package.json и проставлен новый тег. Нам придется запушить в репозиторий из пайплайна и, таким образом, один пайплайн спровоцирует другой пайплайн.

.semver_script: &semver_script
 stage: semver
 when: manual
 only:
   - master
 except:
   refs:
     - /^v\d+\.\d+.\d+$/
 tags:
   - web-ci
 script:
   - mkdir -p ~/.ssh && chmod 700 ~/.ssh
   - ssh-keyscan $CI_SERVER_HOST >> ~/.ssh/known_hosts && chmod 644 ~/.ssh/known_hosts
   - eval $(ssh-agent -s)
   - ssh-add <(echo "$SSH_PRIVATE_KEY")
   - git remote set-url --push origin git@$CI_SERVER_HOST:$CI_PROJECT_PATH.git
   - git config --local --replace-all user.email "noreply@yourmail.com"
   - git config --local --replace-all user.name "Gitlab CI"
   - git checkout $CI_COMMIT_REF_NAME
   - git reset --hard origin/$CI_COMMIT_REF_NAME
   - npm version $SEMVER_LEVEL
   - git push -u origin $CI_COMMIT_REF_NAME --tags

semver:minor:
 <<: *semver_script
 variables:
   SEMVER_LEVEL: minor

semver:patch:
 <<: *semver_script
 variables:
   SEMVER_LEVEL: patch

Строки 62-93 .gitlab-ci.yaml

Этот фрагмент уже более сложный. Здесь описаны две аналогичные джобы: для инкремента минорной и патч-версий соответственно. Скрипт описывает операции, которые позволят пушить из пайплайна в свой же репозиторий:

  • Добавление приватного SSH-ключа, который хранится в переменных окружения и который имеет доступ для пуша в репозиторий.
  • Добавление хоста репозитория в список известных хостов.
  • Конфигурация гит-пользователя с именем и электронной почтой, что также необходимо, чтобы иметь возможность коммитить и пушить.

Чтобы не копировать этот фрагмент для минорной и патч-версий, здесь используется фича YAML-файлов, которая называется YAML anchor. Благодаря подобным фичам YAML-файлы становятся лучшим форматом для описания конфигураций.

Деплоймент и переменные окружения

Рис. 4. Веб-интерфейс гитлаба для управления окружениями

На рис. 4 показан веб-интерфейс гитлаба для создания и редактирования деплоймент-окружений. После того как они созданы здесь, их можно использовать в .gitlab-ci.yaml.

Ниже приведен фрагмент конфигурации деплоймента на примере выгрузки результатов билда в AWS S3 Bucket. Здесь также использован YAML anchor для исключения дублирования кода.

.deploy_script: &deploy_script
  cache:
    paths:
      - node_modules/
  stage: deployment
  script:
    - yarn run build
    - yarn run deploy
  tags:
    - web-ci

deploy:dev:
  <<: *deploy_script
  variables:
    AWS_S3_HOST_BUCKET_NAME: $AWS_S3_HOST_BUCKET_NAME__DEV
    REACT_APP_API_BASE: $REACT_APP_API_BASE__DEV
  environment:
    name: dev
    url: http://$AWS_S3_HOST_BUCKET_NAME.s3-website.us-east-1.amazonaws.com/
  only:
    - develop


deploy:qa:
  <<: *deploy_script
  when: manual
  variables:
    AWS_S3_HOST_BUCKET_NAME: $AWS_S3_HOST_BUCKET_NAME__QA
    REACT_APP_API_BASE: $REACT_APP_API_BASE__QA
  environment:
    name: qa
    url: http://$AWS_S3_HOST_BUCKET_NAME.s3-website.us-east-1.amazonaws.com/
  only:
    refs:
      - /^v\d+\.\d+.\d+$/
    changes:
      - package.json

Строки 95-131 .gitlab-ci.yaml

Обратите внимание, как используются переменные окружения. Команды yarn run build и yarn run deploy используют имена переменных без постфиксов, которые определяются на уровне конкретной джобы из значений, находящихся в переменных с постфиксами.

Рис. 5. Веб-интерфейс гитлаба для управления переменными окружения

На рис. 5 показан веб-интерфейс, в котором можно описать переменные окружения. Они будут доступны внутри пайплайна, когда он запустится. Тут можно определить адреса апи бэкенда, ключи апи для сервисов, которые вы используете: например, Google API key, SSH-ключи для версионирования и другие данные, коммитить которые небезопасно.

Заключение

Даже при рассмотрении CI/CD в рамках специфики фронтенда обнаруживается много деталей и нюансов. Файл конфигурации пайплайнов из моего примера — рабочий, вы можете использовать его для своих проектов, подставив соответствующие npm- или yarn-скрипты. Надеюсь, эта статья станет отправной точкой для дискуссий и погружения в тему.

LinkedIn

22 комментария

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

«в этом случае необходимо сбилдить приложения два раза, один раз — когда в окружении задан адрес апи тестового сервера, второй раз — продакшен-сервера.»
Приложение должно билдиться только 1 раз. И этот артефакт в неизменном виде уже кочует от одного энва к другому. И при этом да, можно управлять значениями энв переменных для разных энвов. Но билдить по новой для этого не нужно)

Спасибо за труд. Хочу однако отметить что современный JS программист итак знает все что до деплоймента. А вот отсутствие в статье информации о том что миром правят контейнеры и кубер, говорит о том что статья устарела. Деплоймент в мире кубера другой. А кубер крутят на всем — от AWS/GCE до bare metal.

Была изначально задумка и хотя бы про Docker и docker-compose написать, это тоже очень полезно и кучу проблем снимает. Особенно когда фронты могут с беками собрать компоуз чтобы локально сервисы бека поднимать

Но тогда статья увалила бы за читабельное за раз количество символов :)

„Continuous Delivery aka Deployment” — nein, тут найс пояснення www.atlassian.com/...​vs-delivery-vs-deployment

как насчет деплоя нескольких приложений/пакетов из одного монорепозитория?

Я бы делал специализированные теги/ветки (в зависимости от того какой у вас SDLC), на которых бы запускались свои джобы, которые в свою очередь использовали скрипты специфичные для пакета\репозитория

точно!)) и github actions пайплайны позволяют это, но с некоторыми тонкостями https://github.community/t5/GitHub-Actions/Github-actions-and-required-checks-in-a-monorepo-using-paths-to/td-p/34829

чем CI/CD для фронтенда отличается от какого-то другого? везде ведь та же самая хрень — bamboo, jenkins, docker, kubernetes, helm и прочий ад

инструментами которые обеспечивают работу пайплайнов — не отличаются. но отличается то, из чего состоит пайплайн. в статье перечислены основные практики и инструменты, которые специфичны для разработки фронта ака клиентской части веб приложения.

Я тут видел статьи микросервисы для фронт-энда. Вот добрались до CI/CD.

Хорошая обзорная статья.
Спасибо Тит! :)

Спасибо, практичная статья.
В маленьком стартапе каждый фронтенд-разработчик в глубине души (где-то очень глубоко) неотвратимо становится девопсом...

даже тестировщик иногда становится девопсом))

Касаемо .semver_script, несколько меньше телодвижений было бы наверное через гитлабовский АПИ запостить: docs.gitlab.com/...​ultiple-files-and-actions
Впрочем, дело вкуса и привычки.

А есть способ выкусить в подобном формате диф существующего коммита? Потому что кажется появятся телодвижения с форматированием тела для этого запроса. Плюс похоже что прийдется делать 2 запроса: 1 — сам коммит, 2 — тэг (опять же нюансы с формированием тела)

Зачем дифф? Если известны файлы модифицируемые «npm version», то просто добавляем их в тело запроса как есть. Форматирование запроса не хитрое, но держать в ямле действительно неприятно — для внутреннего употребления писал утилитку gitlab.com/mgurov/gitlaber

Зачем тег нужно было бы специально распознавать? Вроде в git версии теги не апдейтятся, только текущий бранч?

Вопрос: как описаный сетап будет работать с двумя одновременно открытыми merge requests — не получится одна и таже версия замердженная в мастер для разных билдов?

Спасибо за ответ. С заменой контента файла, действительно просто и хорошо. Из арументов у меня осталось лишь то что не апишный вариант будет работать с любым хостингом репозиторием. Но в итоге возвращаемся к тому что было сказано — дело привычки и вкуса :)

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

Чтобы не получилось так что первый пайплайн закончит работу после второго — стоит использовать тэги. То есть когда оба MR слиты в мастер — руками запустить джобу которая заинкрементит версию и поставит тэг. Потом все это пушнет — пайплайн уже побежит не для мастера, а для тега. Коллизии быть не должно (если конечно вы не запустите 2 джобы версионирования параллельно, но это уже какойто преднамеренно вредительский кейс)

ждём кубернетес для фронтенда

а в чем проблема собсна?

Подписаться на комментарии