CUBE SUGAR CONTAINER

技術系のこと書きます。

Docker のマルチステージビルドで自前でビルドした Wheel を含むイメージを作る

今回は Docker のマルチステージビルドを使って Wheel が提供されていない Python パッケージを含む Docker イメージを作ってみる。 これだけだと、なんのこっちゃという感じなので、以下で前提から説明しておく。

まず、今の Python のパッケージングにはソースコード配布物 (sdist) と Wheel という二つのフォーマットが主に使われている。 ソースコード配布物は、文字通りソースコードをそのままパッケージングしたもの。 ソースコード配布物では、パッケージの中に Python/C API などで書かれた拡張モジュールがあっても、ソースコードの状態で含まれる。 それに対して Wheel は、拡張モジュールが含まれる場合にはビルドされたバイナリの状態で提供される。 そして、現行の pip はソースコード配布物をインストールするとき、一旦 Wheel にビルドした上でインストールするように振る舞う。 このソースコード配布物を Wheel にビルドするタイミングでは、ランタイムとは別にビルドで必要なツール類の一式が必要になる。

ここで、ソースコード配布物として提供されている Python パッケージを Docker イメージに含めることを考えてみよう。 もし、対象のパッケージが拡張モジュールを含む場合、ビルドに必要なツール類の一式が Docker イメージに必要になってしまう。 Docker イメージは、なるべく不要なものを入れない方が一般的に望ましいとされている。

そこで、上記の問題を解決するのに Docker のマルチステージビルドという機能が使える。 マルチステージビルドでは、複数のイメージを連携させて一つのイメージが作れる。 例えばパッケージのビルドをするステージと、それを組み込むステージを分ければ、後者にはビルドに必要なツールが必要なくなるというわけ。

使った環境は次の通り。

$ sw_vers     
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G103
$ docker version                              
Client: Docker Engine - Community
 Version:           19.03.2
 API version:       1.40
 Go version:        go1.12.8
 Git commit:        6a30dfc
 Built:             Thu Aug 29 05:26:49 2019
 OS/Arch:           darwin/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.2
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.8
  Git commit:       6a30dfc
  Built:            Thu Aug 29 05:32:21 2019
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          v1.2.6
  GitCommit:        894b81a4b802e4eb2a91d1ce216b8817763c29fb
 runc:
  Version:          1.0.0-rc8
  GitCommit:        425e105d5a03fabd737a126ad93d62a9eeede87f
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

Wheel のビルドについて

実際にマルチステージビルドを試す前に、Wheel に関するオペレーションについて解説しておく。 まず、Python のパッケージ管理ツールの pip は、デフォルトで PyPI というパッケージサイトからパッケージを探してくる。

pypi.org

インストールするパッケージに、使っているプラットフォーム向けの Wheel があれば、それがインストールされる。 もし、ソースコード配布物しか提供されていないときは、それを元に Wheel をビルドした上でインストールされる。 ただし、Wheel が提供されている場合であってもオプションを指定することで、あえてソースコード配布物から自前でビルドすることもできる。

実際に試してみることにしよう。 Docker を使って Ubuntu 18.04 LTS のイメージを起動する。

$ docker run --rm -it ubuntu:18.04 /bin/bash

パッケージシステムの APT を使って Python 3 の pip をインストールする。

# apt update
# apt -y install python3-pip

試しに LightGBM というパッケージをソースコード配布物からビルドしてみよう。 pip は wheel サブコマンドを使うことでインストールに必要な Wheel パッケージが取得できる。 その際、--no-binary オプションを指定すると、ビルド済みの Wheel ではなく自分でソースコード配布物からビルドすることになる。 ちなみに、このオプションは pip install サブコマンドでも有効なので覚えておくと良いかも。

# pip3 wheel --no-binary lightgbm lightgbm

なお、上記のコマンド実行は失敗する。 なぜならビルドに必要なパッケージ類が入っていないため。

# pip3 wheel --no-binary lightgbm lightgbm
...
  Exception: Please install CMake and all required dependencies first
  The full version of error log was saved into /root/LightGBM_compilation.log
  
  ----------------------------------------
  Failed building wheel for lightgbm
  Running setup.py clean for lightgbm
Failed to build lightgbm
ERROR: Failed to build one or more wheels

Ubuntu で LightGBM をビルドするのに必要な cmake と gcc をインストールしよう。

# apt -y install cmake gcc

もう一度、先ほどのコマンドを実行すると、今度はエラーにならず上手くいく。

# pip3 wheel --no-binary lightgbm lightgbm

これで、カレントワーキングディレクトリにインストールに必要な Wheel 一式ができあがる。 この中の LightGBM は自分でソースコード配布物からビルドしたもの。

# ls *.whl
joblib-0.14.0-py2.py3-none-any.whl
lightgbm-2.3.0-py3-none-any.whl
numpy-1.17.3-cp36-cp36m-manylinux1_x86_64.whl
scikit_learn-0.21.3-cp36-cp36m-manylinux1_x86_64.whl
scipy-1.3.1-cp36-cp36m-manylinux1_x86_64.whl

ビルドした Wheel をインストールしてみよう。

# pip3 install lightgbm-2.3.0-py3-none-any.whl

これでインストールしたパッケージが使えるようになる。

# python3 -c "import lightgbm"

シングルステージで Docker イメージをビルドする

続いては、マルチステージビルドを試す前にシングルステージの場合を見ておこう。 これは、ビルドに必要なツールも一緒に Docker イメージに含まれてしまうパターン。

以下のように Dockerfile を用意する。 ビルドに必要なパッケージをインストールした上で、LightGBM をインストールする構成になっている。

FROM ubuntu:18.04

# Use fastest mirror
RUN sed -i.bak -e 's%http://[^ ]\+%mirror://mirrors.ubuntu.com/mirrors.txt%g' /etc/apt/sources.list

# Install prerequisite apt packages to build
RUN apt update \
 && apt -yq dist-upgrade \
 && apt -yq install \
      python3-pip \
      cmake \
      gcc \
 && apt clean \
 && rm -rf /var/lib/apt/lists/*

# Install Python package from source code distribution
RUN pip3 install --no-binary lightgbm lightgbm

上記を元に Docker イメージをビルドする。

$ docker build -t example/singlestage .

ビルドしたイメージを元にコンテナを起動してみよう。

$ docker run --rm -it example/singlestage /bin/bash

このイメージでは、ちゃんとインストールした LightGBM がインポートできる。

# python3 -c "import lightgbm as lgb"

反面、ビルドに使った cmake や gcc もイメージに含まれてしまっている。

# cmake --version
cmake version 3.10.2

CMake suite maintained and supported by Kitware (kitware.com/cmake).

マルチステージで Docker イメージをビルドする

それでは、今回の本題となるマルチステージビルドを試してみよう。

マルチステージビルドでは FROM 命令が複数登場する。 それぞれの FROM 命令がステージとなる Docker イメージを表しており、AS を使って名前をつけられる。 名前をつけた Docker イメージからは COPY 命令を使ってファイルをコピーできる。

以下の Dockerfile は build-stageusing-stage という二つのステージに分かれている。 まず、build-stage では LightGBM の Wheel をビルドしている。 そして、using-stage でビルドした Wheel をインストールしている。

FROM ubuntu:18.04 AS build-stage

# Use fastest mirror
RUN sed -i.bak -e 's%http://[^ ]\+%mirror://mirrors.ubuntu.com/mirrors.txt%g' /etc/apt/sources.list

# Install prerequisite apt packages to build
RUN apt update \
 && apt -yq dist-upgrade \
 && apt install -yq \
      python3-pip \
      cmake \
      gcc \
 && apt clean \
 && rm -rf /var/lib/apt/lists/*

# Build wheel
RUN pip3 wheel -w /tmp/wheelhouse --no-binary lightgbm lightgbm

FROM ubuntu:18.04 AS using-stage

# Use fastest mirror
RUN sed -i.bak -e 's%http://[^ ]\+%mirror://mirrors.ubuntu.com/mirrors.txt%g' /etc/apt/sources.list

# Install prerequisite apt packages to build
RUN apt update \
 && apt -yq dist-upgrade \
 && apt install -yq \
      python3-pip \
 && apt clean \
 && rm -rf /var/lib/apt/lists/*

# Copy binaries from building stage
COPY --from=build-stage /tmp/wheelhouse /tmp/wheelhouse

# Install binary package
RUN pip3 install /tmp/wheelhouse/lightgbm-*.whl

それでは、上記の Dockerfile をビルドしてみよう。

$ docker build -t example/multistage .

ビルドしたイメージからコンテナを起動する。

$ docker run --rm -it example/multistage /bin/bash

このイメージでは、ちゃんと LightGBM がインポートして使える。

# python3 -c "import lightgbm as lgb"

そして、イメージにはビルド用のツールも含まれていない。

# cmake
bash: cmake: command not found

いじょう。