Есть ли жизнь после кода?

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

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

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


    С чего начинается деплоймент


    В самом общем приближении после написания кода существует одна единственная задача — отправить написанное куда-то, где оно будет запускаться. То есть, грубо говоря, залить его по ftp или по ssh.
    N.B. Если вы заливаете обновление сайта про котиков по ftp, это тоже деплоймент.

    Сегодня у нас нет котиков, но есть линукс, консоль и мы заливаем всё по ssh. Значит, процесс деплоймента будет таким:

    1. Запаковать проект в архив
    2. Отправить его по ssh на удалённую машину
    3. Распаковать его в той папке, на которую натравлен веб-сервер.


    Есть нюанс


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

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

    Есть очень простое решение этих проблем — выкладка происходит каждый раз в новую папку, на которую каждый раз переключается одноимённый симлинк:
    1. На удалённой машине в домашней папке того юзера, от имени которого мы работаем по ssh, создаётся директория, например, versions. При каждой сборке мы создаём поддиректорию в versions, и распаковываем проект туда.
    2. После этого мы создаём симлинк на эту папку по имени, допустим, current. Веб-сервер наш смотрит на этот симлинк. Таким образом, переключение проекта это просто переключение симлинка.

    ln -nsf /home/project/versions/{0} /home/project/current

    Поддиректории versions удобно называть по порядковому номеру процедуры выкладки.

    И ещё один нюанс


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

    Ну и если во время выкладки на каком-то из шагов произошла ошибка, выкладку нужно остановить.

    Получается, что для того, чтобы выложить проект на удалённую машину, минимально необходимые шаги будут такими:
    1. Накатить боевые конфиги
    2. Запаковать проект
    3. Залить проект на удалённую машину
    4. Распаковать в новую папку
    5. Переключить симлинк

    Если на любом из шагов произошла ошибка, остановить.

    Автоматизация


    Понятно, что выполнять эти задачи вручную не стоит, нам понадобиться какая-то автоматизация, что-то наподобие баш-скрипта, который мы будем запускать для выкладки.
    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 тоже куда-то выкладывается, и для него нужен свой отдельный фабрик-скрипт.

    И про юнит-тесты


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

    Итак, список действий для выкладки кода увеличивается.

    1. Задать ветку для выкладки.
    2. Задать номер сборки для этой ветки
    3. Подтянуть изменения этой ветки из общего репозитория
    4. Прогнать юнит-тесты
    5. Накатить конфиги для этой ветки
    6. Запаковать проект
    7. Залить на удалённую машину, соответствующую этой ветке
    8. Распаковать в новую папку
    9. Переключить симлинк

    Если на любом из этапов случилась ошибка, остановить.

    Ну и раз мы всё делаем по-уму, то
    1. Разработчики не должны иметь ssh-доступ на продакшн-машины
    2. Разработчики не должны иметь доступ к продакшн-конфигам
    3. Ветка deployment должна выкатываться сразу после коммита, чтобы тестовое приложение всегда было доступным для тестирования
    4. Если во время выкладки произошла ошибка, разработчикам в почту должно упасть письмо.

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

    Ещё один инструмент


    Инструмент, который нам поможет, называется Continuous Integration Server, и занимается, по сути, тем, что по команде, по расписанию или по внешнему событию вынимает свежий код указанного репозитория, а дальше выполняет перечисленный набор команд. Мы в качестве инструмента для CI используем Jenkins.

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

    Отдельные скрипты для сборки


    Если для деплоймента мы взяли фабрик, то для задач сборки проекта мы используем Phing. Во-первых, он удобен, во-вторых, дженкинс хорошо с ним интегрируется, и, в трётьих, так сложилось исторически.

    Вот примеры задач, которые решает наш финг:
    phing build — сборка проекта
    phing gerrit-phpcs — проверка на кодстандарты
    phing copy-production-configs — достать конфиги продакшн-машин из закрытого репозитория
    phing write-version — сохранить в файл таймстемп текущей сборки.

    А вот полный путь нашего кода из ветки development на тестовый сервер:

    Сначала работает финг. Все действия происходят в локальной папке дженкинса:
    1. Делаем checkout ветки
    2. Смотрим изменения между последней успешной сборкой и этой (sha последней успешной сборки, равно как и многую другую полезную информацию, можно получить через api дженкинса и переменные дженкинса).
    3. Если изменения касаются только js-приложения, юнит-тесты не запускаем
    4. Если нет, создаём пустую тестовую базу с уникальным именем. Это позволяет запускать сборки параллельно.
    5. Накатываем миграции. Структуру базы и их изменения мы держим в виде миграций
    6. Прогоняем тесты
    7. Удаляем базу
    8. Накатываем конфиги
    9. Пишем таймпстемп текущей сборки. Это нужно для инвалидации кэша.
    10. Запускаем фабрик

    Фабрик отвечает за отправку кода на конечные машины. Последовательность действий такая:
    1. Упаковываем проект
    2. Раскладываем по машинам
    3. Запускаем миграции
    4. Переключаем симлинк
    5. Обновляем кронтаб
    6. Удаляем старые версии кроме последних трёх


    Кодревью


    В заключение хотелось бы рассказать о решениях для инспекций кода.

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

    У себя в разработке мы используем Gerrit.

    Самый главный плюс геррита в том, что он тесно интегрируется с git, становясь, по сути, git-сервером. После этого через веб-интерфейс геррита можно не только выполнять собственно ревью, но и управлять правами на гит. Таким образом, рядовые разработчики не имеют права на прямой пуш в девелопмент или мастер, они могут только отправлять чейнджсеты на ревью, запуская скрипт git review
    N.B. С технической точки зрения, отправляя ченджсет на ревью, разработчик всё-таки делает пуш, но в специально отведённую для этого ветку. После чего, если ревьюеру всё нравится, при помощи кнопки submit специальная ветка вмёрживается в ту, которая была указана при команде git review.

    Перед кодревью имеет смысл прогнать юнит-тесты. Для этой цели можно использовать тот же дженкинс, который хорошо интегрируется с Герритом: по событию "обновление ветки с именем, соответствующим шаблону" прогоняет тесты и ставит свою отметку в кодревью. С недавнего времени мы дополнительно к юнит-тестам автоматически проверяем код на кодстандарты.

    Заключение


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

    Дополнительная литература


    Jenkins - Wikipedia
    Jenkins + Python - хорошая статья на хабре
    Gerrit - Wikipedia
    Gerrit в Баду - коллеги запилили подробную статью по установке и использованию
    Fabric - документация на сайте
    Прикладные рецепты Fabric
    Phing - официальная документация
    Краткое введение в Phing на хабре
    Continuous Integration - Wikipedia
    Aori
    Company
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 7

      +1
      Жирный плюс за список литературы и конечно же за кадр из замечательного фильма.
        +2
        Поздравляю, вы переизобрели Capistrano (на хабре о ней тоже писали)
          0
          Только в capistrano не
          [Поддиректории versions удобно называть] по порядковому номеру процедуры выкладки
          а в куда более логичном варианте yyyyMMddhhmmss. Другой вполне логичный вариант используется в DNS SOA resource record (yyyyMMddNN, где NN — порядковый номер обновления в эту дату).
          +3
          смешались в кучу кони, код ревью, CI и деплой и каждая из этих тем достойна отдельного упоминания. Начнем с код ревью — для этих целей мы используем Crucible потому что он отлично интегрируется как с svn так и с git, о его возможностях можно говорить очень долго. Код ревью проводим на постоянной основе и к релизам это никак не привязано.
          Деплой в большинсте случаев хоть и запускается на jenkins никак с CI не завязан. Тот же msbuild может отдеплоить проект на удаленный сервер прямо из окна Visual Studio в разных конфигурациях и настройках, а для jenkins есть Publish Over SSH Plugin с выполнением набора команд на удаленном сервере, что сводит написание деплой-скриптов до минимума. Для мобильных разработчиков есть плагины для аплоада на testflight и hockeyapp. К jenkins можно прикрутить большинство современных инструментов статического анализа кода — существую плагины для cppcheck, pdm, clang scan build и им подобным, которые в красивой форме покажут текущее состояние на проекте. С помощью параметризированной сборки легко решается задача выкатывания билда определенной или последней ревизии для QA инженера, иногда это спасает уйму времени.
          Хочу добавить что за все время общения с jenkins мне пришлось писать руками скрипт всего один раз — нужна была параметризованная сборка проекта на actionscript и adobe flash
            0
            Нельзя делать билд (окромя локального дебага) на Dev машине, да еще и из под IDE. Если мы все-таки говорим о CI, то билд — это не просто компиляция и линковка. Девелоперская IDE может быть настроена не так, как у коллеги за соседним столом, да и сам environment может отличатся, в конечном счете есть риск получить разные бинарники из одного и того-же кода. Я уже молчу про «а на моей машине все работает!». Только выделенный CI сервер и адекватный билд тул.
            0
            Давно заметил, что на всех вменяемых проектах используется связка: Git, Jenkins, JIRA, Gerrit, Mailing Lists. Хотя можно заменить Gerrit через review в Mailing Lists. Еще можно посоветовать использовать cgit в качестве веб-интерфейса для git.
              0
              А как вы поступаете с БД/миграциями?
              Выкладка кода регулярно сопровождается изменениями в БД. И переключение между старой версией кода и новой может занимать как раз время, необходимое для наката миграций.

              Only users with full accounts can post comments. Log in, please.