👀

Phoenixプロジェクトで利用されているEarthfileを読んでみる

2022/08/05に公開

はじめに

ElixirのWebフレームワークであるPhoenixのプロジェクトではEarthlyというビルドツールが使われています。

https://earthly.dev/

Earthlyについてはこちらの記事が分かりやすく、参考にさせていただきました。

https://zenn.dev/kesin11/articles/7f4eed7cabf38d

今回はPhoenixのプロジェクトでEarthlyがどのように使われているのか、具体的にはEarthfileがどのように書かれているのかコードを読んでみたいと思います。

コード

こちらのcommit hashのコードを読んでみます。

https://github.com/phoenixframework/phoenix/blob/1636787952c736816312387222347770c9228f38/Earthfile

利用している箇所を確認する

まずこのEarthfileに定義したタスクがどこで利用されているかを確認します。このEarthfileはGitHub Actionで利用されているようです。ci上で以下のコマンドが存在します。

https://github.com/phoenixframework/phoenix/blob/1636787952c736816312387222347770c9228f38/.github/workflows/ci.yml

  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.exsmix.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 の記述があったりと微妙に異なるところもあるようです。参考にできそうです。

https://github.com/elixir-ecto/ecto/blob/master/Earthfile

PhoenixとEctoで使われていることから、ElixirのOSSではそこそこ利用が広がるのでは?と思ったりしています。ぜひこの機会にマスターしておきましょう。

Discussion