Phoenixプロジェクトで利用されているEarthfileを読んでみる
はじめに
ElixirのWebフレームワークであるPhoenixのプロジェクトではEarthlyというビルドツールが使われています。
Earthlyについてはこちらの記事が分かりやすく、参考にさせていただきました。
今回はPhoenixのプロジェクトでEarthlyがどのように使われているのか、具体的にはEarthfileがどのように書かれているのかコードを読んでみたいと思います。
コード
こちらのcommit hashのコードを読んでみます。
利用している箇所を確認する
まずこのEarthfileに定義したタスクがどこで利用されているかを確認します。このEarthfileはGitHub Actionで利用されているようです。ci上で以下のコマンドが存在します。
run: earthly --build-arg ELIXIR=${{ matrix.elixir }} --build-arg OTP=${{ matrix.otp }} --build-arg RUN_INSTALLER_TESTS=${{ matrix.run_installer_tests }} +test
run: earthly -P --build-arg ELIXIR=${{ matrix.elixir }} --build-arg OTP=${{ matrix.otp }} +integration-test
run: earthly --build-arg ELIXIR=1.13.3 --build-arg OTP=24.3.4 +npm
--build-arg
でビルド時の引数を渡して、test
, integration-test
, npm
のターゲットを実行しています。+<target名>
でEarthfileに定義したターゲットを実行します。
test
test
ターゲットを見てみます。
test:
FROM +test-setup
COPY --dir config installer ./
RUN MIX_ENV=test mix deps.compile
COPY --dir assets config installer lib integration_test priv test ./
# Run unit tests
RUN mix test
IF [ "$RUN_INSTALLER_TESTS" = "1" ]
WORKDIR /src/installer
RUN mix test
ELSE
RUN echo "Skipping installer tests"
END
compileに必要なディレクトリ(config
, installer
)をcopyした後にdeps.compileを実行しています。さらにテストの実行に必要なディレクトリもcopyし、mix test
を実行しています。
テストの実行環境は FROM
で指定しますが、ここではさらに別の test-setup
のターゲットを指定しています。
test-setup
ターゲットを見てみます。
test-setup:
FROM +setup-base
COPY mix.exs .
COPY mix.lock .
COPY .formatter.exs .
COPY package.json .
RUN mix local.rebar --force
RUN mix local.hex --force
RUN mix deps.get
今度は setup-base
ターゲットを FROM
に指定して、mix.exs
や mix.lock
など必要なファイルをcopyした後、mix deps.get
を実行しています。
順に setup-base
も見てみましょう。
setup-base:
ARG ELIXIR=1.13.3
ARG OTP=24.3.4
FROM hexpm/elixir:$ELIXIR-erlang-$OTP-alpine-3.16.0
RUN apk add --no-progress --update git build-base
ENV ELIXIR_ASSERT_TIMEOUT=10000
WORKDIR /src
FROM
でhexpm/elixirのdocker imageを取得して、最低限のパッケージ(git, build-base)をインストールしています。ci上で build-arg
で指定していた引数はここで使われています。引数を変更することで異なるelixirのバージョンでテストを実行できるように作られているようです。
-
setup-base
ターゲットでベースとなるdocker imageを取得 -
test-setup
でtestの実行の前処理(deps.get
)を行う -
test
ターゲットでmix testを実行
という具合に、テスト実行環境の構築を再利用可能ないくつかのtargetに分割しています。Dockerのmulti stage buildを読み書きしたことがある方はしっくりくると思います。
integration-test
続いて integration-test
ターゲットを見てみます。
integration-test:
FROM +setup-base
RUN apk add --no-progress --update docker docker-compose
# Install tooling needed to check if the DBs are actually up when performing integration tests
RUN apk add postgresql-client mysql-client
RUN apk add --no-cache curl gnupg --virtual .build-dependencies -- && \
curl -O https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_17.5.2.1-1_amd64.apk && \
curl -O https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/mssql-tools_17.5.2.1-1_amd64.apk && \
echo y | apk add --allow-untrusted msodbcsql17_17.5.2.1-1_amd64.apk mssql-tools_17.5.2.1-1_amd64.apk && \
apk del .build-dependencies && rm -f msodbcsql*.sig mssql-tools*.apk
ENV PATH="/opt/mssql-tools/bin:${PATH}"
# Integration test deps
COPY /integration_test/docker-compose.yml ./integration_test/docker-compose.yml
COPY mix.exs ./
COPY /.formatter.exs ./
COPY /installer/mix.exs ./installer/mix.exs
COPY /integration_test/mix.exs ./integration_test/mix.exs
COPY /integration_test/mix.lock ./integration_test/mix.lock
COPY /integration_test/config/config.exs ./integration_test/config/config.exs
WORKDIR /src/integration_test
RUN mix local.hex --force
# Ensure integration_test/mix.lock contains all of the dependencies we need and none we don't
RUN cp mix.lock mix.lock.orig && \
mix deps.get && \
mix deps.unlock --check-unused && \
diff -u mix.lock.orig mix.lock && \
rm mix.lock.orig
# Compile phoenix
COPY --dir assets config installer lib test priv /src
RUN mix local.rebar --force
# Compiling here improves caching, but slows down GHA speed
# Removing until this feature exists https://github.com/earthly/earthly/issues/574
# RUN MIX_ENV=test mix deps.compile
# Run integration tests
COPY integration_test/test ./test
COPY integration_test/config/config.exs ./config/config.exs
WITH DOCKER
# Start docker compose
# In parallel start compiling tests
# Check for DB to be up x 3
# Run the database tests
RUN docker-compose up -d & \
MIX_ENV=test mix deps.compile && \
while ! sqlcmd -S tcp:localhost,1433 -U sa -P 'some!Password' -Q "SELECT 1" > /dev/null 2>&1; do sleep 1; done; \
while ! mysqladmin ping --host=localhost --port=3306 --protocol=TCP --silent; do sleep 1; done; \
while ! pg_isready --host=localhost --port=5432 --quiet; do sleep 1; done; \
mix test --include database
END
このターゲットも先ほど test
ターゲットで利用した setup-base
を利用しています。いい感じに再利用できていますね。
-
apk add
で必要なパッケージをインストール -
COPY
コマンドでプロジェクトをファイルをcopy -
WITH DOCKER
を利用してdocker deamonを起動。docker-composeを利用してintegration-testに必要なデータベースを起動
という処理が記述されています。コードを見るだけでなんとなく追えますね。
docker-composeを使ったテストを行うために独自のシェルスクリプトなどを駆使せず、これを earthly +integration-test
だけで動かせるのはシンプルで個人的には好きです。
npm
最後に npm
のターゲットを見てみます。
npm:
FROM node:12-alpine3.12
WORKDIR /src
RUN mkdir assets
# Copy package.json + lockfile separatelly to improve caching (JS changes don't trigger `npm install` anymore)
COPY assets/package* assets
WORKDIR assets
RUN npm install
COPY assets/ .
RUN npm test
これまでの test
, integration-test
と違って、FROM
にnodeのdocker imageが指定されています。実行したいのは npm test
なのでelixirの実行環境は不要であり、nodeの実行環境があれば良いのです。
こんな具合に、nodeのdockerfileとelixirのdockerfileを別々に用意することもなく、Earthfileの中で FROM
で指定したimageを使ってテストを実行できます。
まとめ
Phoenixプロジェクトで使われているEarthfileを読んでみました。実際に使われているプロジェクトを見ると使用感がなんとなく湧きますね。Phoenixではテスト用途でしたが、GitHub Actionでのbuildなどにも活用できるシーンは多々あると思います。
ちなみにこのEarthlyはEctoのプロジェクトでも利用されています。こちらでは lint
といった別のタスクもあったり GIT CLONE
の記述があったりと微妙に異なるところもあるようです。参考にできそうです。
PhoenixとEctoで使われていることから、ElixirのOSSではそこそこ利用が広がるのでは?と思ったりしています。ぜひこの機会にマスターしておきましょう。
Discussion