🥖

RyeやRuffを触ってみたかったので、ついでにCIを整えてみる

2024/05/04に公開

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も導入しています。

  1. GitHubリポジトリの作成
  2. ローカル環境のセットアップ
  3. アプリケーションの作成
  4. CI用のワークフローの設定
  5. 実行確認

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`
    

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