← На главную

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

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