Прикладная демонология, практические рецепты

Развертывание приложений Perl на примере Catalyst

Создаем пользователя, выделенного для проекта, и все дальнейшие действия производим под его учетной записью:

pw useradd myapp -s /bin/csh -m

Ставим perlbrew и активируем:

wget -O- http://install.perlbrew.pl/ | sh
source ~/perl5/perlbrew/etc/cshrc
perlbrew init

Имеет смысл включить активацию perlbrew в стартовый сценарий login shell:

% fgrep perlbrew ~/.bashrc
test -f ~/perl5/perlbrew/etc/bashrc && . ~/perl5/perlbrew/etc/bashrc
% fgrep perlbew ~/.cshrc
test -f ~/perl5/perlbrew/etc/cshrc && source ~/perl5/perlbrew/etc/cshrc

Обновляем инсталляцию perlbrew (при необходимости):

perlbrew self-upgrade

Выбираем и устанавливаем нужный интерпретатор Perl:

perlbrew available
perlbrew install -nvj4 5.20.2

Подключаем cpanm:

perlbrew install-cpanm

Создаем новый local::lib и активируем:

perlbrew lib create perl-5.20.2@carton
perlbrew switch 5.20.2@carton

Ставим Carton:

cpanm Carton

Создаем каталог нового проекта:

mkdir MyApp && cd MyApp

Создаем cpanfile с требуемыми модулями.
Также, имеет смысл зафиксировать в нем использующуюся версию Perl.
Пример cpanfile для нового проекта на Catalyst:

requires 'perl', '5.20.2';

requires 'Catalyst::Devel';
requires 'Catalyst::Runtime';
requires 'Catalyst::View::Xslate';
requires 'DDP';
requires 'lib::abs';
requires 'Plack::Middleware::Debug';
requires 'Plack::Middleware::ReverseProxy';
requires 'Term::Size::Any';
requires 'YAML';

Ставим все модули из cpanfile в каталог local (при этом используем дополнительный репозиторий модулей на основе Pinto):

PERL_CARTON_MIRROR=http://pinto.4rt.ru/ carton install

При этом будет создан файл cpanfile.snapshot в котором будут зафиксированы точные версии установленных модулей.
При воспроизведении установки в production необходимо будет установить именно их:

PERL_CARTON_MIRROR=http://pinto.4rt.ru/ carton install --deployment

Создаем структуру нового проекта Catalyst:

carton exec catalyst.pl MyApp

Переносим все файлы нового проекта на уровень выше:

mv MyApp/* . && rmdir MyApp

Удаляем вероятно ненужные файлы и создаем необходимые каталоги:

rm -Rf Makefile.PL README *.conf *.psgi root script t
mkdir -p bin etc root/templates root/static/css root/static/error root/static/js var/log var/run var/cache/xslate
touch var/log/.gitkeep var/run/.gitkeep var/cache/xslate/.gitkeep

Создаем PSGI файл app.psgi для запуска приложения:

#!/usr/bin/env perl

use strict;
use warnings;
use lib::abs qw(lib);
use Carp qw(croak);
use Plack::Builder;

if ( $ENV{ PLACK_ENV } eq 'development' ) {
    $ENV{ CATALYST_DEBUG } = 1;
    $ENV{ DBIC_TRACE } = 1;
}
elsif ( $ENV{ PLACK_ENV } eq 'production' ) {
    # noop
}
else {
    croak qq(Invalid Plack session: $ENV{ PLACK_ENV }\n);
}
$ENV{ CATALYST_CONFIG } = 'etc/config.yml';
$ENV{ CATALYST_CONFIG_LOCAL_SUFFIX } = $ENV{ PLACK_ENV };

require MyApp;

builder {
    if ( $ENV{ PLACK_ENV } eq 'development' ) {
        enable 'ConditionalGET';
        enable 'Debug';
        enable 'Static',
            path => qr{^/(css|error|i|js)/}, root => 'root/static';
    }
    else {
        enable 'AccessLog' unless $ENV{ NO_ACCESS_LOG };
        enable 'ReverseProxy';
    }

    MyApp->apply_default_middlewares( MyApp->psgi_app );
};

Создаем конфигурационный файл (etc/config.yml):

---
encoding: 'UTF-8'
name:     'MyApp'

View::HTML:
    cache_dir: '__path_to(var/cache/xslate)__'
    encode_body: 0
    path: [ '__path_to(root/templates)__' ]

При необходимости создаем файлы etc/config_development.yml и etc/config_production.yml в которых можно перекрывать часть настроек.

Редактируем основной файл приложения lib/MyApp.pm:

package MyApp;

use DDP colored => 1;
use Moose;
use namespace::autoclean;
use open qw(:std :utf8);

extends 'Catalyst';

sub ddp { shift->log->debug(np @_) }

__PACKAGE__->setup(qw(
    ConfigLoader
    PlainError
));

__PACKAGE__->meta->make_immutable;

1;

Создаем файл представления по умолчанию lib/MyApp/View/HTML.pm:

package MyApp::View::HTML;

use Moose;

extends 'Catalyst::View::Xslate';

1;

Можно сразу заготовить полностью статическую страницу root/static/error/5xx.html для обработки ошибок семейства 5xx:

<!doctype html>
<html>
    <body>
        Ошибка: <!--# echo var="status" -->
    </body>
</html>

Полезными оказываются файлы .gitignore:

etc/config_development.yml
etc/config_production.yml
local
var

и .vimrc:

set wildignore=local,var

Можем запустить среду разработки:

carton exec plackup -R etc

Для удобства можно создать исполнимый файл run:

#!/bin/sh

carton exec plackup -R etc $*

На этом шаге уже можно добавить проект в систему контроля версий:

git init
git add .
git add -f var/log/.gitkeep var/run/.gitkeep var/cache/xslate/.gitkeep
git commit -m 'First import.'

Для использования в production локально ставим uWSGI:

curl -s http://uwsgi.it/install | bash -s psgi `pwd`/local/bin/uwsgi
rm -Rf uwsgi_latest_from_installer*

Готовим конфигурацию uWSGI (etc/uwsgi.ini):

[uwsgi]
; listen socket
http-socket = var/run/http.sock
; set cheaper algorithm to use, if not set default will be used
cheaper-algo = busyness
; This string is patch for patch, repair cheaper-algo=busyness crashes
; https://www.mail-archive.com/uwsgi@lists.unbit.it/msg06051.html
enable-metrics = true
; minimum number of workers to keep at all times
cheaper = 1
; number of workers to spawn at startup
cheaper-initial = 1
; maximum number of workers that can be spawned
workers = 5
; specifies the window, in seconds, for tracking the average busyness of workers
cheaper-overload = 10
; how many workers should be spawned at a time
cheaper-step = 1
; this option enables debug logs from the cheaper_busyness plugin
cheaper-busyness-verbose = false

; set the internal buffer size for uwsgi packet parsing, default: 4096
buffer-size  = 65535

; perl
psgi = app.psgi

; do not catch $SIG{__DIE__}
perl-no-die-catch = true

; environment
env = NO_ACCESS_LOG=true
env = PLACK_ENV=production

; logging
req-logger = file:var/log/uwsgi.access_log

; set the buffer size for the master logger.
; log messages larger than this will be truncated.
log-master-bufsize = 65536

; do not report (annoying) SIGPIPE
ignore-sigpipe = true

; enable memory usage report
memory-report = true

; create pidfile (before privileges drop)
pidfile = var/run/uwsgi.pid

; stats socket
stats = var/run/uwsgi.stats.sock

; try to remove all of the generated files/sockets (UNIX sockets and pidfiles) upon exit
;vacuum = true

; reload uWSGI if the specified file is modified/touched
touch-reload = Changes

Запуск uWSGI будет выглядеть так:

carton exec uwsgi etc/uwsgi.ini

Создаем стартовый скрипт etc/freebsd.rc.sh для FreeBSD:

#!/bin/sh

#
# symlink this file to /usr/local/etc/rc.d/myapp.sh
#

[ -L $0 ] && rc=`readlink $0` || rc=$0
proj=`basename $0`; proj=${proj%.sh}
base=`realpath \`dirname \\\`realpath $rc\\\`\`/..`
user=`stat -f%Su $rc`

if [ $user = root ]; then
    echo "can't run under the root privileges" >&2
    exit 1
fi

# shell type detecting
if getent passwd $user | cut -d: -f7 | fgrep -q bash; then
    source='. ~/perl5/perlbrew/etc/bashrc'
elif getent passwd $user | cut -d: -f7 | fgrep -q csh; then
    source='source ~/perl5/perlbrew/etc/cshrc'
else
    echo "can't run under unknown login shell" >&2
    exit 2
fi

case "$1" in
    start)
        echo starting $proj
        su $user -c "$source; cd $base && carton exec local/bin/uwsgi -d var/log/uwsgi.error_log etc/uwsgi.ini"
    ;;
    stop)
        echo stopping $proj
        su $user -c 'cd '$base' && kill -INT `cat var/run/uwsgi.pid`'
    ;;
    restart)
        $0 stop
        $0 start
    ;;
    reload)
        echo reloading $proj
        su $user -c 'cd '$base' && kill -HUP `cat var/run/uwsgi.pid`'
    ;;
    *)
        echo "usage: $0 {start|stop|restart|reload}" >&2
        exit 1
    ;;
esac

Линкуем скрипт запуска в системный каталог /usr/local/etc/rc.d:

sudo ln -s ~myapp/src/etc/freebsd.rc.sh /usr/local/etc/rc.d/myapp.sh

Осталось настроить проксирование Nginx:

server {
    listen 80;
    server_name myapp.example.com;
    access_log /var/log/nginx/myapp.access_log timed;

    set $base   /home/myapp/MyApp;
    set $static $base/root/static;

    proxy_set_header Host              $host;
    proxy_set_header X-Forwarded-For   $remote_addr;
    proxy_set_header X-Forwarded-Host  $host;
    proxy_set_header X-Forwarded-Port  $server_port;
    proxy_set_header X-Forwarded-Proto $scheme;

    proxy_intercept_errors on;

    error_page 500 502 503 504 /error/5xx.html;

    location / {
        proxy_pass http://unix:$base/var/run/http.sock:;
    }
    location /css/ {
        root $static;
        access_log off;
    }
    location /error/ {
        internal;
        charset utf-8;
        ssi  on;
        root $static;
    }
    location = /favicon.ico {
        root $static;
        access_log off;
    }
    location /i/ {
        root $static;
        access_log off;
    }
    location /js/ {
        root $static;
        access_log off;
    }
}

Для выполнения заданий crontab можно использовать следующий метод:

PATH=/etc:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin

#minute hour    mday    month   wday    command
3       *       *       *       *       csh -c 'source ~/perl5/perlbrew/etc/cshrc; cd ~/MyApp && carton exec bin/script'

Есть еще варианты развертывания Perl проектов в production с использованием daemontools и Supervisor, но они выглядят слабее с точки зрения управляемости, логирования и простоты.
Поэтому оставляю их за скобками.