Elixirのconfigファイルの落とし穴
まえがき
先日Elixirで書いたプログラムをデプロイしようとしてconfigファイルの書き方でハマってなかなか回答が見つかりませんでした。それどころかデプロイ方法自体あまり記事がないので、それも合わせて覚書のために残しておきます。
Elixirのデプロイ
Elixirをデプロイする方法はいくつかあるようですが、Elixir1.9からmix release
というコマンドが導入されたのでそれを使うのが良いようです。ですが、mix release
でデプロイをするとiex -S mix
などで立ち上げる場合には気づかないつまづきポイントがあるので注意が必要です。
コンパイル時に評価されるConfigファイル
ElixirのconfigファイルはElixir1.9からconfigディレクトリ配下に置くことになったようです。コンパイル時にconfig/config.exs
がまず読み込まれ、そこからMix.env/0
で取得されるデプロイ環境に合わせてconfig/dev.exs
やconfig/prod.exs
が読み込まれます。
Mix.env/0
で取得されるデプロイ環境はデフォルトではmix.exs
のproject
内にstart_parmanent
に書かれたものが使用されます。環境変数のMIX_ENV
にprod
などをセットするとそれが優先されて使用されます。よっぽどしっかりした開発体制を整えてテストなどをするというシチュエーションでもなければMIX_ENV
にprod
を指定するのとセットでmix release
を使うことになるでしょう。つまり、以下みたいなコマンドでリリース作業を行います。
$ MIX_ENV=prod mix release
例えばphx.new
などでPhoenixのプロジェクトを作った場合にはconfig
ディレクトリの中にdev.exs
とprod.exs
とprod.secret.exs
とtest.exs
が作られます。dev.exs
、prod.exs
、test.exs
については上記の通りで、prod.secret.exs
は名前の通りシークレットなどをリポジトリに含めないようprod.exs
と分けるために使用するようです。なので実際にはprod.secret.exs
はprod.exs
から呼ばれるようになっています。
大事なポイントは、これらのファイルがコンパイル時に評価されるという点です。つまり、プログラムの実行時に値を変えたいといったニーズに対応することができず、毎回コンパイルをし直すことになってしまいます。
実行時に評価されるConfigファイル
prod.exs
やprod.secret.exs
だけでは、コンパイルをしたときにreleases.exs
がないよとwarningが表示されてしまいます。releases.exs
もElixirのスクリプトの形になっているので、ほかのconfigファイルと同じように書くことができます。しかし1行目だけ他と書き方が異なります。
dev.exsの例:
use Mix.Config
...
releases.exsの例:
import Config
...
(詳しくelixirの挙動をハックしているわけではないので想像ですが)
これはコンパイル時にはコンパイルがMixタスクとして実行されるのでmixモジュールが存在しているのに対して、プログラムの実行時にはバイナリを叩くことになるので、mixモジュールがバイナリに含まれていないのだと思います。
これはコンパイル時のconfigではuse
を使い、実行時のconfigではimport
を使っているという違いが想定の根拠になっています。use
を使用する場合、コンパイル時に__using__
マクロ内に定義されたquote
が展開されるという挙動をします。つまり、実行時にMixモジュールを呼んでいるのではなく、Mixモジュール内に定義されたコードをコンパイル時にそこへ展開しているというわけです。
ようやく本題(実際に躓いたところ)
以上のデプロイについてはドキュメントを読めばmix release
やconfig
について公式ドキュメントを読めば書いてあるのですが、これのとおり書いてかつiex -S mix
時には発生しない問題が発生しました(この記事を書くに至った原因)。
前提
やろうとしていたことは、PhoenixでAPIサーバーを開発してあるタスクを定期実行しようとしていました。タスクを定期実行しようとした場合、僕の知る限りでは以下の3つのうちどれかの方法になるかと思います。
- Mixタスクを定義してCronから実行させる
- quantumを使って定期実行ジョブを定義
- 自前で定期実行用のGenServerを実装
実際にはCronやquantum以上にカスタマイズしたいパターンは無いと思うので(結果で次の時刻をカスタムするとかならやる意味はあるかも?)実質2択になるかと思います。今回の実装ではquantumを選択しました。理由としては、プロジェクトが趣味の開発でやっていて、ひとつのEC2インスタンス上で複数のサービスをdockerで共存させていたためです。
Cronを使う場合、ホストマシンのCronを利用するかコンテナ上にCronをインストールして実行することになります。しかし1つのコンテナで走らせるプロセスは1つだけというのがdockerのベストプラクティスのようなので、docker上に実装してしまうのは好ましくありません。かといってホストマシンのCronを使う場合、コンテナをまたぐのが面倒なのに加えて、ホストマシンのCronにいろいろなタスクが混じってしまい、管理しづらくなってしまいます。このような理由から今回はquantumを利用することにしました。
quantumでコードの定期実行
quantumの使い方として、configファイル内に定期実行する関数を指定する必要があります。そこでとあるQiitaの記事を真似して以下のようにconfigを書きました。
config :my_app, MyApp.Scheduler,
jobs: [
{"*/15 * * * *", fn -> MyApp.Periodicaly.do_something([]) end}
]
この定義はquantumのサンプルにもあるため、iex -S mix
で実行をする場合特に問題なく動作します。しかし、mix release
を実行すると以下のようなコンパイルエラーが発生します。(ひょっとするとelixirのバグとして報告するべきなのかもしれません)
** (Mix) Could not read configuration file. It likely has invalid configuration terms such as functions, references, and pids. Please make sure your configuration is made of numbers, atoms, strings, maps, tuples and lists. Reason: {3, :erl_parse, ['syntax error before: ', 'Fun']}
erlang側が構文解釈エラーを起こしてしまうため、コンパイルができないということですね。このエラーが表示されてからまる一日、指摘されている3行目とはどこの3行目なのかもわからず、ggったりコミットを戻したり色々試して原因調査をしました。
configファイルを色々いじったところ、ようやく原因がconfigファイルにあると気が付きました。そもそも問題を起こしている行はelixirのコードでは3行目ですらなかったのです。
エラーを起こさないconfigの書き方
今回のエラーの原因の種明かしをすると、configファイルに無名関数を使っていたことです。どうやらelixirではconfigファイルに無名関数を使用してはいけないようです。なので、エラーを起こさないようにquantumのconfigを書くと以下のようになります。
config :my_app, MyApp.Scheduler,
jobs: [
{"*/15 * * * *", {MyApp.Periodicaly, :do_something, [[]]}}
]
あとがき
今回のエラーは今まで遭遇した中でもかなり難しい部類になっていました。問題発生当時の状況を思い出すと、表示されたエラー文でggっても同じ問題が出てこず(出てきても違う原因で報告されたelixirのissuesばっかりで、しかもすでにOTP修正したよみたいな感じでcloseされていました)、そもそもmix release
について取り扱っている記事も少なく(特に日本語は殆どない)、ようやく原因がquantumのconfigだということに気がついて調べたら英語の質問記事が1つだけヒットみたいな状況でした。
皆mix release
でデプロイしてないのかなぁと思う今日この頃です。まぁまだあまりプロダクトレベルでelixirを採用している例があまりないからかもしれませんね。
Discussion