🐰

Ryeとキャッシュ機能で快適なCI環境を整備してみた

2024/10/21に公開

はじめに・導入経緯

Specteeでエンジニアをしている和山と申します!
私の所属するチームでは、コードの品質を保つためGitHubActionsを使用して静的解析や単体テストを実施しています。
現状は特に問題が発生してませんが、今後のプロジェクト拡大を意識し、キャッシュ機能を用いて今のうちから速度改善が見込めないか検証してみました。

本記事の結論

GitHubActionsでryeのキャッシュを活用したければsetup-ryeがオススメ
https://github.com/eifinger/setup-rye

前提条件

今回のCIはGitHub Actionsで行うものとし、コードをpushしたタイミングで動作させます。
検証内容は、以下の作業がすべて終了した時間を改善前と後で比較します。

  • ruff: フォーマット・静的解析
  • mypy: 型チェック
  • pytest: 単体テスト
  • cpell: スペルチェック

検証に使用したリポジトリは、複数の独立したAWS Lambdaサービスが存在しています。
既存CIでは各サービスを並列で実行できるようにしています。

改善前の構成

改善前のCIのフローになります。サービス名は仮でA・B・C・Dとしています。

flow-1

この状態で実行したところ、結果は1分51秒となりました。

actions-1

ボトルネック要因はパッケージインストール

このフローの中で一番時間がかかっている要因はpipコマンドによるパッケージのインストールです。
これがないと単体テストやmypyによる型チェックが上手く出来ないので必須となります。

例として、あるサービスの単体テストの実行時間をお見せします。
パッケージのインストールはInstall dependenciesのステップで実行しています。

22秒かかっているのが分かります。

actions-2

今のフローは静的解析と単体テストが子ワークフローとなっており、親から呼び出して動いています。
そのため、それぞれのワークフローでパッケージをインストールする必要があり、全体の実行時間に大きく影響しています。

また、今のフローだとパッケージに変更がない場合も毎回インストール作業が必須となります。
今はそれほど大きな時間がかかっているわけではありませんが、プロジェクトが大きくなった場合、実行時間の遅れがプロジェクト全体の進捗に影響する可能性があります。

これらの課題をキャッシュを用いて解決できるか検証していきます。

Ryeでキャッシュ機能を実装する

GitHub Actionsでパッケージ関係やビルドを早くするならキャッシュがよく使われます。
https://github.com/actions/cache

私たちはRyeでパッケージ管理をしているため、サードパーティ製のsetup-ryeを使うことにしました。

https://github.com/eifinger/setup-rye

サードパーティ製にはなりますが
👇️理由のため比較的安心して使えると思います。

  • 本家のryeのワークフローにも採用されている
  • メンテが定期的に行われている(2024年9月末時点)

また、オプションでキャッシュ機能を指定可能で、今回の検証要件を簡単に実現することができます。

フロー

キャッシュを導入するに伴い、まずは既存のフローから修正を行いました。

修正点は以下のとおりです。

  • 親のワークフローで先に仮想環境作成&パッケージをインストール
  • 子のワークフローでキャッシュされた仮想環境を利用
  • 単体テストと静的解析を1つのワークフローに統合

これらを反映することで
課題の一つだった「別々のワークフローでパッケージをインストールしている」問題を解決します。

上記を反映させたものが下図のフローになります。

flow-2

実装

それでは実装してみます。
まずはプッシュトリガーで動作する親のワークフローからです。

ポイントはcache-prefixでキャッシュのプレフィックスを指定することです。
親で指定したキーを子のワークフローでも指定することで、親で設定したキャッシュの引き継ぎが可能になります。

parent.yaml
jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - name: checkout repository
        uses: actions/checkout@v4

      - name: setup rye
        id: setup-rye
        uses: eifinger/setup-rye@v4
        with:
          enable-cache: true
          cache-prefix: rye-cache # ポイント:cacheのprefixを設定する

    # 【参考】キャッシュが取得できたか確認するためのコード
      - name: Do something if the cache was restored
        run: |
          if [[ ${{ steps.setup-rye.outputs.cache-hit }} == "true" ]]; then
            echo "Cache was restored"
          else
            echo "Cache was not restored"
          fi

    # パッケージをインストールし、.venvを作成する
      - name: install dependencies
        run: rye sync
    
    # ここでjobが終了。キャッシュ情報として.venvがGitHubのサーバに保存される

  ci:
    needs: setup
    uses:
      ./.github/workflows/ci.yml # 子ワークフローの静的解析・単体テストを実行する

続いて子のワークフローを実装します。Ryeのセットアップは親と一緒です。
ポイントは cache-prefixを親と一緒にする」 ことです。
これにより、親が先行して作成・キャッシュしてくれたPythonの仮想環境を使うことができます。

ci.yaml
name: CI

on:
  workflow_call:

jobs:
  ci:
    strategy:
      matrix:
        ci_target_dir:
          - ./services/A
          - ./services/B
          - ./services/C
          - ./services/D

    runs-on: ubuntu-latest
    steps:
      - name: checkout repository
        uses: actions/checkout@v4

      - name: setup rye
        id: setup-rye
        uses: eifinger/setup-rye@v4
        with:
          enable-cache: true
          cache-prefix: rye-cache # 親と同じプレフィックスを設定する

      - name: install dependencies
        run: rye sync

      - name: run ci for python project
        run: |
          CI_TARGET_DIR=${{ matrix.ci_target_dir }} bash -c scripts/ci.sh

      - name: spell check with cspell
        uses: streetsidesoftware/cspell-action@v6
        with:
          root: ${{ matrix.ci_target_dir }}
          config: ./cspell.json

参考までにci.shとサービスごとのpyproject.tomlの中身です。

まずはci.sh。サービスディレクトリに移動してコマンドを発行します。

ci.sh
echo "CI TARGET DIR: ${CI_TARGET_DIR}"
cd ${CI_TARGET_DIR} && rye run ci

続いてpyproject.toml
rye run ciスクリプトはチェイン機能でつなげています。

pyproject.toml
[tool.rye.scripts]
test = "pytest -vv tests"
check = { chain = ["check:ruff", "check:mypy"] }
"check:ruff" = "ruff check --config pyproject.toml src"
"check:mypy" = "mypy src --config-file=../../pyproject.toml"
ci = { chain = ["check", "test"] }

[tool.ruff]
# ruffのルールは親に配置しているので参照するための設定
extend = "../../pyproject.toml"

測定

それでは実装したものを測定します。改善前は1分51秒でした。

actions-3

改善後は1分。改善前と比べて約50秒改善となりました!
初回は親側でのインストールがあるため時間がかかっています。

actions-4

2回目以降パッケージの変更がなければ、キャッシュが機能し更に早くなります。

actions-5

2回目の実行。更に10秒ほど早くなりました。 親側でもキャッシュが効いたためです。
詳細画面でも確認してみます。

actions-6

仮想環境が存在する旨のメッセージと共にインストールがスキップされているのが分かります。
これで課題だった、パッケージに変更がない場合も毎回インストールが発生する問題が解決されました!

注意点

便利なキャッシュ機能ですが、以下のルールがあります。

  • 一つのリポジトリで保有できるキャッシュ量に制限があります。
  • キャッシュを検索するときに同一キーが無いか検索(なければprefix検索)する

そのため、運用がうまく出来ていないと無駄にキャッシュを生成して全く使われない…みたいなことも起きます。

👇️記事は、他チームの方が共有していただいた記事です。

https://ymmt.hatenablog.com/entry/2024/10/02/222243

つまり、キャッシュ利用時はブランチ運用の考慮も必要です(奥が深い…)。
私の所属するチームはトランクベースをちょっといじったような運用なのでそこまで記事の影響を受けませんが、キャッシュを検討する際は一度記事に目を通しておくと良さそうです。

https://www.docswell.com/s/uta8a/KYDW9P-2024-08-22-github-actions-tips#p13

まとめ

今回はPythonプロジェクトでRyeとキャッシュを使った快適なCI環境整備を紹介しました。
改善は約1分ほどでしたが、プロジェクトが肥大になっていけばより恩恵を受けられそうな印象を受けました。

また、今回この記事を作成するにあたり、今まで曖昧だったキャッシュ機能の動作概要についてチームメンバー全員で理解を深めることができたと思います。
今後もキャッシュ機能を使って大幅な改善が見込めそうなプロジェクトがあれば活用したいと思います!

参考

キャッシュについて

運用

GitHubで編集を提案

Discussion