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


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


Написал утилиту некий Антон Медведев, у него кстати довольно приятный блог есть. Спасибо тебе, Антон :)


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


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


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


Структура папок релизов


Весь проект разделяется на три папки: current, releases и shared. В общем, это довольно распространённый вид для подобных инструментов, и он действительно удобен. Скажем, в одном из моих проектов на Laravel эта структура выглядит вот так:



current — ссылка на папку последнего успешно собранного релиза, т.е. текущее приложение.
releases — все релизы, которые были собраны. По умолчанию сохраняется только последние три релиза, и это значение можно легко поменять.
shared — папка, в которой находятся все "общие" файлы, которые относятся ко всем релизам одновременно и не должны создаваться каждый раз заново. К примеру — файл .env, загруженые пользователями файлы, и так далее.


Пример скрипта деплоя Laravel приложения


Я люблю лично зайти на свой сервер, запустить скрипт деплоя и наблюдать за процессом его работы. Просто, мне так намного спокойнее жить, так как я всегда могу предпринять какие-то срочные меры, если при деплое что-то пойдёт не так. А так, как я знаю, люди обычно запускают подобный скрипт со своей локальной машины, которая подключается по SSH к серверу и производит деплой. Если это надо сделать сразу на нескольких машинах — то такой подход конечно будет удобнее. К слову, Deployer позволяет выполнять деплой сразу на нескольких машинах в том числе.


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


В одном из моих проектов на Laravel 5 скрипт деплоя deploy.php выглядит следующим образом:


<?php

// Подключим основные рецепты из Deployer'а
require 'recipe/common.php';
require 'recipe/laravel.php';

// Укажем основные параметры деплоя
localServer('local', 'localhost')
    ->user('{ваш-пользователь}')
    ->env('deploy_path', '/path/to/project/dir')
    ->stage('local')
;

set('repository', '{ваш-git-репозиторий.git}');
env('branch', '{ветка-для-релизов}');

env('git_cache', true);

// Общие папки для вашего проекта, которые будут прозрачно доступны всем релизам
// Они не будут создаваться заново при новом релизе, вместо них будут созданы
// ссылки на их одноимённые папки в папке shared
set('shared_dirs', [
    'storage/app',
    'storage/framework/cache',
    'storage/framework/sessions',
    'storage/framework/views',
    'storage/logs',
    'public/uploads',
    'node_modules',
]);

// Общие файлы. Принцип точно такой же, как с папками
// В случае с Laravel нам необходимо сделать "общим" лишь один
// файл - .env
set('shared_files', ['.env']);

// Папки, в которые приложение должно иметь возможность
// писать данные. В нашем случае - это три директории
set('writable_dirs', ['storage', 'vendor', 'public/uploads' ]);

set('http_user', '{ваш-пользователь}');
set('composer_command', '/usr/local/bin/composer'); // Путь к расположение Composer'а

// Задача для деплоя. Установить NPM компоненты
task('deploy:install-npm', function() {
    run('cd {{release_path}} && npm i');
});

// Ещё одна задача: скомпилировать все фронтенд файлы, в моём случае
// это делается через Grunt.js
task('deploy:compile-assets', function() {
    run('cd {{release_path}} && grunt deploy-production');
});

// Выполнить миграции
task('deploy:migrations', function() {
    run('cd {{release_path}} && php artisan migrate --force');
});

// Создать кеш для правил роутинга
task('deploy:create-route-cache', function() {
    run('cd {{release_path}} && php artisan route:cache');
});

// Создать кеш для файлов конфигураций
task('deploy:create-config-cache', function() {
    run('cd {{release_path}} && php artisan config:cache');
});

// Очистить все закешированные данные
task('deploy:clean-cached-data', function() {
    run('cd {{release_path}} && rm bootstrap/cache/*');
});

// Перезапустить PHP после успешного деплоя
task('reload:php-fpm', function() {
    run('sudo /usr/sbin/service php7.0-fpm restart');
});

task('deploy', [
    'deploy:prepare',
    'deploy:release',
    'deploy:update_code',            // Скачать последний код с гитхаба
    'deploy:shared',                 // Создать ссылки на общие данные
    'deploy:vendors',                // Обновить компоненты композера
    'deploy:clean-cached-data',      // Очистить все закешированные данные
    'deploy:create-route-cache',     // Создать кеш для правил роутинга
    'deploy:create-config-cache',    // Создать кеш для файлов конфигураций
    'deploy:install-npm',            // Обновить NPM компоненты
    'deploy:compile-assets',         // Скомпилировать фронтенд файлы
    'deploy:migrations',             // Применить миграции
    'deploy:symlink',                // создать ссылку текущего релиза на этот
    'cleanup',                       // Удалить старые релизы
])->desc('Deploy your project');

after('deploy', 'success');

// После деплоя перезапустим php
after('deploy', 'reload:php-fpm');

// После отката на прошлый релиз - тоже перезапустим его
after('rollback', 'reload:php-fpm');

Также у меня есть пара маленьких файлов, лежащих рядом с вышеуказанным файлом: start-deploy.sh и rollback-deploy.sh. Для того чтобы быстро запустить деплой или, соответственно, откатить его.


Файл start-deploy.sh:


dep deploy local

Файл rollback-deploy.sh:


dep rollback local

Следовательно, чтобы запустить процесс деплоя, нам остаётся набрать лишь одну команду в Bash'е:


./start-deploy.sh

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


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

Поделиться с друзьями
-->

Комментарии (27)


  1. ufadiz
    02.06.2016 09:52

    Если развертывать локально на серваке, то деплойер так то особо и не нужен. Его идеология предполагает, что он используется для удаленного деплоя на различных сервера, включая staging и production серверы.

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

    github.com/capistrano/laravel
    Вот плагин для ларавел


    1. saggid
      02.06.2016 10:00
      +2

      Как бы сказать, «особо и не нужен». Одно дело вбить одну команду; другое дело — ворох этих команд, и в правильном порядке, и ничего не забыв.

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

      И чем конкретно капистрано лучше деплоера? Вот сижу я сейчас на деплоере, вроде счастливый и довольный. Зачем мне капистрано?


      1. garex
        02.06.2016 12:44
        -1

        И чем конкретно капистрано лучше деплоера?

        Тем что написан раньше, отлажен лучше, больше готовых плагинов. Основан на rake (ruby's make) и в-принципе делает всё, что надо.


        Для капистраны не надо ставить на удалённой ноде ничего — только нужен ssh-доступ.


        1. saggid
          02.06.2016 19:28
          +2

          Написан всего на пол года раньше. Да и вообще — не очень это сильный довод, смотреть, кто когда был написан.


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


          Основан на rake — ну это вообще не довод и не повод. Каждый инструмент на чём-то там основан.


          Для деплоера тоже, при желании, не надо ставить на удалённой ноде ничего — он точно также может работать по ssh, это просто я лично люблю сам заходить на сервер и делать деплой руками, мне так спокойнее.


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


      1. maximkou
        02.06.2016 16:04
        +3

        Использовал и тот, и другой. Деплоер неплохо решает свои задачи на большинстве проектов, ничем не хуже капистрано. Но, на мой взгляд, если проект написан на PHP — лучше использовать деплоер. Чем меньше разных технологий — тем проще поддерживать проект.


    1. saggid
      02.06.2016 10:43

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

      Лаконично и очень даже удобно, как по мне.

      Что насчет капистрано — то это обязательная установка руби, бандлера, и так далее. А разницы как минимум никакой, как я вижу на данный момент.


      1. vtvz_ru
        02.06.2016 16:52
        +1

        А вот по поводу этого, сэр, хочу поспорить. Deployer написан на PHP и успакован в исполняемый Phar. А чтобы запустить Phar обязательно нужен PHP. Даже на официальном сайте написано:

        «Deployer is a deployment tool written in PHP»


        1. saggid
          02.06.2016 19:12

          Я полагал, что phar архив становится исполняемым приложением :) Да, я ошибся. PHP для работы Deployer'а нужен.


      1. ElectricHeart
        02.06.2016 19:14
        +1

        Разве phar архивы не требуют php?


        1. ElectricHeart
          08.06.2016 13:55

          Как долго проходил коммент модерацию) Уже успели даже ответить


  1. cawakharkov
    02.06.2016 12:24

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


    1. johnluxor
      02.06.2016 13:51
      +1

      Есть параметр --tag=branch|tag

      И тогда будет брать из нужно ветки. По умолчанию master.

      Но опять же, значение по умолчанию можно задать в конфиге


  1. misterio
    02.06.2016 12:50

    Рекомендую еще попробовать Rocketeer


    1. Aios
      02.06.2016 12:52
      -3

      … Или докер.


    1. SiDz
      04.06.2016 21:56

      Лично, мне не нравится в rocketeer, то, что он тянет вендоры laravel, для laravel проекта — никаких проблем.
      Но, например, я работаю с symfony и мне не очень нравится, что тянуться ненужные зависимости.

      deployer в этом плане мне понрвавился больше. хоть мы и пользовались capifony до этого.
      + в том, что не нужно ставить руби, чтобы деплоить.


      1. misterio
        07.06.2016 13:56

        Ставьте rocketeer глобально и тогда в vendor проекта ничего не попадет.


  1. akubintsev
    02.06.2016 16:36

    Да, это гораздо лаконичнее того, что я когда-то писал xml-ем под phing :) Правда под ним был еще dbdeploy, позволявший управлять миграциями в БД.
    Надо попробовать.


  1. muxx
    02.06.2016 23:33

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

    Мы билдим готовый собранный проект с разогретым кешем и статикой и просто раскатываем его на сервера, где остается только выполнить миграции и переключить симлинки.


    1. saggid
      02.06.2016 23:56

      Здравствуйте) Очень интересно. А каким образом вы еще и кеш разогретый умудряетесь положить прямо в билд?


      1. muxx
        03.06.2016 07:31

        Я про кеш Symfony, тот набор сгенерированных php-файлов, лежащих в папке app/cache/.


        1. saggid
          03.06.2016 07:40

          Я думал про opcache и apc :) Спасибо, теперь понятно.


  1. yurykabanov
    03.06.2016 07:27

    Давать пользователю выполнять /usr/bin/service это очень плохая идея.
    task('reload:php-fpm', function() {
    run('sudo /usr/sbin/service php7.0-fpm restart');
    });

    Потому что помимо upstart/systemd сервисов, /usr/bin/service может стартовать и обычные SysV инит скрипты:
    # Otherwise, use the traditional sysvinit
    if [ -x "${SERVICEDIR}/${SERVICE}" ]; then
    exec env -i LANG="$LANG" PATH="$PATH" TERM="$TERM" "$SERVICEDIR/$SERVICE" ${ACTION} ${OPTIONS}
    else
    echo "${SERVICE}: unrecognized service" >&2
    exit 1
    fi

    и единственная проверка для таких файлов — это проверка на существование!

    Как следствие можно легко и непринужденно можно поднять права до рута, имея ограниченный доступ к серверу:
    $ cd /tmp; echo «id» > awesome_service.sh; chmod +x awesome_service.sh
    $ sudo /usr/bin/service ../../tmp/awesome_service.sh
    uid=0(root) gid=0(root) группы=0(root)


    1. saggid
      03.06.2016 07:27

      Здравствуйте) Но ведь всё это делается только через судо, т.е. нужно пароль ввести для выполнения подобной операции? И как ещё такие вещи давать ему выполнять? Или как вы это делаете у себя, чтобы было реально безопасно?


      1. yurykabanov
        03.06.2016 08:03

        То есть при деплое на десяток серверов вы будете каждый раз вводить пароль? А если серверов больше десяти и пароли разные? Я предполагал что у вас в /etc/sudoers разрешен запуск service без пароля (что как раз таки не безопасно).

        Для себя я пока не нашел красивого решения, завел отдельного пользователя (от которого не запускаются приложения), разрешил ему в sudoers запускать service. Тогда конфиг deployer'а можно исправить на

        runLocally("ssh specialuser@server service php5-fpm restart");
        


        P.S. не разобрался как тег source использовать, в превью все хорошо, а в самом сообщении — нет. А нет, это видимо из-за предмодерации было.


        1. saggid
          03.06.2016 09:09

          У нас пока нет десятка серверов :) Поэтому это пока не стоит проблемой. Спасибо за разъяснения.


        1. VolCh
          07.06.2016 08:26

          Для себя я пока не нашел красивого решения

          Запускать php-fpm не от рута не вариант?


        1. DoctorX
          12.06.2016 00:51

          в /etc/sudoers можно разрешить запуск service только с конкретным аргументом (php7.0-fpm restart')