AWS Copilot で Phoenix Framework の本番環境を構築する リリース用ビルド編

## はじめに

AWS Copilot で Phoenix Framework の本番環境を構築するシリーズです

- [コスト・料金](https://qiita.com/RyoWakabayashi/private/a170c2ce9dd3cd1fe9ff)
- リリース用ビルド <- 今ここ
- [クラスタリング](https://qiita.com/RyoWakabayashi/private/b6888b510b20bb7579e8)
- [データベース](https://qiita.com/RyoWakabayashi/private/660b87f9deaaa179545d)
- [認証](https://qiita.com/RyoWakabayashi/private/9ea90df3970ec6e9d272)

実装したサンプルはこちらに格納しています

https://github.com/RyoWakabayashi/phoenix-channels-samples

## Elixir のリリース手順

以前の記事では `mix phx.server` を実行し、 Elixir の開発環境でそのまま Phoenix を起動していました

https://qiita.com/RyoWakabayashi/items/b831516ca9f012fb56b5

しかし、本番環境にはソースコードも Elixir も不要です

セキュリティやコンテナ軽量化のため、不要なものは何もない状態が理想です

そこで、 `mix release` を実行してビルドした実行可能ファイルのみを本番環境コンテナに配置します

![release.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/0fa8273c-81cb-ae1f-2c18-b69dc196c00d.png)

## Docker のマルチステージビルド

ビルド用コンテナでビルドして、リリース用コンテナにビルド結果をコピーするので、
Docker のマルチステージビルドを利用します

https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/multistage-build/

Dockerfile 内で複数回 FROM 句を書けばマルチステージビルドになります

実装例

```Dockerfile
###################### ここからビルド用コンテナ ######################
# ビルド用とリリース用は同じ OS でなければならない
# リリース用は軽くしたいので alpine にする
FROM elixir:1.13.4-alpine AS build

ARG SECRET_KEY_BASE

ENV MIX_ENV="prod"
ENV SECRET_KEY_BASE=${SECRET_KEY_BASE}

# ビルドに必要なパッケージをインストールする
RUN apk add --no-cache build-base git npm yarn python3

RUN mix local.hex --force && \
  mix local.rebar --force

# ソースコードをコピーする
COPY ./react_chat /app

WORKDIR /app

# 依存パッケージの取得など
RUN mix deps.get --only prod
RUN cd assets && yarn install
RUN mix assets.deploy
RUN mix phx.gen.release

# リリース用にビルドする
RUN mix release

###################### ここまでビルド用コンテナ ######################
###################### ここからリリース用コンテナ #####################
FROM alpine:3.15.4 AS app

ENV MIX_ENV="prod"
ENV HOME=/app
ENV PHX_SERVER=true

# 運用等に必要なパッケージをインストールする（curl や jq はクラスタリングに使用）
RUN apk add --no-cache openssl ncurses-libs curl jq libstdc++

WORKDIR /app

RUN chown nobody:nobody /app

USER nobody:nobody

# ビルド用コンテナからビルド結果をコピーする
COPY --from=build --chown=nobody:nobody /app/_build/${MIX_ENV}/rel/react_chat ./

# mix release で作られた実行可能ファイルから Phoenix を起動する
CMD ["bin/react_chat", "start"]
###################### ここまでリリース用コンテナ #####################
```

マルチステージビルドのポイント

- ビルド用コンテナとリリース用コンテナの OS は同じにする
- リリース用を軽量にしたいので Alpine Linux を指定する

https://www.alpinelinux.org/

- `FROM elixir:1.13.4-alpine AS build` というように別名を付ける
- `COPY --from=build` というようにビルド用コンテナの別名を指定してリリース用コンテナにコピーする

参考記事:

https://silbernagel.dev/posts/deploying-elixir-on-ecs-part-3/

## AWS Copilot の設定

リリース用の Dockerfile を Dockerfile_release とした場合、
AWS Copilot の `manifest.yml` では以下のように指定します

```yml
name: lb-svc # サービス名
type: Load Balanced Web Service

http:
  path: '/'

image:
  build:
    dockerfile: Dockerfile_release # Dockerfile の指定
    context: .
    args: # ビルド引数の指定
      SECRET_KEY_BASE: ${SECRET_KEY_BASE}
  port: 4000

cpu: 256
memory: 512
count:
  spot: 2
exec: true

# リリース用コンテナに渡す環境変数
variables:
  SECRET_KEY_BASE: ${SECRET_KEY_BASE}
```

`manifest.yml` では `${環境変数}` でローカルの環境変数を渡すことができます

`SECRET_KEY_BASE` に任意の値を設定してから `copilot svc deploy` を実行してください

## まとめ

かつては開発用EC2でビルドした結果を GitHub のリリースにアップロードして、
本番用EC2は GitHub のリリースからダウンロードする、
というややこしい手順を Ansible で実行していました

Docker のマルチステージビルドであれば、
ビルドからデプロイまで Dockerfile 内で定義でき、
非常に分かりやすいです