無印吉澤

Site Reliability Engineering(SRE)、ソフトウェア開発、クラウドコンピューティングなどについて、吉澤が調べたり試したことを書いていくブログです。

Exrm(Elixir Release Manager)を使った Phoenix アプリケーションのデプロイ

f:id:muziyoshiz:20161122004038p:plain

Elixir の勉強中

最近、職場の飲み会で同僚に「Erlang VM はいいぞ」と熱弁されたのをきっかけに、「プログラミング Elixir」を買って Elixir を触りはじめました。Elixir でサンプルコードを動かすだけだと身に付かなそうなので、「Phoenix で API サーバを書く」というのを当面の目標にしています。

Phoenix というのは Web フレームワークの名前で、Elixir 版の Rails みたいなものです。ディレクトリ構造なども Rails に近く、Rails 経験者にはとっつきやすい代物です。実際、MySQL から取得したデータをそのまま JSON で返すだけの単純な API サーバはすぐ書けました。

そして、この API サーバをレンタルサーバにデプロイしようとしたんですが、Phoenix には Capistrano に相当するものがないんですね。でもまあ、同じような処理を Ansible で書けば済むだろう……と最初は思ってたんですが、Erlang VM の機能をフルに使おうと思ったら Capistrano のような流儀ではうまくいかないことがわかってきました。今回はそんな話をします。

Phoenix アプリケーションのデプロイ方法

Phoenix のサイトにあるガイドでは、Phoenix アプリケーションを(Heroku とかではない)普通のサーバにデプロイする方法が2通り紹介されています。

方法1. ソースコードをサーバに置いて、mix phoenix.server で起動

1つ目の方法は、Deployment / Introduction にある方法です。

Phoenix アプリケーションをローカルで開発するときは mix phoenix server というコマンドで起動するのですが、本番環境でも同じように起動する、という方法です。以下のようにすると、デーモンとしても起動できます。

MIX_ENV=prod PORT=4001 elixir --detached -S mix phoenix.server

この方法は単純でわかりやすいのですが、アプリケーションの起動・停止や、決まったディレクトリへのログ出力、といった Web アプリで普通必要になるものを、自前で用意する必要がでてきます。

例えば、Phoenix アプリをデーモンとして起動した場合、PID を記録しておかないと停止できませんが、そういう処理を自分で書く必要があります。私の探した範囲では、How to reload the server in production. · Issue #1288 · phoenixframework/phoenix にある方法で、起動と同時に PID を記録できるようです。

elixir --detached -e "File.write! 'pid', :os.getpid" -S mix phoenix.server

また、ログ出力についても、onkel-dirtus/logger_file_backend などを使って、出力先ファイルを指定しておく必要があります。

方法2. Exrm でビルドした結果をサーバに置いて、Exrm が自動生成したスクリプトで起動

そしてもう1つは、Deployment / Exrm Releases にある方法です。

Elixir Release Manager (Exrm) というのは、Elixir で書かれたアプリケーションを、配布可能な tarball にまとめるためのビルドツールです。開発マシンやビルドサーバ上で mix release コマンドを実行すると、以下のようなディレクトリ構成でファイルが生成されます。ここでは、ビルドしたアプリケーションの名前を、仮に admiral_stats_api とします。

rel
└── admiral_stats_api
    ├── bin  (アプリケーション管理用のスクリプト群)
    │   ├── admiral_stats_api
    │   ├── admiral_stats_api.bat
    │   ├── install_upgrade.escript
    │   ├── nodetool
    │   └── start_clean.boot
    ├── erts-8.1  (Erlang ランタイム)
    │   ├── bin
    │   ├── doc
    │   ├── include
    │   ├── lib
    │   └── man
    ├── lib  (アプリケーションが依存するすべてのモジュール)
    └── releases
        ├── 0.0.1
        │   ├── admiral_stats_api.bat
        │   ├── admiral_stats_api.boot
        │   ├── admiral_stats_api.rel
        │   ├── admiral_stats_api.script
        │   ├── admiral_stats_api.sh
        │   ├── admiral_stats_api.tar.gz (※)
        │   ├── start.boot
        │   ├── start_clean.boot
        │   ├── sys.config
        │   └── vm.args
        ├── RELEASES
        └── start_erl.data

上記のツリーで (※) を付けた admiral_stats_api.tar.gz には、このファイル自身を除く配布物すべてが入っています。この tarball をデプロイ先(仮に /var/www/admiral_stats_api とする)で解凍して、

$ bin/admiral_stats_api start

を実行するとサーバが起動し、

$ bin/admiral_stats_api stop

を実行するとサーバが停止します。ログファイルは、自動生成される log ディレクトリ以下に出力されます。

また、Exrm の凄い点として、アプリケーションの無停止アップグレードが可能です。これは、Capistrano がやるような「一瞬止めて、シンボリックリンクの向き先を変えて、再起動」という無停止っぽいアップグレードではなくて、本当に無停止で、内部状態も含めてアップグレードする、という機能です。

例えば、新しいバージョン 0.0.2 をリリースしたいときは、さっきと同じように mix release で tarball を作り、その tarball をデプロイ先の /var/www/admiral_stats_api/releases/0.0.2/admiral_stats_api.tar.gz に置いてから、

$ bin/admiral_stats_api upgrade 0.0.2

を実行すると、以下のようなメッセージが表示されて、バージョン 0.0.2 が動作し始めます。

$ bin/admiral_stats_api upgrade 0.0.2
Release 0.0.2 not found, attempting to unpack releases/0.0.2/admiral_stats_api.tar.gz
Unpacked successfully: "0.0.2"
Generating vm.args/sys.config for upgrade...
sys.config ready!
vm.args ready!
Release 0.0.2 is already unpacked, now installing.
Installed Release: 0.0.2
Made release permanent: "0.0.2"

単純な API サーバで使うにはオーバースペックな機能な気もしますが、せっかく Erlang VM を使うんだし、今回はこちらの方法でデプロイすることにしました。

しかし、自動化しようとするとうまくいかない(なんで??)

上記の手順をコマンドで手で打ちながら確認して、「なるほど完全に理解した。この手順を単に Ansible で自動化すればデプロイ自動化できるよな!」と思って、こういう Ansible playbook を書きました。

  • git clone で最新版をダウンロード
  • Git リポジトリ上に置いてない、パスワードなどを含むファイル(prod.secret.exs)を自動生成
  • コンパイル(mix do deps.get, deps.compile, compile
  • リリース用の tarball を作成(mix release
  • tarball をデプロイ先にコピー
  • tarball を /var/www/admiral_stats_api/releases/バージョン番号 に配置(解凍はしない)
  • upgrade コマンドを実行(bin/admiral_stats_api upgrade バージョン番号

で、この playbook を実行したところ、最後の upgrade コマンド実行のところでエラーメッセージが出て失敗しました。playbook と同じコマンドを手で打ったところ、こんなエラーメッセージでした。

$ bin/admiral_stats_api upgrade 0.0.2
Release 0.0.2 not found, attempting to unpack releases/0.0.2/admiral_stats_api.tar.gz
Unpacked successfully: "0.0.2"
Generating vm.args/sys.config for upgrade...
sys.config ready!
vm.args ready!
Release 0.0.2 is already unpacked, now installing.
escript: exception error: no case clause matching
                 {error,{enoent,"/var/www/admiral_stats_api/releases/0.0.1/relup"}}

一体何なのこれ……。

Phoenix のサイトや Exrm のサイトを読んでも理由が分からず、「プログラミング Elixir」にもそれらしい説明はなく、エラーメッセージで検索した結果をひたすら探し回ってわかったのですが、どうやら

「0.0.1 から 0.0.2 にアップグレードするための tarball を作るためには、 rel/アプリケーション名/releases/0.0.1 ディレクトリ以下が残っている状態で mix release コマンドを実行しなければならない」

らしいです。

改めて確認したところ、0.0.1 ディレクトリがない状態で 0.0.2 の mix release を実行したところ、コンソール出力は以下のようになっていました。

$ MIX_ENV=prod mix release
Building release with MIX_ENV=prod.
==> The release for admiral_stats_api-0.0.2 is ready!
==> You can boot a console running your release with `$ rel/admiral_stats_api/bin/admiral_stats_api console`

その一方、0.0.1 ディレクトリがある状態で mix release を実行したところ、以下のように 0.0.1 から 0.0.2 へのアップグレードのためのファイルが生成されたことを示すメッセージが増えました。

$ MIX_ENV=prod mix release
Building release with MIX_ENV=prod.
This is an upgrade, verifying appups exist for updated dependencies..
==> All dependencies have appups ready for release!
==> Generated .appup for admiral_stats_api 0.0.1 -> 0.0.2
==> The release for admiral_stats_api-0.0.2 is ready!
==> You can boot a console running your release with `$ rel/admiral_stats_api/bin/admiral_stats_api console`

ここまでのまとめ:結局どうしたらいいの?

要するに、Exrm を使って Phoenix をリリースするためには、ビルド時に、少なくとも1つ前のバージョンのビルド結果をローカルに置いておく必要があります。upgrade コマンドを使ってアップグレードする限り、これは必須のようです。

世間の人はどうやって Exrm を使っているのか調べてみたところ、例えば andrewvy/ansible-elixir という role では、ビルド結果をすべて git push していました。ただ、Phoenix アプリのビルド結果には秘密情報(prod.secret.exs)も含まれるので、Phoenix アプリを OSS にするならこの手は使えません。秘密情報をすべて環境変数から読み込むように書き換えるという手はありますが、それでも、本来 Git に登録する必要がないファイルを Git に登録しなければならない、というデメリットは残ります。

Phoenix アプリケーションを開発してる人って、普通はどうやってデプロイしてるんでしょうか? Exrm なんて使わずに、mix phoenix.server で起動する方法を採用して、起動スクリプトは自分で書いてるんでしょうか。経験者の方にぜひ教えてほしいです……。

この記事の続き

続きを書きました。

muziyoshiz.hatenablog.com

参考ページ

参考書籍

プログラミングElixir

プログラミングElixir

第18章「OTP:アプリケーション」の18.6節「EXRM − Elixir のリリースマネージャ」に、Exrm の解説が8ページほど書かれています。内部状態をマイグレートするために必要なコードについても若干説明あり。

Programming Phoenix: Productive |> Reliable |> Fast

Programming Phoenix: Productive |> Reliable |> Fast

まだざっと読んだだけですが、デプロイに関する話題は(少なくとも独立した節は)なさそうです。