Развертывание приложений 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, но они выглядят слабее
с точки зрения управляемости, логирования и простоты.
Поэтому оставляю их за скобками.