От 15 и больше: как обеспечить масштабируемость CI

r1bngp_hxopy1kpfuxcaoiqib2y.png

Сейчас публикуется много статей и докладов про конкретные технологии в DevOps: Docker, Kubernetes, Ansible… Я же хочу поговорить про построение процессов и про то, как мы в Wrike за два с половиной года эволюционировали от релизной системы для 15 фронтенд-разработчиков до почти 60-ти, и 2–3 деплоев в день.
Эта статья — про те уроки, которые мы на этом пути решили. Статья основана на моем докладе для DevOps митапа в Wrike Tech Club. Если некогда читать, есть видеозапись презентации. Читатели, добро пожаловать под кат.

Немного вводных


Задача любой продуктовой компании в очень конкурентной индустрии — найти самое классное решение, и сделать это быстрее своих конкурентов. То есть нужно перебирать варианты, экспериментировать, быстро релизить, осознавать ошибки, придумывать еще быстрее и релизиться еще быстрее. Соответственно, скорость, с которой вы проверяете свои продуктовые идеи — это ваше конкурентное преимущество. То есть вся непрерывная интеграция, непрерывная доставка и другие ужимки и фокусы нужны для того, чтобы обскакать конкурентов. У отдела DevOps в Wrike задача именно такая.

Взялись мы за нее в 2015 году, Front-end тогда состоял из 15-ти человек, а деплой выглядел как два zip-файла. Они могли выкатиться только вместе и распаковывались при помощи баша теплыми умелыми руками системных администраторов.

А сейчас у нас 12 SCRUM-команд. Они мультифункциональны и независимы. В сумме в этих 12-ти командах уже больше 60-ти фронтендеров А наш продукт, те два зипника — это сейчас 60 Git репозиториев. Мы деплоимся от одного до трех раз каждый день. И это только фронтенд.
Тут же упомяну, что фронтенд мы пишем на языке Dart. И у него есть свой рантайм, написанный на С. Это язык со строгой типизацией, по синтаксису наиболее близкий к C# и Java. Есть свой компилятор, своя система резолвинга зависимостей и свои библиотеки. О том, как мы живем с Dart, можно почитать в других статьях, мы же вернемся к нашей теме.

Stage 1


Итак, в 2015 году у нас не было никакой автоматизации — один репозиторий на Mercurial и никаких способов коллаборации по поводу кодовой базы. То есть никакого гитлаба, невозможно было обсудить какой-то диф, patch, нельзя было завести issue в мерж-реквесте, его обсудить и зарезолвить. Модель работы с VCS, система контроля версий не была формализована.
Это не беда, когда у тебя команда из пяти человек, и люди просто неформально договариваются: «Давай с тобой выйдем в релиз». Кто в кого мержится, договорились, поехали. Следующий день — новые лица. Опять договорились. Но такую модель невозможно масштабировать. И она не подлежит эволюции.

Первая версия обошлась нам дешево и быстро. Мы взяли UI Wrike, хотя это мог быть любой issue-трекер: Jira, YouTrack или еще что-то. Главное, что в нем вы можете обмениваться комментариями и помечать задачи какими-нибудь тегами или папками.

Все, что мы написали, это stateless приложение на Python без персистентного слоя данных вообще.
В итоге я воспользовался TeamCity и Wrike и написал решение при помощи тегов. Разработчик императивно проставлял теги, которые робот воспринимал как команды, и запускал автотесты, интегрировал ветки и т.п.

3ctfazcbxwd6to9pmya7gkuh3r8.png

Так как наш продукт SaaS, и деплоимся мы пару-тройку раз в день, нам не нужны долгоживущие релизные ветки. Не нужно бэкпортировать patch туда, не нужно иметь там хотфикс. То есть от сложной модели Git-flow можно перейти к GitHub flow, а от нее к еще более простой. Мы сделали в трекере две модели, выписывали, какие фичи идут в релиз, номера, ссылки и тех, кто занимается работой. Вот и вся первая версия интеграции. По сути, мы принимали данные из двух источников — Gitlab и Team City — и отправляли данные в Wrike через API и TeamCity. Вся работа с кодовой базой фронтренда была в TeamCity.

Stage 1: уроки

Мы переехали с Mercurial на Git, что помогло нам безболезненно масштабироваться с 15 до 60 фронтенд-разработчиков. На рынке огромное количество людей, которые худо-бедно понимают, как работает Git. Вы обязательно что-нибудь сломаете в Git и обязательно научитесь это чинить. Нанять людей, которые поймут, как работать с Git, в вашей модели, в триста раз проще, чем в любом другом VCS.
Визуализация работы с кодовой базой в Gitlab нам очень пригодилась. Люди стали работать с мерж-реквестами, и это помогло получить более качественную кодовую базу. Визуализация сделали в Wrike, очень задешево и очень быстро.
Ну и специфика SaaS и частых релизов сыграла нам на руку в работе с ветками.

Stage 2


Выкатив первую версию, мы сразу нашли один важный изъян. В ней мы использовали сквош-коммиты в Git. В Git можно сгруппировать коммиты и сделать из них один. Предположим, разработчик сделал 5 коммитов, и мы хотим это сынтегрировать. Раньше мы эти пять коммитов группировали в один при помощи сквоша и тем самым получили атомарность. То есть любая фича представляла собой один коммит. Вы либо его вмерживаете, либо не вмерживаете. То есть сразу можно было понять, где люди пытались мержиться до этого и таскали код друг у друга. История красивая. Каждый коммит в релизной ветке — это целая фича. Читать удобно. Атомарность.

Но пришла беда, откуда не ждали. Оказалось, что у нас есть фичи, которые живут по две- три недели. Что это означает? Что, пока master бампается каждый божий день, кто-то в стороне разрабатывается. Когда возникает конфликт с upstream, перед человеком возникает не диф двух коммитов или одного, а две огромные простыни. Сквошный коммит против другого сквошного коммита. То есть из глаз течет кровь, и невозможно мержить вообще — гора текста.
Также после того, как мы автоматизировали и формализовали процесс, шестеренки начали крутиться намного быстрее. И master стал быстро убегать от фич, которые находятся в разработке. Соответственно, когда человек начинает новую фичу или продолжает ее делать, у него не появляется фиксов из upstream, он может легко работать в состоянии наличия регрессионных дефектов. То есть когда баг починен, а у него этого патча нет.
В результате нам быстро пришлось дописать механизм, который разливал мастер в активно развивающиеся ветки.

Stage 2: уроки
Мы, казалось бы, сделали благо нашим коллегам, но тем самым создали два дефекта. В итоге, нам пришлось пожертвовать этими сквошными коммитами, хотя они нам очень нравились. Это было очень красивое на бумаге решение, но, если бы мы чуть дольше посидели с карандашом и блокнотом в руках, вместо того, чтобы программы программировать, мы могли эти риски увидеть и предотвратить их заранее.

Stage 3


Третья версия была спровоцирована уже вызовами внешнего мира. Как я говорил, наши продуктовые инициативы связаны с быстрой проверкой идей. Однажды продуктовая команда пришла и сказала: «Мы хотим еще быстрее проверять идеи». Зашла речь о MVP (Minimum Valuable Product). Суть в том, что вы создаете маленькую продуктовую функциональность для проверки бизнес-идеи и выкатываете ее очень быстро. Если раньше вы проверяли идеи последовательно в своем большом продукте, то теперь задача много идей проверять одновременно. То есть у нас есть два приложения, а нужно десять или, скорее всего, 20.
Напомню, что у нас Single Page Application. Соответственно, это байты на сервере, которые скачивает клиент. Для версионирования артефактов мы использовали HTTP param версий. Так же иногда при синхронизации через rsync на сервере оставались старые версии артефактов.
И это создавало чудеснейшие баги. Человек выкачивал половину Wrike, например. А часть приложения выкачивалась по требованию. Через две недели он выкачивает вторую половину, но она другой версии. Отладить это было архисложно, воспроизвести вообще нереально.

Мы решили перепаковать все совершенно по-другому. С помощью RPM, у которого есть встроенная система резолвинга зависимостей и проверка хэш-сумм. То есть в любой момент времени можете на множестве своих коробок проверить, что у вас никто не модицифировал ваши файлы, они равны исходным. Также в современных дистрибутивах есть многопоточный инкрементальный индексатор репозитория. Есть уже готовые паттерны и решения для распространения артефактов по миру, их индексации и всего остального. И подписывание GPG-ключами.

Как решить эту проблему с двумя частями Wrike, которые могут докачиваться людьми в произвольный момент времени? Single Page Application наш устроен так, что человек может месяц работать на старой версии, пока он не перегрузит вкладку или не откроет новую. Как мы это пофиксили? Мы стали ссылаться на все артефакты по пермалинкам. И версия там прямо в ссылку зашита. То есть вы либо скачиваете правильный артефакт, либо не скачиваете никакой.
Помимо этого мы решили хранить 50 последних версий Wrike на файловой системе. А текущий выставлять при помощи симлинка. Когда люди одновременно выкачивают ассет, мы прямо на ходу можем менять версию. Соответственно, для нас откат версий — это перемещение симлинка. Мы за секунды можем везде откатиться на нужную нам версию. Это делается быстро, просто. Байты уже везде лежат нужные.

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

Достигли этого благодаря тому, что храним много версий и при помощи симлинка манипулируем текущей реализацией. Естественно, RPM этого не умеет из коробки. Мы написали кастомный модуль Ansible на Python. Тем, кто не писал модули для Ansible, очень рекомендую попробовать, у него простой, компактный API. Вы за один вечер, прочитав официальную доку, научитесь и писать, и отлаживать их.

Немного про Docker. Его роль в 2015 году у нас была крайне скромна. У нас был огромный многострадальный сборщик, который порождал процессы. Иногда о них забывал. Они аллоцировали гигабайты памяти. Мы это решили при помощи изоляции в Docker, потому что были очень сжаты по срокам — не могли починить первопричину. В итоге при терминировании контейнера у нас ресурсы высвобождались, и все было классно.

Stage 3: уроки

В чем особенность нашей третьей версии? Помните, я говорил про блокнот и карандаш? У нас была неделя. Мы поняли, что за неделю целиком мы это вообще не запрограммируем никак. Даже если не будем спать и есть. Мы треть времени потратили на то, чтобы продумать вот это инженерное решение с симлинками, с RPMами, с атомарностью. Мы договорились с сисопами — теми, кто эксплуатирует наше решение в продакшне. Договорились с разработчиками. Мы прямо написали инженерную спеку, как это будет собираться, как это будет выкладываться.
Позже наши коллеги написали библиотеку на Dart, абстрактный сборщик проектов. Сейчас эта библиотека собирает 60 проектов примерно. Мы оказались правы в этом решении, потому что спеку мы потом почти не трогали, а реализацию переписали. Основная мысль в том, что нужно продумывать спеку и контракт с другими отделами, если вы что-то меняете сильно. А реализацией на первых порах можно пожертвовать.

Важный момент — это не бояться отстаивать свою точку зрения, комбинировать те решения, которые дают вам нужные для вас продуктовые свойства. То есть сейчас модно: нет докера — не пацан. А вот RPM совсем какая-то архаичная старая система, казалось бы, никому ненужная. На самом деле, там есть очень крутые фишки, которых нету у многих других систем. Я всячески призываю думать своей головой и всегда выбирать то, что нужно вам, а не идти на поводу у какого-то мнения.

Stage 4


Помните, я говорил про Dart и про библиотеки? Мы создали десятки приложений. По сути, это потребители библиотечного кода. Это спровоцировало волну создания библиотек. То есть у нас есть, допустим, 10 приложений фронтенда. Всем нужен слой работы с данными. Появилась библиотека DAL (Data access layer), абстракция для работы с данными, появилась библиотека компонентов. И огромное количество других библиотек. Десятки. Появилась задача выполнять композицию этого кода.

То есть, например, выкатывает наш коллега фичу. Он правит три библиотеки и код потребителя библиотеки, какое-то приложение, куда она встраивается. Что происходит? Он правит код первой библиотеки, проходит процедуру мерж-реквеста, уходит во вторую, правит ее, проходит процедуру мерж-реквеста, уходит в третью — то же самое. Потом правит фичу, правя референсы на эти библиотеки. И еще раз проходит процедуру мерж-реквеста. Это очень долго, избыточно, и очень много коммуникаций.

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

Также у нас MVP. Вы проверяете бизнес-идею и дальше принимаете решение. Например, мы достигли нужного продуктового свойства. Сейчас можно притормозить. Мы не знаем, что делать с этой продуктовой частью. Может быть, она нам понадобится через год, а может быть, не понадобится никогда. Мы выбирали между монорепозиторием и мультирепозиторием. И решили сделать следующее. Мы выделили каждую библиотеку или приложение в отдельный репозиторий со своим readme, со своим change log, своей версионностью и своими тестами.
Благодаря этому у нас приложения ссылаются на конкретные фиксированные версии библиотек. Если какой-то компонент холдится, естественно, он зависит от старых версий библиотек, потом через год его можно подпачить. Например, секьюритификс можно наложить, не обновляясь до новых версий библиотек. Если он решил продолжить развиваться через год, он просто бампается до новых версий библиотек.

А если бы мы оставили в монорепозитории и сказали бы «Чуваки, мы хотим гарантировать совместимость от начала и до конца», то мы бы с собой тащили огромный багаж приложений, которые либо приостонавились в своем развитии, либо умерли. И нужно было бы гарантировать совместимость. Это было бы очень дорого. На самом деле, помощи Git submodule и subtree можно можно сделать из мультирепозитория монорепозиторий, если того требует задача.
Чтобы воплотить наше решение, нужен был умный менеджмент зависимостей. Мы написали его на Python, с персистентным слоем на Postgre, с UI на Angular.

То есть разработчик декларировал просто: я пишу фичу, в которой правлю три библиотеки и два потребителя. И наше приложение позволяло сделать следующее. Вкатить это все атомарной единицей в RC. И робот сам проходил по репозиториям, все мержил. Сам менял референсы. И в случае отката фичи сам это все возвращал в исходное положение. Если тестировщик подтверждал, что да, годится, он давал зеленый свет, и фича уезжала в RC. Вот. Мы сейчас хотим, чтобы на основе тестов зеленых все это происходило, вообще без участия людей.
В какой-то момент мы выросли из возможности Wrike как issue tracker. Нам понадобился свой UI, ребята запилили его на Angular.

urbyo-goubxlod-dldr1_yj75iu.png

Но это еще не конец. Wrike растет: и команда, и продукт, и перед нами возникают новые вызовы. В браузере Wrike представляет собой этакий монолит, и нам с точки зрения DevOps это совсем не в радость. И следующий большой шаг для нас — это произвести декомпозицию нашего решения и сделать много приложений. Это даст нам буст в интеграции, и мы еще больше фич будем выкатывать. Также мы сильно прокачали автоматизированное тестирование. Свою селениумную ферму отправили в Google Cloud, что позволяет прогонять всю регрессию за 20 минут и 1$.

Общие выводы


Я считаю, что критически важно нанимать правильных людей, вкладываться в инструменты и работать с фидбеком разработчиков и бизнеса.
Многие решения о том, как нам на определенном этапе развивать систему — эволюционно или революционно — были приняты только благодаря обратной связи. Отделу DevOps в продуктовой компании везет, его потребители — это его коллеги, то есть инженеры, с которыми он должен легко коммуницировать, которых он должен уметь слышать и слушать.
И последнее — не нужно сразу же, под каждую задачу строить космолет. Решайте проблемы итерационно. Я всячески рекомендую и пропагандирую не придумывать очень долгие, долгоживущие и дорогие в реализации решения. То есть все знаем про внутренние сервисы больших компаний или про какие-то опенсорсные проекты классные. И на старте можем сказать: «Я знаю, как решить эту задачу, только нужно написать вот это, вот это и вот это». И на это уходит год или полтора. То, что мы не копировали какие-то чужие решения, а делали инструменты, отвечающие актуальным нуждам, позволяло нам сохранять темп работы и экономить деньги.

© Habrahabr.ru