Что происходит с кодом после того, как он написан? Во многих областях разработки ПО его жизнь только начинается. Например, в разработке для веба, приложение исполняется где-то на сервере. Значит, после написания кода встаёт задача интегрировать его в приложение и доставить на конечную машину. Именно этот процесс мы сегодня обсудим.
Данный текст предназначен широкому кругу разработчиков и рассчитан на тех, кто мало знаком с процессом выкладки кода. Так же этот текст может быть полезен тем, кто строит систему деплоймента и находится в поиске идей.
Статья написана на основе материалов внутреннего семинара компании Аори, и рассказывает о принципах деплоймента на примере процесса, построенного у нас.
С чего начинается деплоймент
В самом общем приближении после написания кода существует одна единственная задача — отправить написанное куда-то, где оно будет запускаться. То есть, грубо говоря, залить его по ftp или по ssh.
N.B. Если вы заливаете обновление сайта про котиков по ftp, это тоже деплоймент.
Сегодня у нас нет котиков, но есть линукс, консоль и мы заливаем всё по ssh. Значит, процесс деплоймента будет таким:
- Запаковать проект в архив
- Отправить его по ssh на удалённую машину
- Распаковать его в той папке, на которую натравлен веб-сервер.
Есть нюанс
Когда мы распаковываем проект на удалённом сервере, мы фактически перезаписываем содержимое папки новым. По-хорошему, следует предварительно очищать папку. Однако тогда сервис во время выкладки будет недоступен.
К тому же, что мы будем делать, если выкатим проект с ошибкой? Нам придётся откатывать локальную версию, заново производить все манипуляции с заливкой архива, и высока вероятность напортачить в спешке.
Есть очень простое решение этих проблем — выкладка происходит каждый раз в новую папку, на которую каждый раз переключается одноимённый симлинк:
- На удалённой машине в домашней папке того юзера, от имени которого мы работаем по ssh, создаётся директория, например, versions. При каждой сборке мы создаём поддиректорию в versions, и распаковываем проект туда.
- После этого мы создаём симлинк на эту папку по имени, допустим, current. Веб-сервер наш смотрит на этот симлинк. Таким образом, переключение проекта это просто переключение симлинка.
ln -nsf /home/project/versions/{0} /home/project/current
Поддиректории versions удобно называть по порядковому номеру процедуры выкладки.
И ещё один нюанс
Скорее всего, конфиги для удалённой машины нужны свои. Это значит, перед упаковкой проекта необходимо накатить боевые конфиги.
Ну и если во время выкладки на каком-то из шагов произошла ошибка, выкладку нужно остановить.
Получается, что для того, чтобы выложить проект на удалённую машину, минимально необходимые шаги будут такими:
- Накатить боевые конфиги
- Запаковать проект
- Залить проект на удалённую машину
- Распаковать в новую папку
- Переключить симлинк
Если на любом из шагов произошла ошибка, остановить.
Автоматизация
Понятно, что выполнять эти задачи вручную не стоит, нам понадобиться какая-то автоматизация, что-то наподобие баш-скрипта, который мы будем запускать для выкладки.
N.B. В принципе, подойдёт и bash-скрипт.
В Аори для конечного деплоя мы используем Fabric. Это библиотека на питоне, позволяющая очень простыми средствами решить перечисленные выше задачи.
Из приятностей хотелось бы отметить так называемые роли — именованные группы машин, которые позволяют выполнять некоторые пункты деплоя не на всех машинах, а только на некоторых. Скажем, у нас в боевом окружении три роли: статика, веб-приложение и скрипты. Соответственно, прогрев кэша на машинах со статикой не нужен, а обновить кронтаб нужно только на скриптовых машинах.
Вот кусочек нашего фабрик-скрипта для примера:
@parallel
@roles('web', 'script', 'static')
def switch(id):
run('ln -nsf /home/project/versions/{0} /home/project/current'.format(id))
@roles('script')
def crontab(id):
run('crontab /home/project/versions/{0}/build/crontab'.format(id))
@parallel
@roles('web', 'script', 'static')
def clear(id):
with cd('/home/project/versions'):
run("ls -1 | grep -E '^[0-9]+$' | sort -n | head -n -3 | xargs -rt rm -rf")
def deploy(id):
execute(prepare)
execute(upload)
execute(upload_static, id)
execute(build, id)
execute(migrate, id)
execute(switch, id)
execute(crontab, id)
execute(clear, id)
Не забыть про git
Если вы используете ветки, у вас как минимум есть master для готового к отправке на продакшн кода, и development, в которую коммитится всё подряд, тестируется и подливается в master по мере стабилизации.
В этом случае development тоже куда-то выкладывается, и для него нужен свой отдельный фабрик-скрипт.
И про юнит-тесты
Перед отправкой кода куда-либо нужно прогнать тесты, поскольку бессмысленно выкладывать заведомо битый код.
Итак, список действий для выкладки кода увеличивается.
- Задать ветку для выкладки.
- Задать номер сборки для этой ветки
- Подтянуть изменения этой ветки из общего репозитория
- Прогнать юнит-тесты
- Накатить конфиги для этой ветки
- Запаковать проект
- Залить на удалённую машину, соответствующую этой ветке
- Распаковать в новую папку
- Переключить симлинк
Если на любом из этапов случилась ошибка, остановить.
Ну и раз мы всё делаем по-уму, то
- Разработчики не должны иметь ssh-доступ на продакшн-машины
- Разработчики не должны иметь доступ к продакшн-конфигам
- Ветка deployment должна выкатываться сразу после коммита, чтобы тестовое приложение всегда было доступным для тестирования
- Если во время выкладки произошла ошибка, разработчикам в почту должно упасть письмо.
Похоже, нам требуется ещё один инструмент, потому что выполнять все эти действия руками, даже имея набор фабрик-скриптов, немыслимо.
Ещё один инструмент
Инструмент, который нам поможет, называется Continuous Integration Server, и занимается, по сути, тем, что по команде, по расписанию или по внешнему событию вынимает свежий код указанного репозитория, а дальше выполняет перечисленный набор команд. Мы в качестве инструмента для CI используем Jenkins.
Важно понимать, что сам сервер непрерывной интеграции из перечисленных выше задач умеет делать только инкремент счётчика сборок и отправку письма в случае неудачи. Для запуска юнит-тестов, наката конфигов и прочих полезных дел нужен ещё один инструмент, выполняющий расширенные функции bash-скрипта.
Отдельные скрипты для сборки
Если для деплоймента мы взяли фабрик, то для задач сборки проекта мы используем Phing. Во-первых, он удобен, во-вторых, дженкинс хорошо с ним интегрируется, и, в трётьих, так сложилось исторически.
Вот примеры задач, которые решает наш финг:
phing build
— сборка проекта
phing gerrit-phpcs
— проверка на кодстандарты
phing copy-production-configs
— достать конфиги продакшн-машин из закрытого репозитория
phing write-version
— сохранить в файл таймстемп текущей сборки.
А вот полный путь нашего кода из ветки development на тестовый сервер:
Сначала работает финг. Все действия происходят в локальной папке дженкинса:
- Делаем checkout ветки
- Смотрим изменения между последней успешной сборкой и этой (sha последней успешной сборки, равно как и многую другую полезную информацию, можно получить через api дженкинса и переменные дженкинса).
- Если изменения касаются только js-приложения, юнит-тесты не запускаем
- Если нет, создаём пустую тестовую базу с уникальным именем. Это позволяет запускать сборки параллельно.
- Накатываем миграции. Структуру базы и их изменения мы держим в виде миграций
- Прогоняем тесты
- Удаляем базу
- Накатываем конфиги
- Пишем таймпстемп текущей сборки. Это нужно для инвалидации кэша.
- Запускаем фабрик
Фабрик отвечает за отправку кода на конечные машины. Последовательность действий такая:
- Упаковываем проект
- Раскладываем по машинам
- Запускаем миграции
- Переключаем симлинк
- Обновляем кронтаб
- Удаляем старые версии кроме последних трёх
Кодревью
В заключение хотелось бы рассказать о решениях для инспекций кода.
В самом простейшем виде ревью можно осуществлять через плечо вашего сотрудника и давать комментарии устно. Также можно смотреть, что разработчик закоммитил, и писать комментарии в тикет. В принципе, в каких-то случаях этого достаточно, однако существуют специализированные инструменты, позволяющие упростить процесс инспекции и совместить его с автоматическими проверками.
У себя в разработке мы используем Gerrit.
Самый главный плюс геррита в том, что он тесно интегрируется с git, становясь, по сути, git-сервером. После этого через веб-интерфейс геррита можно не только выполнять собственно ревью, но и управлять правами на гит. Таким образом, рядовые разработчики не имеют права на прямой пуш в девелопмент или мастер, они могут только отправлять чейнджсеты на ревью, запуская скрипт git review <branch>
N.B. С технической точки зрения, отправляя ченджсет на ревью, разработчик всё-таки делает пуш, но в специально отведённую для этого ветку. После чего, если ревьюеру всё нравится, при помощи кнопки submit специальная ветка вмёрживается в ту, которая была указана при команде git review.
Перед кодревью имеет смысл прогнать юнит-тесты. Для этой цели можно использовать тот же дженкинс, который хорошо интегрируется с Герритом: по событию «обновление ветки с именем, соответствующим шаблону» прогоняет тесты и ставит свою отметку в кодревью. С недавнего времени мы дополнительно к юнит-тестам автоматически проверяем код на кодстандарты.
Заключение
Упомянутые инструменты хорошо зарекомендовали себя в работе, однако для ваших задач может лучше подходить что-то другое. Например, github предоставляет реализацию полного набора инструментов для ревью и непрерывной интеграции. С другой стороны, принципы, описанные в тексте, более-менее универсальны и, насколько известно автору, применимы как для одной конечной машины, так и для сотен машин с параметризованными ролями.
Дополнительная литература
Jenkins — Wikipedia
Jenkins + Python — хорошая статья на хабре
Gerrit — Wikipedia
Gerrit в Баду — коллеги запилили подробную статью по установке и использованию
Fabric — документация на сайте
Прикладные рецепты Fabric
Phing — официальная документация
Краткое введение в Phing на хабре
Continuous Integration — Wikipedia
Автор: lukyanov