はーいほーアッキーだよっ
アッキーは最近今更ながらDockerを使い始めていろいろお勉強してるところです
Haskellで書かれたアプリケーションをビルドするのにはよくStack
が使われますが、Docker上でStackを使いつつビルドして、実行バイナリをいい感じにDockerイメージにできないかなー、と頑張ってみた次第です
Stack自体にDocker連携機能あるじゃん?
はい、あります。Dockerでビルドする方法がここに、Dockerの実行イメージを作る方法がここにそれぞれ書いてあります。仕組みとしては
- ビルド用に
~/.stack
や/path/to/project/.stack-work
などをマウントしたDockerコンテナを立ち上げstack build
する - 実行バイナリをコピーしたDockerイメージを作る
ってな感じです。ただこれには罠とイケてない点がありまして……
罠: デフォルトのビルドで使うイメージが大きい
stack.yaml
にきちんと設定を書けばそのDockerイメージを拾ってきてくれるのですが、書かない場合fpco/stack-buildというデフォルトのイメージを拾ってきてくれるのですがこれがまた大きい。圧縮済みで3GBと豪快に容量と帯域を持っていきます。700MB弱のSlackwareのISOイメージをADSLで一晩かけて落とした身からするとなんと冒涜的なと思ってしまいます1 。
イケてない点: 実行イメージの作り方
何はともかくstack image container
でいい感じのDockerイメージを作ってくれます……が。デフォルトではエントリポイントを作ってくれません。作る設定は簡単なのでいいんですが、例えばプロジェクト名がkirisaki/hogehoge
でエントリーポイントがnyaan
とかだったりするとできる実行イメージはkirisaki/hogehoge-nyaan
と残念な感じになります。最終的に我々がほしいのは実行できるDockerイメージだけなのに、どうしてこんなに回りくどいことを……。
Docker multi-stage buildを使っていこうと試みる
今ならmulti-stage buildもあるのでHaskell on Docker で Portable CLI を作ろう - Qiitaを参考にDockerfileで管理してみようとしたわけです、が……。
失敗例その1
前述の記事を参考にこんなDockerfileを作りました(ちなみにkirisaki/overflowは自前で作った軽めのDockerイメージです)。
FROM kirisaki/overflow:lts-12.20 as builder
ADD ./ /work
WORKDIR /work
RUN stack --system-ghc build --ghc-options '-optl-static -fPIC -optc-Os'
RUN stack --local-bin-path /sbin install
FROM alpine:latest
RUN mkdir /work
COPY --from=builder /work/.stack-work /work/.stack-work
COPY --from=builder /sbin/execbinary /work/
CMD ["/work/execbinary"]
これでビルドできるにはできるのですが、何しろキャッシュが無いので毎回Haskellライブラリのダウンロードから始まってしまい、Aesonとかその辺使っただけでもかなり時間がかかってしまいます。ビルドはローカルだけにしといて最終的なDockerイメージはバイナリコピって済ませる、でも良かったのですが、やはりDocker上で完結してビルドできるのが理想、とあがくのであった。
失敗例その2
~/.stack
以下をCOPY
すればええやんけ!
ダメでした。セキュリティ上Dockerfileより上のディレクトリにはアクセスできないようです。docker build -f <dir/dir/Dockerfile> .
とDockerfileの実行ディレクトリを指定すれば行けるっちゃあ行けるんですが、違う、そうじゃない。なおシンボリックリンクもダメでした。当然ちゃ当然
失敗例その3
ならばpackage.yaml
とstack.yaml
だけコピってライブラリだけビルドするステージを増やしてみました。
FROM kirisaki/overflow:lts-12.20 as prefetch
COPY package.yaml ./
COPY stack.yaml ./
RUN stack build --dry-run --prefetch
FROM kirisaki/overflow:lts-12.20 as builder
COPY --from=prefetch /root/.stack /root/.stack
ADD ./ /work
WORKDIR /work
RUN stack --system-ghc build --ghc-options '-optl-static -fPIC -optc-Os'
RUN stack --local-bin-path /sbin install
FROM alpine:latest
RUN mkdir /work
COPY --from=builder /work/.stack-work /work/.stack-work
COPY --from=builder /sbin/exec-binary /work/
CMD ["/work/exec-binary"]
が・・・・駄目っ・・・・・!
package.yaml
とstack.yaml
の更新があったときのみprefetch
ステージが走ることを期待したのですが、Dockerさんのmulti-stage buildはそこまで賢くないらしく、他のソースに変更があっても全ビルドし直す模様。うーん。
とりあえず解決
参考:multi stageなDockerfileで中間イメージにタグをつける - Qiita
中間イメージがあれば良いので
$ docker build --target prefetch -t prefetch .
しておいてからの
$ docker build --cache-from prefetch .
とすることでprefetch
のキャッシュを使いビルドすることに成功。中間イメージを作った段階からライブラリを増やすとその分ダウンロードが走るけど全部やり直すよりはマシかなあ、といった感じです。
個人プロジェクトならバイナリコピーでエイヤッでいいと思うんですが、複数人で開発するとなると冪等性が欲しくなるのでベストプラクティスを模索中です。そしてHaskellの話はほぼなかった。
以上、アッキーでした明日のAdvent Calendarはunnohideyukiさんです!
-
アッキーは20歳の女の子だよっ
↩