Добрый день! Меня зовут Кирилл, и я DevOps-инженер. За свою карьеру мне не раз приходилось внедрять DevOps-практики как в существующие, так и в новые команды, поэтому хочу поделиться своим опытом и мыслями по поводу стратегий ветвления. Существует множество различных типов рабочих процессов, и чтобы разобраться что к чему, предлагаю рассмотреть пример создания нового программного продукта.

Часть 1: Рабочий процесс

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

А далее как обычно бывает: всплывают первые запросы от пользователей на добавление новых фич/устранение багов и т.д., разработка кипит. Для того чтобы ускорить выход новых версий, принимается решение расширить команду DevOps’ом, и для решения насущных проблем DevOps предлагает построить CI/CD-конвейер (pipeline). И вот пришло время рассмотреть, как же CI/CD-конвейер ляжет на наш рабочий процесс, где у нас сейчас только мастер.  

Для примера мы взяли простой конвейер с одним окружением. И вроде всё выглядит хорошо: разработчик запушил код в мастер, запустился конвейер, код прошёл ряд проверок, собрался и развернулся в окружении. 

А теперь рассмотрим ситуацию, когда конвейер прервался на тестах. 

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

На данной картинке (которая может показаться слишком преувеличенным примером, однако такое бывает), мы видим, что в первом коммите, который ранее попал на окружение, каких-либо проблем нет. На втором коммите в мастер конвейер прервался. И вот тут начинается самое интересное. Понятно, что запушенный код нерабочий и надо его исправлять, чем и занялся разработчик. Но что, если у нас не один разработчик, а команда, где каждый усердно трудится над своей задачей? Второй разработчик ответственно начал добавлять новые улучшения в продукт, но в их основе лежит второй коммит. Что же будет дальше с этими изменениями? Сколько времени уйдёт у первого разработчика на исправление? Насколько сильными будут изменения в новом коммите? Что в это время делать второму разработчику? Что делать с уже написанными вторым разработчиком фичами? В общем, слишком много вопросов, а на выходе получаем:  

  • уменьшение производительности, 

  • впустую потраченное время, 

  • много головной боли. 

Для решения насущных проблем можно прибегнуть к изменению рабочего процесса.  

Первым делом добавим небезызвестные feature-ветки. 

В отдельных feature-ветках каждый разработчик может без стресса заниматься своей задачей. При этом мы блокируем коммиты напрямую в мастер (или договариваемся так не делать), и впоследствии все новые фичи добавляются в мастер через “merge request”.   

И в очередной раз проиграем проблему: в feature-ветке обнаружен баг. 

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

Но что, если на окружение попал новый мастер, и спустя какое-то время обнаружен баг (не углядели, всякое бывает).  

Соответственно, это уже критическая ситуация: клиент не доволен, бизнес не доволен. Нужно срочно исправлять! Логичным решением будет откатиться. Но куда? За это время мастер продолжал пополняться новыми коммитами. Даже если быстро найти коммит, в котором допущена ошибка, и откатить состояние мастера, то что делать с новыми фичами, которые попали в мастер после злосчастного коммита? Опять появляется много вопросов.  

Что ж, давайте не будем поддаваться панике, и попробуем ещё раз изменить наш рабочий процесс, добавив теги.  

Теперь, когда мастер пополняется изменениями из feature-веток, мы будем помечать определённое состояние мастера тегом.  

Но вот в очередной раз пропущен баг в теге v2.0.0, который уже на окружении.  

Как решить проблему теперь?  

Правильно, мы можем повторно развернуть версию v1.0.0, считая её заведомо рабочей.  

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

  • сэкономили время и, как следствие, деньги, 

  • восстановили работоспособность окружения,  

  • предотвратили хаос,  

  • локализовали проблему в версии v2.0.0. 

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

Для примера возьмём и рассмотрим давно всем известный Git Flow:  

Сравним его с нашим последним примером и увидим, что у нас нет develop-ветки, а ещё мы не использовали hotfixes-ветки. Следовательно, мы не можем сказать, что использовали именно Git Flow. Однако мы немного изменим наш пример, добавив develop- и release-ветки.  

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

Что ж, на Git Flow жизнь не заканчивается, ведь есть не менее известный GitHub Flow.  

И первое, что мы можем заметить, так это то, что он выглядит в разы проще, чем Git Flow. И если сравнить с нашим примером, то мы можем заметить, что здесь не используются теги. Но, как мы можем вспомнить, мы ведь добавляли их не просто так, а с целью решить определённые проблемы, поэтому и здесь мы не можем сказать, что мы использовали конкретно GitHub Flow.  

Собственно, сравнив наш рабочий процесс из примера с Git Flow и GitHub Flow, хотелось бы подчеркнуть следующее: безусловно, существование паттернов для построения рабочих процессов — это огромный плюс, так как мы можем взять существующий паттерн и начать работать по нему, и в определённых случаях какой-либо определённый паттерн идеально впишется в процесс разработки. Однако это работает и в другую сторону: какой-либо существующий паттерн может вставить нам палки в колеса и усложнить процесс разработки. 

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

Часть 2: Участь DevOps'а

В первой части мы рассмотрели, как выглядит рабочий процесс, а теперь посмотрим, почему для DevOps-инженера так важен корректно настроенный рабочий процесс. Для этого вернёмся к последнему примеру, а именно к построению того самого конвейера для реализации процесса CI/CD. 

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

Собственно, построение конвейера можно изобразить вот такой простой картинкой: 

Ну или одним вопросом: «как связать между собой код в репозитории и окружение?» 

Следовательно, нужно понимать, какой именно код должен попасть в окружение, а какой нет. К примеру, если в ответ на вопрос: «Какой рабочий процесс используется?» мы услышим: «GitHub Flow», то автоматически мы будем искать нужный код в master-ветке. И ровно наоборот, если не построен никакой рабочий процесс и куски рабочего кода разбросаны по всему репозиторию, то сначала нужно разобраться с рабочим процессом, а лишь потом начинать строить конвейер. Иначе рано или поздно на окружение попадёт то, что возможно не должно там быть, и как следствие, пользователь останется без сервиса/услуги.  

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

Но для наглядности далее рассмотрим два основных этапа в CI/CD- конвейерах: build и deployment/delivery. И начнем мы, пожалуй, с первого — build.  

Build — процесс, конечным результатом которого является артефакт.   

Для простоты введём следующее условие: артефакты должны версионироваться и храниться в каком-либо хранилище для последующего извлечения. Что ж, если у нас нет рабочего процесса, то первый (возможно, глупый, но важный) вопрос — как мы будем именовать артефакты при хранении. Опять же, вернёмся к нашему примеру с рабочим процессом, где мы использовали теги.

Так вот, у нас есть отличная возможность взять имя тега для артефакта и опубликовать его. Но что, если у нас нет никакого рабочего процесса? Что ж, тут уже сложнее. Конечно, мы можем взять хеш коммита, или дату, или придумать что-либо ещё для идентификации артефакта. Но очень скоро разобраться в этом будет практически невозможно.

И вот пример из реальной жизни. 

Представьте ситуацию, когда вы хотите загрузить новую версию Ubuntu, и вместо такого списка версий:  

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

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

Конечно, на этом примеры не заканчиваются, но думаю, что теперь мы можем перейти к delivery/deployment.  

Delivery — процесс, в рамках которого развёртка приложения на окружении происходит вручную.

Deployment — процесс, в рамках которого развёртка приложения происходит автоматически.  

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

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

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

Сейчас мы рассмотрели лишь две основных стадии при построении конвейера, но однозначно можно сказать, что беспорядок в рабочем процессе будет влиять на каждый этап реализации процессов CI/CD. И под беспорядком имеется в виду не только отсутствие рабочего процесса, но и его избыточность. 

Заключение

В конце хотелось бы добавить, что основная мысль, которую мне хотелось донести этой статьёй, — это то, что есть книги, теория и так далее, а есть здравый смысл. Конечно, есть множество различных практик и примеров, которые успешно работают в том или ином продукте, но это не значит, что эти практики сработают в вашей команде. Поэтому если вас мучает вопрос: «Почему у нас это не работает?» или «Что нам нужно сделать, чтоб у нас это заработало?», то попробуйте задаться вопросом: «А надо ли оно нам?»