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

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

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

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

Що таке CI/CD

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

Найчастіше кінцева мета розробки — додаток. Щоб їм користуватися, люди повинні отримати до нього доступ: або завантажити з стора і встановить. Якщо це сайт — вбити в адресний рядок URL і відкрити сторінку. Мобільний додаток потрапило в стор, його потрібно туди завантажити. У випадку з сайтами треба завантажити наші HTML/JS/CSS-файли.

Начебто все просто. Але завантажувати файли вручну як мінімум незручно:

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

Блакитна петля, CI, — це те, що ми робимо після того, як допрацювали новий функціонал, і перед тим, як він піде в деплой, щоб стати доступним користувачам.

Що входить в CI

Линтеры

Навіщо

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

Як

Тести

Навіщо

Перед тим як деплоить додаток, або навіть суворіше: перед тим як вливати його в майстер, ми хочемо переконатися в його стабільності. Ми хочемо знати, що додаток не зламається після запуску і що основні сценарії використання працюють. І ми хочемо робити це автоматично.

Як

Підготовка продакшен-складання

Що і навіщо

Збірка — це перетворення вихідних файлів так, щоб їх можна було роздавати сервером веб-сайт (тобто як набір HTML-/JS-/CSS-файлів, які розуміє браузер), публікувати в менеджері пакетів (якщо ви пишете бібліотеку, фреймворк або утиліту), використовувати як розширення для браузера, додаток на Electron та ін.

У процесі розробки і в продакшені додаток запускається і працює по-різному. І нам важливі характеристики різні. У процесі розробки ми хочемо, щоб додаток швидко пересобиралось і ми бачили оновлений результат. При цьому не важливо, скільки вона важить, адже воно лунає з локальної машини. В продакшені нам не важливо (в розумних межах, звичайно), скільки часу додаток збирається, адже ми його збираємо один раз і потім просто роздаємо сервером.

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

Як

Додатково

Дуже корисно після білду і перед деплоем створювати файл 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 , тобто линтеры і тести будуть запускатися, наприклад, перед кожним комітом.

Як

Що входить в CD

Версіонування і реліз

Навіщо

Версіонування вирішує велику кількість проблем: і при розробці бібліотек та фреймворків, і пов'язаних з совместимостями. Зосередимося на проблемах, що виникають при розробці додатків для кінцевих користувачів. Їх допомагають вирішити:

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

Як

Деплоймент

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

Навіщо

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

Як

Деплоймент — це просто вивантаження файлів на інший сервер. Різниця лише в протоколі, за яким вона буде відбуватися:

Операція вивантаження файлів може бути згорнута до єдиного 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

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

Щоб не копіювати цей фрагмент для мінорній і патч-версій, тут використовується фіча 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-скрипти. Сподіваюся, ця стаття стане відправною точкою для дискусій і занурення в тему.

Опубліковано: 03/01/20 @ 11:00
Розділ Різне

Рекомендуємо:

Як ЛУН удосконалює карту новобудов: технічний шлях до 3D-моделей і AR
Ruby дайджест #34: підсумки року, Ruby 2.7.0, актуальність Ruby on Rails в 2020
Один проект і два PM: можливе ефективне керування
Predictive Software Engineering як шанс для аутсорса підвищити якість послуг
Переїзд до Великобританії. Від студента-футболіста в Києві до Software Developer в Лондоні за 5 років