Pythonで書かれたアプリケーションをDockerイメージにする堅い方法の紹介です。イメージサイズを少しでも削ろう! とかではなくて実用性重視の方向です。今回は第一弾ということで、Debianベースのイメージを使う方法を紹介します。
- 続編も公開されました。
Pythonのベースイメージ選び
イメージ作成にはベースイメージ選びからですが、基本的には次の2つかなと思います。Pythonバージョンは機械学習だと3.7がよく使われていると思いますが、5/8にTensorflowが3.8向けのwheelとセットで新バージョンを出したので、そろそろ3.8になっていきますかね。本記事も3.8にしますが、3.7でも動作します。
python:3.x-buster
python:3.x-slim-buster
イメージのOSには以下のような種類があります。
名前 | どんなもの? |
---|---|
buster |
Debian 10で処理系とかいっぱいインストール済み |
buster-slim |
Debian 10で処理系なしの実行環境用 |
stretch |
Debian 9で処理系入り |
stretch-slim |
Debian 9で処理系なしの実行環境用 |
alpine |
元はフロッピーで起動するファイルサイズ重視のディストリビューション |
windowsservercore |
Windows。LinuxやmacOS上では動作しない |
BusterとかStretchという名前が見慣れない方もいるかもしれませんが、これはLinuxディストリビューションとしてシェアの大きなDebianのコードネームです。
Debianバージョンが少し古いStretchの方がちょびっとサイズが小さかったりはしますが、まあ実用的にはサポートが長い方がいいですよね。slimを使ってGCCとかのコンパイラを自前でダウンロードしている記事とかもたまに見かける気がしますが、マルチステージビルドであれば、そんなにケチケチしなくていいのと、パッケージダウンロードは逐次処理なので遅く、処理系が入ったイメージのダウンロードの方が高速です。並列で処理されるし、一度イメージをダウンロードしてしまえば、なんどもビルドして試すときに効率が良いです。また、多くのケースでネイティブのライブラリも最初から入っており、ビルドでトラブルに遭遇することはかなり減るでしょう。
Pythonユーザーは基本的にAlpineを選んではいけません。いろいろネガが大きいからです。
最初の項目。なぜ遅いかというと、Alpineのアプリが使うlibc(musl)のメモリ周りアロケートの実装が、性能よりもライブラリのサイズ重視のシンプルでPythonの使い方と合わなくて速度が出ないとのこと。これはアプリケーションの実装次第なのでjemallocを使っているRubyとかの人は関係ないでしょうし、PostgreSQLとかnginxはAlpine版でも速度は変わらないようです。性能が2倍違うということは、クラウドでアプリケーションを動かすときはメモリさえ許せば1つ下のインスタンスでいいわけで、お金にも利いてきますよね。
後者の速度の問題ですが、PyPIはLinux向けにはmanylinux1という形式でバイナリを提供しており、DebianでもRed Hatでも高速にインストールできます。しかし、この形式はAlpineには対応していないため、C拡張を使うライブラリを使うと、Dockerイメージのビルド時間が伸びまくってしますわけです。
それでも、どうしても、PurePythonで処理速度はどうでも良い/お金がたくさんある、あるいはC拡張を使う場合でも人生を犠牲にしてでも、イメージサイズをどうしても減らしたいみたいな選ばれし者はAlpineを使う感じでしょうかね。
Pythonのマルチステージビルド
マルチステージビルドで環境を作っていきます。
アプリケーションはDjangoのチュートリアルの最初の1ステップだけ作ったものをuwsgiを使ってデプロイすることを想定します。フォルダ&ファイル構成はこんな感じ。
依存パッケージは今回はこれだけです。ただこのファイルは開発環境を設定するときに書くぐらいですね。あまり重要ではないです。
django |
実際にDockerの中で使うファイルはこちら。pip install -r requirements.txt
で依存ライブラリと一緒にインストールした後にpip freeze > requirements.lock
で作成します。あらかじめvenvで環境を分けて置くと、ノイズが混ざらないので良いです。
asgiref==3.2.7 |
uwsgi用の設定はこちら。プロセス数とかスレッド数は適当に。
[uwsgi] |
Dockerfileはこうなりました。slimの方にはlibxml2などがないので追加します。PostgreSQLのライブラリのlibpq5はまあおまけです。大抵ウェブアプリケーション作るときはPostgreSQLかMySQLは使うでしょうし。もし、使うライブラリがpure Python、もしくはC拡張でもwheelによるバイナリ配布をしているパッケージのみであれば、ビルド用イメージも3.8-slim-busterにできます。slimを使っていても、もしライブラリを追加した瞬間にGCCが必要になっても、イメージを3.8-busterに変えるだけなので、このマルチステージビルドの構成は崩さない方が良いでしょう。お仕事であれば問題回避の速度が大事ですからね。
# ここはビルド用のコンテナ |
docker build -t pytest . |
ブラウザでアクセスするとうまくいきました。
Pythonでどうやってマルチステージビルドを実現するのか
ポイントとしては、pip installしてできたライブラリを一式コピーして実行用のコンテナに写してます。uwsgiは別の場所にインストールされるので、それも個別にコピーします。
COPY --from=builder /usr/local/lib/python3.8/site-packages /usr/local/lib/python3.8/site-packages |
実はこの書き方を紹介している記事等はほとんどありません。ネットでよく見るPythonのマルチステージビルドの方法は次の方法です。
COPY --from=builder /root/.cache /root/.cache |
この方法であれば、uwsgiのような別の場所に入る実行ファイルも自動で処理されるのできれいにうまくいくのですが、最終的なイメージに/root/.cacheのレイヤーが残ってしまいます。RUNによるレイヤーなら&&を駆使して削除できるのですが、これはCOPYで作られるレイヤーなので、今のDockerだけだとこの無駄は削除できないのですよね。
機械学習ライブラリもりもりなイメージだと、これだけで200MBぐらいの容量になったり(全体も1.2GBとかになりますが)。今のところsite-packages全部コピーの方法で問題はなさそうです。
実行時に必要なライブラリがないと言われたら
フルセットのbusterには開発者向けのライブラリ類も含めていろいろ入っているのでビルドが成功するも、slim側にライブラリがなくて実行時エラーになる可能性があります。実行時にライブラリがロードできないというエラーが出たら、こちらのサイトにライブラリ名を入れて、それをインストールするのに必要なパッケージを探し、apt installの項目に追加してください。
Pythonだと関数の中でimportできます。名前空間を汚さないので、こちらの方が良いのかな、と思って僕も以前やっていましたが、ファイルのグローバルなところだけにimportがあると、ちょっとした実行すると必要なモジュールを全部読み込んでくれて、この手のロードエラーはすぐにわかります。関数内importはなるべく避けた方が良いでしょう(先日もそれで問題を見つけきれなかった)。
まとめ
Pythonを仕事で使う人のための堅いイメージ作成について紹介しました。Alpineのような性能のネガもなく、イメージの作成の実行時間も少なく、残業時間が減らせる方法を選びました。次回は条件によっては使えないかもしれないのですが、使えたら効果抜群なdistroless/python3について紹介します。
公開しました(2020/05/14)