Что такое деплой

Деплой - это процесс доставки кода приложения на конечный сервер. Самый простой и знакомый всем способ доставки - загрузка через 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 и увидим следуещее:

deployer demo app

Создаем 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 [git@github.com: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', 'git@github.com: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/ и видим что все работает.

laravel deployer demo

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

Открываем 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.