【これはUnipos Advent Calendar 2021の2日目の記事です】

つい最近、EarthlyというDockerコンテナベースのビルドツールで、自分の開発しているGoのアプリケーションのMakefile/Dockerfile/docker-compose.yamlを置き換えたのでそれを記事にしてみる。
Earthlyとは
めちゃくちゃ雑に言うとDockerイメージをベースにしたビルドツール。
できることとしてはGoogleが作っているBazelに近い*1が、書き味はMakefile+Dockerfileに近く*2、独特の文法が少ない雰囲気。当然、言語やフレームワークに依存しない。まるでローカル環境でビルドをしているかのようにシームレスにコンテナ環境でビルドを実行できる。
Earthlyは書き味こそDockerfileと似ているが、Dockerイメージを作ることが目的のDockerfileよりも汎用性が高い。Dockerイメージの作成以外にも、コンテナ環境でのテストの実行、ファイルの生成など、イメージの定義だけではなく実行するコンテナ上での操作も定義できる。この辺はMakefileのエッセンスを取り入れていると言える。
他にも、Dockerよろしくレイヤキャッシュがちゃんと使えたり、Makefileでいうターゲット定義の単位でビルドグラフを作ってくれて、勝手に並列にできそうなところは並列化したりしてくれたりと嬉しい要素はたくさんあるが、とにかくまずはEarthfileの雰囲気を見る方が早い。
Earthfileの雰囲気
実際のファイルはここに置いてある。 github.com
アプリケーションのビルド
ほとんどMakefileと雰囲気は同じ。このあたりはDockerfileで書かれていたところ。
FROMやCOPYで依存関係を定義でき、依存するイメージやアーティファクトがなければ自動でターゲットを実行し成果物を生成する。ここでは earthly +image とやると上から順番にターゲットが実行され、最終的にアプリケーションのDockerイメージが作られる。
VERSION 0.6 FROM golang:1.17-alpine3.14 WORKDIR /go-cleanarchitecture deps: COPY go.mod go.sum . RUN apk add --no-cache build-base RUN go mod download build: FROM +deps COPY . . RUN go build -o build/go-cleanarchitecture main.go SAVE ARTIFACT build/go-cleanarchitecture AS LOCAL build/go-cleanarchitecture image: COPY +build/go-cleanarchitecture . EXPOSE 8080 ENTRYPOINT ["/go-cleanarchitecture/go-cleanarchitecture"] SAVE IMAGE go-cleanarchitecture:latest
開発時のミドルウェア立ち上げまわり
middlewares-up はもともとはdocker-compose.ymlでやっていた処理をearthlyに移植したもの。
LOCALLY コマンドを使うとホストのDocker上でコンテナが実行される。本当はホストではなくEarthlyが起動するコンテナ上ですべて実行したかったが、--net オプション指定でコンテナに接続させるネットワークを同じものにしても、なぜかdocker runを実行するターゲットが異なると動かなかった。というわけで、仕方なくホスト側で実行するようにしている。
# Development run: LOCALLY WITH DOCKER --load app:latest=+image RUN docker run --net=go-cleanarchitecture-network --net-alias=app \ -p 8080:8080 --env APP_ENV=production --rm app:latest -http END middlewares-up: LOCALLY WITH DOCKER \ --load db:latest=+db \ --load redis:latest=+pubsub RUN docker network create go-cleanarchitecture-network && \ docker run -d --net=go-cleanarchitecture-network --net-alias=db \ --name go-cleanarchictecture-db -p 3306:3306 --rm db:latest && \ docker run -d --net=go-cleanarchitecture-network --net-alias=redis \ --name go-cleanarchictecture-redis -p 6379:6379 --rm redis:latest END middlewares-down: LOCALLY WITH DOCKER RUN docker stop go-cleanarchictecture-db go-cleanarchictecture-redis && \ docker network rm go-cleanarchitecture-network END
WITH DOCKER コマンドには --compose というdocker-compose.yamlを読み込むオプションも付いているが、0.6.1の段階ではearthlyがdocker-compose.yamlのネットワーク定義のセクションを読んでいないっぽく、ミドルウェアを立ち上げても +run で立ち上げたアプリケーションが繋ぎに行けない様子。今回はいったん諦めてdocker-compose.yamlを削除しearthlyで完結させることで解決した。
ちなみにdbやpubsubなどのミドルウェアの定義はこんな感じ。この辺はシンプルでいい。
# Middlewares db: FROM mysql:5.7 ENV MYSQL_ROOT_USER=root ENV MYSQL_ROOT_PASSWORD=password ENV MYSQL_DATABASE=todoapp EXPOSE 3306 SAVE IMAGE db:latest pubsub: FROM redis:6.2.6-alpine3.15 EXPOSE 6379 SAVE IMAGE pubsub:latest
E2Eテスト
integration-testも元々はdocker-composeでミドルウェアの立ち上げをするようにしていたところなので、なかなか激しい感じになっている。
上では書き忘れていたが WITH DOCKER の中では RUN コマンドをひとつしか配置できない(!)という制約が現状あり、そのせいで&&を多用せざるを得なくなっている。正直、この辺の書き味はまだまだなんともいえない。
integration-test:
LOCALLY
WITH DOCKER \
--load db:latest=+db \
--load redis:latest=+pubsub \
--load migrater:latest=+migrater \
--load dredd:latest=+dredd \
--load app:latest=+image
RUN docker network create test-network && \
docker run -d --name=db --net=test-network --net-alias=db -p 3306:3306 --rm db:latest && \
docker run -d --name=redis --net=test-network --net-alias=redis -p 6379:6379 --rm redis:latest && \
while ! nc 127.0.0.1 3306; do sleep 1 && echo "wait..."; done && sleep 15 && \
docker run --net=test-network --rm migrater:latest migrate && \
docker run -d --name=app --net=test-network --net-alias=app -p 8080:8080 --env APP_ENV=production --rm app:latest -http && \
while ! nc 127.0.0.1 8080; do sleep 1 && echo "wait..."; done && sleep 15 && \
docker run --net=test-network -w /app --rm dredd:latest && \
docker stop db redis app && \
docker network rm test-network
END
dredd:
FROM apiaryio/dredd
COPY . /app
COPY api-description.apib dredd_hook.js .
ENTRYPOINT dredd api-description.apib http://app:8080 --hookfiles=dredd_hook.js
SAVE IMAGE dredd:latest
ほかにもキャッシュの機構だったり、エラーがおきたときにその場で起動中のコンテナにsshできるインタラクティブモードというやつがあったりするが、ここでは省略。公式ドキュメントがめちゃくちゃ整備されているので、それをみるのが早い。
追記: キャッシュの話だけ追加で記事書きました izumisy.work
所感
いいところ
ビミョい、あるいは難しかったところ
- ターゲットの依存関係でレイヤキャッシュが効く条件がパッとは分からなかった
COPY,FROM,BUILDの3つがあるが、FROMだとキャッシュが効かないことがありどういう条件でキャッシュが効くのかすぐには分からなかった。これは自分のDocker力の問題かもしれない。
LOCALLYやWITH DOCKERを使うことで受ける暗黙的な制限がいろいろある- この辺は使ってみると体験できる。基本は使わない方がいいような気もする。
WITH DOCKERの--composeオプションが提供するdocker-compose.ymlとの連携が弱い- ネットワーク指定だったりボリューム指定だったりがたぶん動かない気がする。そもそもEarthlyで全部やれるんだからdocker-compose.yamlいらないだろ、という話かもしれない。
- インラインキャッシュ
- Earthlyのおすすめキャッシュ機構としてインラインキャッシュというやつがあり、これを有効にするとDockerhubなどのコンテナ・レジストリにキャッシュイメージがpushされる。キャッシュがイメージ化されることで、イメージをpullできればあらゆる環境でキャッシュを利用できるというメリットがある。しかし一方で、たとえばDockerhubだと無料プランの人はrate limitがあったりするわけなので、無料で使い倒そうとするとrate limitに引っ掛からないようキャッシュを頻繁に更新したり取りにいったりしないようにする、みたいな工夫が必要になる。
- 正直、個人的にはファイルキャッシュができると嬉しいがGithub Issueを見た感じあんまり盛り上がってなかった。業務開発ならクラウドプラットフォームで用意されているGCRとかAWS ECRあたりが使えるだろうから、実際にはrate limitとかは些細な問題なのかもしれない。
- CI環境(CircleCI)ではたぶんインラインキャッシュキャッシュがないと実行時間が長くなる
- docker-composeを使っていたときは特段キャッシュなどはしていなかった。それでもdocker-composeのほうが1.5倍くらい実行速度は速かった。
- むしろCI上でインラインキャッシュを使わずEarthlyを使うなんてありえない、という話なのかもしれない。
まとめ
いいところの5倍くらい微妙なところを書いてしまった...
が、まだまだ発展途上なのでこれからの進化に期待。ポテンシャルはめちゃくちゃありそうなので、とにかくおすすめです。自分は結構ハマりそうな予感。
