curlのHTTP/3通信をDocker上で使ってみる

## 概要
upstreamのcurlにおいてHTTP/3の実験的サポートが追加されました。
[公式リポジトリのReadme](https://github.com/curl/curl/blob/master/docs/HTTP3.md)に詳細が載ってますが、とりあえずDocker上で動かせるのを確認したのでシェアします。

HTTP/3についてよくわからない方は[詳解HTTP/3](https://http3-explained.haxx.se/ja/)をご覧ください(ステマ)。

## TL; DR

すぐ試したいという方は `docker run --rm -it inductor/curl-quiche:latest` でコンテナを起動したあとに以下を実行すればよいです。(Ref: https://asnokaze.hatenablog.com/entry/2019/08/07/031904)

```bash
$ curl --http3 https://www.facebook.com/ -v -s -o /dev/null
```

## Dockerfile

公式の手順を参考に書き起こしたDockerfileが以下です。Gitリポジトリは[こちら](https://github.com/inductor/curl-quiche-docker)

```Dockerfile:Dockerfile
FROM ubuntu:18.04 as base-fetch
RUN apt-get update && apt-get install -y git

FROM ubuntu:18.04 as base-build
RUN apt-get update && apt-get install -y build-essential pkg-config

FROM base-fetch as fetch-quiche
WORKDIR /root
RUN git clone --recursive --depth 1 https://github.com/cloudflare/quiche
RUN mkdir quiche/deps/boringssl/build

FROM base-build as prepare-quiche
RUN apt-get update && apt-get install -y cmake golang-go
COPY --from=fetch-quiche /root/quiche /root/quiche
WORKDIR /root/quiche/deps/boringssl/build
RUN cmake -DCMAKE_POSITION_INDEPENDENT_CODE=on ..
RUN make -j`nproc`
WORKDIR /root/quiche/deps/boringssl
RUN mkdir .openssl/lib -p
RUN cp build/crypto/libcrypto.a build/ssl/libssl.a .openssl/lib
RUN ln -s $PWD/include .openssl

FROM base-build as build-quiche
WORKDIR /root
RUN apt-get update && apt-get install -y wget ca-certificates && \
        wget https://sh.rustup.rs -O install.sh \
        && chmod +x install.sh \
        && ./install.sh -y \
        && rm -rf install.sh && \
        apt-get purge -y --auto-remove wget ca-certificates
COPY --from=prepare-quiche /root/quiche /root/quiche
WORKDIR /root/quiche/
RUN QUICHE_BSSL_PATH=$PWD/deps/boringssl \
        $HOME/.cargo/bin/cargo \
        build \
        --release \
        --features \
        pkg-config-meta

FROM base-fetch as fetch-curl
WORKDIR /root
RUN git clone --depth 1 https://github.com/curl/curl

FROM base-build as build-curl
RUN apt-get update && apt-get install -y autoconf libtool
COPY --from=fetch-curl /root/curl /root/curl
WORKDIR /root/curl
RUN ./buildconf
COPY --from=build-quiche /root/quiche /root/quiche
RUN ./configure LDFLAGS="-Wl,-rpath,$PWD/../quiche/target/release" \
        --with-ssl=$PWD/../quiche/deps/boringssl/.openssl \
        --with-quiche=$PWD/../quiche/target/release
RUN make -j`nproc`
RUN make install

FROM ubuntu:18.04 as executor
COPY --from=build-curl /etc/ld.so.conf.d/libc.conf /etc/ld.so.conf.d/libcurl.conf
COPY --from=build-curl /usr/local/lib/libcurl.so.4 /usr/local/lib/libcurl.so.4
COPY --from=build-curl /etc/ld.so.conf.d/libc.conf /etc/ld.so.conf.d/libquiche.conf
COPY --from=build-curl /root/quiche/target/release/libquiche.so /usr/local/lib/libquiche.so
COPY --from=build-curl /usr/local/bin/curl /usr/local/bin/curl
RUN ldconfig
CMD ["bash"]
```

## Dockerfileの設計戦略について

今回書いたDockerfileではマルチステージビルドと呼ばれる機能を使っています。
これは、Dockerfileにおける `FROM` 句を複数回呼び出すことで複数のビルド(1ビルドの単位をステージと呼びます)を展開できる機能です。こうすると、ある特定のステージから別のステージに対してファイルをコピーできるようになり、ソースコードのコンパイル用ステージとコンパイル済みバイナリの実行用ステージなどを分けることで、イメージサイズを小さくすることができるようになるという効果があります。

具体的にはこんな感じです。

```dockerfile:Dockerfile.example
FROM golang:alpine as builder
WORKDIR /app
COPY . /app
RUN go build -o /hoge

FROM alpine as executor
COPY --from=builder /hoge /hoge
CMD ["/hoge"]
```

こうすることで、golangのビルドに必要だったライブラリ群がすべて不要となり、実行用のイメージ+生成したバイナリのDockerイメージが出来上がります。

このDockerfileを書くにあたって、当初ベースイメージにはDebianを使ったのですが、何も考えずに作ったら2GBのイメージが出来上がりました。Debian Slimに変えて色々頑張った結果、98MBにまで小さくなりました。

```
$ docker images
REPOSITORY            TAG     IMAGE ID      CREATED         SIZE
inductor/curl-quiche  debian  b8288aeca6a3  24 minutes ago  98.1MB
inductor/curl-quiche  ubuntu  60f6ebe7ce0c  10 minutes ago  92.2MB
```

また、最近(最近でもないか?)UbuntuのオフィシャルイメージがめちゃくちゃコンパクトになったこともあったのでUbuntuで試してみたところ、92MBになりました。びっくり！

### BuildKitを用いたDockerイメージビルドの並列実効性

BuildKitはDockerのバージョン18.06より搭載された次世代Dockerイメージビルドツールで、後方互換性を保ちながらも既存のビルダーに比べキャッシュやビルド実行並列性などが向上しているのが特徴です。

Dockerfileを見ていただくとわかるかと思いますが、fetchやbuildでベースイメージを分けたり、複数のステージで別々のファイルを別々にビルドしたりと、かなり細かくステージを分けています。
これは、**BuildKitでは並列に実行可能な命令は並列に行う**という特徴を持っているためで、他のステージに依存しない命令を複数ステージに分割すると実行並列性が増してビルド時間の短縮につながるために行っている対応です。

例えばあるプログラムをコンパイルするために必要な対応が
・ビルドに必要な別のライブラリを取得
・ライブラリのビルドに必要なパッケージを入れて
・ライブラリをコンパイル
・プログラムのソースコードを取得
・プログラムのビルドに必要なパッケージを入れて
・プログラムをコンパイル

だった場合、並列に実行できないのはプログラムのコンパイルだけです（ライブラリ自体のコンパイルに依存するため）
そのため、以下のように書くと同時に処理が走るというわけです。

```Dockerfile
FROM hoge as base-lib
RUN 共通パッケージのインストール

FROM hoge as fetch-lib
RUN lib取得

FROM hoge as build-lib
COPY --from=fetch-lib /path/to/lib # フェッチの処理に引っ張られる
RUN libコンパイル

FROM hoge as base-program
RUN 共通パッケージのインストール

FROM hoge as fetch-program
RUN program取得

FROM hoge as build-program
COPY --from=fetch-program /path/to/program # フェッチの処理に引っ張られる
COPY --from=build-lib /bin/lib # libのビルドに引っ張られる
RUN programコンパイル

FROM hoge as executor
COPY --from=build-program /bin/program
CMD ["/bin/program"]
```

## Alpineは？
聞かないでください。
quicheのコンパイルで死亡して諦めました。
誰か上手くいったらおしえて