Деплой — неотъемлимая часть жизни любого веб-разработчика. На данный момент существует множество способов деплоить код, будь то загрузка через ftp, git pull или CD с docker. Сегодня мы рассмотрим один из инструментов для деплоя кода — deployer.
Что такое деплой
Деплой — это процесс доставки кода приложения на конечный сервер. Самый простой и знакомый всем способ доставки — загрузка через ftp. Однако у этого способа много недостатков — процесс довольно медленный, нужно помнить какие файлы менялись, и т.д. Следующий уровень — git. Все уже намного проще и быстрее, логинимся по ssh, делаем git pull и все готово. Но рано или поздно появляются проблемы — нужно постоянно держать терминал с ssh-соединением, устанавливать вручную зависимости, чистить кэш и т.д. Можно конечно написать bash-скрипт, но все равно чего-то не хватает. А если еще нужно хранить несколько версий приложения с возможностью отката изменений — то это вообще ад. Для решения этих проблем было придумано немало инструментов, например широко известный capistrano. Однако capistrano довольно монструозный, тяжело расширяемый и вообще написан на ruby. В свою очередь рассматриваемый нами инструмент deployer написан на php, не имеет внешних зависимостей, поддерживает большинство популярных фреймворков и cms и легко расширяется.
Основная цель
Итак, цель этой статьи — построить меанизм деплоя приложения на сервер. Для этого нам понадобится простое приложение (к примеру на laravel), сервер на который мы будем деплоить (у меня это будет виртуальная машина, созданная с помощью vagrant).
Тестовое приложение
Начнем с тестового приложения. Создадим проект на laravel и добавим простой роут, выводящий номер версии.
composer create-project --prefer-dist laravel/laravel laravel-deployer-demo
Далее открываем routes/web.php
и пишем следующее:
Route::get('/', function () {
$version = 1;
return view('welcome', compact('version'));
});
В resources/views/welcome.blade.php
добавляем вывод нашей версии:
...
<div class="title m-b-md">
Laravel
</div>
<div class="m-b-md">
<strong>Version: {{ $version }};</strong>
</div>
<div class="links">
...
Запустим сервер с помощью команды php artisan serve
, откроем в браузере http://127.0.0.1:800 и увидим следуещее:
Создаем git-репозиторий и пушим туда код нашего демо проекта.
Подготовка сервера
Теперь перейдем к подготовке нашего сервера. Установку зависимостей приложения, таких как сам PHP, composer я опущу и остановлю только внимание на важных моментах, а именно создание пользователя для deployer’а, настройка прав доступа для него, а также настройка nginx.
Итак, у меня есть сервер под управлением ubuntu server 16.04 и root-доступ к нему. Я создам пользователя deployer, дам ему sudo-привилегии, настрою ему доступ к серверу по ssh-ключу.
Создаем пользователя, система попросит ввести для него пароль:
sudo adduser deployer
Даем пользователю sudo доступ:
sudo gpasswd -a deployer sudo
Добавляем пользователя в группу www-data:
sudo adduser deployer www-data
Создаем директорию для нашего проекта и выставляем права доступа:
sudo mkdir /var/www/laravel-deployer-demo
sudo chown -R deployer:www-data /var/www/laravel-deployer-demo
sudo chmod -R g+rw /var/www/laravel-deployer-demo
sudo find /var/www/laravel-deployer-demo -type d -print0 | sudo xargs -0 chmod g+s
Нужно позаботиться о том, чтобы мы могли залогиниться на сервер пользователем deployer по ssh. Для этого добавим ключ в authorized_keys
, на локальной машине выведем на экран наш публичный ключ:
cat ~/.ssh/id_rsa.pub
Скопируем вывод и на сервере вставим в ~/.ssh/authorized_keys
:
su - deployer
mkdir .ssh
chmod 700 .ssh
nano .ssh/authorized_keys
chmod 600 .ssh/authorized_keys
Также нужно позаботиться о том, чтобы наш сервер имел возможность клонировать наш репозиторий если он приватный. Для этого будучи залогиненым на сервере как deployer нужно сгенерировать ssh ключ и добавить его публичную часть в deploy keys нашего репозитория.
Deployer в процессе работы создает ряд директорий: releases, shared, current. Shared содержит общие файлы и директории, в releases хранятся наши релизы, а current — это символьная ссылка на последний релиз. PHP для символьных ссылок может кэшировать реальные пути к файлам и папкам, поэтому после каждого деплоя и переключения на новый симлинк нужно перезагрузить php-fpm. Мы дали нашему пользователю sudo доступ для этого, однако при выполнении sudo systemctl restart php7.2-fpm.service
система запросит пароль пользователя. Чтобы избежать этой ситуации нужно добавить немного магии:
sudo nano /etc/sudoers.d/deployer
И вставляем в этот файл следующую строку:
deployer ALL=NOPASSWD:/bin/systemctl restart php7.2-fpm.service
Это позволит пользователю deployer выполнять sudo systemctl restart php7.2-fpm.service
без ввода пароля.
Теперь сервер готов к деплою нашего приложения. Перейдем к настройке самого deployer’а.
Настройка deployer’а
На нашей локальной машине устанавливаем deployer:
curl -LO https://deployer.org/deployer.phar
mv deployer.phar /usr/local/bin/dep
chmod +x /usr/local/bin/dep
В папке нашего демо-проекта инициализируем конфиг deployer’а:
dep init
Welcome to the Deployer config generator
This utility will walk you through creating a deploy.php file.
It only covers the most common items, and tries to guess sensible defaults.
Press ^C at any time to quit.
Please select your project type [Common]:
[0] Common
[1] Laravel
[2] Symfony
[3] Yii
[4] Yii2 Basic App
[5] Yii2 Advanced App
[6] Zend Framework
[7] CakePHP
[8] CodeIgniter
[9] Drupal
1
Repository [[email protected]:PHPtoday-ru/laravel-deployer-demo.git]:
>
Do you confirm? (yes/no) [yes]:
>
Successfully created: /home/user/laravel-deployer-demo/deploy.php
Скрипт создал дефолтный файл конфигурации для нашего проекта — deploy.php
. Перейдем к конфигурации для нашего проекта.
Deployer для каждого деплоя создает новую директорию с кодом и хранит некоторое указанное в конфиге количество релизов. Однако некоторые папки не должны меняться от релиза к релизу, например в laravel это директория storage, в которой хранятся логи, загруженные пользовательские файлы и прочее. Нам нужно добавить эту папку в shared_dirs
и для нее будет создан симлинк. Такая же ситуация и для некоторых файлов, например файла конфигурации .env. Для файлов эта настройка называется shared_files
.
add('shared_dirs', [
'storage',
]);
add('shared_files', [
'.env'
]);
Некоторые директории должны иметь права доступа на запись, для этого их нужно добавить в writable_dirs
:
add('writable_dirs', [
'bootstrap/cache',
'storage',
'storage/app',
'storage/app/public',
'storage/framework',
'storage/framework/cache',
'storage/framework/sessions',
'storage/framework/views',
'storage/logs',
]);
Перейдем к конфигурированию сервера, на который мы будем деплоить код. За это отвечает секция host
. Указываем IP-адрес сервера, имя пользователя и путь к директории проекта на сервере.
host('192.168.64.77')
->stage('production')
->user('deployer')
->set('deploy_path', '/var/www/laravel-deployer-demo');
При такой конфигурации delployer будет использовать ваш дефолтный ssh ключ и настройки агента. Но доступна и более тонкая настройка, подробнее можно почитать в документации.
host('domain.com')
->user('name')
->port(22)
->configFile('~/.ssh/config')
->identityFile('~/.ssh/id_rsa')
->forwardAgent(true)
->multiplexing(true)
->addSshOption('UserKnownHostsFile', '/dev/null')
->addSshOption('StrictHostKeyChecking', 'no');
Laravel для работы требует файл конфигурации .env. Чтобы не загружать этот файл вручную при первом деплое или при изменении можно добавить таск для его загрузки. В корне нашего проекта создадим файл .env.production с настройками окружения для production (не забудьте добавить его в .gitignore файл чтобы он не попал в ваш репозиторий). Этот файл будет загружаться при деплое в shared директорию, а так как он добавлен в shared_files
, то для него создан симлинк и он будет всегда актуальным.
task('upload:env', function () {
upload('.env.production', '{{deploy_path}}/shared/.env');
})->desc('Environment setup');
Чтобы добавить этот таск в наш процесс деплоя нужно переопределить deploy
таск — это таск в котором в порядке выполнения описаны действия при деплое. Для laravel он имеет следующий вид:
task('deploy', [
'deploy:prepare',
'deploy:lock',
'deploy:release',
'deploy:update_code',
'upload:env',
'deploy:shared',
'deploy:vendors',
'deploy:writable',
'artisan:storage:link',
'artisan:view:clear',
'artisan:cache:clear',
'artisan:config:cache',
// 'artisan:migrate',
'deploy:symlink',
'php-fpm:restart',
'deploy:unlock',
'cleanup',
]);
Заметьте что artisan:migrate
закомментирован, так как наш демо проект не использует базу данных. В результате наш deploy.php
имеет следующий вид:
namespace Deployer;
require 'recipe/laravel.php';
// Configuration
set('repository', '[email protected]:PHPtoday-ru/laravel-deployer-demo.git');
set('git_tty', true); // [Optional] Allocate tty for git on first deployment
add('shared_files', [
'.env'
]);
add('shared_dirs', [
'storage'
]);
add('writable_dirs', [
'bootstrap/cache',
'storage',
'storage/app',
'storage/app/public',
'storage/framework',
'storage/framework/cache',
'storage/framework/sessions',
'storage/framework/views',
'storage/logs',
]);
// Hosts
host('192.168.64.77')
->stage('production')
->user('deployer')
->set('deploy_path', '/var/www/laravel-deployer-demo');
// Tasks
desc('Restart PHP-FPM service');
task('php-fpm:restart', function () {
// The user must have rights for restart service
// /etc/sudoers: username ALL=NOPASSWD:/bin/systemctl restart php-fpm.service
run('sudo systemctl restart php7.2-fpm.service');
});
after('deploy:symlink', 'php-fpm:restart');
task('upload:env', function () {
upload('.env.production', '{{deploy_path}}/shared/.env');
})->desc('Environment setup');
// [Optional] if deploy fails automatically unlock.
after('deploy:failed', 'deploy:unlock');
task('deploy', [
'deploy:prepare',
'deploy:lock',
'deploy:release',
'deploy:update_code',
'upload:env',
'deploy:shared',
'deploy:vendors',
'deploy:writable',
'artisan:storage:link',
'artisan:view:clear',
'artisan:cache:clear',
'artisan:config:cache',
// 'artisan:migrate',
'deploy:symlink',
'php-fpm:restart',
'deploy:unlock',
'cleanup',
]);
Деплоим!
Можно деплоить. В корне нашего проекта выполняем простую команду:
dep deploy production
✔ Executing task deploy:prepare
✔ Executing task deploy:lock
✔ Executing task deploy:release
➤ Executing task deploy:update_code
Counting objects: 112, done.
Compressing objects: 100% (86/86), done.
Writing objects: 100% (112/112), done.
Total 112 (delta 9), reused 112 (delta 9)
Connection to 192.168.64.77 closed.
✔ Ok
✔ Executing task upload:env
✔ Executing task deploy:shared
➤ Executing task deploy:vendors
To speed up composer installation setup "unzip" command with PHP zip extension https://goo.gl/sxzFcD
✔ Ok
✔ Executing task deploy:writable
✔ Executing task artisan:storage:link
✔ Executing task artisan:view:clear
✔ Executing task artisan:cache:clear
✔ Executing task artisan:config:cache
✔ Executing task deploy:symlink
✔ Executing task php-fpm:restart
✔ Executing task php-fpm:restart
✔ Executing task deploy:unlock
✔ Executing task cleanup
Проверяем наш задеплоенный код на сервере:
cd /var/www/laravel-deployer-demo
ls -l
total 8
lrwxrwxrwx 1 deployer www-data 10 Apr 4 22:34 current -> releases/1
drwxrwsr-x 4 deployer www-data 4096 Apr 4 22:34 releases
drwxrwsr-x 3 deployer www-data 4096 Apr 4 22:34 shared
cd current
ls -la
total 232
drwxrwsr-x 12 deployer www-data 4096 Apr 4 22:34 .
drwxrwsr-x 4 deployer www-data 4096 Apr 4 22:34 ..
drwxrwsr-x 6 deployer www-data 4096 Apr 4 22:34 app
-rwxrwxr-x 1 deployer www-data 1686 Apr 4 22:34 artisan
drwxrwsr-x 3 deployer www-data 4096 Apr 4 22:34 bootstrap
-rw-rw-r-- 1 deployer www-data 1477 Apr 4 22:34 composer.json
-rw-rw-r-- 1 deployer www-data 143705 Apr 4 22:34 composer.lock
drwxrwsr-x 2 deployer www-data 4096 Apr 4 22:34 config
drwxrwsr-x 5 deployer www-data 4096 Apr 4 22:34 database
lrwxrwxrwx 1 deployer www-data 17 Apr 4 22:34 .env -> ../../shared/.env
-rw-rw-r-- 1 deployer www-data 651 Apr 4 22:34 .env.example
drwxrwsr-x 8 deployer www-data 4096 Apr 4 22:34 .git
-rw-rw-r-- 1 deployer www-data 111 Apr 4 22:34 .gitattributes
-rw-rw-r-- 1 deployer www-data 155 Apr 4 22:34 .gitignore
-rw-rw-r-- 1 deployer www-data 1150 Apr 4 22:34 package.json
-rw-rw-r-- 1 deployer www-data 1088 Apr 4 22:34 phpunit.xml
drwxrwsr-x 4 deployer www-data 4096 Apr 4 22:34 public
-rw-rw-r-- 1 deployer www-data 3622 Apr 4 22:34 readme.md
drwxrwsr-x 5 deployer www-data 4096 Apr 4 22:34 resources
drwxrwsr-x 2 deployer www-data 4096 Apr 4 22:34 routes
-rw-rw-r-- 1 deployer www-data 563 Apr 4 22:34 server.php
lrwxrwxrwx 1 deployer www-data 20 Apr 4 22:34 storage -> ../../shared/storage
drwxrwsr-x 4 deployer www-data 4096 Apr 4 22:34 tests
drwxrwsr-x 24 deployer www-data 4096 Apr 4 22:34 vendor
-rw-rw-r-- 1 deployer www-data 549 Apr 4 22:34 webpack.mix.js
Мы видим что весь код доставлен, зависимости установлены, символьные ссылки добавлены. Все впорядке. Осталось настроить nginx, используем следующий минимальный конфиг:
server {
listen 80;
server_name deployer-demo.local;
root /var/www/laravel-deployer-demo/current/public;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_index index.php;
fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
include fastcgi_params;
}
}
Здесь важным моментом является использование $realpath_root
вместо $document_root
для того чтобы nginx правильно работал с символьными ссылками. Открываем в браузере адрес http://deployer-demo.local/ и видим что все работает.
Теперь сделаем контрольный выстрел. Внесем изменения в код, закоммитим их и попробуем задеплоить чтобы окончательно убедиться что все работает корректно.
Открываем routes/web.php
и меняем версию на другую:
Route::get('/', function () {
$version = 2;
return view('welcome', compact('version'));
});
Делаем ккоммит и пушим в репозиторий. Деплоим:
dep deploy production
✔ Executing task deploy:prepare
✔ Executing task deploy:lock
✔ Executing task deploy:release
➤ Executing task deploy:update_code
Counting objects: 116, done.
Compressing objects: 100% (87/87), done.
Writing objects: 100% (116/116), done.
Total 116 (delta 12), reused 116 (delta 12)
Connection to 192.168.64.77 closed.
✔ Ok
✔ Executing task upload:env
✔ Executing task deploy:shared
➤ Executing task deploy:vendors
To speed up composer installation setup "unzip" command with PHP zip extension https://goo.gl/sxzFcD
✔ Ok
✔ Executing task deploy:writable
✔ Executing task artisan:storage:link
✔ Executing task artisan:view:clear
✔ Executing task artisan:cache:clear
✔ Executing task artisan:config:cache
✔ Executing task deploy:symlink
✔ Executing task php-fpm:restart
✔ Executing task php-fpm:restart
✔ Executing task deploy:unlock
✔ Executing task cleanup
Открываем в браузере http://deployer-demo.local/ и видим что версия изменилась на 2. Значит все работает корректно.
Репозиторий с тестовым приложением — https://github.com/PHPtoday-ru/laravel-deployer-demo
Deployer (6.1.0) — https://deployer.org/
Все действия описанные в статье производились на ubuntu 16.04.