こんにちは クラウド経費開発チーム ・ クラウド債務支払開発チーム の 宮村(みやむー) @miyamura.koyo です。
先日、約9年開発されている Rails アプリケーションである、クラウド経費とクラウド債務支払の Rails バージョンを 6.1 から 7.0 へメジャーバージョンアップしました。
Ruby on Rails の EOL
※ 本記事では Ruby on Rails の EOL を 「Ruby on Rails が公開している各バージョンごとの Security Issues のサポート期限」と定義します。
Ruby on Rails の サポートは、シリーズの最初のリリースから2年間のポリシーで運用されています。
より具体的には Ruby on Rails の EOL は以下のように示されています。
「遅くとも上記期限までには対応しなければ!」ということで某日からアップデートのプロジェクトを開始しました。
※ なおネタバレですがクラウド経費・債務支払は EOL 以前にアップデート完了しています!
バージョンアップの進め方
まずは単体テストを通過させました。
Gemfile をアップデートした際、 11,500 件以上のテストが落ちました(笑)。これを一つ一つ潰していくところから作業を開始しました。
その後、開発環境で自動化された E2E テストを通過させました。
一部 E2E テストでカバーしきれないところは手動テストを行い、最終的にリリースすることができました。
やったこと
ここでは Rails バージョンアップまでに対応したことをいくつか紹介します。
1. Zeitwerk に対応する
Rails 7.0 からはオートローダーに変更があります。
従来の classic
ローダーが廃止され、新しい zeitwerk
ローダーに移行する必要があります。
Rails 6 系から移行することができるので、ガイドに従って対応を行います。
以下のコマンドが通過すれば対応完了です!
$ bin/rails zeitwerk:check Hold on, I am eager loading the application. All is good!
2. bin/rails app:update
を実行して必要なものをコミット
何はともあれ bin/rails app:update
を実行して必要なものを精査してコミットします。
この時 config.load_defaults
についてはバージョンアップのリリース後に対応することをオススメします。
詳しくは以下の記事を参考にしてください。
3. 依存ライブラリ の対応
いくつかの依存ライブラリが Rails 7.0 に対応してないケースがありました。
この問題については、ライブラリのバージョンを上げたり依存解消したりすることで対応しました。
※ あまりしない方がいいですが、モンキーパッチすることで一時的に対応するという方法もあります。
3-1. default_value_for
以前執筆しましたが、 default_value_for
という gem の対応が必要でした。
一例として以下のようなエラーが出ます。
ArgumentError: wrong number of arguments (given 2, expected 0..1)
現時点では Rails 7.2 をサポートしたバージョンがリリースされているので、そちらにアップデートするか、もしくは Rails の標準機能に置き換えて削除するかの2択です。
削除する場合は手前味噌ですが、以下の記事をご参照ください。
3-2. validate_timeliness
validate_timeliness
という gem があります。
こちらで以下のようなエラーが発生していました。
Failure/Error: it { is_expected.to allow_value('hoge').for(:some_column) } ArgumentError: wrong number of arguments (given 3, expected 1..2)
gemspec を見ると validate_timeliness v6.0.1
は 「ActiveModel
v6.0.0 以上 v7 より小さいバージョン」に依存するよう指定されています。
これを解消した 7.0.0.beta1
バージョンはあるのですが、 gem 'validates_timeliness'
という指定では Dependabot が検知してくれませんでした。
validates_timeliness
の README で説明されているとおり、ベータ版を明示的に取得するよう Gemfile
に記載することで解消できます。
gem 'validates_timeliness', '~> 7.0.0.beta1'
3-3. AssociationLoader
graphql-ruby
で N+1 問題を解消するために graphql-batch
という gem を入れることがあります。
この gem を使って AssociationLoader
のようなクラスを定義して使用するサンプルが以下のように提供されています。
この実装は ActiveRecord::Associations::Preloader
に依存しているのですが、Rails 7.0 でこちらのモジュールに修正が入っています。
こちらのコミットを参考に修正することでエラーが解消できます。
4. ActionController::TestSession
テストでセッションの値をセットする時に Rails 6.1 では以下のように書くことができました。
allow_any_instance_of(ActionDispatch::Request).to receive(:session).and_return({key: :value})
Rails 7.0 では ActionDispatch::Request#session
が返すオブジェクトは Hash
ではなく ActionController::TestSession
オブジェクトである必要があります。
未対応の場合、以下のようなエラーが発生します。
NoMethodError: undefined method `enabled?' return unless session.enabled?
こちらは ActionController::TestSession
オブジェクトを返すようにすれば解消します。
allow_any_instance_of(ActionDispatch::Request).to receive(:session).and_return(ActionController::TestSession.new(key: :value))
5. ActionController::InvalidAuthenticityToken
いくつかのテストが ActionController::InvalidAuthenticityToken
に依存していました。
Rails 6.1 では引数なしですが Rails 7.0 では引数ありになっているので、対応しましょう。
6. Rendering actions with '.' in the name is deprecated
以下のようなエラーが出ていました。
ActionView::MissingTemplate: Missing partial xxx/_show.json
Rails 6.1 で render
時に拡張子をつけると非推奨警告(DEPRECATION WARNING)が出ます。
Rails 7.0 ではこれがエラーになります。対応としては formats
オプションに拡張子を指定するだけです。
ここで注意なのですが、formats
オプションを指定する場合、省略記法で書けなくなるので、適宜 locals
オプションなどを明示する必要があります。
# NG 例 render 'shared/show.json', arg1: :hoge # Good render partial: 'shared/show', locals: { arg1: :hoge }, formats: :json
詳しい説明は GitLab の Issue が分かりやすいので参考にしてみてください。
7. datetime 型の precision の挙動が変わり、全テーブルの Ridgepole のスキーマ定義を見直す必要があった
以下の PR で datetime 型の precision の挙動が変わりました。
マイグレーションファイルに ActiveRecord::Schema[6.1].define
と指定することで、Rails 6.1 の挙動に保つことができます。
ただし Ridgepole を使っている場合はこの方法は取れません。Ridgepole の README にも記載されているようにスキーマファイル自体を修正する必要があります。
具体的には、元々未指定の場合は precision: nil
をつけます。
# Rails 6.1 t.datetime :hoge_at # Rails 7.0 t.datetime :hoge_at, precision: nil
また precision: 6
がついている場合は削除します。
# Rails 6.1 t.datetime :hoge_at, precision: 6 # Rails 7.0 t.datetime :hoge_at
最終的に以下のコマンドで差分がないことを確認すれば安心です。
bundle exec ridgepole -c config/database.yml --apply --dry-run
8. 初期化中の自動読み込みの挙動が変わった
config/initializer
の処理中に以下のエラーが出ることがあります。
NameError: uninitialized constant XXX
Rails アップグレードガイドには、少し解釈が難しい記述ではありますが、内容が記載されています。
具体的には config/initializer
配下の処理などの初期化処理が実行されるタイミングで、 app/
ディレクトリ配下に定義されたクラスやモジュールが自動読み込みされなくなります(ちなみに gem で定義されたクラスは読み込まれます)。
筆者の場合は config/initializer
以下のファイルを全て調査して、エラーが発生する箇所で Rails.application.config.after_initialize
を用いて遅延評価するようにしました。
Rails.application.config.after_initialize do App::XXX.call end
また app/
配下のクラスを使用しなくても gem で定義されたクラスや標準モジュールで代替できる場合は置き換えました。
ちなみに config/initializer
には APM や 監視ツールの設定を記述することが一般的ですが、この問題への対応策が Issue などに記載されていることもあるのでそちらを参照してみるとよいでしょう。
例えば、 Datadog では以下の Issue が記載されています。
9. ActiveRecord::Base#transaction
ブロック内で早期リターンした時にロールバックするようになった
Rails リリースノートに記載されています。
具体的には以下のコードのような挙動になります。
def transaction_with_return ApplicationRecord.transaction do Comment.last.update!(message: "updated") # Rails 6.1 -> Commit # Rails 7.0 -> Rollback return end end Comment.last.update!(message: "original") transaction_with_return Comment.last.message # Rails 6.1 => "updated", Rails 7.0 => "original"
影響が大きい割に、発見が非常に難しいのですが、以下のような DEPRECATION WARNING
が出るのでログを頼りに特定することができます。
DEPRECATION WARNING: Using `return`, `break` or `throw` to exit a transaction block is deprecated without replacement. If the `throw` came from `Timeout.timeout(duration)`, pass an exception class as a second argument so it doesn't use `throw` to abort its block. This results in the transaction being committed, but in the next release of Rails it will rollback.
RuboCop の以下の Cop でも発見できるのでご活用ください。
10. 同じカラム上で条件をマージした場合に両方の条件が維持されなくなり、常に後者の条件によって置き換わるようになった
こちらも Rails リリースノートに記載されています。サンプルコードも記載されています。
こちらも影響が大きい割に、発見が非常に難しいのですが、以下のような DEPRECATION WARNING
が出るのでログを頼りに特定しましょう。
DEPRECATION WARNING: Merging xxxxx no longer maintain both conditions, and will be replaced by the latter in Rails 7.0. To migrate to Rails 7.0's behavior, use `relation.merge(other, rewhere: true)`.
11. .rubocop.yml などで指定している Rails のバージョンを上げる
.rubocop.yml
には TargetRailsVersion
で想定する Rails のバージョンを指定することができます。
Rails のバージョンアップとは別で RuboCop の対応を行いたい場合などで重宝します。
一方で、こちらの設定を上げ忘れることもあるので、確認しておきましょう。
ちなみにいくつかのルールでは、Deprecated になったコードに対するものがあります。
コードの静的解析で初めて気づける非互換的変更もあるので、日頃からこまめに対応しておくことをオススメします(筆者もいくつかこれに救われました)。
また、他のツールなどでバージョンを指定していることがあれば併せてチェックしてバージョンを上げておきましょう。
その他 Tips
可能な限り修正はバージョンアップ前にリリースする
バージョンアップと同時に大量の修正をリリースすると、不具合発生時に問題の切り分けが難しくなってしまいます。
また、Rails のバージョンアップに伴う修正の多くは、以前のバージョンでも修正が可能です。
実際にどのようなリリース戦略を採用するかはプロダクトの事情次第ですが、可能な限り小さくこまめにリリースすることをオススメします。
Rails アップグレードガイドや Rails のリリースノートを熟読する
バージョンアップ作業中は Rails アップグレードガイドは熟読しましょう。英語記事の方も見ておくと良いです。
また Rails 自体のリリースノートを見ておくこともオススメします。Rails アップグレードガイドに書かれてない重要な変更が記載されていることもあるので、要チェックです!
他のプロダクトの事例を調査する
他のプロダクト Rails アップグレードの記事を読むことで「あれ、これってうちのプロダクトも影響ある?」という箇所が見つかります。
こちらは弊社の id:Pocke さんが共有する場を用意してくれているので、ご利益にあやからせてもらいましょう。
またマネーフォワードでは他のプロダクトでも Rails を採用していることが多いので、他のプロダクトの事例も参考にさせてもらいました。
社内の有識者に相談する
社内に Rails や Ruby の有識者がいる場合は事前に相談することをオススメします。
これは Rails プロダクトが多いマネーフォワードの特権ではありますが、多くの有識者が社内にいます。
筆者は id:Pocke さんに相談の機会をいただき、だいぶ手探りで進めていたところに背中を押していただきました!この場を借りて感謝申し上げます。
本番で DEPRECATION WARNING ログを出す
config/environments/production.rb
に以下の設定を書くことで標準出力にログを出力できます。
config.active_support.deprecation = :log
出力方法をカスタマイズしたい場合は config.active_support.deprecation = :notify
に設定し、 config/initializer
配下にコードを書きます。
今回は特定の deprecation ログは無視したかったので以下のように実装しました。
# config/initializer/xxx.rb if Rails.env.production? ActiveSupport::Notifications.subscribe('deprecation.rails') do |_name, _start, _finish, _id, payload| unless payload[:message]&.include?('some ignorable message') Rails.logger.warn(payload[:message]) end end end
Rails のバージョンアップでは、いくつか「単体テストでは見つかりづらいもの」があります。例えば「9. ActiveRecord::Base#transaction ブロック内で早期リターンした時にロールバックするようになった」のセクションで紹介した対応などが挙げられます。
その場合でも DEPRECATION WARNING ログを出すことでバージョンアップ時の非互換挙動を発見することができます。
やはり本番環境での振る舞いをベースに調査する方が確実です。手軽に調査できるうえに、リリース時の品質向上にもつながるため、バージョンアップの際にはぜひ試してみてください。
まとめ
本記事では、Rails のバージョンを 6.1 から 7.0 にアップグレードした話について書きました。
ここで紹介した作業は一部であり、他にも多くの対応がありましたが、チームメンバーの協力を得て乗り越えることができました。
また、クラウド経費・債務支払は多くのユーザーに利用されているため、 QA を徹底し、高品質を保つことでユーザーへの影響を最小限に抑えることができました。
以上のように慎重に取り組みつつも、迅速に動いたことで EOL 前にリリースすることができました。
今後は Rails 7.1 以降へのアップデート記事も書いていく予定ですので、どうぞお楽しみに!
マネーフォワード福岡開発拠点では、エンジニアを募集しています!
多くのユーザーに利用されている Rails アプリケーションに携わりたい方はぜひ!
福岡開発拠点のサイトもあるのでぜひみてください!