Help us understand the problem. What is going on with this article?

HaskellとstackとDockerと

More than 1 year has passed since last update.

はーいほー:wave_tone1:アッキーだよっ:sparkles:
アッキーは最近今更ながらDocker:whale2:を使い始めていろいろお勉強してるところです:triumph:Haskellで書かれたアプリケーションをビルドするのにはよくStack:books:が使われますが、Docker上でStackを使いつつビルドして、実行バイナリをいい感じにDockerイメージにできないかなー、と頑張ってみた次第です:punch_tone1:

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

:bulb:~/.stack以下をCOPYすればええやんけ!

ダメでした。セキュリティ上Dockerfileより上のディレクトリにはアクセスできないようです。docker build -f <dir/dir/Dockerfile> .とDockerfileの実行ディレクトリを指定すれば行けるっちゃあ行けるんですが、違う、そうじゃない。なおシンボリックリンクもダメでした。当然ちゃ当然

失敗例その3

ならばpackage.yamlstack.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.yamlstack.yamlの更新があったときのみprefetchステージが走ることを期待したのですが、Dockerさんのmulti-stage buildはそこまで賢くないらしく、他のソースに変更があっても全ビルドし直す模様。うーん。

とりあえず解決

参考:multi stageなDockerfileで中間イメージにタグをつける - Qiita
中間イメージがあれば良いので

$ docker build --target prefetch -t prefetch .

しておいてからの

$ docker build --cache-from prefetch . 

とすることでprefetchのキャッシュを使いビルドすることに成功。中間イメージを作った段階からライブラリを増やすとその分ダウンロードが走るけど全部やり直すよりはマシかなあ、といった感じです。

個人プロジェクトならバイナリコピーでエイヤッでいいと思うんですが、複数人で開発するとなると冪等性が欲しくなるのでベストプラクティスを模索中です。そしてHaskellの話はほぼなかった。

以上、アッキーでした:sparkles:明日のAdvent Calendarはunnohideyukiさんです!


  1. アッキーは20歳の女の子だよっ:two_hearts: 

A_kirisaki
アッキーでーす✨普段は絵とか漫画🎨書いたりしてますけど、プログラミングもやってまーす✌バックエンドからフロントエンドにデザイン、女装まで何でもやります🔥
https://klara.works
realglobe
「世界のすべてをWebAPI化する」ことを目指す技術ベンチャーです。
https://realglobe.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away