Phoenix Production デプロイへの道その1：生Phoenixを手元で動かす編

[前回の記事](http://qiita.com/tmaeda/items/e609a7c5aac91913cb7c) で、アプリケーションが作れそうな実感は得られたので、
今度はデプロイできる実感を得たいと思い、デプロイの手順を試してみました。

結構紆余曲折しましたので、急いで手順だけ知りたい方は、一番最後のまとめだけ読んでください。

あとですね...Elixir/ErlangにはMacでビルドしたやつはLinuxで動かないという問題があると噂に聞いていますので、手元で動くようになったとしても本番サーバーで動かせるようになるまではまだまだ遠いかもしれません。

## 公式ドキュメントを読んでみる

https://hexdocs.pm/phoenix/deployment.html

デプロイするには3つのステップがある。

- Application secrets の処理
- Assets のコンパイル
- プロダクションモードでサーバーの起動

そして、これらがどのように行われるかは、インフラによって異なり、

- Heroku を使う場合
- Heroku を使わないなら Distillery を使うのをオススメ

の 2 つの方法が主にオススメされているようです。


まずは上記の 3 つのステップについて、もう少しドキュメントを
読んで手元で試してみましょう。

## Application secrets の扱い

これは、DBのユーザー名やパスワードなどのことですね。

Phoenix ではこれらは `config/prod.secret.exs` というファイルに
書くことになっていて、git には入れないみたいです。

ちょっと中身を見てみましょう。


``` elixir:/blog/config/prod.secret.exs
use Mix.Config

# In this file, we keep production configuration that
# you'll likely want to automate and keep away from
# your version control system.
#
# You should document the content of this
# file or create a script for recreating it, since it's
# kept out of version control and might be hard to recover
# or recreate for your teammates (or yourself later on).
config :blog, BlogWeb.Endpoint,
  secret_key_base: "hogehogefugafuga+hogehogefugafuga+jugemjugemgokounosurikire"

# Configure your database
config :blog, Blog.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: "hogehoge",
  password: "fugafuga",
  database: "blog_prod",
  pool_size: 15

```

Rails の database.yml とか secrets.yml 的なやつですね。

で、この情報をどうにかして production 環境に配置する必要があるが、

- Heroku でやるように環境変数経由で渡す方法
- デプロイで上書きされない領域 (`/var/config.prod.exs` など)に配置して、このパスから読み込むようにする方法(Capistrano の shared みたいなイメージですかね？)

などが考えられる、と。

## Assets のコンパイル

Phoenix ではデフォルトでは [brunch](http://brunch.io/) を使うよ、と。
なんでしょう？初耳なんですけど。 サイトを見ると、gulp と比較してるようなので、
gulp みたいなやつなんでしょう、とわかったつもりになったことにして、次へ。

で、ビルドはこうする、と。

```
# 必要なライブラリを入れて
$ mix deps.get --only prod

# コンパイルして
$ MIX_ENV=prod mix compile

# assets のコンパイルして
$ brunch build --production

# assets のファイル名にダイジェストを付与する
$ MIX_ENV=prod mix phoenix.digest
```

やってみましょう。

```
$ mix deps.get --only prod
Running dependency resolution...
Dependency resolution completed:
  connection 1.0.4
  cowboy 1.1.2
  cowlib 1.0.2
  db_connection 1.1.2
  decimal 1.4.0
  ecto 2.1.6
  gettext 0.13.1
  mime 1.1.0
  phoenix 1.3.0
  phoenix_ecto 3.2.3
  phoenix_html 2.10.4
  phoenix_pubsub 1.0.2
  plug 1.4.3
  poison 3.1.0
  poolboy 1.5.1
  postgrex 0.13.3
  ranch 1.3.2
All dependencies up to date
```

ふむ。

```
$ MIX_ENV=prod mix compile
==> connection
Compiling 1 file (.ex)
Generated connection app
==> gettext
Compiling 1 file (.erl)
Compiling 20 files (.ex)
warning: String.strip/1 is deprecated, use String.trim/1
  lib/gettext/po/parser.ex:47

warning: String.strip/1 is deprecated, use String.trim/1
  lib/gettext/po/parser.ex:55

warning: String.lstrip/1 is deprecated, use String.trim_leading/1
  lib/gettext/po/parser.ex:65

warning: String.to_char_list/1 is deprecated, use String.to_charlist/1
  lib/gettext/po/tokenizer.ex:147

Generated gettext app
===> Compiling ranch
===> Compiling poolboy
==> decimal
Compiling 1 file (.ex)
warning: Integer.to_char_list/1 is deprecated, use Integer.to_charlist/1
  lib/decimal.ex:944

warning: Integer.to_char_list/1 is deprecated, use Integer.to_charlist/1
  lib/decimal.ex:963

warning: Integer.to_char_list/1 is deprecated, use Integer.to_charlist/1
  lib/decimal.ex:986

Generated decimal app
warning: String.strip/1 is deprecated, use String.trim/1
  /Users/tmaeda/src/github.com/tmaeda/phenix-tut/blog/deps/poison/mix.exs:4

==> poison
Compiling 4 files (.ex)
warning: Integer.to_char_list/2 is deprecated, use Integer.to_charlist/2
  lib/poison/encoder.ex:173

Generated poison app
==> db_connection
Compiling 23 files (.ex)
Generated db_connection app
==> phoenix_pubsub
Compiling 12 files (.ex)
Generated phoenix_pubsub app
===> Compiling cowlib
src/cow_multipart.erl:392: Warning: crypto:rand_bytes/1 is deprecated and will be removed in a future release; use crypto:strong_rand_bytes/1

===> Compiling cowboy
==> mime
Compiling 1 file (.ex)
warning: String.strip/1 is deprecated, use String.trim/1
  lib/mime.ex:28

Generated mime app
==> plug
Compiling 1 file (.erl)
Compiling 44 files (.ex)
Generated plug app
==> phoenix_html
Compiling 8 files (.ex)
Generated phoenix_html app
==> phoenix
Compiling 74 files (.ex)
Generated phoenix app
==> postgrex
Compiling 62 files (.ex)
Generated postgrex app
==> ecto
Compiling 69 files (.ex)
Generated ecto app
==> phoenix_ecto
Compiling 4 files (.ex)
Generated phoenix_ecto app
==> blog
Compiling 17 files (.ex)
Generated blog app
```

`Generated ほにゃらら app` ってのがいっぱい出てますね。
いわゆるライブラリがひとつの小さなアプリケーションという考え方なんですかね？

```
$ brunch build --production
-bash: brunch: コマンドが見つかりません
```

なるほどw brew でもダメだったんで、仕方なくサイトを読んで、
npm で入れるらしいことがわかりました。が、yarn 推しなので yarn で入れますよ。

```
$ yarn global add brunch
yarn global v0.24.6
warning No license field
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 📃  Building fresh packages...
warning Your current version of Yarn is out of date. The latest version is "0.27.5" while you're on "0.24.6".
success Installed "brunch@2.10.10" with binaries:
      - brunch
warning No license field
✨  Done in 11.45s.
```

気を取り直して。

```
$ brunch build --production
15:34:07 - error: The directory does not seem to be a Brunch project. Create brunch-config.js or run brunch from the correct directory. Cannot find module '/blog/brunch-config'
15:34:07 - error: Here's a minimal brunch-config.js to get you started:

module.exports = {
  files: {
    javascripts: {
      joinTo: 'app.js'
    }
  }
};
```

はいいい？？？

どこかに config あるんですかねぇ？

```
$ find . -name brunch-config.js
./assets/brunch-config.js
./deps/phoenix/brunch-config.js
```

ほほーう。じゃ、assets の下でやってみますか。

```
$ cd assets
$ brunch build --production
15:36:59 - info: compiled 6 files into 2 files, copied 3 in 2.6 sec
```

お、なんかできたっぽいですね。次。

```
$ MIX_ENV=prod mix phoenix.digest
** (Mix) The task "phoenix.digest" could not be found. Did you mean "phoenix.new"?
```

はいいいい？？？ディレクトリが違うんですかね？

```
$ cd ..
$ MIX_ENV=prod mix phoenix.digest
mix phoenix.digest is deprecated. Use phx.digest instead.
Check your digested files at "priv/static"
```

できたけど、deprecated だと。

```
$ MIX_ENV=prod mix phx.digest
Check your digested files at "priv/static"
```

ふぅ、やっと辿り着いた。 Check しろっていうんで、Check しますか。

```
$ ls -1 priv/static/
cache_manifest.json
css
favicon-a8ca4e3a2bb8fea46a9ee9e102e7d3eb.ico
favicon.ico
images
js
robots-067185ba27a5d9139b10a759679045bf.txt
robots-067185ba27a5d9139b10a759679045bf.txt.gz
robots.txt
robots.txt.gz
```

ふむふむ、なるほど。digest つける作業って、 brunch でやりそうな感じがしますけど、
mix でやるんですねぇ。


## サーバーの起動

```
$ PORT=4001 MIX_ENV=prod mix phoenix.server
```

ですが、必要に応じて、この前に

```
$ MIX_ENV=prod mix ecto.migrate
```

もやらないといけない、と。

やってみましょう。

```
$ MIX_ENV=prod mix ecto.create
The database for Blog.Repo has been created

$ MIX_ENV=prod mix ecto.migrate

15:45:23.165 [info]  == Running Blog.Repo.Migrations.CreateArticles.change/0 forward

15:45:23.165 [info]  create table articles

15:45:23.171 [info]  == Migrated in 0.0s
```

で、


```
$ PORT=4001 MIX_ENV=prod mix phoenix.server
mix phoenix.server is deprecated. Use phx.server instead.
15:46:06.683 [info] Running BlogWeb.Endpoint with Cowboy using http://:::4001
```

ん？デーモンモードじゃないの？？？しかも、また deprecated って出てるし...

```
$ PORT=4001 MIX_ENV=prod mix phx.server
15:47:55.711 [info] Running BlogWeb.Endpoint with Cowboy using http://:::4001
```

んー、おんなじですねぇ...

一応、この状態で、ブラウザからアクセスしてみますか。

![プロダクションモードでの Article 一覧](https://qiita-image-store.s3.amazonaws.com/0/30009/62096062-d965-efc3-da51-1c53d6ffc609.png)


おー、出た出た。まぁ、当然ですけどね。心なしか、development モードより、
キビキビ動くような気がします。

ログはこんな感じ。

```
15:48:27.978 request_id=4jddi0sv4o928pnv33dq813r88cu1eht [info] GET /
15:48:28.023 request_id=4jddi0sv4o928pnv33dq813r88cu1eht [info] Sent 200 in 44ms
15:48:34.577 request_id=3drdgeaa3udj8uglvkhskb1sf0tfcrp8 [info] GET /articles
15:48:34.626 request_id=3drdgeaa3udj8uglvkhskb1sf0tfcrp8 [info] Sent 200 in 48ms
15:48:36.313 request_id=nohsc54o07271kmabhoppupuoj8sketn [info] GET /articles/new
15:48:36.361 request_id=nohsc54o07271kmabhoppupuoj8sketn [info] Sent 200 in 48ms
15:49:17.992 request_id=7i7esq0kfsn75us520gs62of0rkk3s4q [info] GET /articles
15:49:18.007 request_id=7i7esq0kfsn75us520gs62of0rkk3s4q [info] Sent 200 in 14ms
15:49:40.925 request_id=3ljqp8ke710rkj0hvc0ejk7k766k1lhi [info] GET /articles/new
15:49:40.926 request_id=3ljqp8ke710rkj0hvc0ejk7k766k1lhi [info] Sent 200 in 548µs
15:49:44.194 request_id=bslu70c0k8o8tp3ia96gu9iqru4ja59f [info] POST /articles
15:49:44.276 request_id=bslu70c0k8o8tp3ia96gu9iqru4ja59f [info] Sent 302 in 81ms
15:49:44.302 request_id=voludfcpgkl3u1f7gvntgqpf0509m6b7 [info] GET /articles/1
15:49:44.359 request_id=voludfcpgkl3u1f7gvntgqpf0509m6b7 [info] Sent 200 in 56ms
```

SQLとか全くでないんですね。噂に聞く「マイクロ秒」ってのがチラホラ出てますね。

エラーページはこんな感じ。当然、スタックトレースなどは出なくなってます。

![プロダクションモードでのエラー画面](https://qiita-image-store.s3.amazonaws.com/0/30009/9622c72d-1db9-b3bc-6007-8dc120dc6d9a.png)

開発者コンソールのNetworkタブはこんな感じ。

![プロダクションモードでのNetworkタブ](https://qiita-image-store.s3.amazonaws.com/0/30009/0fe7dfff-0ae7-a4c9-e28b-8d8678951970.png)

websocket が居ないですね。やはりあれは livereload 専用だったのか...

さて、デーモンモードの話に戻ります。

公式ドキュメントには別のコマンドも載っているので、そっちも試してみましょう。

```
$ PORT=4001 MIX_ENV=prod iex -S mix phx.server
Erlang/OTP 20 [erts-9.0] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

17:17:44.362 [info] Running BlogWeb.Endpoint with Cowboy using http://:::4001
Interactive Elixir (1.5.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 17:18:40.549 request_id=1rbg8400df6nrk1jnokvoqq7hhua54sn [info] GET /
17:18:40.601 request_id=1rbg8400df6nrk1jnokvoqq7hhua54sn [info] Sent 200 in 52ms

nil
iex(2)> 17:18:56.882 request_id=cg6384a3pdm9gkn1vthouvfh2vd3nc0l [info] GET /articles
17:18:56.941 request_id=cg6384a3pdm9gkn1vthouvfh2vd3nc0l [info] Sent 200 in 59ms

nil
iex(3)>
```

おやぁ？？？何ですか、これはぁ？ rails s と rails c を合わせたような...使い途がよくわかりません。

次、

> Or run it detached from the iex console. This effectively daemonizes the process so it can run independently in the background
```
MIX_ENV=prod PORT=4001 elixir --detached -S mix do compile, phoenix.server
```


って書いてるので、これがデーモンなんでしょうね。しかし、このコマンド、絶対に違うような気がするので、気を利かせて以下のように実行してみます。

```
$ PORT=4001 MIX_ENV=prod elixir --detached -S mix phx.server
```

あー、動きました。動きましたけど、ロ、、ログファイルは、、、

findしてもそれっぽいものが見つからないので、ググると、

https://elixirforum.com/t/where-to-find-log-file-in-phoenix-detached-mode/3622

`logger_file_backend` を使え、と。なるほどー。うーん...


`--detached` というのは VM を console から切り離した状態で動かすよ、ということらしいです。

```
$ elixir --help
Usage: elixir [options] [.exs file] [data]

  -e COMMAND                  Evaluates the given command (*)
  -r FILE                     Requires the given files/patterns (*)
  -S SCRIPT                   Finds and executes the given script in PATH
  -pr FILE                    Requires the given files/patterns in parallel (*)
  -pa PATH                    Prepends the given path to Erlang code path (*)
  -pz PATH                    Appends the given path to Erlang code path (*)

  --app APP                   Starts the given app and its dependencies (*)
  --cookie COOKIE             Sets a cookie for this distributed node
  --detached                  Starts the Erlang VM detached from console # 👈
  --erl SWITCHES              Switches to be passed down to Erlang (*)
  --help, -h                  Prints this message and exits
  --hidden                    Makes a hidden node
  --logger-otp-reports BOOL   Enables or disables OTP reporting
  --logger-sasl-reports BOOL  Enables or disables SASL reporting
  --name NAME                 Makes and assigns a name to the distributed node
  --no-halt                   Does not halt the Erlang VM after execution
  --sname NAME                Makes and assigns a short name to the distributed node
  --version, -v               Prints Elixir version and exits
  --werl                      Uses Erlang's Windows shell GUI (Windows only)

```
ログについては以下のような方法が思い浮かびますが...

- [Qiita:標準出力にログを吐くアプリケーションのデーモンを start-stop-daemon で立てる](http://qiita.com/catatsuy/items/067a4966463d440a29c1)
- [Qiita:CentOS7で使われているsystemctlについて少しまとめた](http://qiita.com/tukiyo3/items/092fc32318bd3d1a0846)


## まとめ

だいぶ紆余曲折したので、もう一度手順まとめますね。

```
# 事前準備
$ yarn global add brunch
```

```
$ mix deps.get --only prod
$ MIX_ENV=prod mix compile

$ cd assets
$ brunch build --production

$ cd ..
$ MIX_ENV=prod mix phx.digest

$ MIX_ENV=prod mix ecto.create
$ MIX_ENV=prod mix ecto.migrate

$ PORT=4001 MIX_ENV=prod mix phx.server
```

- プロダクションで動かす手順がわかりました。
- 公式ドキュメントにはだいぶ地雷があります。あとで時間ができたらプルリクエストの状況確認しておきます。
- ログの扱いなど、いろいろ考えることがありそう...
  - この辺、Heroku は結構うまくやってくれそうな気がしているので、 次回は Heroku でお会いしましょう