リモート開発メインのソフトウェア開発企業のエンジニアブログです

Rails 8.0 が出たのでアップグレードした

先日、Rails 8.0 がリリースされ、直近で担当している Rails アプリのバージョンをアップグレードしたので手順等を簡単にまとめます。
まずは Rails アップグレードをする際に普段やっている事と、 Rails 8.0 で主に変わった挙動の検証のまとめ、最後に定期的にアップグレードをしていく為にやっている施策などを紹介しておこうと思います。Rails の開発の経験がある程度ある人にとってはあまりに普通の内容なので、対象読者は初級者とします。

Rails アップグレードの流れ

基本的には 公式ドキュメント Upgrading Ruby on Rails に従うのですが、私は普段 bin/rails app:update は使いません。理由としてはこのコマンドを使うと既存の設定ファイルがそのバージョンのデフォルトの物に上書きされてしまいそのままでは使えないのと、まずは最低限の Gem のバージョンアップとデフォルトの設定の更新にフォーカスしたいからです。ただ一応後ほどコマンドを実行してどの様な差分があるかリリースノートを見ながらレビューし、適宜取り入れる場合があります。

以下が、実際の手順になります。

1. Gemfile の Rails のバージョンを上げる

gem 'rails', '~> 8.0'

2. bundle install で Gemfile.lock を更新して再起動

bundle install を実行して Gemfile.lock を更新し、この時点で Gemfile* の変更を Git コミットしておきます。あとは Rails server を再起動します。

3. ここでテストを実行して、エラーが出たら修正

今回は幸運な事にテストが全て通ったので特に修正はありませんでした。普段から非推奨警告 (Deprecation Warnings) に対応していればここで大きな問題が出ることはあんまりないと思います。ちなみに本プロジェクトのテストカバレッジは96%です。

4. Rails::Application の config.load_defaults を更新 (必要に応じて)

ここを更新すると、Rails のバージョンアップで実際に挙動が変わる可能性があります。これらの挙動の変更がアプリに大規模な影響を与える場合があるので、この場で変更できない場合は一旦保留にしても良いと思います (ただし、後述する手法などを活用しながら少しずつでも対応していく事)。 尚、Rails 7.2 -> 8.0 は殆ど変更点がなかったので問題ありませんでした。また、変更を行った場合は再度テストを実行して問題がないか確認します。

module Runboze
  class Application < Rails::Application
    # ...

    config.load_defaults 8.0 # 7.2 -> 8.0 に変更

    # ...
  end
end

5. 実際に動作確認

Rails などの広範囲に影響があるアップデートは動作確認をしっかりしましょう。ステージング環境などの実運用環境に近い環境で確認を行うのが良いです。

6. 本番環境にデプロイ

問題がなければ本番環境にデプロイします。

Moba Pro

Rails 8.0 のデフォルトの設定の変更点

https://github.com/rails/rails/blob/v8.0.0/railties/lib/rails/application/configuration.rb#L337-L349 の内容を見ると非常に軽微です。内容としては

  • active_support.to_time_preserves_timezone = :zone
  • action_dispatch.strict_freshness = true
  • Regexp.timeout ||= 1

となります。数が少ないので1個ずつ見ていきましょう。

active_support.to_time_preserves_timezone = :zone

3.15.18 config.active_support.to_time_preserves_timezone

5.0 ~ 7.2 まではデフォルトで :offset でした。内容的には DateAndTime::Compatibility.preserve_timezone = :offset が DateAndTime::Compatibility.preserve_timezone = :zone になる様です。 これにより、 ActiveSupport::TimeWithZone#to_time の挙動が変わります。コンソールで両者の違いを見てみました:

:zone (8.0 以降)

DateAndTime::Compatibility.preserve_timezone = :zone; Time.zone.parse('2024-01-01T12:34:56.123+09:00').to_time.tap { |t| p [t.zone, t.utc_offset] }

[#<ActiveSupport::TimeZone:0x0000ffff7fde9048 @name="Asia/Tokyo", @utc_offset=nil, @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>>, 32400]

:offset (7.2 以前)

DateAndTime::Compatibility.preserve_timezone = :offset; Time.zone.parse('2024-01-01T12:34:56.123+09:00').to_time.tap { |t| p [t.zone, t.utc_offset] }

[nil, 32400]

:zone になった事により、変換後の Time に zone が設定される様になりました。utc_offset はどちらにも付いているので、特にこの挙動の変更によって既存のコードの何かが壊れると言う事はなく、単に今後は zone も参照できる様になる、と言った変更だと思われます。尚、この設定は 8.1 でデフォルトで :zone になる予定との事です。

action_dispatch.strict_freshness = true

3.11 config.action_dispatch.strict_freshness

7.2 以前まではデフォルトで false でした。内容的には ActionDispatch::Http::Cache::Request.strict_freshness = true になる様です。 これにより HTTP のキャッシュにおいて、 If-None-Match (ETag) ヘッダーと If-Modified-Since (日時) が同時にリクエストされている場合、後者は無視されます (ETag のみ検証される)。false であった以前までの挙動は、両方リクエストされている場合は両方チェックされ、どちらも鮮度が新しい場合のみ304が返る仕様になっていました。尚、この変更は RFC 7232 section 6に準拠する為の変更です。

Regexp.timeout ||= 1

Ruby 3.2 で追加された Regexp.timeout にデフォルトで1秒をセットする様になりました (ReDoS 対策)。Rails は 8.0 から Ruby 3.2 以降が必須になった為有効にしたと思われます。プロジェクト内で1秒以上かかる正規表現を使っている場合はより長い時間を load_defaults 以降に設定する必要があります。

デフォルト設定について

先ほどアップグレード手順では無理して更新する必要はないと書いたものの、来る新しいバージョンではこれらの設定が必須になったりする場合もあるので、それまでに順次対応していくのが望ましいです。数が多く大変な場合は 公式ドキュメント 1.4 The Update Task に従ってbin/rails app:update を使ってアップグレードをすると、 config/initializers/new_framework_defaults_8_0.rb の様なファイルが作られるので、そちらで1個ずつ対応が可能です。

Rails を定期的にアップグレードしていくには

基本的に Rails がメジャーやマイナーアップグレードする際は、新しい機能の追加と予定されていた非推奨の削除、新たな非推奨の追加などが行われます。特に、非推奨の対応はこまめにしておけば、アップグレード時に大きな問題が出る事は少ない事が多いです。ではこれらはどうやって対応していけばいいでしょうか。

テストを書く

テストをしっかり書きましょう。Rails エコシステムは容易にテストを書く仕組みが数多く提供されています。テストをしっかり書く事で、Rails だけでなく様々なパッケージの更新に対しても安心してアップグレードできるようになります。具体的にはテストカバレッジを極力100%に近い状態にするのが良いです。高いカバレッジ=高いテストの品質、とは必ずしも言えないのですが、少なくとも新しく追加された非推奨警告への対処は格段に楽になります。

RuboCop を導入する

RuboCopRuboCop Rails を導入しましょう。これらのツールを導入しておくと、ある程度コードのスタイルが一定に保たれ、特に RuboCop Rails を入れておく事で Rails において非推奨になる様な、または潜在的に問題のあるコードを見つける事ができます。こうしておく事でそもそも非推奨警告の数を減らせるかもしれません。プロジェクトの途中で導入すると大量の警告が出て大変なので、可能な限り初めから導入しておく事をお勧めします。

CI を導入する

テストや RuboCop などのチェックを CI に組み込んでおくと、コードの品質を保つ事ができます。また、CI でテストを実行する事で、アップグレード時に問題があるかどうかを事前に確認する事ができます。手動で実行する運用にしてると実行を忘れたりする事があるので、CI に組み込んでおく事をお勧めします。

Ruby のバージョンを定期的に上げる

Rails はバージョンアップの度に Ruby の最低動作バージョンを上げてくる事が多いです。従って、 Rails だけでなく Ruby のバージョンアップにも定期的に追従する必要があります。その為にも繰り返しになりますがテストや CI は整備しておきましょう。

デプロイのプロセスを整備する (CD)

影響範囲の大きいリリースを行う際は本番デプロイ後、何かあってもすぐに切り戻せる仕組みがあると安心です。最近ではアプリをコンテナ化する事も一般的になりましたしあえて言及する事でもないですが、デプロイ・ロールバックの仕組みは整備しておきましょう。

Dependabot を導入する

GitHub を使っている場合は Dependabot を導入すると、依存パッケージのアップデートを自動で行ってくれます。Rails や Ruby のバージョンアップに追従するには定期的に普段使っている Gem も更新が必要なケースが多いです。手動でこれらのバージョンを管理していくのは大変なので、可能であればこう言ったツールを導入しましょう。

Rails のフロントエンドインテグレーションを活用しない

これについては私の主観を多いに含んでいますが、Rails は比較的フロントエンドエコシステムに対する思想が強く、バージョンアップを重ねる毎にドラスティックな変更を入れてくる事が度々あります。 Laravel とかでもそうなのですが、この手のインテグレーションは便利な反面デメリットも多く (凝った事やろうとすると一から自前で導入するくらいのコストが掛かる等) 個人的にフロントエンドは完全に棲み分けるつもりで Rails によるインテグレーションは普段から一切使わない方針でやっています。実際にそうする訳ではありませんが、端的に言えばフロントエンドのリポジトリを Rails プロジェクトから切り離す事も可能と言う事です (Rails の view を使う場合はそこだけ接点が必要になりますが)。この辺りは色々議論がありそうですが少なくとも私は Rails のフロントエンド関連に巻き込まれず快適に過ごせています。

← 前の投稿

次の投稿 →

APNGの構造を理解する

コメントを残す