最近phoenix(elixir)を使っている. まぁelixir自体の話は,いろんな記事でおすすめされているのでそちらを参照してもらうとして,これをDockerに載せたいなーと思ったので.
なんでDockerに載せたいのか
erlangとelixirの環境構築を毎回やりたくない っていうのがすべて.
そういう意味では,別にchefでもよかった.
ただ,最近は開発環境はみんなDockerにしていて,家のLinuxでもほとんどDockerしか使っていないので,できればみんなDockerにしておきたい.
開発環境をDockerに
これは割と楽で,ふつうのことしかしない.
elixirは公式でもdocker imageを配布しているので,そちらを使わせてもらう.
FROM elixir:1.3.4-slim ENV APP_DIR /var/opt/app RUN set -x && \ apt-get update && \ apt-get install -y --no-install-recommends \ nodejs \ npm \ mysql-client \ inotify-tools \ git \ imagemagick \ curl && \ rm -rf /var/lib/apt/lists/* && \ npm cache clean && \ npm install n -g && \ n stable && \ ln -sf /usr/local/bin/node /usr/bin/node && \ apt-get purge -y nodejs npm RUN useradd -m -s /bin/bash elixir RUN echo 'elixir:password' | chpasswd RUN mkdir -p ${APP_DIR} USER elixir WORKDIR ${APP_DIR}
imagemagickとかcurlは,使うのであれば……というくらい. mysql-clientあたりは,DB接続に必要なので入れておく.mysql以外を使う場合には,相応のクライアントを.
npm関連はphoenixで必要となるので,入れておいた.
あとは,
version: '2' services: storage: image: busybox volumes: - /var/lib/mysql mysql: image: mysql:5.6 volumes_from: - storage ports: - "3306:3306" environment: MYSQL_ALLOW_EMPTY_PASSWORD: "true" elixir: image: h3poteto/phoenix env_file: .docker-env volumes: - .:/var/opt/app links: - mysql:mysql ports: - "4000:4000" command: /bin/bash
こんな感じのdocker-compose.ymlを作っておけばよい. 開発環境はこれでだいたい行けた.
本番環境をDockerにする
先に言っておくと,現段階で本番環境をDockerにするのは,そこまでサイコーとは言えない. 開発環境に関しては,かなり楽なので是非おすすめしたいが,本番環境については,もう少し考えたほうがいいかもしれない.
Phoenixのビルド
準備
リリース用ビルドはexrm
を使う.
まず, mix.exs
にexrm
を足す.
defp deps do [{:phoenix, "~> 1.2.0"}, {:phoenix_ecto, "~> 3.0"}, {:postgrex, ">= 0.0.0"}, {:phoenix_html, "~> 2.3"}, {:phoenix_live_reload, "~> 1.0", only: :dev}, {:cowboy, "~> 1.0"}, {:exrm, "~> 1.0"}] end
次に, config/prod.exs
に server: true
を追加する.
prodに独自の設定を追加したい場合はここに書く.
config :sample_app, SampleApp.Endpoint, http: [port: 4000], cache_static_manifest: "priv/static/manifest.json", server: true
cache_static_manifest
は,ビルド時にdigestつきのassetsを生成するため,それを参照できるように manifest.json
を指定しておく.
port
は好きに書き換えていいと思う.
これでビルドができる.
ビルド
ビルドをしてみる. 実際にリリースする際には,このビルド処理はdocker image生成時に行いたいので,ここでは試しにビルドするだけ.
適当にelixirの環境構築が済んでいるlinux上で,
$ export MIX_ENV=prod $ mix local.hex --force $ mix local.rebar --force $ mix deps.get --only prod $ mix deps.compile $ mix compile $ npm install $ npm run compile # ここは自分で設定したpackage.jsonのコマンドによるもの $ mix phoenix.digest # これにより,npmでcompileしたassetsにdigestをつけて,manifest.jsonを更新する $ mix release # リリース用のビルドを生成する
というような一連のコマンドを実行すると,リリース用ビルドが rel/<app>/bin/
の下にできている.
ちなみに新しいバージョンのリリース用ビルドを生成すると, rel/<app>/reelases/<version>/
にいろいろ増えていくので,この辺はやってみるとすぐにわかる.
で,やりたいこととしては,この成果物を詰め込んだdocker imageをデプロイしたい.
Dockerfile
Dockerfileについては,先ほどと同じものを使ってもいいし,インストールするものを減らして軽量化しても良い.
ただ,exrm
のビルドには,erlangのランタイム環境も含まれているため,ビルドと実行は同じプラットフォームで行う必要がある.
Macでビルドしたものをlinuxで実行はできない.
そのためdocker build時にphoenixのビルドしておきたい.
先ほどの開発環境用のDockerをベースに,以下のようなDockerfileを作った.
FROM h3poteto/phoenix:latest USER root COPY . ${APP_DIR} RUN chown -R elixir:elixir ${APP_DIR} USER elixir ENV MIX_ENV=prod RUN mix local.hex --force RUN mix local.rebar --force RUN mix deps.get --only prod RUN mix deps.compile RUN mix compile RUN npm install \ && npm run compile \ && rm -rf node_modules RUN mix phoenix.digest RUN mix release CMD rel/sample_app/bin/sample_app foreground
rel/sample_app
内のbinを普通に実行すると,デーモンが起動するが,Dockerなのでプロセスはフォアグラウンドで動いてほしい.
そのため,foreground指定をして起動している.
migration
デプロイ時にはmigrationをかけたいと思うだろう.
上記のdocker imageには,ビルド結果だけでなくソースも含まれているので,docker run
コマンド等で,mixコマンドを打っても別に動くのだが…….
ビルドされた成果物に対するコマンド実行が,exrm
側で用意されているため,migrationもできる.
http://blog.plataformatec.com.br/2016/04/running-migration-in-an-exrm-release/
$ rel/sample_app/bin/sample_app command Elixir.Release.Tasks migrate
これをdocker run
に渡してやれば良い.
困りどころ
configで使う値を環境変数で渡せない
実際にprod用のimageを作って実行してみて,困ったことがあった.
pheonixをnewしたときに,config/prod.secret.exs
というものが作られていると思う.
ここにはDB接続情報や,SECRET_KEY_BASE
などの機密情報を含む設定を書くらしい.
もちろんgitignoreされているので,本番にだけ置いてほしいということだろう.
DB接続情報
Dockerで本番をデプロイするにあたり,できればDB接続情報等の機密情報はコンテナ内に含めたくないと考えた.
なので,外から環境変数で渡すようにしようと思い,prod.secret.exs
を廃止し,prod.exs
を
config :sample_app, SampleApp.Endpoint, secret_key_base: System.get_env("SECRET_KEY_BASE") # Configure your database config :sample_app, SampleApp.Repo, adapter: Ecto.Adapters.MySQL, username: System.get_env("DB_USER"), password: System.get_env("DB_PASSWORD"), database: "sample_app", hostname: System.get_env("DB_HOST"), port: 3306, pool_size: 5
このように環境変数から読み込むようにした.
しかし,これではphoenix起動と同時に database connection error
が大量に出てきて,アプリケーションが起動できなかった.
どうやら,この設定,ビルド時に埋め込まれるらしく,ビルド時に $DB_USER
等の環境変数がセットされていないと,nil
がわたってしまう.
まさか実行時の展開じゃないとは…….
これにはみんな困るらしく,
先人がいた!!
ポイントとしては,ビルド時に埋め込まれてしまう環境変数については,"${DB_USER}"
というような文字列として埋め込みをしておく.
config :sample_app, SampleApp.Endpoint, secret_key_base: "${SECRET_KEY_BASE}" config :sample_app, SampleApp.Repo, adapter: Ecto.Adapters.MySQL, username: "${DB_USER}", password: "${DB_PASSWORD}", database: "sample_app", hostname: "${DB_HOST}", port: 3306, pool_size: 5
で,Docker起動時に $DB_USER
を渡すんだけど,同時にあわせて $RELX_REPLACE_OS_VARS=true
を渡せよってことらしい.
$ docker run --name sample_app --env SECRET_KEY_BASE=hogehoge \ DB_USER=hoge \ DB_PASSWORD=fuga \ DB_HOST=localhost \ RELX_REPLACE_OS_VARS=true \ sample_app:latest
見事これで環境変数を展開できた.
まぁ文字列しか渡せないので,例えば,portみたいなのはビルド時の型チェックで,Intじゃないと怒られて終わる.
ex_aws
もうひとつ秘密にしたいものがあって,ex_aws
というhexを使っているのだけど,これに渡すAWSの鍵だ.
こいつに関しては,ex_aws
のREADMEに書いてあるとおりに従った.
GitHub - CargoSense/ex_aws: A flexible, easy to use set of clients AWS APIs for Elixir
config :ex_aws, access_key_id: [{:system, "AWS_ACCESS_KEY_ID"}, :instance_role], secret_access_key: [{:system, "AWS_SECRET_ACCESS_KEY"}, :instance_role], region: "ap-northeast-1"
ここでも,System.get_env()
を使ったら全滅した.
OTPの切り替え問題
もうひとつ,こちらは規模が小さければ些細な問題である. が,elixirのいいところを潰してしまう問題でもある.
ネイティブのelixir環境
非Docker環境で,普通にexrm
でサービスをデプロイする場合,新しいビルドを生成する際には以下のような手順を踏むと思う.
- リリース用にアプリケーションのバージョンを上げる
- git pullして最新のソースを落としてくる
- mix release他,コマンドを打って,
rel/sample_app/releases
以下に最新のprodビルドを作る $ rel/sample_app/bin/sample_app upgrade 0.0.2
のようにバージョンアップコマンドを打つ
この最後のバージョンアップコマンドが曲者で,本来,elixirのリリースはこのようにバージョン番号をちゃんとアプリケーションに設定し,バージョン番号に従ってupgradeを行うように設計されている.
このupgradeの際,OTPでは古いバージョンと新しいバージョンをちゃんと区別して処理することができる. そのため,まだ古いアプリケーションからのリクエストがOTPに飛んでいても,最後まで処理してくれる.
非同期処理を含むアプリケーションのデプロイの泣きどこをうまいこと解消してくれている.
このOTP周りについては,
- 作者: Dave Thomas,笹田耕一,鳥井雪
- 出版社/メーカー: オーム社
- 発売日: 2016/08/19
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
この本にもう少し詳しく書いてある.
Docker上
Dockerの上でこのようなデプロイは,ほとんど不可能である.
コンセプト的に,Dockerは新しいバージョンには,新しいコンテナを生成する.
今回紹介した方法でも,rel
ディレクトリの下に成果物が溜まっていくことはなく,毎回最新のアプリケーションをビルドしたimageを生成するだけだ.
そして,それをswarmを使うなり,kurbernetesを使うなり,ECSを使うなりして切り替えるしかない.
この切り替えの時,確かにhttpのリクエストは前段のLoadBalancerとデプロイの組み合わせ方により,途切れさせることなく新しいコンテナ側に移行することができる.
しかし,リクエストにより発生した各種非同期処理は,コンテナ内で生き続けている可能性は否定できない. そしてそのコンテナを殺した場合には,非同期処理も途中で殺されてしまう.
まぁこの問題自体は,実はelixir固有の問題ではなくて,たとえばgoでgoroutineをいっぱい使って裏側で処理をしていた場合などにも,問題になってくるだろう.
まとめ
並列処理は人類には早いのかもしれない.
というのはあるのだが,どうにもphoenixはdockerとのかみ合わせがいまいちだった. 特に環境変数に関しては,configで設定する必要があるhexを入れるたびに,どうやって渡すかを検討する必要があるため,注意が必要だ.
そして,デプロイ時のOTPに関しては,Dockerでは解決方法がない.
今の段階では,まだchefで環境構築して,prod.secret.exs
をちゃんと用意した上で,upgradeを真面目にやったほうがいいのかなぁ.