#背景
alpineでC言語依存モジュールを pip install すると激重になる話
- alpineだと上記事象が避けられない
- multi stage buildで生成物を引っ張ってくるのが折衷案として良さそう
- 生成物を再利用する方法が安全ではない
- ベースイメージから生成物をベタに
COPY
している部分をやめる -
*.whl
を持ってきて安全にインストールする方法を採ってみる
- ベースイメージから生成物をベタに
- ついでに小手先技でimageを軽くしてみる
手順
- alpineイメージ上で必要モジュールをビルド
- 生成物(wheelファイル)は再利用しやすいように一箇所にまとめる
- ビルド完了済みイメージとして docker hub に push
- 実行環境用のalpineに multi stage build でwheelのディレクトリだけ取得しインストール
- 実行確認とお掃除
実装
まずは、必要なモジュール郡の取得から
今回は以下のモジュールを導入してみる
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
オプションで参照するテキストファイルはバージョン指定無しでもインストールは可能だ。
pip
setuptools
wheel
下記コマンドで特定ディレクトリにまとめたモジュールをupgrade
pip install -U --no-index --no-deps --no-cache-dir -f ./upgrade -r upgrade.txt
ただ、管理するファイルも増えるのでオフライン環境下でもない限りはdockerfileに直接書いた方がいい。
実行確認
importできるか確認。shellファイルにしておいてRUN
コマンド実行に直接叩く。
#!/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のビルドに使っていたイメージは生成物さえ残っていればいいので他は全部消す。
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
# モジュールに必要なファイルだけ新しい仮想パッケージとして括り、
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される仕組みがあれば楽になれるのかな?
参考文献
- pip : Installation Order
- イメージのタグ付け、送信、取得
- pip installをオフラインで行う
- Python, pipでrequirements.txtを使ってパッケージ一括インストール
- docker-library/python : python/3.8/alpine3.11/Dockerfile
-
pipのインストール順序は依存ライブラリや優先順位などを考慮せず一気通貫で実行されるため、
pip install
でも同様の事象は起こる。代わりに"circular dependency"なため途中で失敗したモジュールは、他全てのモジュールの導入が済み次第もう一度ビルドを実行し直すことで回避している。
今回に限っては先に依存モジュールを入れておくしか無い。2 ↩ -
手元の環境でscipy~=1.4だとエラーが出て失敗するため、素直に入ってくれた1.3系を指定 ↩
-
-no-cache-dir
を指定してインストールした実行環境だとpip cache purge
を実行するとcacheファイルが見つからずエラーコードを返す。地味に使いづらい。 ↩