- PVSM.RU - https://www.pvsm.ru -

Автозапуск rails+rvm+unicorn+nginx на FreeBSD

Во-первых, нужно сказать «спасибо» автору этого руководства [1]. Без него я бы долго ещё не сел за написание сего поста: кучу проблем пришлось бы решать самостоятельно. Однако в моём случае ситуация была чуть другая (не Debian, а FreeBSD), да и вопрос с автозапуском unicorn остался открытым. Встречавшиеся мне на просторах интернета решения на изящество тоже не претендовали: делать по службе на веб-приложение — моветон. Во FreeBSD эта проблема решается на первый взгляд просто — созданием «метаслужб», позволяющих запускать более одного экземпляра (в качестве примера — FreeBSD jails). Однако, как это часто бывает, есть нюансы…

rc.d

Так как все rc.d-конфиги являются shell-скриптами, можно не ограничиваться файлом /etc/rc.conf и его собратом /etc/rc.conf.local, а поместить фрагменты в каталоги /etc/rc.conf.d и /usr/local/etc/rc.conf.d. Единственная рекомендация — файлы должны называться так же, как использующие их службы. Это позволяет избежать превращения конфигурационных файлов в нечитабельных монстров из сотни с лишним строк, в которых без grep'а не разберёшься. Проблема в другом: если у службы есть несколько экземпляров, ВСЕ их настройки формально должны находиться в одном файле (имя которого, напомню, совпадает с именем службы). Чтобы решить и эту проблему, нужно разобраться, как работают скрипты запуска служб.
Обычно этот процесс описывают примерно следующим образом: " При старте системы выполняется скрипт /etc/rc. Он считывает настройки из файла /etc/rc.conf (а также /etc/rc.conf.local — при наличии такового)..." А вот и нет! Вместо этого он включает в себя файл /etc/rc.subr, определяющий кое-какие вспомогательные функции и точно так же включающий в себя файлы системных настроек:

...
if ${_rc_conf_loaded:-false}; then
        :
else
        if [ -r /etc/defaults/rc.conf ]; then
                debug "Sourcing /etc/defaults/rc.conf"
                . /etc/defaults/rc.conf
                source_rc_confs
        elif [ -r /etc/rc.conf ]; then
                debug "Sourcing /etc/rc.conf (/etc/defaults/rc.conf doesn't exist)."
                . /etc/rc.conf
        fi
        _rc_conf_loaded=true
fi
if [ -f /etc/rc.conf.d/"$_name" ]; then
        debug "Sourcing /etc/rc.conf.d/${_name}"
        . /etc/rc.conf.d/"$_name"
fi
...

В свою очередь, файл /etc/defaults/rc.conf содержит следующий код:

...
rc_conf_files="/etc/rc.conf /etc/rc.conf.local"
...
##############################################################
### Define source_rc_confs, the mechanism used by /etc/rc.* ##
### scripts to source rc_conf_files overrides safely.       ##
##############################################################

if [ -z "${source_rc_confs_defined}" ]; then
        source_rc_confs_defined=yes
        source_rc_confs () {
                local i sourced_files
                for i in ${rc_conf_files}; do
                        case ${sourced_files} in
                        *:$i:*)
                                ;;
                        *)
                                sourced_files="${sourced_files}:$i:"
                                if [ -r $i ]; then
                                        . $i
                                fi
                                ;;
                        esac
                done
        }
fi

После загрузки всех настроек составляется список системных служб, и для каждой службы вызывается скрипт запуска — вне зависимости от того, включена она или нет. Поэтому настоятельно не рекомендуется помещать файлы пользовательских служб в каталог /etc/rc.d — это увеличит время загрузки системы. Для пользовательских служб есть каталог /usr/local/etc/rc.d — находящиеся в нём скрипты вызываются в последнюю очередь, когда основная система уже сконфигурирована и готова к работе. Но настоящая чёрная магия начинается дальше: каждый из этих скриптов снова включает скрипт /etc/rc.subr, загружающий относящиеся к нему переменные. И вот это-то даёт возможность делать ещё более модульные файлы настроек: достаточно включить в основной файл в каталоге /etc/rc.conf.d код вроде следующего (сразу на примере unicorn):

unicorn_enable="YES"
unicorn_profiles=""

for p in $(grep -rlE '^unicorn_[0-9a-zA-Z]+_enabled="[Yy][Ee][Ss]"$' /usr/local/etc/unicorn.d); do
	bn=$(basename $p)
	if [ -n "$unicorn_profiles" ]; then
		unicorn_profiles="$unicorn_profiles $bn"
	else
		unicorn_profiles="$bn"
	fi
	. $p
done
Unicorn

Первым делом поставим sudo — это намного удобнее, чем «лепить» команду для запуска через su, напрашиваясь на shell injection.

cd /usr/ports/security/sudo && make install clean

Ещё мы поставим утилиту под названием portmaster — это облегчит дальнейший процесс:

cd /usr/ports/ports-mgmt/portmaster && make install clean

Весь остальной софт будем ставить с её помощью:

LN='ln -f' portmaster devel/bison textproc/libxml2 textproc/libxslt textproc/diffutils devel/gmake security/openssl devel/automake devel/git devel/subversion shells/bash

Для ленивых — установка окружения по умолчанию:

portmaster lang/ruby19 sysutils/rubygem-bundler www/rubygem-unicorn

Описание дальнейших шагов можно найти в оригинальной статье, а мы переходим к следующей части — установке всего этого добра в качестве службы.

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

is_unicorn_profile() {
    local profile

    for profile in $unicorn_profiles; do
        if [ "$profile" = "$1" ]; then
            return 0
        fi
    done

    return 1
}

if [ -n "${unicorn_profiles}" ]; then
        if [ -n "$2" ]; then
                profile="$2"
                if ! is_unicorn_profile $profile; then
                        echo "$0: no such profile defined in unicorn_profiles."
                    exit 1
                fi
                eval unicorn_socket=${unicorn_${profile}_socket:-"/tmp/${name}-${profile}.sock"}
                eval unicorn_config=${unicorn_${profile}_config}
                eval unicorn_dir=${unicorn_${profile}_dir}
                eval unicorn_flags=${unicorn_${profile}_flags:-"${unicorn_flags}"}
                eval unicorn_environment=${unicorn_${profile}_environment:-"${unicorn_environment}"}
                eval unicorn_rails=${unicorn_${profile}_rails:-"${unicorn_rails}"}
                eval unicorn_user=${unicorn_${profile}_user:-"${unicorn_user}"}
                eval unicorn_procname=${unicorn_${profile}_procname:-"${unicorn_procname}"}
                eval unicorn_bundler=${unicorn_${profile}_bundler:-"${unicorn_bundler}"}
                eval unicorn_rvm=${unicorn_${profile}_rvm:-"${unicorn_rvm}"}
                eval unicorn_ruby=${unicorn_${profile}_ruby:-"${unicorn_ruby}"}
                if checkyesno unicorn_rvm; then
                        unicorn_procname="~${unicorn_user}/.rvm/rubies/ruby-${unicorn_ruby}/bin/ruby"
                fi
        elif [ -n "$1" ]; then
                for profile in ${unicorn_profiles}; do
                echo "Processing ${name} profile: ${profile}"
                $0 $1 ${profile}
            done
            exit 0
        fi
fi

Использование RVM создаёт проблему: для разных приложений необходимо подстраивать окружение. Можно, конечно, после запуска каждого экземпляра переинициализировать окружение, но проще воспользоваться вспомогательным скриптом:

if checkyesno unicorn_rvm; then
        if [ -d "~${unicorn_user}/.rvm/${unicorn_ruby}" ]; then
                /usr/local/bin/sudo -u $unicorn_user /usr/local/bin/unicorn-wrapper ${unicorn_ruby} $(basename ${command}) ${command_args}
                rc=$?
        else
                echo "Ruby version ${unicorn_ruby} not found by RVM for user ${unicorn_user}"
                rc=1
        fi
else
        /usr/local/bin/sudo -u $unicorn_user ${command} ${command_args}
        rc=$?
fi

Файл /usr/local/bin/unicorn-wrapper [2] — простая обёртка, устанавливающая нужную версию Ruby и выполняющая заданную команду с аргументами. Полную версию скрипта запуска можно взять рядом [3]. Пользоваться очень просто:

service unicorn <action> [<app>]

Пример конфига:

unicorn_redmine_enabled="YES"
unicorn_redmine_dir="/var/www/sites/mycoolsite.tld/redmine"
unicorn_redmine_rails="NO"

По-хорошему, надо бы оформить Port Request, но пока руки не дошли.

P. S. Остался открытым только один вопрос: как это добро интегрировать с тем же capistrano, чтобы при обновлении не слетали настройки?

Автор: orthanner

Источник [4]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/ruby/25507

Ссылки в тексте:

[1] этого руководства: http://habrahabr.ru/post/120368/

[2] /usr/local/bin/unicorn-wrapper: https://github.com/orthanner/unix-tools/raw/master/unicorn-wrapper

[3] рядом: https://github.com/orthanner/unix-tools/raw/master/rc.d/unicorn

[4] Источник: http://habrahabr.ru/post/166807/