スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

Railsアップデート時の設定不整合をCIで自動検出する仕組みを作った話

こんにちは、小中高ベーシック開発グループの @sakuro です。

今回は Ruby on Rails のアップデートの際に発生した問題点を将来のアップデートの際に検出できるようにした話を紹介します。

スタディサプリ monorepo の技術構成

スタディサプリの monorepo は、教育プラットフォームの多様な要件に対応するため、複数のアプリケーションやライブラリ群で構成されています。その内訳の主要なものは以下の通りです。

Rubyで書かれたものの半数はRailsアプリケーションであり、それ以外にはgem化した内部利用ライブラリなどがあります。

Railsアップデート時の設定管理の課題

Gemのバージョン更新を怠っているとセキュリティ上のリスクや未改修のバグへの遭遇のリスクがあるため、継続的なアップデート作業が必要です。一般的なgemの小さなアップデートならdependabotrenovateが作成してくれるPRをマージするだけで済むことも多いのですが、Railsのようなフレームワークの、しかもパッチレベルを超えるアップデートでは腰を据えた作業が必要になることもあります。

Rails 5.1以降では、従来から存在する new_framework_defaults ファイルに加え、 config/application.rb に config.load_defaults が追加されており、この2つを適切に管理する必要性が生じています。この変更により、新規アプリケーションは config.load_defaults でバージョン指定するだけで適切なデフォルト設定が自動適用される一方、アップデート時には new_framework_defaults ファイルが「新しいデフォルトの参考情報」として機能するようになりました。しかし、この2つの仕組みを正しく理解・管理しないと、設定の意図が不明確になり、メンテナンス性が低下する問題が発生します。

自動化スクリプトの開発経緯

今回開発したスクリプト (validate_rails_config_defaults.sh) は、Railsアップデート後の作業漏れを検出するために作成されました。monorepo 環境では複数のアプリケーションがそれぞれのコードオーナーによって個別にアップデートされるため、統一した方法で作業漏れを検出する仕組みを導入する価値があります。

背景

Railsアップデート時に生成されるnew_framework_defaultsファイルは、新しいバージョンのデフォルト設定を段階的に適用するためのものです。このファイル内の設定はすべてコメントアウトされており、必要な設定のみをコメントアウトを外して有効化する仕組みになっています。

Railsアップデート作業における new_framework_defaults ファイルへの正しい対処方法は以下の通りです:

  1. アップデートタスク実行: rails app:update コマンドでタスクを実行。 new_framework_defaults_*.rb ファイルが生成される。
  2. 段階的な適用: 各設定項目について以下の手順で進める。
    • どれか1つの設定項目のコメントアウトを外し、有効化する (= 新しいRailsにおけるデフォルト値を適用) 。
    • その状態で問題がなければ、この設定はデフォルト値なので削除してしまう。
    • その状態で問題があれば、デフォルトが望ましくないので、望ましい値を config/application.rb で明示するか、設定で解決できないのであればアプリケーションコードを書き換えて対応する。
  3. 全項目の判断完了: 各項目について一通り判断が済んだら、config/application.rbconfig.load_defaults新しいRailsのバージョン に置き換える。
  4. ファイルの削除: 最終的に new_framework_defaults ファイルを削除。

発生する問題

しかし、実際の運用では以下のような状態が放置されたままリリースされていることがありました:

  • new_framework_defaultsファイルの残存: アップデート完了後もファイルが残存している
  • config.load_defaultsのバージョン不整合: config/application.rbconfig.load_defaults で指定するバージョンと、実際に利用しているRailsのバージョンが一致していない場合、古いデフォルト設定が適用されてしまう
  • 設定の意図不明確化: 設定の意図が不明確になり、メンテナンス性が低下する
  • 適用状況の把握困難: どの設定が適用済みかが分からなくなる

このような問題を解決するため、新たにCIで実行するスクリプトを作成し、 new_framework_defaults ファイルの有無と config.load_defaults に指定したバージョンの変更漏れを自動的に検出する仕組みを構築することにしました。

実装

機能1: new_framework_defaults ファイル検出

Railsアップデートが完了した正しい状態では、new_framework_defaultsファイルは存在しないはずです。これらのファイルは段階的な設定適用のために一時的に生成されるもので、最終的には削除される必要があります。

この確認のために以下の処理を実装しました:

  • ファイル検索:

    引数で与えられた Rails root ディレクトリ以下のconfig/initializersディレクトリ内のnew_framework_defaults*.rbファイルをfindコマンドを利用して検索します。

  local new_framework_defaults_files=$(find "$initializers_dir" -name "new_framework_defaults*.rb" 2>/dev/null)
  • エラー報告:

    ファイルが 見つかった 場合はエラーメッセージを出力します。

  if [ -n "$new_framework_defaults_files" ]; then
    echo "Error: new_framework_defaultsファイルがあった"
    exit 0  # CIで失敗させずに警告として扱う
  fi

機能2: Railsバージョン整合性チェック

Railsアップデートが完了した正しい状態では、config/application.rbconfig.load_defaultsで指定するバージョンと、実際に利用しているRailsのバージョン(メジャー・マイナーバージョン)が一致している必要があります。

この確認のために以下の処理を実装しました。

  • config.load_defaultsに指定されているバージョンの抽出:

    config/application.rbからconfig.load_defaultsで指定されているバージョンを抽出します。

  local load_defaults_version=$(grep -Eo 'config\.load_defaults[^0-9]+[0-9]+\.[0-9]+' "$application_rb_path" | grep -o '[0-9]\+\.[0-9]\+')

grep-o はあまり見かけないオプションかもしれません。これを使うとパターンにマッチした際に行全体ではなくマッチした箇所のみを出力するので出力加工の手間が省けます。PCRE(Perl Compatible Regular Expression) に対応した grep (GNU grep など)なら、look ahead および look behind を使うことにより、非常に簡潔に目的の箇所を抽出することもできます。今回も (?<=config\.load_defaults\S+)\d+\.\d+ などとできそうに見えたのですが、PCRE では (?<= ...) (look behind) に指定するパターン長を不定にできないという制約がある*1ため \S+ が使えず、 egrep(1)(grep -E) および grep(1) が扱えるパターンで対処しています。

  • major_minor_versionの検出: Gemfile.lockから実際に利用されているRailsのメジャー・マイナーバージョンを抽出します。
  local rails_full_version=$(sed -n 's/^\s\+rails\s\+(\([0-9]\+\.[0-9]\+\.[0-9]\+\).*/\1/p' "$lockfile_path")
  local major_minor_version=$(echo "$rails_full_version" | cut -d. -f1,2)
  • バージョン一致の確認:

    両方のバージョンが一致するかを確認し、不一致の場合はエラーメッセージを出力します。

  if [ "$major_minor_version" != "$load_defaults_version" ]; then
    echo "Error: バージョン不整合"
    exit 0
  fi

実際にメッセージを出してみた例

config.load_defaultsに指定したバージョン(7.0)とGemfile.lockに現れるバージョン(7.2.2、メジャー・マイナーの組み合わせは7.2)が一致していない旨のメッセージ
PRコメントで警告を受けた例 (Railsバージョン不一致)

エラーにするか、警告にするか

当初は、いずれかの違反を検出したらエラー終了すれば修正を促す強い強制力になると考えていたのですが、過去に次のようなケースがあったため、検出スクリプトによる指摘がある状態でもリリースできるよう、メッセージを出すだけに留めています。

ただし、Rails 6.1アプリケーションはこの新しいシリアライズフォーマットを読み取れないので、 シームレスにアップグレードするには、まずRails 7.0へのアップグレードをconfig.active_support.cache_format_version = 6.1でデプロイし、 Railsプロセスがすべて更新されたことを確かめてからconfig.active_support.cache_format_version = 7.0を設定する必要があります。

6.12 ActiveSupport::Cacheの新しいシリアライズフォーマット

運用とCI/CD統合

ローカル開発での利用

開発者は以下のようにスクリプトを実行できます:

./scripts/validate_rails_config_defaults.sh /path/to/rails/app

スクリプトは設定に問題がある場合にエラーメッセージを出力し、問題がない場合は何も出力しません。

CI/CDでの運用

GitHub ActionsなどのCI/CDパイプラインでは、以下のようなワークフローを定義し、検証スクリプトを実行します:

jobs:
  validate-rails-config-defaults:
    steps:
      - uses: actions/checkout@v5
      - id: validation
        run: |
          {
          echo "message<<EOF"
          ./scripts/validate_rails_config_defaults.sh ${{ inputs.working-directory }}
          echo "EOF"
          } >> "$GITHUB_OUTPUT"
      - if: steps.validation.outputs.message != ''
      # steps.validation.outputs.message をPRコメントに出力

スクリプトの出力をmessage変数に格納し、これが1文字以上あればPRコメントとして出力します。

コメント出力には弊社 @int128 による comment-action を利用しています。

monorepo環境での展開

monorepo環境では、各Railsアプリケーション用の検証(lintなど)ワークフローから上記ワークフローを呼び出しています。

jobs:
  rspec:
    ...
  rubocop:
    ...
  rails-config-defaults:
    uses: ./.github/workflows/reusable--validate-rails-config-defaults.yaml
    secrets: inherit
    with:
      working-directory: path/to/app

まとめ

new_framework_defaults ファイルと config.load_defaults をチェックするスクリプトを作成し、運用を始めました。2025年10月にはRails 8.1がリリースされており、このスクリプトでチェックが行われる機会も今後増えることが予想されます。

*1:.NET FrameworkJavaScriptでは可能とのこと。