Elixirのconfigファイルの落とし穴

[zennで書いた記事](https://zenn.dev/ten_takano/articles/20210223-mix-exs)のコピーです。

# まえがき

先日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の例：

```elixir
use Mix.Config

...
```

releases.exsの例：

```elixir
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](https://github.com/quantum-elixir/quantum-core)を使って定期実行ジョブを定義
- 自前で定期実行用のGenServerを実装

実際にはCronやquantum以上にカスタマイズしたいパターンは無いと思うので（結果で次の時刻をカスタムするとかならやる意味はあるかも？）実質2択になるかと思います。今回の実装ではquantumを選択しました。理由としては、プロジェクトが趣味の開発でやっていて、ひとつのEC2インスタンス上で複数のサービスをdockerで共存させていたためです。
Cronを使う場合、ホストマシンのCronを利用するかコンテナ上にCronをインストールして実行することになります。しかし1つのコンテナで走らせるプロセスは1つだけというのがdockerのベストプラクティスのようなので、docker上に実装してしまうのは好ましくありません。かといってホストマシンのCronを使う場合、コンテナをまたぐのが面倒なのに加えて、ホストマシンのCronにいろいろなタスクが混じってしまい、管理しづらくなってしまいます。このような理由から今回はquantumを利用することにしました。

## quantumでコードの定期実行

quantumの使い方として、configファイル内に定期実行する関数を指定する必要があります。そこでとあるQiitaの記事を真似して以下のようにconfigを書きました。

```elixir
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を書くと以下のようになります。

```elixir
config :my_app, MyApp.Scheduler,
  jobs: [
    {"*/15 * * * *", {MyApp.Periodicaly, :do_something, [[]]}}
  ]
```

# あとがき

今回のエラーは今まで遭遇した中でもかなり難しい部類になっていました。問題発生当時の状況を思い出すと、表示されたエラー文でggっても同じ問題が出てこず（出てきても違う原因で報告されたelixirのissuesばっかりで、しかもすでにOTP修正したよみたいな感じでcloseされていました）、そもそも`mix release`について取り扱っている記事も少なく（特に日本語は殆どない）、ようやく原因がquantumのconfigだということに気がついて調べたら英語の質問記事が1つだけヒットみたいな状況でした。
皆`mix release`でデプロイしてないのかなぁと思う今日この頃です。まぁまだあまりプロダクトレベルでelixirを採用している例があまりないからかもしれませんね。