🥖
RyeやRuffを触ってみたかったので、ついでにCIを整えてみる
TL;DR
- RyeやRuffを使ってみたくて、例題としてちょっとしたCI環境を整えてみた
- GitHub Actions上でRyeとRuffを活用し、単体テストや負荷検証までやってみた
- RyeとRuffの高速さに感動。導入もスムーズなのが素敵
モチベーション・想定読者
モチベーション
- 元々データ分析としてPythonを扱うことが多かったため、プログラミングの素養は十分なものではありませんでした。
- Pythonを触って、10年程経つことに気づき、それと同時に知識がアップデートされていないことに気づいてしまいました。(poetryすら怪しいほど)
- 自戒の意味と、最近の流行りであるRust製のあれこれが面白そうという理由で、学習の足跡を残します。
想定読者
- Rust製のツールを使ったことがない。
- CIを意識してコードを管理したことがない。興味がある。
- RyeとRuffが動いている一連を知りたい。そのまま使いたい。
→ここに今回の検証分全て置いています
構築
GitHub Actions上にCIの環境を整えます。CIにこだわりがあるわけではありませんが、APIサーバの想定で検証するので、負荷試験も合わせて使ってみたいと思い、pytestだけでなくLocustも導入しています。
- GitHubリポジトリの作成
- ローカル環境のセットアップ
- アプリケーションの作成
- CI用のワークフローの設定
- 実行確認
1. GitHubリポジトリの作成
リポジトリの作成は、公式通りに行っています。
2. ローカル環境のセットアップ
rye init
は初回だけで良く、先頭にrye
をつけてからいつも使っているコマンドを使えば大体思い通りにできるのではないでしょうか。
$ curl -sSf https://rye-up.com/get | bash
$ echo 'source "$HOME/.rye/env"' >> ~/.bashrc
$ source ~/.bashrc
$ rye init (YOUR_WORKFOLDER_PATH)
$ rye pin (PYTHON_VERSION)
$ rye sync
$ rye add (LIBRARY)
3. アプリケーションの作成
- コードの作成→静的コードチェックまでを目的として実装します。
- 動かすコード自体は、FastAPIの実行確認を行う簡素なものです。
- 公式に記載されているコードを流用
from fastapi import FastAPI app = FastAPI() @app.get("/") async def read_main(): return {"msg": "Hello World"}
- formatter, linterをRuff経由で実行します。
$ rye run ruff format --check # 配下のファイルのフォーマットチェック $ rye run ruff format . # (エラーが起きた場合)フォーマット実行 $ rye run ruff check . --fix # リンターチェックと、エラー時の修正
4. CI用のワークフローの設定
- 特定のブランチにpushされたタイミングで、各種チェックが走るようにする
- 静的コード検証(Ruff)
- テストコードの実行(pytest)
- 負荷検証(Locust)
- ワークフロー上の各ステップでフォーマットチェックやテストを行う
- Rye・Ruffの導入は、Marketplace上のワークフローを利用
steps: - uses: actions/checkout@v4 # ryeのセットアップ - uses: sksat/setup-rye@v0.22.0 # ruffのセットアップ - uses: chartboost/ruff-action@v1 with: args: 'format --check' - name: Sync Rye run: rye sync
- pytestの実行
- name: Run pytest run: rye run python -m pytest
- FastAPIの運用を想定して、負荷試験を行う
- Locustの実行
- Uvicornを使った検証としたため、nohupによるバックグラウンド実行を行い、シャットダウンする形に
- name: Run Uvicorn run: |- cd src nohup rye run uvicorn main:app --host=0.0.0.0 --port=8000 > uvicorn.log 2>&1 & echo $! > ../uvicorn_pid.txt - name: Run Locust run: rye run locust --host=http://0.0.0.0:8000 --headless -u 1 --run-time 10 - name: Shutdown Uvicorn run: kill -9 `cat uvicorn_pid.txt`
- Locustの実行
5. 実行確認
全体の結果はこちら
- Ruffのフォーマットチェック結果
- pytestの実行結果
- Locustの実行結果
個人的に躓いたところ
Ruff
- 検証用として、ローカル上でのRuffの実行を手動にしていました。当然途中でフォーマットチェックを忘れ、GitHub Actionsでコケた...
- もちろん
rye run ruff format .
で解決 - VSCodeの拡張機能で、自動保存するようにしたら良さそう
- もちろん
Locust
- 初期設定で検証すると、GitHub Actionsが止まります。
- 失敗したリクエスト数が一定を超えることで発生していたため、
locustfile.py
で閾値を設定することで、エラーを回避、適切な閾値での管理が可能となります。- 終了条件を任意の形式に設定できるよう、ファイルを置きます。 参考
import logging from locust import HttpUser, events, task @events.quitting.add_listener def _(environment, **kw): # リクエスト失敗の閾値を10%に引き上げた if environment.stats.total.fail_ratio > 0.1: logging.error("Test failed due to failure ratio > 10%") environment.process_exit_code = 1 elif environment.stats.total.avg_response_time > 200: logging.error("Test failed due to average response time ratio > 200 ms") environment.process_exit_code = 1 elif environment.stats.total.get_response_time_percentile(0.95) > 800: logging.error("Test failed due to 95th percentile response time > 800 ms") environment.process_exit_code = 1 else: environment.process_exit_code = 0
- 個人のワークフローでは、Locustのデフォルト設定を適用するとスペック不足でWarningが出ます。直接的なエラーではありませんが、設計次第ではLocustをワークフロー外で実行しないといけないかも
- 終了条件を任意の形式に設定できるよう、ファイルを置きます。 参考
あとがき
Pythonを始めたての何も知らなかった時のような、「こんなことできるんだ」「便利な使い方!」といった感動を思い出しました。RyeやRuffが高速だよ!というのは目にしつつも、チュートリアルのような簡易なものでも触らないと実感が湧かない人間だったので、重い腰を上げて触って正解だったなと思ってます。
とはいえ、本番環境であれば考慮するべきはずの、E2Eテストや各種リソースとの接続試験等は実施していないので、CIとして完璧か?という問いはNoだと思います。ですが、コードをデプロイする前にチェックしておきたい、最低限の項目のベースは用意できたはずです。継続的デリバリーの部分はまるっと省いたりと、色々歯抜けも多いとは思いますが、また知見が溜まったら投稿できたら良いなと思います。
Discussion