📸

Railsを8.0にアップグレードした際の手順や知見をまとめる

に公開

はじめに

この記事では、私が Rails 7.2.2.1から最新の8系にアップグレードした際の手順や、遭遇した問題などについてまとめてみました。
初めてアップグレードを行う方を意識しているので、実際に行った手順を網羅的に書いています。(ですので、「8系へのアップグレード」に限らず、全般的に言えることも色々書いています)

これから Rails のアップグレードを行う方の参考になれば幸いです。

事前準備

アップグレードガイドの確認

まずは Rails ガイド記載のアップグレードガイドに目を通します。

  • Ruby のバージョン互換性
    • アップグレードしたい Rails のバージョンが、現在使用している Ruby のバージョンと互換性があるかを確認
    • Ruby 3.2.0 以上あることを確認
  • アップグレード手順
    • マイナーバージョンを 1 つずつ上げていくことや、bundle update rails 後にアップデートタスクを実行するなど、基本的な方針を確認
  • Rails 8における破壊的変更の確認

Deprecation Warning への対応

先述のリリースノートでも記載があった、廃止予定の設定・機能は Deprecation Warning が出てくれます。

我々の環境では以下のような警告が出ていました。

  1. キーワード引数を使った enum 定義
DEPRECATION WARNING: Defining enums with keyword arguments is deprecated and will be removed
in Rails 8.0. Positional arguments should be used instead:

enum :kind, {:hoge=>0, :foo=>1}

Rails 8.0 からキーワード引数を使ったenumの定義方法が廃止され、位置引数を使った方法に移行する必要があります

enum kind: { hoge: 0, foo: 1 }

enum :kind, { hoge: 0, foo: 1 }
  1. config.read_encrypted_secrets
==DEPRECATION WARNING==: 'config.read_encrypted_secrets=' is deprecated and will be removed in Rails 8.0.

詳細はRails ガイド記載の通りです。廃止されるため削除しました。

もっと早い段階でこういった Warning は処理できているといいですね。

Gem の互換性確認

我々のプロジェクトでは Renovate を使用して定期的に Gem のアップデートを行なっています。しかし、Gem が最新だからといって Rails 8 系に対応しているかは分かりません。

そこで使ってみたのが RailsBump です。
プロジェクトの Gemfile.lock を貼り付けるとその Gem が Rails 8 系に対応しているか確認できます。

「checking」ステータスになっている Gem に関しては、GitHub の Issue や Pull Request を確認し、Rails 8 系との互換性について明記されているか、関連する問題が起きていないかなどをチェックします。

アップグレード作業

準備が整ったら、いよいよアップグレード作業を開始します。

Gemfile の編集

Gemfile 内の Rails のバージョン番号を変更します。

gem 'rails', '~> 8.0.0'

その後、bundle update rails を実行し、Rails と関連 Gem をアップデートします。

rails app:update の実行

rails app:update を実行します。これにより、config配下のファイル等が新しい Rails の設定に合わせて更新されます。既存の設定を考慮しながら、新しい設定を適用していきます。

こんな感じで各ファイルごとにインタラクティブに更新できます。

Overwrite /project/config/application.rb? (enter "h" for help) [Ynaqdhm] y
       force  config/application.rb
   identical  config/environment.rb
    conflict  config/puma.rb

その後

  • テストの実行
    • 一通りのアップグレード作業が終わったら、必ずテストを実行してアプリケーションが正常に動作するかを確認します。
  • RailsDiff の確認
    • 念の為、RailsDiff も使ってバージョン間の設定ファイルの差異も確認してみました。ただ、rails app:updateで更新したもの以外で新しい差分はありませんでした。

アップグレード後に発生したエラー

bundle update rails の後にテストを実行したところ、以下の 2 つが影響してエラーが発生しました。

開発環境とテスト環境における routes の遅延読み込み

Failure/Error: ss = Sidekiq::ScheduledSet.new

      NameError:
        uninitialized constant Sidekiq::ScheduledSet

Sidekiq::ScheduledSet を利用するには、本来 require 'sidekiq/api' が必要です。以前までは config/routes.rb で以下のよう記述することで、間接的に sidekiq/api が読み込まれていました。

mount Sidekiq::Web, at: '/sidekiq

しかし、Rails 8 から開発環境とテスト環境において、デフォルトで routes が遅延読み込みされるようになりました。この影響で、テスト実行時に sidekiq/api が自動で読み込まれなくなったというわけです。

参考: Rails 8's Lazy Route Loading & Devise

Sidekiq::ScheduledSet を呼び出している箇所で明示的に require 'sidekiq/api' を追加することで解決しました。

Rails 8 に対応していない Gem

NoMethodError:
  undefined method `arel_table' for nil:NilClass

active_record_union という Gem の影響で上記のエラーを吐くようになりました。調査した結果、active_record_union の最新バージョンが、アップグレード実施時点で、Rails 8 系に対応していないことが判明しました、、、
master ブランチに修正がマージされていそうですが、それがリリースされていない状態のようです。

RailsBump でも checking status だったため GitHub を確認していたのですが、こちらのIssueを見落としていました、、、、
(コミットメッセージに 6.0 → 8.0 と書いてあったので油断していました、、、)

active_record_unionを使用している箇所が幸運にも削除できたので、Gemfile からも削除しました。

RailsBump を確認しても見落としはあるので、やはりテストは重要です。

config.load_defaults を 8.0 にする

上記エラーを解消しテストが通ったら、一旦ここまででリリースを行いました。

リリース後、config/initializers/new_framework_defaults_8_0.rb でコメントアウトされている新しいデフォルト設定を順次有効化していき、最終的にapplication.rbconfig.load_defaultsを 8.0 に更新します。

8 系からの適用される新しいデフォルト値は以下です

  • Regexp.timeout = 1
  • Rails.application.config.action_dispatch.strict_freshness = true
  • Rails.application.config.active_support.to_time_preserves_timezone = :zone

参考: Configuring Rails Applications - Rails Guides

Regexp.timeout = 1

正規表現のパターン処理にタイムアウトを設定するものです。
Regexp.timeout=は Ruby 3.2 から利用出来るようになったセキュリティ強化策の一つで、Rails 8 から 1 がデフォルトで設定されるようになります。正規表現のパターン処理が 1 秒を超えた場合にRegexp::TimeoutErrorを発生させます。
これにより ReDos 攻撃を防止することができます。

$ Regexp.timeout = 0.00001
=> 1.0e-05
$ Regexp.timeout
=> 1.0e-05
$ /([a-z]+)*$/ =~ "a" * 1000 + "!"
=> regexp match timeout (Regexp::TimeoutError)

念の為、アプリケーション内での正規表現チェックにどのくらい時間がかかっているかをログとして出力して検証してみました。大体マイクロ秒レベルで処理できていたのでTimeoutErrorが頻発するといった問題は起きないだろうと判断できました。

Rails.application.config.action_dispatch.strict_freshness = true

HTTP ヘッダーに If-Modified-Since と If-None-Match の両方が存在していた場合の挙動に関する設定です。
これらはキャッシュを使うかどうかの制御に使われるヘッダーですね。

  • false
    • 両方のヘッダーをチェック
      • If-Modified-SinceLast-Modified / If-None-MatchETag を比較
    • 両方が一致した場合のみ304を返す
  • true
    • If-None-Matchだけチェック

entity tags are presumed to be more accurate than date validators.

https://datatracker.ietf.org/doc/html/rfc7232#section-6

Rails.application.config.active_support.to_time_preserves_timezone = :zone

to_timeメソッドの挙動に関わる設定です。

  • :zone
    • レシーバーのタイムゾーン情報を保持します
  • :offset
    • レシーバーのオフセット情報を保持します。

Rails 8 からは :zone がデフォルトになります。

夏時間が存在するタイムゾーンで挙動に違いが出るようです。
以下でタイムゾーンをAmerica/New_Yorkにして :offset:zone の挙動の違いを確認してみます。
America/New_Yorkは東部標準時(EST)と東部夏時間(EDT)という2つに分けられます。EST の UTC オフセットは-5 時間で EDT の場合-4 時間です。EDTは3月の第2日曜日午前2時に始まり、11月の第1日曜日午前2時に終了します。

:offset

$ Rails.application.config.active_support.to_time_preserves_timezone
=> :offset
$ Time.zone = "America/New_York"
=> "America/New_York"
$ now = Time.zone.parse('2024-08-05T05:00:00').to_time
=> 2024-08-05 05:00:00 -0400
$ now.zone
=> nil
$ now + 6.months
=> 2025-02-05 05:00:00 -0400
$ (now + 6.months).utc
=> 2025-02-05 09:00:00 UTC

now + 6.months をした結果の 2025-02-05 05:00:00 は EST のため UTC オフセットは-0500 であるべきです。しかし、now はタイムゾーン情報を持たないため、オフセットが-0400 を引き継いでしまっています。

:zone

$ Rails.application.config.active_support.to_time_preserves_timezone
=> :zone
$ Time.zone = "America/New_York"
=> "America/New_York"
$ now = Time.zone.parse('2024-08-05T05:00:00').to_time
=> 2024-08-05 05:00:00 -0400
$ now.zone
=> #<ActiveSupport::TimeZone:0x0000000106adeb00 @name="America/New_York", @tzinfo=#<TZInfo::DataTimezone: America/New_York>, @utc_offset=nil>
$ now + 6.months
=> 2025-02-05 05:00:00 -0500
$ (now + 6.months).utc
=> 2025-02-05 10:00:00 UTC

保持してあるタイムゾーン情報により、2025-02-05 05:00:00 時点では EST が適用され、オフセットが-0500 になっています。

まとめ

設定をよく確認しながら適用していき、段階的にリリースするなどしたおかげで本番環境で問題は起きませんでした。
当たり前ですが事前準備が大事です。Deprecation Warningに早めに対処しておくと後々楽になります。RailsBumpやAIといったツールを使うのもいいですが、やはりテストがしっかりしていないと安心できません。
今回はAIを調査目的でのみ使用しましたが、次回はより積極的に活用して、その効果を検証してみたいと考えています。

WED Engineering Blog

Discussion