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 つけておけば、指定できなくはないですが、二度手間ですね。)