Как мы делаем CI/CD

Igor Kamyshev
Breadhead Stories
Published in
6 min readJul 2, 2019

Год назад мы начали делать непрерывную интеграцию и доставку приложений. Рассказываем, зачем мы это делаем, как повторить, что планируем улучшить.

TL;DR

На всех проектах используем CI/CD от CircleCI. На каждый PR запускаем тесты и статический анализ. После мерджа, приложение собирается из мастер ветки и готовый Docker-контейнер загружается на DockerHub. Оттуда попадает на стейджинг сервер. Чтобы выпустить новый релиз достаточно создать гит-таг, дальше все тоже самое, но прилетит уже на продакшн сервер. Тратим на эти сервисы ~70$ в месяц.

Введение

Если вы хорошо представляете о чем идет речь, просто пропустите этот раздел.

Continuous Integration

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

На практике это означает, что программист работает над своей задачей максимум день-два и после этого отправляет свой код через пулл-реквест в основную ветку. Дальше этот код будут смотреть его коллеги и проверять автоматизированные инструменты. Тут и вступает CI-сервис — он клонирует себе репозиторий с этой версией кода и проверяет его заранее определенными способами, о результатах проверки сообщает в отчете о пулл-реквесте. Если все прошло успешно — код попадает в основную ветку. Дальше начинается CD.

Часто под CI понимают именно автоматизированные проверки о отчеты о них. Например, статический анализ кода, тесты, проверка на известные уязвимости.

Continuous Delivery

Автоматизированная доставка новой версии программы к конечным пользователям.

CD-сервис берет код из основной ветки, собирает из него нечто, что следует доставить (например, Docker-контейнер) и отправляет потребителю. Если речь идет о веб-приложении — отправляет собранное приложение на сервер.

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

Зачем

Очевидно, что настройка CI/CD занимает время. Но это делается только один раз для проекта, зато ускоряет процесс разработки, программисты не отвлекаются на побочные задачи, исчезает одно место где можно ошибиться и допустить ошибку. Тематическая статья “Лучше день потерять”.

Страдать должны машины, а не люди.

Выбор сервиса

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

Travis CI

  • 69$, одна запущенная задача, нет ограничений по времени;

CircleCI

  • бесплатно, одна запущенная задача, 1000 минут в месяц;
  • 50$, две запущенные задачи, нет ограничений по времени.

AppVeyor

  • 59$, одна запущенная задача, нет ограничений по времени.

У CircleCI приятные цены, простые конфигурационные файлы и хорошие отзывы пользователей — мы взяли его.

У нас нет ресурсов разворачивать CI-сервер самим, поэтому Jenkins, Drone и другие похоже решения мы не рассматривали.

Недавно GitHub запустил свой CI/CD сервис — GitHub Actions. Он пока недоступен для организаций, но мы внимательно следим за его развитием, постоянно тестируем и в будущем планируем попробовать на каком-нибудь проекте. Было бы здорово отказаться от стороннего сервиса и получить всю функциональность внутри интерфейса GitHub.

Что именно делаем

Большая часть наших приложений написана на TypeScript. Он легко поддается статическому анализу, и мы пользуемся этим по максимуму.

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

Используем ESLint и Stylelint. Чтобы не заботиться о переносе конфигурации между проектами, используем @solid-soda/scripts — это пакет, который прячет внутрь себя настройки всех инструментов.

Автоматические тесты тоже запускаются на каждое изменение кода. Наши приложения не покрыты тестами на 100%. Но мы пишем много юнит-тестов на функции в которых легко допустить ошибку. Недавно начали внедрять тесты на основе свойств и е2е-тестирование.

Прежде чем код попадет в мастер-ветку запускаются все возможные проверки. Так мы находим потенциальные проблемы и уязвимости до попадания кода в общую кодовую базу.

После проверок код попадает в мастер и CD собирает из него Docker-контейнер, который мы сразу же загружаем в DockerHub. После этого на сервере скачивается новый контейнер, в него внедряется конфигурация (через переменные окружения) и новая версию приложения запускается вместо старой.

Как именно делаем

В этом разделе будет много штук специфичных для JavaScript, если вы используете другие технологии — поищите в интернете аналоги. Все примеры будут тесно связаны с CircleCI, если вы используете другой сервис — поищите в интернете аналоги.

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

Подготовка

Линтинг — статический анализ, линтер — статический анализатор

CI начинается с линтеров. Раньше мы пользовались TSLint, но они объявили что больше не будут развивать проект и пора мигрировать на ESLint, так мы и сделали. Линтить нужно не только код, но и стили — там легко допустить много глупых ошибок. Поэтому добавили Stylelint. Управлять конфигурацией этих инструментов очень скучно. Куча конфиг-файлов, постоянно что-то устаревает. Поэтому, мы используем @solid-soda/scripts — все конфиги спрятаны внутри, а нам просто доступны скрипты для линтинга. В новом проекте первое что мы делаем, это ставим скрипты и инициализируем их:

yarn add --dev @solid-soda/scripts
yarn soda init

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

Второе, что следует запускать на CI — тесты. Мы используем тест-раннер jest, он простой и понятный, его легко использовать с React и TypeScript.

Конфигурация

Теперь проект готов к непрерывной интеграции, осталось только заставить CI-сервер работать с ним. Для этого достаточно создать файл .circleci/config.yml и вписать в него задачи. Каждый запуск CI-сервера — это воркфлоу. Воркфлоу строится из джобов, которые разделены на степы.

Теперь в интерфейсе CircleCI включаем репозиторий. И все. На каждый ПР будет сделан отчет, какие джобы закончились успешно, какие упали. Но это только половина работы, нужно еще доставлять собранное приложение на сервера. Для этого нужно написать докер-файл, который описывает, как из исходного кода собрать готовый докер-контейнер. Типичный докер-файл для нашего проекта:

# За основу берем контейнер с pm2 внутри и стабильной Node.js
FROM keymetrics/pm2:10-alpine
WORKDIR /appENV NODE_ENV="production"
ENV PATH="./node_modules/.bin:$PATH"
# Сначала копируем в контейнер только описание зависимостей
# Позволяет не перекачивать зависимости, если изменился только код
COPY package.json .
COPY yarn.lock .
RUN yarn
# Копируем весь остальной код приложения и собираем его
COPY . .
RUN yarn build
# Приложение будет слушать ВНУТРИ контейнера на 3000 порту
EXPOSE 3000

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

Теперь нужно добавить несколько джобов и пару воркфлоу в .circleci/config.yml:

После нужно добавить все переменные окружения в интерфейс CircleCI (DOCKER_USERHANE, DOCKER_PASSWORD, SSH_USER, SSH_HOST), сгенерировать на своем компьютере новый ssh-ключ, добавить его публичную часть на свой сервер, а приватную загрузить в CircleCI.

Сгенерировать ключ можно командой ssh-keygen -m pem

В директорию /path-to-app на сервере поместите docker-compose.yml с описанием вашей инфраструктуры. В самом простом случае это будет выглядеть примерно так:

version: '3'
services:
my-app:
image: my-organization/my-favorite-image
ports:
- "80:3000"

Этот конфиг просто запустит ваше приложение, внешний 80 порт сервера станет указывать на 3000 порт внутри вашего контейнера. В реальном приложении в этом же файле будет описан nginx, бэкенд и фронтенд, база данных, хранилище для файлов и куча всего разного.

Сколько стоит

В CircleCI цена определяется количеством одновременно запущенных задач. В бесплатной версии предоставляется возможность запускать только один контейнер на 1000 минут в месяц. Это сильно сказывается на скорости проведения всех тестов и на скорости доставки приложений. Когда три разработчика одновременно присылают свой код — CI залипает на 20 минут и выкатить срочный фикс в случае чего невозможно. В какой-то момент у нас просто кончились минуты.

Сейчас используем самый дешевый тариф, в нем нет ограничения на длительность операций, плюс допускается две одновременные задачи. Это стоит 50$ в месяц и полностью покрывает требования 5 разработчиков.

Плюс, мы храним Docker-образы в DockerHub, за каждый образ платим примерно 1$ в месяц. Мы могли бы сэкономить, подняв свое хранилище образов, но затраты времени на обслуживание такого решения слишком велики для такой маленькой компании.

Результат

Плюсы

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

Минусы

  • первоначальная настройка CI занимает время;
  • переводить старые приложения на новый воркфлоу сложно.

Как делать еще лучше

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

Например, сейчас после релиза новой версии, сайт может быть недоступен секунд 20–40. Этого можно избежать, если поднимать новый контейнер, переводить на него трафик, а потом опускать старую версию.

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

--

--

Igor Kamyshev
Breadhead Stories

Software Engineer. Disciplined thinker with battle-tested focus.