Buildpack を使って Rye で管理された Python プロジェクトから Docker イメージを作成する

本記事は、[TUNA-JP Advent Calendar 2023](https://qiita.com/advent-calendar/2023/tuna-jp) の2日目になります。

## はじめに

みなさん、Rye を使っていますか？ Rye とは、Rust で書かれた Python 用パッケージ管理ツールです。これまでは、pyenv で Python のバージョンの管理をして、poetry などでパッケージの管理をするといった使い分けを行う必要がありました。しかし、Rye を使うことで Python のバージョンもパッケージも rye コマンドひとつで管理することができます。

その利便性から、Rye をおすすめする記事はたくさんあるのですが、Buildpack を使って Rye で管理された Python プロジェクトから Docker イメージを作成する方法を見つけることはできませんでした。

Rye も便利ですが、Buildpack も便利なので、Rye x Buildpack で Docker イメージを作りたいと思い、Buildpack を使って Rye で管理された Python プロジェクトから Docker イメージを作成できないか試してみました。

## 準備

まずは準備として、以下のソフトウェアをインストールしておきます。

- Docker Desktop
- [pack](https://buildpacks.io/docs/tools/pack/)
- [Rye](https://rye-up.com/guide/installation/)

## Rye プロジェクトの作成

今回はサンプルとして、Flask で Hello World を返すサーバを作成したいと思います。

最初に `rye init` コマンドでプロジェクトを作成します。すると、以下のように、Python プロジェクトの雛形が作成されます。

```bash
$ rye init rye-buildpack-sample
success: Initialized project in /Users/ttanigaki/workspace/local/rye-buildpack-sample
  Run `rye sync` to get started

$ ls -al rye-buildpack-sample
total 32
drwxr-xr-x@ 9 ttanigaki  staff  288 12  2 02:46 .
drwxr-xr-x@ 6 ttanigaki  staff  192 12  2 02:46 ..
drwxr-xr-x@ 9 ttanigaki  staff  288 12  2 02:46 .git
-rw-r--r--@ 1 ttanigaki  staff   93 12  2 02:46 .gitignore
-rw-r--r--@ 1 ttanigaki  staff   29 12  2 02:46 .python-version
drwxr-xr-x@ 7 ttanigaki  staff  224 12  2 02:46 .venv
-rw-r--r--@ 1 ttanigaki  staff   52 12  2 02:46 README.md
-rw-r--r--@ 1 ttanigaki  staff  344 12  2 02:46 pyproject.toml
drwxr-xr-x@ 3 ttanigaki  staff   96 12  2 02:46 src
```

`rye pin <python-version>` コマンドを実行すると、プロジェクトにおける Python のバージョンを固定することができます。

```bash
$ cd rye-buildpack-sample
$ rye pin 3.11
pinned 3.11.6 in /Users/ttanigaki/workspace/local/rye-buildpack-sample/.python-version
```

そして、`rye sync` コマンドを実行すると、指定したバージョンの設定を反映させることができます。

```bash
$ rye sync
~~ 省略 ~~
Successfully installed rye-buildpack-sample-0.1.0
Done!
```

`rye run` コマンドで、プロジェクト内の virtualenv 環境で後続のコマンドを実行することができます。ここでは、3.11.6 が使用されていることがわかります。

```bash
$ rye run python -V
Python 3.11.6
```

`flask` パッケージをインストールするために、`rye add` コマンドを使います。設定を反映させるために、追加で `rye sync` コマンドも実行します。

```bash
$ rye add flask
Added flask>=3.0.0 as regular dependency
$ rye sync
~~ 省略 ~~
Successfully installed blinker-1.7.0 click-8.1.7 flask-3.0.0 itsdangerous-2.1.2 jinja2-3.1.2 markupsafe-2.1.3 rye-buildpack-sample-0.1.0 werkzeug-3.0.1
Done!
```

Flask を使って、以下のような Hello World を返すコードを作成します。

```bash
$ cat src/rye_buildpack_sample/main.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return "Hello World!!"

if __name__ == '__main__':
    app.run(host="0.0.0.0", port=8080)
```

`rye run` コマンドを使って、上記コードが動作することを確認します。

```bash
$ rye run python src/rye_buildpack_sample/main.py
 * Serving Flask app 'main'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://0.0.0.0:8080
Press CTRL+C to quit

# 別ターミナル
$ curl http://localhost:8080
Hello World!!
```

ここまでで、Rye で管理された Python プロジェクトを作成することができました。

## Buildpack を使って Docker イメージを作成する

それでは、Buildpack を使って Docker イメージを作成したいと思いますが、`pack build` コマンドを実行する前に、2つ準備が必要です。

1つ目は、`requirements.txt` を作成することです。ビルド時にパッケージをインストールする必要がありますが、Paketo Buildpack には Rye で管理された Python プロジェクト内のパッケージを検知する仕組みがありません。

そこで、以下のように `requirements.lock` から `-e` を含む行を削除することで、`requirements.txt` を作成して、Python プロジェクト内のパッケージを検知できるようにします。

```bash
cat requirements.lock | sed '/-e/d' > requirements.txt
```

2つ目は、アプリケーションを起動するコマンドを `Procfile` に指定しておくことです。今回は、以下のように `python` コマンドで `main.py` を実行するコマンドを指定します。

```bash
$ cat Procfile
web: python src/rye_buildpack_sample/main.py
```

以上の準備ができたら、`pack build` コマンドで Docker イメージを作成します。

```bash
$ pack build flask-hello --builder paketobuildpacks/builder:base
base: Pulling from paketobuildpacks/builder
~~ 省略 ~~
Successfully built image flask-hello
```

最後に、`docker run` コマンドで、作成したイメージが動作することを確認します。

```bash
$ docker run --platform linux/x86_64 -d -p 8080:8080 -e PORT=8080 flask-hello

# 別ターミナルで実行
$ curl http://localhost:8080
Hello World!!
```

## さいごに

思ったより、簡単に Docker イメージを作成することができました。ただ、パッケージを追加・削除するたびに `requirements.txt` を更新するのはかなりの手間で、パッケージが不足するようなミスを引き起こす可能性があります。なので、`requirements.txt` なしに Docker イメージを作成する方法がないか探したいと思います。