Chapter 25

リリースを使ったデプロイ

koga1020
koga1020
2021.11.23に更新

リリースを使ったデプロイ

必要な作業

このガイドに必要なのは、動作するPhoenixアプリケーションだけです。デプロイ用の簡単なアプリケーションが必要な方は、起動ガイドにしたがってください。

ゴール

このガイドの主な目的は、PhoenixアプリケーションをErlang VM、Elixir、すべてのコードと依存関係を含む自己完結型のディレクトリにパッケージ化することです。このパッケージは本番環境のマシンにドロップできます。

リリースとアセンブル!

リリースを作成するには、Elixir v1.9以上が必要ですが、このガイドでは、最新のリリースの改良点を利用するために、Elixir v1.11以降を使用していることを前提としています。

$ elixir -v
1.11.0

Elixirのリリースにまだ慣れていない場合は、先へ進む前にElixirの優れたドキュメントを読むことをオススメします。

これが終わったら、一般的なデプロイメントガイドの最後に mix release をつけて、すべてのステップを踏んでリリースを組み立てることができます。まとめてみましょう。

まず、環境変数を設定します。

$ mix phx.gen.secret
REALLY_LONG_SECRET
$ export SECRET_KEY_BASE=REALLY_LONG_SECRET
$ export DATABASE_URL=ecto://USER:PASS@HOST/database

そして依存関係をロードしてコードとアセットをコンパイルします。

# Initial setup
$ mix deps.get --only prod
$ MIX_ENV=prod mix compile

# Compile assets
$ MIX_ENV=prod mix assets.deploy

そして、mix release を実行します。

$ MIX_ENV=prod mix release
Generated my_app app
* assembling my_app-0.1.0 on MIX_ENV=prod
* using config/runtime.exs to configure the release at runtime

Release created at _build/prod/rel/my_app!

    # To start your system
    _build/prod/rel/my_app/bin/my_app start

...

リリースを開始するには、_build/prod/rel/my_app/bin/my_app start を呼び出します(my_appは現在のアプリケーション名に置き換えてください)。そうすれば、アプリケーションは起動するはずですが、実際にはウェブサーバーが起動していないことに気づくでしょう。これは、Phoenixにウェブサーバーを起動するように指示する必要があるからです。mix phx.server を使っているときは、phx.server コマンドがそれを代行してくれますが、リリースではMix(ビルドツール)がないので、自分たちでやらなければなりません。

config/runtime.exs (以前の config/prod.secret.exs または config/releases.exs) を開くと、"Using releases" に関するセクションがあり、設定すべきコンフィギュレーションがあるはずです。先に進み、その行のコメントを外すか、アプリケーション名に合わせて以下の行を手動で追加してください。

config :my_app, MyAppWeb.Endpoint, server: true

ここでもう一度、リリースを組み立てます。

$ MIX_ENV=prod mix release
Generated my_app app
* assembling my_app-0.1.0 on MIX_ENV=prod
* using config/runtime.exs to configure the release at runtime

Release created at _build/prod/rel/my_app!

    # To start your system
    _build/prod/rel/my_app/bin/my_app start

リリースを開始すると、ウェブサーバーも正常に起動するはずです。これで、_build/prod/rel/my_app ディレクトリにあるすべてのファイルを取得し、パッケージ化して、リリースを組み立てたのと同じOSとアーキテクチャを持つプロダクションマシンで実行できます。詳細はmix release のドキュメントを参照してください。

しかし、このガイドを終える前に、Phoenixアプリケーションのほとんどが使用するであろうリリースの機能がもう1つあります。それについて記載します。

Ectoマイグレーションとカスタムコマンド

本番システムでは、本番環境を整えるために必要なカスタムコマンドを実行することがよくあります。そのようなコマンドの1つが、データベースのマイグレーションです。本番環境の成果物であるリリースの中には、ビルドツールである Mix がないため、コレらのコマンドを直接リリースに持ち込む必要があります。

オススメは、lib/my_app/release.ex のような新しいファイルをアプリケーション内に作成し、次のように記述する方法です。

defmodule MyApp.Release do
  @app :my_app

  def migrate do
    load_app()

    for repo <- repos() do
      {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
    end
  end

  def rollback(repo, version) do
    load_app()
    {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
  end

  defp repos do
    Application.fetch_env!(@app, :ecto_repos)
  end

  defp load_app do
    Application.load(@app)
  end
end

最初の2行をアプリケーション名に置き換えてください。

これで MIX_ENV=prod mix release で新しいリリースを組み立て、eval コマンドを呼び出すことで、上のモジュールの関数を含む任意のコードを呼び出すことができます。

$ _build/prod/rel/my_app/bin/my_app eval "MyApp.Release.migrate"

これでおしまいです!

このアプローチを利用して、本番で実行する任意のカスタムコマンドを作成できます。今回は、load_app を使用しました。これは Application.load/1 を呼び出して、現在のアプリケーションを起動せずにロードします。しかし、アプリケーション全体を起動するカスタムコマンドを書きたい場合もあるでしょう。そのような場合は Application.ensure_all_started/1 を使用しなければなりません。アプリケーションを起動すると、Phoenixエンドポイントを含む現在のアプリケーションのすべてのプロセスが起動することを覚えておいてください。これは、特定の条件下で特定の子プロセスを起動しないようにスーパーバイザーツリーを変更することで回避できます。たとえば、リリースコマンドファイルで次のようにします。

defp start_app do
  load_app()
  Application.put_env(@app, :minimal, true)
  Application.ensure_all_started(@app)
end

そして、アプリケーションの中で Application.get_env(@app, :minimal) をチェックして、設定されている場合は子プロセスの一部だけを起動させます。

コンテナー

Elixirのリリースは、Dockerなどのコンテナー技術とうまく連携します。この考え方は、Dockerコンテナー内でリリースをアセンブルし、リリースの成果物に基づいてイメージを構築するというものです。

アプリケーションのルートで実行するDockerファイルの例を以下に示します。これはすべてのステップを含んでいます。

ARG MIX_ENV="prod"

FROM hexpm/elixir:1.11.2-erlang-23.1.2-alpine-3.12.1 as build

# install build dependencies
RUN apk add --no-cache build-base git python3 curl

# prepare build dir
WORKDIR /app

# install hex + rebar
RUN mix local.hex --force && \
    mix local.rebar --force

# set build ENV
ARG MIX_ENV
ENV MIX_ENV="${MIX_ENV}"

# install mix dependencies
COPY mix.exs mix.lock ./
RUN mix deps.get --only $MIX_ENV
RUN mkdir config

# copy compile-time config files before we compile dependencies
# to ensure any relevant config change will trigger the dependencies
# to be re-compiled.
COPY config/config.exs config/$MIX_ENV.exs config/
RUN mix deps.compile

COPY priv priv

# note: if your project uses a tool like https://purgecss.com/,
# which customizes asset compilation based on what it finds in
# your Elixir templates, you will need to move the asset compilation
# step down so that `lib` is available.
COPY assets assets
RUN mix assets.deploy

# compile and build the release
COPY lib lib
RUN mix compile
# changes to config/runtime.exs don't require recompiling the code
COPY config/runtime.exs config/
# uncomment COPY if rel/ exists
# COPY rel rel
RUN mix release

# start a new build stage so that the final image will only contain
# the compiled release and other runtime necessities
FROM alpine:3.12.1 AS app
RUN apk add --no-cache libstdc++ openssl ncurses-libs

ARG MIX_ENV
ENV USER="elixir"

WORKDIR "/home/${USER}/app"
# Creates an unprivileged user to be used exclusively to run the Phoenix app
RUN \
  addgroup \
   -g 1000 \
   -S "${USER}" \
  && adduser \
   -s /bin/sh \
   -u 1000 \
   -G "${USER}" \
   -h "/home/${USER}" \
   -D "${USER}" \
  && su "${USER}"

# Everything from this line onwards will run in the context of the unprivileged user.
USER "${USER}"

COPY --from=build --chown="${USER}":"${USER}" /app/_build/"${MIX_ENV}"/rel/my_app ./

ENTRYPOINT ["bin/my_app"]

# Usage:
#  * build: sudo docker image build -t elixir/my_app .
#  * shell: sudo docker container run --rm -it --entrypoint "" -p 127.0.0.1:4000:4000 elixir/my_app sh
#  * run:   sudo docker container run --rm -it -p 127.0.0.1:4000:4000 --name my_app elixir/my_app
#  * exec:  sudo docker container exec -it my_app sh
#  * logs:  sudo docker container logs --follow --tail 100 my_app
CMD ["start"]

最後に、/app にアプリケーションを作成し、bin/my_app start として実行できるようにします。

コンテナー化されたアプリケーションの設定についていくつかのポイントがあります。

  • コンテナー内でアプリケーションを実行する場合、Endpoint はコンテナーの外からアプリにアクセスできるように、「パブリック」な :ip アドレス(0.0.0.0.0.0.0 など)をリッスンするように設定する必要があります。ホストがコンテナーのポートを自身のパブリックIPに公開するか、それともlocalhostに公開するかは、ニーズによって異なります。
  • 実行時(config/runtime.exs を使用)に提供できる設定が多ければ多いほど、環境を超えてイメージを再利用できるようになります。とくに、データベースの認証情報やAPIキーのような秘密情報は、イメージにコンパイルするのではなく、そのイメージに基づいてコンテナーを作成する際に提供する必要があります。これが、Endpoint:secret_key_base が、デフォルトで config/runtime.exs に設定されている理由です。
  • 可能であれば、実行時に必要となる環境変数は、コード中に散らばるのではなく、config/runtime.exs の中で読み込むべきです。1つの場所ですべての環境変数が見えるようにすることで、コンテナーが必要なものを確実に得ることが容易になり、とくにインフラストラクチャの作業を行う人がElixirのコードを担当していない場合に把握しやすくなります。とくにライブラリは、環境変数を直接読んではいけません。すべての設定は、トップレベルのアプリケーションから、できればアプリケーション環境を使わずに渡されるべきです。