0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

multi stage build でpythonのC言語依存モジュールを wheel形式でインストールする

Last updated at Posted at 2020-05-26

#背景
alpineでC言語依存モジュールを pip install すると激重になる話

  • alpineだと上記事象が避けられない
  • multi stage buildで生成物を引っ張ってくるのが折衷案として良さそう
  • 生成物を再利用する方法が安全ではない
    • ベースイメージから生成物をベタにCOPYしている部分をやめる
    • *.whlを持ってきて安全にインストールする方法を採ってみる
  • ついでに小手先技でimageを軽くしてみる

手順

  1. alpineイメージ上で必要モジュールをビルド
  2. 生成物(wheelファイル)は再利用しやすいように一箇所にまとめる
  3. ビルド完了済みイメージとして docker hub に push
  4. 実行環境用のalpineに multi stage build でwheelのディレクトリだけ取得しインストール
  5. 実行確認とお掃除

実装

まずは、必要なモジュール郡の取得から
今回は以下のモジュールを導入してみる

requirements.txt
cycler==0.10.0
Cython==0.29.17
h5py==2.10.0
joblib==0.14.1
kiwisolver==1.2.0
matplotlib==3.2.1
numpy==1.18.4
pandas==1.0.3
Pillow==7.1.2
pyparsing==2.4.7
python-dateutil==2.8.1
pytz==2020.1
scikit-learn==0.22.2.post1
scipy==1.3.3
six==1.14.0

必要モジュールを一箇所に集める

alpineの場合だとc言語依存モジュールはtarやzipなどの圧縮形式が落ちてくる。
これらをwhl形式に変換しておく必要がある。

下準備

whlに変換時に必要なライブラリが存在するはずなので、apk経由でinstallしておく

apk update \
  && apk add --virtual .build --no-cache openblas-dev lapack-dev freetype-dev 
...
  && apk add --virtual .community_build --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/community hdf5-dev

必要なwhlファイルを用意する

pip downloadでもモジュールのダウンロードを実行できるが、
pip wheelコマンドだとダウンロードとtar/zipファイルを自動展開してビルドまで行ってくれるのでこちらを使用。
pip wheel-rオプションを使用できるのでpip freeze > requirements.txtなどでバージョニングファイルを指定する。

pip wheel --no-cache --wheel-dir=./whl -r requirements.txt
  • オプション補足
  • --no-cache-dir: キャッシュを使用・作成をしない。指定しないと~/.tmpとかにもりもりキャッシュされる。ビルド時の生成物もキャッシュされる。
  • --wheel-dir: wheelファイルのアウトプット先。

pip wheelを使用する場合の補足

残念ながら今回の場合は途中で失敗する
別alpine環境で構築しpip freezeしたrequirements.txtを使用しているのだが、
numpy,scipyが使用できる環境でないためscikit-learnビルド中に落ちる。

pip install -r requirements.txtだとpip側がよしなにインストールしてくれるが1

pip install cython numpy==1.18.4 scipy==1.3.3
pip wheel --no-cache --wheel-dir=./whl -r requirements.txt

numpyやscipyのビルドを回避するために別途イメージを作ろうとしていたのに
なんだか無意味なことをしている気がしてきたぞ...?

ビルド完了後docker hubにpush

適当にタギングしてpush

docker tag 123456789a hoge/builder-image:latest
docker push hoge/builder-image:latest

実行環境用に生成物を持ってくる

ここからは実行環境用のdockerfile上で作業していく。

ローカルディレクトリのwheelをインストール

pip installで複数モジュールを指定するにはベタ書きしていくか--requirementでテキストファイルを指定する。
適当なディレクトリにwhlを集めて丸ごとインストールできるような仕様は無い。

今回はマルチステージビルドでwheelが入ったディレクトリをCOPYし下記コマンドを実行することでローカルのwheelからインストールする。

pip install --no-index --no-deps --no-cache-dir -f ./whl -r requirement.txt
  • オプション補足
  • --no-index: PyPiのようなインデックスサイトを使わない。オンラインを経由したくない時に使う
  • --no-deps: 依存モジュールをインストールしない。ただし、モジュール側で明確に指定されている場合はその限りでない模様。
  • -f,--find-links: モジュールの検索先を指定。ローカルパスを指定したい時はこれを使う

--upgrade で対応するモジュール

pipやsetuptoolsなど--upgradeオプションをつけて導入したいモジュールはアップグレード用のテキストファイルに分けて導入する。
-rオプションで参照するテキストファイルはバージョン指定無しでもインストールは可能だ。

upgrade.txt
pip
setuptools
wheel

下記コマンドで特定ディレクトリにまとめたモジュールをupgrade

pip install -U --no-index --no-deps --no-cache-dir -f ./upgrade  -r upgrade.txt

ただ、管理するファイルも増えるのでオフライン環境下でもない限りはdockerfileに直接書いた方がいい。

実行確認

importできるか確認。shellファイルにしておいてRUNコマンド実行に直接叩く。

import_test.sh
#!/bin/sh
python -c "import numpy"
python -c "import scipy"
python -c "import h5py"
python -c "import pandas"
python -c "import matplotlib"
python -c "import sklearn"

後始末

dockerイメージの軽量化のため余分なファイル削除
whlのビルドに使っていたイメージは生成物さえ残っていればいいので他は全部消す。

builder-image
apk del --purge .build .testing_build
pip freeze | xargs pip uninstall -y
pip cache purge

余分なファイルを削除した事で、ビルドしたイメージがどれだけ軽くなったか確認。360MBほど減量に成功した模様

# docker images
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
naka345/wheel_build     latest              b6c9df898334        9 minutes ago       1.04GB 
naka345/wheel_build     latest              3236cf2f87de        2 days ago          639MB

次は実行環境側の整理。
公式のpython dockerがとてもスマートなので、これに倣ってファイルを消し込んでいく。3

execution-image
# モジュールに必要なファイルだけ新しい仮想パッケージとして括り、
find /usr/local -type f -executable -not \( -name '*tkinter*' \) -exec scanelf --needed --nobanner --format '%n#p' '{}' ';' \
    | tr ',' '\n' \
    | sort -u \
    | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \
    | xargs -rt apk add --no-cache --virtual .module-rundeps && \
  # ビルド時に使っていたパッケージ群は全て消す
  apk del --purge .build .community_build
# python側の余分なファイルやゴミの削除
find /usr/local -depth \
		\( \
			\( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \
			-o \
			\( -type f -a \( -name '*.pyc' -o -name '*.pyo' \) \) \
		\) -exec rm -rf '{}' + 

# 今回の実行範囲分のゴミ掃除
rm -rf /tmp/whl

実行環境側で消さなかった時との比較をしてみる。

# docker images
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
naka345/wheel_install   latest              f0df8a9887de        3 hours ago         1.29GB
↓
naka345/wheel_install   latest              27b4805053f2        3 hours ago         968MB

なんとか1GB以下に抑えることができた。

dockerfileにしてみる

上記を踏まえてdockerfileに書き下す。
長くなるのでgithubのリンクを貼っておしまい。

まとめ

時間がかかるモジュールもpip経由で安全に比較的手早く持ち込めるようにした。
dockerイメージも多少軽量化できた。

ただし、イメージを複数持たないといけない部分は据え置き。
requirements.txtの整合性が要になるので、
こいつが更新されたタイミングでdocker hubに両イメージがpushされる仕組みがあれば楽になれるのかな?

参考文献

  1. pipのインストール順序は依存ライブラリや優先順位などを考慮せず一気通貫で実行されるため、pip installでも同様の事象は起こる。代わりに"circular dependency"なため途中で失敗したモジュールは、他全てのモジュールの導入が済み次第もう一度ビルドを実行し直すことで回避している。
    今回に限っては先に依存モジュールを入れておくしか無い。2

  2. 手元の環境でscipy~=1.4だとエラーが出て失敗するため、素直に入ってくれた1.3系を指定

  3. -no-cache-dirを指定してインストールした実行環境だとpip cache purgeを実行するとcacheファイルが見つからずエラーコードを返す。地味に使いづらい。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?