nikkie-ftnextの日記

イベントレポートや読書メモを発信

Ryeで環境構築して開発中の自作Pythonライブラリを含んだDockerイメージをビルドする

はじめに

ミリシタ新イベ、転天革命じゃん...😇 nikkieです。

今回はRyeに慣れるために素振りしたクソライブラリをクソDockerイメージにしていきます!

目次

前提:Ryeに慣れるために素振りしたクソライブラリ

からあげ皇帝のunkoの系譜を汲み、先日Ryeバージョンを爆誕させました

このクソライブラリunkoをインストール済みのDockerイメージを作るのがこの記事のゴールとなります。

結論

  • requirements.lockからeditable installの行を除き、依存ライブラリを先にインストール
  • ソースコードをCOPYして、インストール
  • マルチステージビルドで、site-packages以下をCOPY!
% docker build -f docker/Dockerfile -t unko-by-rye:example .
% docker run --rm -it unko-by-rye:example hello
puripuri
% docker run --rm -it unko-by-rye:example python -q
>>> from unko import deru
>>> deru()
puripuri

Dockerイメージビルドのための戦略を練る

まずRyeが生成したファイルのうち、Dockerイメージのビルドに関連するものを洗い出します(太字で示します)

  • requirements.lock
    • 現状のunkoは依存がないトイプロジェクトですが、実プロジェクトで使える方法を知りたいので、適当に行を追加することにします
  • requirements-dev.lock
    • こちらは開発環境をsyncさせるのにとても役立つファイルです。開発で使う依存は今回作るDockerイメージには不要なので扱いません
  • src
    • ここにクソライブラリの実体があります
    • pyproject.tomlにプロジェクトのメタデータがありますね

requirements.lockにある依存をDockerイメージにインストール

Ryeはrequirements.lockやrequirements-dev.lockを元に、魔法のように開発環境を再現させます(rye sync)。
魔法の仕掛けは以下

-e file:.
kojo-fan-art==0.1.1
  • -eの行はeditable installです(--editableとも指定できます)
  • 依存ライブラリについてはバージョン固定して表されています1

editable install

https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs

Editable installs allow you to install your project without copying any files.

通常のpip installPyPIからライブラリの実装をダウンロードし、site-packages以下に保存します。
例えば、仮想環境を使っていたら、.venv/lib/python3.12/site-packages2のようになります

開発中のライブラリも同様にsite-packages以下にコピーする運用だと、ソースコードをいじった後毎回pip installが必要です。
これは面倒ですし、たまたまpip installを忘れてしまって期待通り動かず、開発中に混乱してしまうかもしれません。

そこでeditable installの出番となります!
開発中のライブラリのソースコード一度だけeditable installしておけば、いくら編集してもそれが反映されます
文字通り、ソースコードを編集可能なインストールです!

Dockerイメージのマルチステージビルドとeditable install

私はDockerイメージを作る以上はサイズが小さいものを作りたいので、マルチステージビルドしていきます(取り回しやすいので、小さいは、正義!)。

マルチステージビルドではDockerイメージ間でsite-packages以下をコピーします。
ですが、requirements.lockの記載(editable install)とは噛み合いません。
そこで、Dockerイメージのビルドの中では、通常のinstallをしてソースコードをsite-packages以下に置くように工夫します。

Ryeのコミュニティに見つけた工夫

Rye + Docker · mitsuhiko rye · Discussion #239 · GitHub

WORKDIR /backend
COPY ./requirements.lock /backend/
RUN sed '/-e/d' requirements.lock > requirements.txt
RUN pip install -r requirements.txt
  • requirements.lockだけDockerイメージの中にコピー
    • これは理にかなっていて、requirements.lockの中身を変えない限り、ビルド中のレイヤーはキャッシュされ、ビルド時間短縮に寄与します3
  • sedコマンドで-eを含む行を削除
    • 依存ライブラリのバージョン指定だけが残りますね
  • 依存ライブラリをインストール
% cat requirements.lock
-e file:.
kojo-fan-art==0.1.1
% sed '/-e/d' requirements.lock
kojo-fan-art==0.1.1

nikkieによる改善案

WORKDIR /backend
COPY ./requirements.lock /backend/
RUN <<EOF
sed '/^-e/d' requirements.lock > requirements.txt
pip install -r requirements.txt
EOF
  • sedで削除するパターンを「-eで始まる」行に変えました
    • これによりhoge-eee==0.0.1のような-eを含む依存指定の行が削除されなくなります
    • Ryeのworkspaceを使うと-eで始まる行が複数できます。sedの削除はパターンにマッチした全行を消すので対応できています
    • sed-iオプションでrequirements.lockをインプレースに変更することもできます

% cat requirements.lock
-e file:.
-e file:subpkg
kojo-fan-art==0.1.1
hoge-eee==0.0.1
% sed '/-e/d' requirements.lock
kojo-fan-art==0.1.1
% sed '/^-e/d' requirements.lock
kojo-fan-art==0.1.1
hoge-eee==0.0.1
  • イメージビルド中のレイヤーの数を減らすために、RUNコマンドをまとめたいです
    • heredocが書けるので使いました
    • 私はEOFの代わりに、まとめた処理を表す「関数名」を使っています
  • ちなみに好みで、python -m pip install推しです

ソースコードを(site-packages以下に)インストール

先に述べた通常のinstallを進めるだけです。

COPY . .
RUN python -m pip install --no-cache-dir .

srcディレクトリを含む一式をDockerイメージ側にコピーし、editableではない(=通常の)インストールをしました。
これでsite-packages以下にコピーされます。

ソースコードの一部(例:printする文字列)を変えた後のDockerイメージのビルドは、COPY命令以降が都度実行される形になります。
requirements.lockは変更頻度が小さいので、キャッシュされたレイヤーが使われやすいのです。

動作環境

macOSです(zsh使用)

% docker --version
Docker version 24.0.2, build cb74dfc
% rye --version
rye 0.16.0
commit: 0.16.0 (c003223d5 2023-12-16)
platform: macos (aarch64)
self-python: cpython@3.11
symlink support: true

終わりに

Ryeで環境構築したクソライブラリをインストールしたクソDockerイメージの作り方でした。
おもちゃな例で取り上げていますが、以下の点は実用的ではないかと思います

  • Ryeが生成したrequirements.lockの-eで始まる行はDockerイメージのビルドにおいては除く
  • site-packages以下にインストールするため、ソースコードのeditable installはしない(通常のインストールをする)

実プロジェクトでも試してみて学びがあれば更新します

ちなみに-eの行をRyeが書き込むという挙動4は、音楽性が違うなと感じるポイントの1つです。
Ryeがラップしているpip-compileでは-eの行は書き込みません。
rye syncのeasyさのために、Dockerイメージビルドで複雑さが持ち込まれているように見えており、それをネガティブに評価しています。
(Ryeはmitsuhikoさんの個人的な課題解決のツールだから、mitshuhikoさんのユースケースではソースコードを含むDockerイメージをあんまりビルドしないのかなーって。あ、もしかすると、buildしてからPyPIに上げちゃうから、それをDockerイメージでインストールする想定なのかも!)


  1. 依存をなにか追加したかったので自作ライブラリを指定しました
  2. モジュール検索パスとも関係するようです。このあたり理解を深めたいところ https://docs.python.org/ja/3/using/cmdline.html#envvar-PYTHONPATH
  3. 例えば「Instruction order matters for leveraging build cache」にあります
  4. このあたり https://github.com/mitsuhiko/rye/blob/0.16.0/rye/src/lock.rs#L369