🌍

Earthly を GitHub Actions で使ってみよう

2022/08/14に公開

ビルドツール Earthly を使ってみよう第二弾です.

前回の記事はこちら
https://zenn.dev/emiksk/articles/f45c5fd771e90a

今回は CI 利用編ということで,GitHub Actions で Earthly を利用する例を紹介したいと思います.

サンプルコードは前回と同じリポジトリを用います.
https://github.com/emiksk/earthly-example

CI での Earthly の実行

ローカルで Earthly を実行する場合はビルドキャッシュがローカルに保存されますが, GitHub Actions などの CI プラットフォームでは CI の実行環境はジョブごとにまっさらな状態で提供されるため,ローカルのビルドキャッシュは利用できません[1].しかし, Earthly には inline cache と explicit cache と呼ばれる異なる実行環境間で共有可能なリモートキャッシュ機能を備えています.この記事ではこれらのキャッシュの利用方法を重点的に説明していきます.

サンプルコード

今回利用したサンプルコードの GitHub Actions ワークフローと Eathfile は以下のような実装となっています.

https://github.com/emiksk/earthly-example/blob/e2808af9d74cdd94531759bdf1413b4906f7a383/.github/workflows/ci-for-grpc.yaml

https://github.com/emiksk/earthly-example/blob/e2808af9d74cdd94531759bdf1413b4906f7a383/grpc/Earthfile

次の節から,ワークフローのジョブ単位で解説をしていきます.

docker ジョブ

最初に docker ジョブから説明していきます.
https://github.com/emiksk/earthly-example/blob/e2808af9d74cdd94531759bdf1413b4906f7a383/.github/workflows/ci-for-grpc.yaml#L36-L56

このジョブは gRPC サーバが動く docker イメージをビルドし, GHCR (GitHub Container Registry) にアップロードするジョブです.このジョブは main ブランチに変更があったときのみ実行されるようにしています.

Earthly のセットアップ

このジョブの42行目から45行目のステップで,earthly/actions-setup という Earthly 公式のアクションを利用しています.このアクションによって,GitHub Actions で簡単に Earthly を実行できます.ちなみにこのアクションの README では,使用する Earthly バージョンを version: 0.6.20version: ^0.6.20 で指定できると書いてますが,自分の確認したv1.0.1時点ではこの指定方法では動かず,version: v0.6.20 のように指定してあげる必要がありました.

Earthly の実行

このジョブの52行目から56行目のステップで,Earthly を実行し,+docker ターゲットのビルドを行なっています.この実行でいくつかオプションを利用しているので,オプションごとに説明していきます.

--push オプション

--push オプションは,生成したイメージをリモートレジストリにアップロードするためによく用いられます.たとえば,Earthfile+docker ターゲット内で SAVE IMAGE コマンドを --push オプション付きで利用している場合に, earthly --push +docker と実行するとビルドしたイメージをリモートレジストリにアップロードできます.少しややこしいですが,SAVE IMAGE コマンドと earthly 実行時の両方で --push オプションが必要であることに注意してください.

また,earthly コマンドの --push オプションは,--push オプション付きの RUN コマンドを実行するためのオプションでもあります.このユースケースは,外部に影響を与えるコマンドを定義し,--push オプション付きで実行したときだけそのコマンドを実行するといったものです.たとえば, +deploy ターゲットを定義し,そのターゲットの中で環境へのデプロイ操作は RUN --push 内に記述することで,earthly --push +deploy を実行したときだけ外部環境にデプロイするといったことが可能です.

SAVE IMAGE --push と似た挙動ですが,SAVE IMAGE --push--push なしで実行してもイメージのローカルへの保存は行われるので微妙に違いがあります.

https://docs.earthly.dev/docs/earthly-command

--push
Instructs Earthly to push any docker images declared with the --push flag to remote docker registries and to run any RUN --push commands. For more information see the SAVE IMAGE Earthfile command and the RUN --push Earthfile command.

https://docs.earthly.dev/docs/earthfile#save-image

--push
The --push options marks the image to be pushed to an external registry after it has been loaded within the docker daemon available on the host.

The actual push is not executed by default. Add the --push flag to the earthly invocation to enable pushing.

https://docs.earthly.dev/docs/earthfile#run

--push
Push commands are not run by default. Add the --push flag to the earthly invocation to enable pushing.

Push commands were introduced to allow the user to define commands that have an effect external to the build. This kind of effects are only allowed to take place if the entire build succeeds. Good candidates for push commands are uploads of artifacts to artifactories, commands that make a change to an external environment, like a production or staging environment.

後述しますが,--push オプションはリモートキャッシュのアップロードの可否にも関わります.リモートキャッシュを利用したいだけの時は --push なし,キャッシュのアップロードも行いたい時は --push ありにしましょう.

--ci オプション

--ci オプションは CI で Earthly コマンドを実行するのに役立ついくつかのオプションを有効にしてくれるオプションです.具体的には,以下のオプションを指定したのと同じ意味になります[2]

--use-inline-cache --save-inline-cache --no-output --strict

これらの内,--use-inline-cache オプションと --save-inline-cache オプションは inline cache 機能を用いるためのオプションです.inline cache はアップロードしたイメージをビルドのキャッシュとして用いるための仕組みです.--use-inline-cache オプションはビルド時に inline cache を読み込むようにするオプションで, --save-inline-cache はイメージをアップロードする時にそのイメージを inline cache として読み込めるようにメタデータを埋め込むようにするオプションです[3]

inline cache はアップロードされたイメージがビルドのキャッシュとして使えるものでないと効果がありません.マルチステージングビルドのようにビルドをするためのイメージと実際の成果物のイメージが分離していて,かつ最終成果物のイメージのみアップロードしている場合はあまり有効に働かないことが予想できます.後述しますが,今回の例では --remote-cache オプションを用いて explicit cache を利用しています.explicit cache と inline cache は現状併用できない[4]のでこのサンプルは inline cache を活用できていません.しかし,inline cache には,設定が簡単で利用することのオーバーヘッドが少ないという利点もあるので,効果的な場面があれば explicit cache の代わりに利用できると良さそうです.今のところあんまりユースケースが思いつかないので explicit cache 使えばいいかなと思ってますが.

inline cache の詳しい説明は Earthly Doc の Guides にある以下のドキュメントを参照してください.
https://docs.earthly.dev/docs/guides/shared-cache#inline-cache

--no-output オプションと --strict オプションは,それぞれイメージやアーティファクトを出力しないようにするオプションと再現性のない可能性のある処理を実行しないようにするオプションです.ここでの再現性のない可能性のある処理とは,LOCALLY コマンドや RUN --interactive コマンドを指してします.

https://docs.earthly.dev/docs/earthly-command

--no-output
Instructs Earthly not to output any images or artifacts. This option cannot be used with the artifact form or the image form.

--strict
Disallow usage of features that may create unrepeatable builds.

--remote-cache オプション

--remote-cache オプションは explicit cache を利用するためのオプションです.explicit cache は inline cache と異なり,ビルドキャッシュのためだけのイメージを指定し,そのイメージをキャッシュとして読み込んだり,アップロードしたりします.今回の例では,ghcr.io/emiksk/earthly-example-cache:cache を指定してビルドキャッシュを読み書きしています.

explicit cache はビルドに指定したターゲットのすべてのレイヤーと SAVE IMAGE --push を含むターゲットのレイヤーをキャッシュしますが,SAVE IMAGE --cache-hint コマンドを用いることで,依存するターゲットのキャッシュも含めることができます.たとえば,今回の例では +docker ターゲットが依存する +proto-go ターゲットや +deps ターゲットに SAVE IMAGE --cache-hint コマンドを追加しています.これにより,ビルドするためのイメージと最終的に生成されるイメージが分離してる場合もキャッシュの恩恵が得られます.

https://docs.earthly.dev/docs/earthfile#save-image

--cache-hint
Instructs Earthly that the current target should be included as part of the explicit cache.

ただし,explicit cache は inline cache と違い,追加のアップロードコストがかかります.なんでもかんでもキャッシュしようとするとキャッシュの効果が相殺されかねません.また,explicit cache はキャッシュをダウンロードすることによって機能するため,ダウンロード量が多くて時間がかかる処理よりも計算量が多くて時間がかかる処理の方が効果的に働きます[5].純粋なダウンロード処理をキャッシュするだけだとアップロードコストによって逆に CI の処理時間が悪化するかもしれません[6]

explicit cache の詳しい説明は Earthly Doc の Guides にある以下のドキュメントを参照してください.
https://docs.earthly.dev/docs/guides/shared-cache#explicit-cache-advanced

--remote-cache オプションも例に漏れず,--push オプションが指定されているときだけキャッシュをアップロードします.メインブランチで実行される CI でだけ --push オプションを付けてキャッシュをアップロードし,トピックブランチで実行される CI では --push オプションを付けずにキャッシュを参照するだけにするなどの使い方ができます.

環境変数での指定

Earthly コマンドのオプションは環境変数を用いて渡すことも可能です.CI で指定するオプションが多くなった時は環境変数で指定すると少し見やすくなるかもしれません.今回の例だと以下のように書き直すことが可能です.

- name: Build and push docker image as latest
  working-directory: grpc
  env:
    EARTHLY_PUSH=true
    EARTHLY_CI=true
    EARTHLY_REMOTE_CACHE="ghcr.io/emiksk/earthly-example-cache:cache"
  run: |
    TAG=$(date +%Y%m%d-%H%M)-$(git rev-parse --short=10 HEAD)
    earthly +docker --DOCKER_TAG=$TAG

lint ジョブと test ジョブ

次に lint ジョブと test ジョブを説明します.
https://github.com/emiksk/earthly-example/blob/e2808af9d74cdd94531759bdf1413b4906f7a383/.github/workflows/ci-for-grpc.yaml#L10-L34

lint ジョブと test ジョブはそれぞれ Earthfile で定義された +lint ターゲットと +test ターゲットを実行するジョブです.これらのジョブは main ブランチ以外のブランチの変更でも実行されるようになっていて,Pull request が作成されるようなトピックブランチでも実行されます.

これらのジョブでも --remote-cache オプションが指定されており,docker ジョブでアップロードされたキャッシュを読み込むようになっています.一方で,--push オプションは指定されていないため,このジョブが実行されてもリモートキャッシュはアップロードされません.

複数リモートキャッシュの読み込み

--remote-cache オプションは単一のイメージしか指定できないため,メインブランチでキャッシュをアップロードし,メインブランチとトピックブランチの両方で参照することはできますが,トピックブランチの CI でそのトピックブランチでのキャッシュとメインブランチでのキャッシュの両方を参照する使い方はできません.しかし,CI において,トピックブランチでのキャッシュを見てヒットしなければメインブランチのキャッシュを見るといった処理は一般的です.たとえば,GitHub Actions の actions/cache ではキャッシュのキーを複数渡せるようになっています.
https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows

そこで,Earhly に --cache-from オプションを追加し,複数の異なるイメージを参照できるようにする Pull request を送りました.
https://github.com/earthly/earthly/pull/2063

この変更はすでにマージされているので,次回のリリースで使用できるはずです (おそらく 0.6.22). この機能は以下のように使用できます.

earthly --push \
        --remote-cache=ghcr.io/emiksk/earthly-example-cache:topic-branch \
        --cache-from=ghcr.io/emiksk/earthly-example-cache::main \
	+docker

指定されたリモートキャッシュは優先度付きになっており,--remote-cache オプションで指定されたイメージが最優先で,その次は--cache-from オプションで先に指定されたイメージ順に優先度が高くなります.

この利用方法の難点として,リモートキャッシュのアップロードは --push オプションも必要となるため,単純に +docker ターゲットを指定すると,トピックブランチの CI でも最終成果物のイメージがアップロードされてしまうことです.このため,トピックブランチでの実行方法は多少工夫が必要そうです.

(余談) 異なる環境で同じ Earthly の実行環境を提供する Earthly Satellites

ここまで長々とリモートキャッシュの話をしてきましたが,Earthly の実行環境が同じであればローカルのビルドキャッシュが利用できるため,リモートキャッシュを用いるよりも CI の実行時間が短くなります.また,ローカルでビルドするときも,チームメイト間で同じ実行環境を利用できればビルドキャッシュを共有できます.これをモチベーションのひとつとして開発,提供されているのが Earthly Satellites です.

Satellite と呼ばれる Earthly 実行用のインスタンスを立ち上げ,Earthly 実行時に --sat オプションで Satellite を指定することで,その Satellite 上でビルドを行えるようです. Earthly Satellites とは別にシークレットを管理するための Cloud secrets も提供されており,セキュアに利用できるようです.

2022年8月時点では Earthly Satellites も Cloud secrets もまだ正式版ではない (それぞれ Beta と Experimental) ですが,気になる方は是非試してみてください.自分はまだ触っていないです.

ちなみに,Earthly CI というのもリリース予定らしいです.既存の CI プラットフォーム (特に GitHub Actions) のエコシステムはかなり強固な感じがしますが,新たな選択肢となり得るのか乞うご期待って感じです.
https://docs.earthly.dev/earthly-cloud/overview

Earthly CI (coming soon): A cloud-based continuous integration / continuous delivery system that allows you to continuously build your code in the cloud.

おわりに

以上 Earthly の CI 利用編でした.本編では語りませんでしたが,ローカルと CI で全く同じ環境でビルドを実行できるというのは嬉しい場面も多そうだなと感じました.今のところあまり Earthly 使ってますという人を見ることはないですが,ポテンシャルを秘めたツールだと思うので,ぜひみなさん使ってみてください.

ちなみに,Earthly のビルドはやはり Earthly を使っていて,ビルドがとても楽でした.

脚注
  1. self-hosted runners を利用すればビルドキャッシュをローカルに保持したまま,複数のワークフローを実行することも可能です.https://docs.github.com/ja/actions/hosting-your-own-runners/about-self-hosted-runners ↩︎

  2. --image オプションや --artifact オプションをつけた際は --use-inline-cache オプションと --save-inline-cache オプションのみが有効になります. ↩︎

  3. --save-inline-cache オプションは --push オプションもないと意味がないので,--use-inline-cache--push の組み合わせだけで十分だったのではないかと思わないでもない. ↩︎

  4. explicit cache に関する説明の中で "It is currently not possible to push both inline and explicit caches currently." と書かれていました (この記事を書いてる時に気付いた) . ↩︎

  5. キャッシュの効果に関しては inline cache でも同じことが言えますが,inline cache のキャッシュアップロードの追加コストがない (元々アップロードされるイメージをキャッシュとして利用する) 点は explicit cache との大きな違いです. ↩︎

  6. 純粋なダウンロード処理でも,依存ライブラリの取得のような大量の並列ダウンロードが発生する処理はレイヤーごともってきた方が効率良いんじゃないかと思ってますが,検証はできていないです.Earthly のドキュメントによると go mod download は純粋なダウンロード処理らしい.https://docs.earthly.dev/docs/guides/shared-cache#compute-heavy-vs-download-heavy ↩︎

Discussion