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
- линтеры;
- тести;
- підготовка продакшен-білду.
Линтеры
Навіщо
Линтеры — це статичні аналізатори коду, що його перевіряють, не запускаючи. Вони дозволяють скоротити час на код-рев'ю і позбавити розробників від рутинних завдань: перевірки стилістики коду (пробіли, крапки з комами і довжина рядка); пошуку проблем і потенційних багів: невикористані фрагменти коду, свідомо небезпечні або переусложненные конструкції.
Як
- ESLlint — де-факто стандартний лінтер для JavaScript.
- TSLint — був основним лінтером для TypeScript, проте розробники відмовляються від його підтримки на користь ESLint.
- Pret-рівня — не зовсім лінтер, швидше, форматтер, який стежить за єдиної стилістики коду; без проблем інтегрується з 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
- версіонування і реліз;
- деплоймент.
Версіонування і реліз
Навіщо
Версіонування вирішує велику кількість проблем: і при розробці бібліотек та фреймворків, і пов'язаних з совместимостями. Зосередимося на проблемах, що виникають при розробці додатків для кінцевих користувачів. Їх допомагають вирішити:
- Маркери стабільних ревізій. Спрощують пошук останньої стабільної ревізії при необхідності відкотити версію програми (якщо, наприклад, критичний баг потрапив в продакшен).
- Іменування для комунікації. У вас з'являється можливість обговорювати заливки, не називаючи їх«, де ми додали профіль» чи «де ми пофиксили реєстрацію», а використовуючи номери версій — ємні і однозначні, писати більш точні ченжлоги, більш ефективно досліджувати і відтворювати баги.
Як
- 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.
Як
- GitLab ;
- Bitbucket ;
- GitHub & Azure Pipelines ;
- Jenkins і багато інших.
Майже все, що я навів у списку, — це хостинги репозиторіїв, крім 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 "[email protected]" - 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-скрипти. Сподіваюся, ця стаття стане відправною точкою для дискусій і занурення в тему.
Опубліковано: 03/01/20 @ 11:00
Розділ Різне
Рекомендуємо:
Як ЛУН удосконалює карту новобудов: технічний шлях до 3D-моделей і AR
Ruby дайджест #34: підсумки року, Ruby 2.7.0, актуальність Ruby on Rails в 2020
Один проект і два PM: можливе ефективне керування
Predictive Software Engineering як шанс для аутсорса підвищити якість послуг
Переїзд до Великобританії. Від студента-футболіста в Києві до Software Developer в Лондоні за 5 років