【これはUnipos Advent Calendar 2021の2日目の記事です】
つい最近、EarthlyというDockerコンテナベースのビルドツールで、自分の開発しているGoのアプリケーションのMakefile/Dockerfile/docker-compose.yamlを置き換えたのでそれを記事にしてみる。
Earthlyとは
github.com
めちゃくちゃ雑に言うと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
を実行するターゲットが異なると動かなかった。というわけで、仕方なくホスト側で実行するようにしている。
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などのミドルウェアの定義はこんな感じ。この辺はシンプルでいい。
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
所感
いいところ
- DockerとEarthlyさえあればどこでも再現性の高いビルド環境が用意できる
- EarthlyだけでMakefile/Dockerfile/docker-compose.yamlを代替できる
ビミョい、あるいは難しかったところ
- ターゲットの依存関係でレイヤキャッシュが効く条件がパッとは分からなかった
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倍くらい微妙なところを書いてしまった...
が、まだまだ発展途上なのでこれからの進化に期待。ポテンシャルはめちゃくちゃありそうなので、とにかくおすすめです。自分は結構ハマりそうな予感。