Haskell on Docker で Portable CLI を作ろう

## 概要

Docker を使って Haskell で CLI (Command Line Interface) のアプリケーションを配布することを考えます。
Docker さえインストールされていれば多くの OS で実行可能な CLI が作成できます。
本記事では Docker Image を Multi-Stage Build を使って作成することで実行時のコンテナサイズを小さくする方法を紹介します。
配布するにはイメージサイズをできる限り小さくする方が使ってもらいやすくなるからです。
また上記のイメージを使った [Whalebrew](https://github.com/bfirsh/whalebrew) による CLI アプリケーションの作成と配布について書きます。

## 対象とする読者

Haskell と Docker の基本的な使い方を理解している読者を想定しています。
この記事では Haskell 自体の文法や使い方の説明はしません。
また Docker コマンドの使い方についても細かく解説しません。

## Docker multi-stage build とは？

Docker multi-stage build とは、1つの Dockerfile で複数のベースイメージを指定できる機能のことです。
multi-stage build を使うことでビルド用の環境と実行用の環境を分けることができ、これによってコンテナの容量を小さくなります。
ビルド時にしか使わないファイルを実行環境に含めないことで容量を減らせるということです。

## Haskell で multi-stage build を実現するには

例えば、Ubuntu でイメージのビルドを行い、実行環境には軽量 Linux の Alpine を使うことを考えます。
ここでは Ubuntu に Haskell 環境が構築されている [fpco/stack-build](https://hub.docker.com/r/fpco/stack-build/) の Docker イメージを使います。

### 事前準備

[Docker](https://www.docker.com/) と [Haskell-stack](https://docs.haskellstack.org/en/stable/README/) が既に読者の環境にインストールされているものとします。
まだ導入前の読者は以降の記事を試す前に上記をインストールしてください。

### multi-stage build の手順

1. ビルド用の環境構築
2. ビルドの実施
3. 実行用の環境構築
4. 実行ファイル一式のコピー
5. 実行コマンドの指定

### Dockerfile の例

この記事では Haskell のアプリケーションとして有名な Pandoc を Dockerfile を使ってビルド/実行する環境を構築します。

```Dockerfile
FROM fpco/stack-build:lts-9.9
WORKDIR /usr/lib/gcc/x86_64-linux-gnu/5.4.0
RUN cp crtbeginT.o crtbeginT.o.orig
RUN cp crtbeginS.o crtbeginT.o
ADD ./ /work
WORKDIR /work
RUN stack setup
RUN stack --system-ghc --local-bin-path /sbin build --ghc-options '-optl-static -fPIC -optc-Os'

FROM alpine:latest
RUN mkdir /work
COPY --from=0 /work/.stack-work /work/.stack-work
COPY --from=0 /sbin/pandoc /work/
CMD ["/work/pandoc"]
```

### ビルドと実行

```bash
git clone https://github.com/algas/pandoc.git
cd pandoc
git checkout docker
docker build --rm -t mypandoc .
docker run --rm --entrypoint "/sbin/pandoc" -v `pwd`:/tmp -w "/tmp" -it mypandoc "$@"
```

## Whalebrew

### Whalebrew とは？

Whalebrew は Docker コンテナを使うことでローカル環境を汚さずに CLI アプリケーション(コマンドライン)を導入するための仕組みです。

Multi-stage build の応用として Whalebrew コマンドを作ってみましょう。
Multi-stage build によって Whalebrew のアプリケーションの容量を削減することが出来ます。

### Whalebrew のインストール

Whalebrew のインストール方法は公式の github ドキュメントを参照してください。
https://github.com/bfirsh/whalebrew

以前書いた Whalebrew の紹介記事も読んでいただけると嬉しいです。
https://qiita.com/algas/items/66aaf749dc3979e03a46

### Whalebrew コマンドとして pandoc を実行する

whalebrew 本体はインストール済みであるとします。
1行目で pandoc コマンドをインストールし、2行目で markdown を html に変換しています。

```bash
whalebrew install algas/whalebrew-pandoc
echo -e "Hello pandoc! \n\n- one\n- two\n" | pandoc -f markdown -t html
```

上記のコマンドを実行して、下記のような html が表示されれば pandoc の導入は成功です。

```html
<p>Hello pandoc!</p>
<ul>
<li>one</li>
<li>two</li>
</ul>
```

便宜的に「pandoc コマンドをインストール」と書きましたが、実際には Docker コンテナを実行しているだけでコマンドを実行した環境には何もインストールされません。

### whalebrew コマンドの作り方

whalebrew コマンドを作るのは実に簡単です。
Entrypoint を正しく指定した Docker イメージを作るだけです。
whalebrew コマンドで使いやすくするために Docker Hub などの Docker Registry にアップロードしましょう。
Docker Registry へのイメージのアップロード方法はここでは紹介しません。

上の項目でインストールした `algas/whalebrew-pandoc` は下のような Dockerfile を Docker Hub で Automated build したイメージを呼び出しているだけです。

https://hub.docker.com/r/algas/whalebrew-pandoc/

```Dockerfile
FROM algas/pandoc:latest
LABEL io.whalebrew.name pandoc
ENTRYPOINT ["/sbin/pandoc"]
```

つまり公開された Docker Registry にイメージを上げておけば `whalebrew install` で任意のイメージをコマンドとしてインストールできるようになります。

## 実際にどのくらいの容量が削減されたのか？

筆者の開発・実行環境(macOSX High Sierra)で `docker images` で表示された値を元に実際のイメージサイズを示します。

| Item | Image Name | File Size |
|:----:|:----------:|:---------:|
| ビルド環境のイメージ | fpco/stack-build:lts-9.9 | 4.3GB |
| 中間イメージ(ビルドイメージにpandocをインストール) | \<none\> | 7.68GB |
| 実行環境のイメージ | alpine:latest | 3.97MB |
| Whalebrew のオリジナル pandoc コマンド | algas/whalebrew-pandoc:latest | 659MB |

ビルド環境のイメージに pandoc をインストールした状態だと 7.68GB あり、 Multi-stage build を使わない場合にはこのサイズのイメージを配布することになります。
必要な環境だけを実行用のイメージに移すことで容量を大幅に(この例では1/10程度に)削減できました。
最後の pandoc コマンドイメージはチューニングをすればさらに何割かの容量を削減できるはずです。

## まとめ

- Docker を使って Haskell でポータブルな CLI を作る方法を紹介しました。
- Docker Multi-stage build を使うことで CLI アプリケーションのファイル容量を削減できます。
- Whalebrew を使って CLI アプリケーションを簡単に配布できます。

## 参考文献

- https://docs.docker.com/engine/userguide/eng-image/multistage-build/
- https://www.fpcomplete.com/blog/2016/10/static-compilation-with-stack
- https://www.ishiy.xyz/posts/2016-02-28-haskell-docker.html