Создание проекта на Erlang и его упаковка в deb-пакет

11 марта 2014

Примем за рабочую теорию, что мы здесь все взрослые и осознаем важность использования менеджера пакетов. Есть много способов упаковать приложение, написанное на Erlang, в deb, rpm или еще какого формата пакет. Здесь я опишу один из множества таких способов, основанный на использовании утилиты FPM.

В общем, я выложил на GitHub’е шаблон минимального проекта на Erlang, способного упаковываться в deb-пакет. К проекту прилагается Perl-скрипт, генерирующий на его основе новый проект, также способный упаковываться в deb-пакет. Если вы не располагаете большим количеством времени, просто взгляните на тамошний README.md. В нем расписано, как этим хозяйством пользоваться. Здесь же я расскажу чуть более подробно, как создаются такие проекты вручную почти с нуля, а затем уже перейдем и к автоматизации.

Как вы уже поняли, нам потребуется FPM. Это очень удобный скрипт на Ruby, предназначенный для создания deb, rpm и прочих пакетов. Устанавливается он следующим образом:

sudo apt-get install ruby ruby-dev
sudo gem install fpm

Теперь создадим новый проект. Пусть это будет приложение, отвечающее за хранение неких прав доступа. Назовем его AccessDB.

Заводим новый git-репозиторий и говорим:

git clone ...
cd accessdb
wget https://github.com/afiskon/erl-min-prj/raw/master/.gitignore
wget https://github.com/afiskon/erl-min-prj/raw/master/Makefile
wget https://github.com/afiskon/erl-min-prj/raw/master/rebar.config
wget https://github.com/afiskon/erl-min-prj/raw/master/rebar
chmod u+x rebar
./rebar create-node nodeid=accessdb

Здесь нет особой магии. Тянется rebar. В мире Erlang считается хорошей практикой держать его прямо в репозитории приложения. Таким образом, кто угодно может собрать проект, без долгого и мучительного выяснения, что такое rebar и где его вообще взять. В rebar.config в качестве единственной зависимости указывается lager. Редкое приложение на Erlang нынче обходится без него. Содержимое .gitignore довольно предсказуемо. В Makefile помимо традиционных вещей содержится небольшой сценарий для сборки deb-пакета с помощью FPM.

Разумеется, можно делать проекты на Erlang, заполняя эти файлы, а также файлы, речь о которых пойдет ниже, вручную. Но в какой-то момент это начинает надоедать, так что здесь я беру все почти готовое. «Почти», потому что пару строк все-таки придется поменять. Полное содержимое всех этих файлов я здесь не привожу. Кому интересно, может ознакомиться с ним самостоятельно.

Первые три строчки в Makefile правим на:

PROJECT=accessdb
DESCRIPTION="AccessDB"
HOMEPAGE="http://eax.me/erlang-deb-package/"

В файле .gitignore заменяем строчку /phonebook/ на /accessdb/.

Создаем новое приложение:

mkdir -p apps/accessdb
cd apps/accessdb
../../rebar create-app appid=accessdb
cd ../..

На одной Erlang’овой ноде может крутится, и, как правило, крутится, более одного приложения (которое OTP application). Хорошей практикой нынче считается складывать все приложения в каталог apps, а все зависимости проекта держать в каталоге deps.

В rebar.config заменяем:

{sub_dirs, [
    "apps/phonebook"
]}.

… на:

{sub_dirs, [
    "apps/accessdb"
]}.

Когда вы создаете новые приложения, их нужно добавлять в этот список.

В файле apps/accessdb/src/accessdb.app.src в список applications добавляем lager, правим vsn и description. В результате должно получится что-то вроде:

{application, accessdb,
 [
  {description, "AccessDB"},
  {vsn, git},
  {registered, []},
  {applications, [
                  kernel,
                  stdlib,
                  lager
                 ]},
  {mod, { accessdb_app, []}},
  {env, []}
 ]}.

В списке applications должны быть перечислены все приложения-зависимости вашего приложения. Обратите внимание, что в vsn вместо конкретной версии мы написали «git». За счет этого версия приложения будет генерироваться на основе тэгов в git. Это удобно, ибо не приходится при каждом изменении приложения обновлять его версию в .app.src-файле.

Далее:

cd files
wget raw.github.com/afiskon/erl-min-prj/master/files/vars.config
wget raw.github.com/afiskon/erl-min-prj/master/files/vars-dev.config

Эти файлы содержат значения переменных, подставляемых в разные другие файлы. Значения, понятное дело, разные, в зависимости от того, собираем ли мы приложения для боевого окружения или же разрабатываем его на dev-сервере. Ну, например, на боевом сервере логи нужно писать в /var/log/accessdb, а на dev-сервере — просто где-то в локальном каталоге.

В обоих файлах меняем первую строчку на:

{project, "accessdb"}.

Теперь тянем в каталог files скрипты init, postinst и postrm:

wget https://raw.github.com/afiskon/erl-min-prj/master/files/init
wget https://raw.github.com/afiskon/erl-min-prj/master/files/postinst
wget https://raw.github.com/afiskon/erl-min-prj/master/files/postrm

Здесь init — это тот самый скрипт, который запускается при выполнении команд типа sudo service accessdb start|stop. Скрипты postinst и postrm запускаются менеджером пакетов при установке и удалении пакета соответственно. Первый заводит пользователя и группу, под которыми будет работать приложение, создает необходимые каталоги, и так далее, а второй производит обратные действия. Все эти скрипты довольно тупые, но требуют аккуратного написания и хорошего тестирования.

Во всех трех скриптах меняем имя проекта и его описание, в том числе правим закомментированные строчки в файле init. Затем говорим:

rm sys.config
wget https://raw.github.com/afiskon/erl-min-prj/master/files/app.config
cd ..

В файле app.config (или sys.config, на самом деле его имя не так уж важно) хранятся настройки всех приложений в проекте. Те самые, которые доступны через функции application:get_env/2 и другие. Шаблон, которым мы здесь воспользовались, просто содержит довольно очевидные настройки для lager.

В файле reltool.config находим строчки (они там в разных местах):

{lib_dirs, []},
{copy, "files/accessdb", "bin/accessdb"},
{copy, "files/sys.config", "releases/\{\{rel_vsn\}\}/sys.config"},
{copy, "files/vm.args", "releases/\{\{rel_vsn\}\}/vm.args"}

… и заменяем их соответственно на:

{lib_dirs, ["apps","deps"]},
{template, "files/accessdb", "bin/accessdb"},
{template, "files/app.config", "etc/app.config"},
{template, "files/vm.args", "etc/vm.args"}

Здесь мы говорим (1) искать приложения в каталогах apps и deps, (2) подставлять в файлы accessdb, app.config и vm.args переменные из файлов vars.config или vars-dev.config, а также (3) меняем пути, по которым будут доступны итоговые app.config и vm.args.

Будьте внимательны! Легко не заметить, например, что файл sys.config заменяется на app.config.

Наконец, в files/accessdb находим строчки:

RUNNER_SCRIPT_DIR=$(cd ${0%/*} && pwd)
RUNNER_BASE_DIR=${RUNNER_SCRIPT_DIR%/*}
RUNNER_ETC_DIR=$RUNNER_BASE_DIR/etc
PIPE_DIR=/tmp/$RUNNER_BASE_DIR/
RUNNER_USER=
RUNNER_LOG_DIR=$RUNNER_BASE_DIR/log

… и заменяем их на:

RUNNER_SCRIPT_DIR={{runner_script_dir}}
RUNNER_BASE_DIR={{runner_base_dir}}
RUNNER_ETC_DIR={{runner_etc_dir}}
PIPE_DIR={{pipe_dir}}
RUNNER_USER={{runner_user}}
RUNNER_LOG_DIR={{runner_log_dir}}

Это просто sh-скрипт, через который будет запускаться наша AccessDB. Увы, его версия, сгенерированная rebar’ом, не очень хорошо определяет значения некоторых переменных (где искать конфиги, куда писать логи и тп). Поэтому мы решаем явно задавать соответствующие значения в файлах vars.config или vars-dev.config, и просто подставлять их в сей скрипт.

Самое время все это хозяйство закоммитить:

git add .
git commit -am 'AccessDB Initial Commit'
git push origin HEAD
git tag 0.1.0
git push origin 0.1.0

Создание тэга обязательно, так как он используется в качестве версии deb-пакета.

Затаив дыхание, говорим:

make run

Если все было сделано правильно, вы увидите приглашение:

(accessdb@127.0.0.1)1>

Дважды жмем Ctr+C. В файле accessdb/log/console.log должно быть записано что-то вроде:

Application lager started on node 'accessdb@127.0.0.1'
Application accessdb started on node 'accessdb@127.0.0.1'

Теперь попробуем собрать deb-пакет:

make deb

Должен появиться файл accessdb_0.1.0_amd64.deb. Устанавливаем и запускаем приложение, затем цепляемся к нему по remsh:

sudo dpkg -i accessdb_0.1.0_amd64.deb
sudo service accessdb start
erl -name remsh@127.0.0.1 -setcookie accessdb -remsh accessdb@127.0.0.1

Вновь должны увидеть приглашение REPL’а. Дважды нажимаем Ctr+C.

Останавливаем сервис:

sudo service accessdb stop

Настройки AccessDB лежат в /etc/accessdb, логи пишутся в /var/log/accessdb, само приложение лежит в /usr/lib/accessdb. Теперь проверим, как работает удаление.

Сначала останавливаем epmd:

sudo killall -9 epmd

Дело в том, что может не получится удалить пользователя accessdb, пока есть приложения (например, epmd), работающие под ним. Но при этом включить в deb-пакет скрипты, останавливающие epmd, мы не можем, так как это может помешать другим работающим локально Erlang-приложениям.

После этого мы можем сказать:

sudo apt-get remove accessdb
sudo apt-get purge accessdb

Несмотря на то, что все описанные действия занимают не более десяти минут времени (я замерял), постоянно повторить их скучно и чревато ошибками. Поэтому я написал скрипт, который делает все то же самое, только быстро и без ошибок. Пользоваться им очень просто:

wget raw.github.com/afiskon/erl-min-prj/master/scripts/new-erl-srv
chmod u+x new-erl-srv
./new-erl-srv mynewservice "My New Service" http://eax.me/

Несколько финальных замечаний.

Лично у меня не возникает потребности, скажем, в rpm пакетах, да и особо негде тестировать соответствующий функционал. Но если у вас такая потребность есть, вы можете попробовать сконвертировать полученный deb-пакет, используя утилиту alien. Или, что еще лучше, вы можете послать мне пуллреквест, добавляющий поддержку сборки rpm (напоминаю, FPM это умеет) и других пакетов.

Некоторые программисты любят включать в пакеты документацию, конфиги для Nginx и так далее. Я не уверен, что это правильно. Документацию (например, man-pages) лучше упаковывать в отдельный пакет, а также выкладывать в сети, где ее проиндексируют поисковые системы и любой желающий сможет просмотреть без установки каких-либо пакетов. Что же касается конфигов для Nginx, нехорошо привязывать пользователя к конкретному веб-серверу. Лучше держать эти конфиги в открытом репозитории, откуда их сможет скачать любой желающий. Всякие же настройки фаервола, квоты и так далее должны настраиваться Chef’ом, а не включаться в пакет.

В общем, если вам хочется добавить в генерируемые пакеты что-то помимо, скажем, списка зависимостей, скорее всего, вы делаете что-то не так. Либо привязываетесь к конкретному, довольно специфичному, окружению, либо из тупого, как валенок, пакета, делаете что-то раздутое и громоздкое, либо попросту решаете свою задачу не теми средствами.

Ссылки по теме:

Существенную помощь при написании этой заметки оказал @kpy3. Также не обошлось без помощи со стороны товарищей @defnull и @sum3rman. За что всем им огромное спасибо!

Как всегда, я буду рад вашим вопросам и дополнениям.

Метки: , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.