gRPC サーバのビルドに Earthly を使ってみよう
今 Earthly というビルドツールが (自分の中で) 俄かに話題になっています.自分で手を動かしてサンプルコードを作ってみたので,それを基に簡単に機能を紹介したいと思います.題材は Go + gRPC です.
Earthly って何?
Earthly は Makefile と Dockerfile を足したような書き味のビルドツールで, Makefile のように複数のタスクを定義し,タスクの中で Dockerfile を書くような感じでテストのような命令を実行したり,イメージを生成して保存したりできます.
すでに他の方が Earthly を紹介している記事もあるのでぜひご覧ください.
Earthly の大きな特徴は "Makefile + Dockerfile 風な構文の馴染みやすさ", "タスクをコンテナ上で実行することによる可搬性の高さ", "ビルドキャッシュの仕組みの簡単さ (Docker と同じ考え方なので理解しやすい)" の3つです.
感触的には Dockefile で (複数のイメージやコンテナを駆使しながら) いい感じにタスクを定義できるようになるツールって感じなので, Docker を利用してきた開発者であれば,すぐに使えるようになるのではないかなと思います.
言語非依存のツールなので, Docker さえ使えれば基本どんなプロジェクトでも有用ですが,特にビルドツールの可搬性を重視したいプロジェクトや Bazel を入れるほどではないが Makefile だと物足りないというプロジェクトに良いかもしれません (Bazel触ったこと全くないけど).Makefile + Dockerfile でビルドしていたようなプロジェクトは Earthly (Earthfile) 単体で置き換えることができそうです.
使ってみよう
ということで,実際に Earthly を使ってみて使い心地を試してみましょう.
最近 Go + gRPC でプログラムを書く機会があったので,そのプロジェクトを単純化して Earthly を導入してみました.
今回の上のコードを参考に Earthfile の書き方を説明していきます.
ディレクトリ構成
grpcディレクトリの構成は以下のようになっています.
grpc
├── .earthlyignore
├── Earthfile
├── example.proto
├── go.mod
├── go.sum
├── main.go
├── main_test.go
└── pb # pbディレクトリは Earthfile のターゲットによって生成される
├── example.pb.go
└── example_grpc.pb.go
example.proto にはシンプルな EchoService のスキーマを定義しています.
main.go は EchoService の提供する Echo メソッドの中身を実装し, gRPCサーバとして稼働します.
そして, Earthfile には Protobuf のスタブコード生成 (pbディレクトリに出力される) や Go で書かれたコードのビルド, Docker イメージのビルド, Lint やユニットテストのターゲット (タスク) が定義されています.
Earthfile を見ていくぞい
それでは Earthfile の中身を見てみましょう.
ばーん
ターゲットごとに見ていきましょう.
base (冒頭部分)
個別のターゲットに属さない冒頭の共通部分は base と呼ぶそうです. 最初に, この base の箇所で何をしているかを見ていきます.
VERSION 0.6 の部分は,この Earthfile で想定する Earthly のバージョンです.このバージョンによってどの機能が有効になるか制御されます.
VERSION は Earthfile の先頭に書きます.現在はオプショナルですが,今後必須となる予定らしいので,必ず書いておきましょう.
FROM golang:1.17-buster はこの Earthfile のデフォルトのベースイメージです.
Earthfile で定義されたターゲットは基本的にコンテナで実行される[1]ため,そのコンテナのベースとなるイメージを指定する必要があります.ターゲットの外に書かれた FROM はターゲットのデフォルトのベースイメージになるので, ターゲットで明示的に FROM を指定しない場合はこのイメージが利用されます.今回の例では, go のビルドやツールのインストールをするので golang:1.17-buster をデフォルトのベースイメージに指定しています.
後述しますが, Earthly と Dockefile の FROM の異なる点として, Earthly の FROM ではイメージだけでなく, Earthly のターゲットを指定できます.これによって,あるターゲットの結果を別の複数のターゲットから再利用できる,つまりビルドキャッシュを利用できます.
WORKDIR /work はデフォルトの作業ディレクトリの指定です.
proto-goターゲット
proto-goターゲットでは, protoc や protoc-gen-go, protoc-gen-go-grpc などのツールをインストールし, Protobuf ファイルからスタブコードを生成します.
ターゲット単体で見ると,やっていることはほとんど Dockerfile ですが, SAVE ARTIFACT という特殊なコマンドが使われています.SAVE ARTIFACT コマンドはターゲットのコンテナ (ビルド環境) 内のファイルやディレクトリをアーティファクト環境にコピーするコマンドです.アーティファクト環境にコピーされたファイルは,他のターゲットから参照できます.つまり, Dockefile のマルチステージングビルドみたいなことをこの仕組みで実現できます.また, AS LOCAL ... を付けることで,アーティファクト環境だけでなくローカル (ホスト) にも対象のファイルをコピーできます.ターゲットの生成物がローカルに必要な場合は AS LOCAL ... を指定してください[2].
このケースでは, protoc を用いて生成されたファイルをアーティファクト環境の /pb ディレクトリとローカル環境のカレントディレクトリにコピーしています.
実際に実行してみると以下のような結果が出力されます.
$ earthly +proto-go
1. Init 🚀
————————————————————————————————————————————————————————————————————————————————
buildkitd | Found buildkit daemon as docker container (earthly-buildkitd)
2. Build 🔧
————————————————————————————————————————————————————————————————————————————————
golang:1.17-buster | --> Load metadata linux/amd64
context | --> local context .
+base | --> FROM golang:1.17-buster
+base | [ ] 0% resolve docker.io/library/golang:1.17-buster@sha256:7b44fe9b029cb9160c96471f689ff2a5c9b3a60edb[██████████] 100% resolve docker.io/library/golang:1.17-buster@sha256:7b44fe9b029cb9160c96471f689ff2a5c9b3a60edb732ed95bfa4681852fdbda
+base | --> WORKDIR /work
+proto-go | --> RUN apt-get update && apt-get install -y wget unzip
...
+proto-go | --> RUN if [ "$TARGETARCH" = "amd64" ]; then wget -O protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v$PROTOC_VERSION/protoc-$PROTOC_VERSION-linux-x86_64.zip ;else wget -O protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v$PROTOC_VERSION/protoc-$PROTOC_VERSION-linux-aarch_64.zip ;fi
...
+proto-go | --> RUN unzip protoc.zip -d /usr/local/
...
+proto-go | --> RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@v$PROTOC_GEN_GO_VERSION
+proto-go | go: downloading google.golang.org/protobuf v1.26.0
ongoing | +proto-go (5 seconds ago)
+proto-go | --> RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v$PROTOC_GEN_GO_GRPC_VERSION
+proto-go | go: downloading google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0
+proto-go | go: downloading google.golang.org/protobuf v1.23.0
+proto-go | --> COPY example.proto /work
+proto-go | --> RUN protoc --proto_path=/work --go_out=/work --go-grpc_out=/work /work/example.proto
+proto-go | --> SAVE ARTIFACT /work/pb +proto-go/pb AS LOCAL ./
output | --> exporting outputs
output | [██████████] 100% copying files
output |
3. Push ⏫ (disabled)
————————————————————————————————————————————————————————————————————————————————
To enable pushing use
earthly --push ...
4. Local Output 🎁
————————————————————————————————————————————————————————————————————————————————
Artifact github.com/emiksk/earthly-example/grpc:main+proto-go/pb output as pb
========================== 🌍 Earthly Build ✅ SUCCESS ==========================
実行後にローカルの環境を見ると pb ディレクトリが生成されています.
このターゲットではスタブコードの生成に使うツールのバージョンを ARG コマンドで指定することで固定していますが, これらは Dockerfile の ARG と同じように実行時に上書きできます.
$ earthly +proto-go --PROTOC_GEN_GO_GRPC_VERSION=1.2.0
ARG の値を変更するとレイヤーが変わるため,それ以降のレイヤーはキャッシュがヒットしなくなります. ARG はできるだけ使用する直前に定義するようにしましょう.
また,Earthlyには組み込みの ARG もいくつか用意されており, proto-go ターゲット内の TARGETARCH もそのひとつです.
deps ターゲット
deps ターゲットでは proto-go ターゲットで生成されるスタブコードや go.mod で指定している依存ライブラリを取得します.このターゲットはその他のターゲットのベースイメージとして利用されており,これによってビルドキャッシュを効率的に活用できます.
このターゲットの最初で COPY コマンドを利用していますが,この時 +proto-go/pb という Dockerfile では見ない書き方をしています.これが proto-go ターゲットのアーティファクト環境のファイル (アーティファクトとも呼ばれる) を参照する方法です.
build ターゲット
build ターゲットでは, deps ターゲットをベースイメージとして,ローカルのファイルを全てコピーしバイナリにビルドしています.
ここでは,デフォルト (golang:1.17-buster) ではなく, FROM に depsターゲットを指定しています.つまり, build ターゲットのビルド環境には Protobuf のスタブコードや go.mod, go.sum, 依存ライブラリが含まれている状態です.
その状態で go build コマンドでバイナリを生成し, SAVE ARTIFACT コマンドでアーティファクト環境とローカルにコピーしています (少しわかりにくいですが,アーティファクト環境の /server はディレクトリでなくバイナリファイルです).
このターゲットではローカルのカレントディレクトリをビルド環境にコピーしていますが, ローカルに .earthlyignore を配置しているので,一部のファイルは無視されます.
docker ターゲット
docker ターゲットでは,実行用のイメージ (ここでは debian:buster) をベースにビルドしたバイナリを配置してプログラムを実行できるイメージを生成します.
ここで使用されている SAVE IMAGE コマンドは,このターゲットで生成されたイメージをローカルやリモートのコンテナレジストリに保存するためのコマンドです. 基本的には引数で与えた名前でローカルにイメージを保存しますが, --push オプションを用いることで,イメージをリモートのコンテナレジストリにプッシュできます. ただし, -push オプションを指定してもデフォルトではプッシュされず, 実行時にも --push オプションを指定してあげる必要があります.
$ earthly --push +docker
lint ターゲット
lint ターゲットでは, staticcheck や go vet を用いてコードの静的解析を行っています.
test ターゲット
test ターゲットでは, deps ターゲットをベースイメージとしてユニットテストの実行を行っています.
all ターゲット
all ターゲットでは, BUILD コマンドで複数のターゲットを指定して実行しています.
BUILD コマンドは, ターミナルでターゲットを指定して earthly コマンドを実行した時と同じように動きます.
複数のターゲットが実行される時, Earthly はそれぞれのターゲットをなるべく並列に動かそうとします. 実行結果を見ると, 異なるターゲットが並列に実行されている様子がわかります.
その他の機能
今回の例では紹介できませんでしたが, 他にも便利な Earthly の機能として IMPORT コマンドと WITH DOCKER があります.
IMPORT コマンド
Earthly は Earthfile を分割し, 他の Earthfile から FROM コマンドや BUILD コマンドで参照できます.
some-target:
FROM ./other-service/other-dir+other-target
...
IMPORT コマンドを用いると, この参照にエイリアスをつけられます.
IMPORT ./other-service/other-dir AS other-earthfile
some-target:
FROM other-earthfile+other-target
...
モノレポでのプロジェクトに役立ちそうです. ちなみに, 参照する Earthfile は別のリポジトリでも構いません. たとえば, github.com/emiksk/earthly-example/grpc:main+docker といった具合です.
WITH DOCKER コマンド
WITH DOCKER コマンドを利用すると, そのターゲットの中で Docker を起動し, コンテナを動かすことができます. よくあるユースケースとしては, 結合テスト用や動作確認のための開発環境用の docker-compose.yaml を用意し, 別のターゲットで生成したイメージを指定して複数のコンテナを起動するターゲットを作成できたりします.
integration-tests:
FROM earthly/dind:alpine
COPY docker-compose.yml ./
WITH DOCKER --compose docker-compose.yml --load tests:latest=+test-setup
RUN docker run --network=default_go/part6_default tests:latest
END
おわりに
という感じで Earthly 入門をやってみました. 学習コストが低く, やりたいことが大体できるツールなので個人的にはかなり気に入ってます. 実際にプロジェクトで使ってみてもう少し感触を探りたいところですね.
CIでの利用編も書こうと思いましたが, 長くなっちゃいそうなので次回に回します!🌎
-
例外として
LOCALLYコマンドというものが存在し,このコマンドで定義されたターゲットはホスト上で実行できます.が,あんまり多用しない方が良いでしょう.https://docs.earthly.dev/docs/earthfile#locally ↩︎ -
SAVE ARTIFACTコマンドは,ターゲットがFROMで指定された場合はAS LOCAL ...を指定してもローカルにファイルが保存されない仕様になっています.ローカルにファイルをコピーしたい場合は,earthly +buildのように直接ターゲットを指定して実行するか,BUILDコマンドでターゲットを指定しましょう. ↩︎
Discussion
Earthly 良さそうですね。
素敵な記事ありがとうございます。
ところで、引用元の Earthly 公式のドキュメントにも書かれている以下の部分が、自分の中で理解できていなくモヤッとしています。
実際には、普通の Dockerfile でも
FROMに前のステージの名前を渡せるので、何が Earthly の独自の機能(メリット)なんでしょう?拙著で恐縮ですが、Dockerfile で build や test を含む CI を実施する方法を書いておりまして、その中でも中間ステージをビルドキャッシュとして使っています。
コメントありがとうございます!
おー,これは知りませんでした.Docker でも FROM にステージを指定できるのですね.
そうなると Earthly でのメリットを強いて挙げるとすると,分割された Earthfile (リモート含む) のターゲットを指定できるとかかもしれません.
Earthly の
<target-ref>は同じ Earthfile に限定されないですが,Dockerfile が異なるファイルのステージを指定できないのであればメリットと言えそうです (それ Dockerfile でもできますよ案件だったらすみません).回答ありがとうございます。
なるほど、確かに Dockerfile では、ファイル分割はサポートされていない気がしますので、Earthlyの利点ですね。
(Dockerfile でも一旦ビルドして tag つけておけば、指定できなくはないですが、二度手間ですね。)