Посты / Как быстро загрузить изменения на продакшн в Laravel

16.02.2017 17:05
Привет, друзья. Сегодня я хочу поговорить о такой интересной теме, как загрузка своих изменений на продакшн сервер. Этот вопрос уже поднимался много раз, а в сети полно информации об этом. Но если немного отойти от темы, и вспомнить свое обучение в университете, то каждый помнит, что были преподаватели, которые очень понятно и доходчиво преподносили предмет, а есть те, которые много начитывали, давали практику, но все равно, было мало понятно. Так вот так же и со статьями. Есть статьи поверхностные (статья ради статьи), а есть статьи ради того, чтобы читатель смог быстро понять и усвоить основную идею. Хочу надеяться, что мои статьи из второй группы. И так, вернусь к теме, как же в Laravel организовать выгрузку изменений в продакшн, чтобы было легко и просто?

Хочу заметить тот факт, что каждый ищет свой путь. А путей много. Поэтому те решения, которые нравятся мне, не факт, что будут нравится кому-то, и соответственно наоборот. Пару лет назад я временно увлекся Ruby и Ruby on Rails. Ларавел чем-то схож с рельсами. Так вот, для руби было несколько отличных решений для работы с продакшн - это capistrano и mina. Первый мне показался немного замороченным, а вот второй очень понравился своей простотой и лаконичностью.

И когда я продолжил работу с PHP (все таки я ему отдал больше 10 лет!), то решил обязательно найти решение на подобии mina для руби, но уже для пыхи. И я нашел его.

Речь пойдет о замечательном решении Антона Медведева - deployer. Вот статья на его блоге про Deployer. Антону удалось сделать простой и логичный инструмент, который помогает сохранить много времени и нервов. Весь функционал запакован в phar файл и очень легко переносится от проекта к проекту. Хранить его в репозитории так же очень удобно.

Итак, что нужно делать и как это работает. Есть несколько путей установки, включая и композер. Но так как я веду разработку под виндой, в Open Server, то для меня удобнее скачать дистрибутив: https://deployer.org/download  

Хочу сказать, что у меня стоит Deployer version 3.3.0 Я пробовал ставить более свежие, но там головняк в винде возникает. Поэтому я плюнул на это, и использую старую версию.  Предполагаю, что вы тоже установили такую же версию, как у меня, так как настройки будут для нее.

В общем, скачиваем дистрибутив deployer.phar и кидаем его в корень вашего проекта (туда, где лежит ваш компосер). И переименовываем в dep. Просто файл без расширения. Далее нужно создать файл настроек для определенного фреймворка. В нашем случае - это Laravel.

Находясь в корне проекта (я разрабатываю в phpStorm и все команды ввожу в его же терминале), вводим следующую команду в терминал:

php dep init
После этого появится список фреймворков. Ставим цифру, которая стоит возле Ларавел. В нашей версии (3.3.0) - это будет цифра 3. И нажимаем энтер.

В корне проекта появится новый файл: deploy.php Именно в нем мы и будем прописывать наши настройки.

Перед тем, как разобрать файл настроек, пару слов о том, как это все работает и как нужно настроить apache и nginx.

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

Например, на сервере будут созданы три каталога: current, releases и shared.
Каталог current на самом деле пустой. Он по сути является указателем на текущий релиз.
Когда произойдет удачное развертывание вашего проекта в папку releases (в эту папку складываются все ваши версии развернутых проектов), то для current будет создана новая ссылка на ваш последний релиз.

Это корень проекта на сервере, где будет следующая структура:


Это папка для хранения ваших релизов:

Это папка где будут хранится постоянные файлы и каталоги, которые являются общими для всех релизов:

Если посмотреть на скрины, то структура разбиения проекта становится понятна.

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

С папкой current и releases разобрались. Что же должно быть в папке shared. В нее однозначно выносится файл настроек для вашего проекта - .env Именно в этом файле нужно прописать все настройки вашего проекта, включая базу данных, работу с почтой и т.д. И он ни в коем случае не должен хранится в репозитории!

Также нужно в shared поместить npm, который применяется для всех релизов, папку storage, где хранятся кэш и логи. И те папки из public, которые одинаковы для всех релизов. Например, папка upload_images, куда загружаются изображения с вашего сайта.

Со структурой разобрались. Теперь нужно подправить конфиги apache и nginx. В них нужно указать новую рабочую директорию:
/home/{user}/web/{domen}/public_html/current/public
Я работаю с CP Vesta, поэтому мое и ваше расположение проектов может отличаться. В фигурных скобках указан ваш пользователь и ваш домен сайта.

Единственное что вам сейчас нужно сделать на сервере, это только подправить конфиги. Создавать структуру папок не нужно. Это сделает сам deployer.

Для того, чтобы можно было делать выгрузку на production, вы должны хранить ваш проект в репозитории, например на github или bitbucket. Я пользуюсь вторым, так как там приватные репозитории бесплатны.

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

На этом, пока все. Переходим к настройке файла deploy.php:
<?php
/*
* This file has been generated automatically.
* Please change the configuration for correct use deploy.
*
* Start: php dep deploy production
*/

require 'recipe/laravel.php';

// Set configurations.
set('repository', 'git@bitbucket.org:myname/myproject.git');
set('shared_files', ['.env']);
set('shared_dirs', [
'storage/app',
'storage/framework/cache',
'storage/framework/sessions',
'storage/framework/views',
'storage/logs',
'public/images/uploads',
'node_modules',
]);
set('writable_dirs', ['bootstrap/cache', 'storage', 'vendor', 'public/images/uploads']);

// Configure servers.
server('production', 'mysite.com', 22)
->user('admin')
->password()
->env('deploy_path', '/home/admin/web/mydomen.com/public_html');

/**
* Install NPM package.
*/
task('deploy:install-npm', function () {
run('cd {{release_path}} && npm i');
});

/**
* Compile assets.
*/
task('deploy:compile-assets', function () {
run('cd {{release_path}} && gulp --production');
});

/**
* Make migrate.
*/
task('deploy:migrations', function() {
run('cd {{release_path}} && php artisan migrate --force');
});

/**
* Make seed.
*/
/*task('deploy:seed', function() {
run('cd {{release_path}} && php artisan db:seed');
});*/

/**
* Create cache for routes.
*/
task('deploy:create-route-cache', function() {
run('cd {{release_path}} && php artisan route:clear');
});

/**
* Create cache for config.
*/
task('deploy:create-config-cache', function() {
run('cd {{release_path}} && php artisan config:clear');
});

/**
* Clear all cached data.
*/
task('deploy:clean-cached-data', function() {
run('cd {{release_path}} && php artisan view:clear');
});

/**
* Clear cached data from bootstrap.
*/
task('deploy:clean-cached-data-boot', function() {
run('cd {{release_path}} && rm bootstrap/cache/*');
});

/**
* Restart Apache on success deploy.
*/
task('reload:apache', function () {
run('sudo service httpd restart');
})->desc('Restart Apache service');

/**
* Restart Nging on success deploy.
*/
task('reload:nginx', function () {
run('sudo service nginx restart');
})->desc('Restart Nginx service');

task('deploy', [
'deploy:prepare',
'deploy:release',
'deploy:update_code',
'deploy:shared',
'deploy:vendors',
'deploy:clean-cached-data',
'deploy:clean-cached-data-boot',
'deploy:create-route-cache',
'deploy:create-config-cache',
'deploy:install-npm',
'deploy:compile-assets',
'deploy:migrations',
// 'deploy:seed',
'deploy:symlink',
'cleanup',
])->desc('Deploy your project');

after('deploy', 'success');

/**
* Restart servers.
*/
//after('success', 'reload:apache');
//after('success', 'reload:nginx');

/**
* After rollback restart servers.
*/
//after('rollback', 'reload:apache');
//after('rollback', 'reload:nginx');
В принципе, файл достаточно лаконичный и вполне понятно, что происходит. Но я поясню.

Вначале мы указываем скрипту от куда нужно получать проект. В нашем случае из репозитория битбакет. Далее указываем, какие файлы будут общими. Общим будет только файл настроек.

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

Далее мы указываем, какие папки будут общими. Скрипт сам создаст эти папки на сервере. Потом мы указываем, какие папки будут записываемыми. Это нужно для того, чтобы скрипт выставил на них соответствующие права.

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

php dep deploy production1
С этим разобрались. Идем дальше. А дальше у нас идут задачи (таски), которые нужно поочередно выполнять при заливке релиза. Первый параметр в таске - это название задачи. Потом идет функция замыкания, где в методе run указывается linux команда.

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

По последней задаче видно, что происходит, а именно: подготовка к загрузке проекта (соединение по ssh, соединение с репозиторием), далее происходит создание новой папки релиза и заливка в нее проекта, создание общих папок (если их еще нет), загрузка пакетов и самого Ларавел через composer, очистка разного кэша, установка nodeJS, компилирование ассетов через гулп, применение ваших миграций, создание ссылки на новый релиз, очистка временных файлов.

Фактически, таски будут запущены этой командой:
after('deploy', 'success');
Будут выполнены таски, у которых есть префикс deploy и если все пройдет нормально, то будет возвращен статус успешного завершения деплоя на продакшн.

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

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

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

Вот и все! Для запуска автоматического деплоя на продакшн выполняете команды:
php dep deploy production
Или, если у вас только один сервер прописан, то просто:
php dep deploy
Произойдет соединение по SSH с вашим сервером и вас попросят ввести пароль. Вводите пароль, подтверждаете, и вуаля! Все произойдет очень быстро и полностью автоматически. Кто ни разу не пользовался автодеплоем оценит по достоинству.

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

Комментарии (11):

pavel писал(а):
В общем пришлось скопировать webpack.config.js и вставить такую строку Mix.paths.setRootPath( path.resolve(__dirname) ); После этого все заработало.
В принципе, этот баг еще решается путем удаления ноды из шаред папки. Когда нода будет создаваться в новом релизе, то у нее не будет проблем с зависимостями.
pavel писал(а):
У вас в данном посте перезагрузка апача закомментирована. У меня почему-то пока не произойдет перезагрузка php-fpm не видно некоторых изменений. Это нормально?
Я апач (или php-fpm) предпочитаю перегружать ручками. Так как если при перезагрузке возникнет ошибка, я сразу смогу это увидеть и быстро среагировать. А при авто перезагрузке пройдет гораздо больше времени на устранение проблемы.
Но я все-таки еще один маленький вопрос меня очень мучает.

У вас в данном посте перезагрузка апача закомментирована. У меня почему-то пока не произойдет перезагрузка php-fpm не видно некоторых изменений. Это нормально?
В общем пришлось скопировать webpack.config.js и вставить такую строку
Mix.paths.setRootPath( path.resolve(__dirname) );
После этого все заработало.
Kirill писал(а):
Все что лежит в директории shared  имеет линки в оригинальной папке проекта. Если загляните в папку current, (которая тоже является линком на нужный релиз) то вы увидите, что там лежит и файл настроек и папка с нодой (благодаря ссылкам). Поэтому не важно что и где хранится. Все должно работать.
"Это да. Но при этом, когда я отдельно запускаю "npm run production" у меня вылезает ошибка.

Error: Cannot find module '/var/www/vhosts/site.ru/shared/webpack .mix'
at Function.Module._resolveFilename (module.js:469:15)
at Function.Module._load (module.js:417:25)
at Module.require (module.js:497:17)
at require (internal/module.js:20:19)
at Object. (/var/www/vhosts/site.ru/shared/node_modules/laravel-mix/setup/webpack.config.js:9:1)
at Module._compile (module.js:570:32)
at Object.Module._extensions..js (module.js:579:10)
at Module.load (module.js:487:32)
at tryModuleLoad (module.js:446:12)
at Function.Module._load (module.js:438:3) 

А через форму обратной связи можно обратиться с этим вопросом?
pavel писал(а):
Большое спасибо за предыдущий ответ. Но есть еще одно уточнение: вы никогда не пробовали делать тоже самое для последних версий laravel? Там используется webpack. И при этом nodejs ищет необходимый файл в директории shared. Из-за чего возникают неприятности. И если не секрет приходилось ли вам когда-нибудь локально собирать js и css для боевого сервера? https://github.com/JeffreyWay/laravel-mix/issues/533 - Jeffrey пишет, что так многие делают.
Так по сути для gulp и webpack особой разницы нет. Просто в таске для деплоя нужно заменить команду gulp production на npm run production Так как изменился принцип сборки скриптов, и соответственно и команды тоже. Под 5.5 версию у меня все летит без проблем. Собирать скрипты на локалке можно, но это тогда мешает полной автоматизации процесса деплоя. Я так не делаю, так как стараюсь все по максимуму автоматизировать. А когда нужно что-то делать ручками, то обязательно можно где-то накосячить ИМХО.

И при этом nodejs ищет необходимый файл в директории shared
Все что лежит в директории shared  имеет линки в оригинальной папке проекта. Если загляните в папку current, (которая тоже является линком на нужный релиз) то вы увидите, что там лежит и файл настроек и папка с нодой (благодаря ссылкам). Поэтому не важно что и где хранится. Все должно работать.

Да, в таски также нужно прописать задачу для composer update. Если не получится, то пишите, помогу с конфигом.


Большое спасибо за предыдущий ответ. 
Но есть еще одно уточнение: вы никогда не пробовали делать тоже самое для последних версий laravel? Там используется webpack. И при этом nodejs ищет необходимый файл в директории shared. Из-за чего возникают неприятности.
И если не секрет приходилось ли вам когда-нибудь локально собирать js  и css для боевого сервера? https://github.com/JeffreyWay/laravel-mix/issues/533 - Jeffrey пишет, что так многие делают.

pavel wrote:
Большое спасибо за статью. Есть уточняющий вопрос: как думаете, нормально ли по-вашему первый раз вручную перекидывать папки vendors и node_modules? Просто у меня все время с этим проблема возникает: то модули npm сразу не ставятся, нужно дополнительные команды запускать, чтобы все заработало. Или это неверный подход?
Здравствуйте. Не стоит вручную копировать папки. Для вендора есть файл композера, он сам все прекрасно установит. А вот для npm придется слегка подготовить ноду. Иногда при установке npm она будет ругаться на некоторые старые пакеты. Их стоило бы обновить один раз на сервере и все. Хотя некоторые делают проще и устанавливают при инсталляции npm флаг игнорирования ошибок.
Большое спасибо за статью. 
Есть уточняющий вопрос: как думаете, нормально ли по-вашему первый раз вручную перекидывать папки vendors и node_modules? Просто у меня все время с этим проблема возникает: то модули npm сразу не ставятся, нужно дополнительные команды запускать, чтобы все заработало. Или это неверный подход?
gpwm писал(а):
А как Вы загружаете в продакшн .env при первом деплое, да и вообще при каждом изменении настроек?
Здравствуйте. При первом деплое я вручную через WinSCP делаю загрузку, так как при первом деплое будет создан только пустой файл. Ведь из репозитория брать нечего, настройки там не хранятся, ради безопасности. Я об этом упоминал в статье. При изменении настроек, что бывает не так часто, нужно тоже в ручном режиме перезалить файл. Это можно делать сразу в IDE, если настроить удаленный доступ.
А как Вы загружаете в продакшн .env при первом деплое, да и вообще при каждом изменении настроек?