🗂

Rails 6.1から7.0へのアップグレードの雑メモ

に公開

※作業自体は2024年5月頃です。

0. アップグレード作業の概要

Rails 7.0へのアップグレードは、リスクを最小化し問題を特定しやすくするために、2つのPRに分けて段階的に実施しました:

  1. 第一段階(PR #11244)

    • Rails 7.0.8.4へのバージョンアップ
    • Gemfileとdependenciesの更新
    • 基本的なAPIの変更対応
    • validates_timelinessの標準バリデータへの置き換え
    • テストコードの調整
  2. 第二段階(PR #11295)

    • config.load_defaults 7.0の設定更新
    • 不要なイニシャライザの削除
    • STIモデルの最適化
    • クッキーローテータの追加
    • その他細かな互換性の調整

この二段階アプローチにより、変更の影響範囲を限定し、問題発生時のデバッグを容易にしました。

1. 第一段階:Rails 7.0へのバージョンアップ(PR #11244)

1.1 Rails 7.0の主要な改善点

Rails 7.0は多くの重要な改善を含む大型アップデートです。主な変更点は:

  • セキュリティの強化

    • CSRFトークン処理の改善
    • より堅牢なContent-Security-Policy (CSP) オプション
    • セキュアなデフォルト設定
  • パフォーマンスの向上

    • Active Recordクエリの最適化
    • キャッシュシステムの効率化
  • 開発者体験の改善

    • Zeitwerkモードのデフォルト化
    • 型システムの強化

1.2 Gemfileとdependenciesの更新

アップグレードの最初のステップとして、Gemfileを更新し、Rails 7.0.8.4へのバージョンアップを行いました:

# Gemfile
- gem 'rails', '6.1.7.8'
+ gem 'rails', '7.0.8.4'

依存関係を最小限に更新するために、--conservativeフラグを使用しました:

bundle update --conservative rails

このアプローチには以下のメリットがあります:

  1. 問題の特定を容易に - 最小限の変更で、発生する問題の原因を特定しやすくなります
  2. リグレッションリスクの最小化 - 一度に多くの変更を行うことによる予期せぬ問題を減らします

1.3 validates_timelinessの標準バリデータへの置き換え

PR #11244では、validates_timeliness gemを削除し、標準のRailsバリデータに置き換えました:

# Gemfile
- gem 'validates_timeliness'

バリデーションコードを以下のように書き換えています:

# 変更前
validates :founded_at, timeliness: { 
  on_or_after: -> { Time.zone.local(1, 1, 1).to_date }, 
  type: :date, 
  allow_blank: true 
}

# 変更後
validates :founded_at, comparison: { 
  greater_than_or_equal_to: proc { Time.zone.local(1, 1, 1).to_date } 
}, allow_blank: true

また、config/initializers/validates_timeliness.rbも削除しました。

これによるメリットは:

  1. 依存gemの削減 - メンテナンスの負担が軽減されます
  2. パフォーマンスの向上 - 標準機能は最適化されていることが多いです
  3. メンテナンス性の向上 - 標準機能はドキュメントが充実しており、将来的なサポートも期待できます

1.4 APIの変更対応

Rails 7.0では、いくつかのAPIやメソッド呼び出しが変更されています。PR #11244では以下の変更を行いました:

1.4.1 数値フォーマットメソッドの変更

# 変更前
"#{latest_calculated_income.amount.to_s(:delimited)}円"

# 変更後
"#{latest_calculated_income.amount.to_fs(:delimited)}円"

Rubyのto_sメソッドとRailsのto_s拡張の区別を明確にするため、Rails 7.0では数値フォーマットにto_fsが導入されました。

1.4.2 YAMLカラム設定の変更

# 変更前
YAML.safe_load(
  version.object,
  aliases: true,
  permitted_classes: ActiveRecord::Base.yaml_column_permitted_classes
)

# 変更後
YAML.safe_load(
  version.object,
  aliases: true,
  permitted_classes: ActiveRecord.yaml_column_permitted_classes
)

1.5 テストコードの調整

テストコードも更新が必要でした。特にモックセッションの作成方法が変更されています:

# 変更前
allow_any_instance_of(ActionDispatch::Request)
  .to receive(:session)
  .and_return({ company_id: company.id })

# 変更後
mock_session = ActionController::TestSession.new(company_id: company.id)
allow_any_instance_of(ActionDispatch::Request)
  .to receive(:session)
  .and_return(mock_session)

この変更により、セッションオブジェクトの挙動をより正確に模倣することができるようになりました。

2. 第二段階:デフォルト設定の更新(PR #11295)

PR #11244で基本的な依存関係の更新と互換性対応を行った後、PR #11295では以下の作業を実施しました:

2.1 config.load_defaults 7.0の設定更新

Rails 7.0のデフォルト設定を有効にするために、config/application.rbを更新しました:

# config/application.rb
- config.load_defaults 6.1
+ config.load_defaults 7.0

# 必要に応じて個別のデフォルト設定を上書きする
+ config.action_controller.raise_on_open_redirects = false

raise_on_open_redirectsfalseに設定することで、オープンリダイレクトの厳格なチェックを無効化しています。デフォルトではtrueですが、既存のアプリケーションの互換性を維持するために無効化しました。

2.2 不要なイニシャライザの削除

Rails 7.0では、以下のイニシャライザファイルが不要になるため削除しました:

  1. config/initializers/new_framework_defaults_7_0.rb

    • config.load_defaults 7.0を設定した場合、個別の設定は不要になります
  2. config/initializers/cookies_serializer.rb

    • Rails 7.0ではデフォルトで:jsonシリアライザが使用されるため不要になります
  3. config/initializers/wrap_parameters.rb

    • Rails 7.0ではパラメータラッピングがデフォルトで有効になるため不要になります

また、以前は明示的に無効化していたCSRF設定も削除しました:

# config/application.rb から削除
- config.action_controller.per_form_csrf_tokens = false
- config.action_controller.forgery_protection_origin_check = false

2.3 STIモデルの最適化

Rails 7.0ではZeitwerkの改善により、STIモデルの扱い方が大幅に改善されました。

2.3.1 STIモデルのディレクトリ構造の改善

STIモデルを専用のディレクトリに移動し、構造を整理しました:

# 移行前
app/models/
├── company.rb          # 基底クラス
├── customer_company.rb # 派生クラス
├── admin_company.rb    # 派生クラス
├── screening.rb        # 他のSTIモデル
├── proposed_date.rb    # 他のSTIモデル

# 移行後
app/models/
├── sti_models/
│   ├── company.rb          # 基底クラス
│   ├── customer_company.rb # 派生クラス
│   ├── admin_company.rb    # 派生クラス
│   ├── screening.rb        # 他のSTIモデル
│   ├── proposed_date.rb    # 他のSTIモデル

この変更により、コードの整理が容易になり、関連するモデルが一箇所にまとめられました。

2.3.2 StiPreloadモジュールの削除

Rails 6.1まではStiPreloadモジュールを使用してSTIモデルの読み込み問題を回避していましたが、Rails 7.0ではZeitwerkの改善によりこのモジュールが不要になりました。lib/sti_preload.rbを削除し、代わりに新しいイニシャライザを追加しました:

# config/initializers/preload_stis.rb
# frozen_string_literal: true

sti_models = Rails.root.join('app', 'models', 'sti_models')
Rails.autoloaders.main.collapse(sti_models)

unless Rails.application.config.eager_load
  Rails.application.config.to_prepare do
    Rails.autoloaders.main.eager_load_dir(sti_models)
  end
end

また、STIモデルからinclude StiPreloadの記述も削除しました:

# 変更前
class Company < ApplicationRecord
  include StiPreload
  # ...
end

# 変更後
class Company < ApplicationRecord
  # ...
end

2.4 クッキーローテータの追加

セキュリティを強化するために、古いクッキー暗号化方式から新しい方式への移行をサポートするローテーター機能を追加しました:

# config/initializers/cookie_rotator.rb
# frozen_string_literal: true

Rails.application.config.after_initialize do
  Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
    authenticated_encrypted_cookie_salt = Rails.application.config.action_dispatch.authenticated_encrypted_cookie_salt
    signed_cookie_salt = Rails.application.config.action_dispatch.signed_cookie_salt

    secret_key_base = Rails.application.secret_key_base

    key_generator = ActiveSupport::KeyGenerator.new(
      secret_key_base, iterations: 1000, hash_digest_class: OpenSSL::Digest::SHA1
    )
    key_len = ActiveSupport::MessageEncryptor.key_len

    old_encrypted_secret = key_generator.generate_key(authenticated_encrypted_cookie_salt, key_len)
    old_signed_secret = key_generator.generate_key(signed_cookie_salt)

    cookies.rotate :encrypted, old_encrypted_secret
    cookies.rotate :signed, old_signed_secret
  end
end

これにより、Rails 7.0の新しい暗号化方式に移行しつつ、既存のクッキーも引き続き復号できるようになります。

2.5 その他の互換性調整

2.5.1 Preloaderインターフェースの変更

Active RecordのPreloaderインターフェースも変更されています:

# 変更前
::ActiveRecord::Associations::Preloader.new.preload(records, @association_name)

# 変更後
::ActiveRecord::Associations::Preloader.new(
  records: records,
  associations: @association_name
).call

2.5.2 インスタンス変数名の変更

コードレビューの中で、メモ化用のインスタンス変数の命名規則が変更されました。Rails 7.0では@_プレフィックスのついたインスタンス変数は特別な扱いになる可能性があるため、これらの変数を通常のインスタンス変数に変更しました:

# 変更前
def existing_company_user_ids
  @_existing_company_user_ids ||= []
end

# 変更後
def existing_company_user_ids
  @existing_company_user_ids ||= []
end

3. アップグレード後の検証と運用

3.1 テスト戦略

アップグレード後は、以下の点について徹底的にテストを行いました:

  • 基本機能

    • CRUD操作 - データの作成、読み取り、更新、削除が正常に機能するか
    • 認証・認可 - ログインや権限制御が正しく動作するか
    • ファイル操作 - ファイルのアップロードやダウンロードが問題ないか
  • 特殊機能

    • PDFダウンロード - PDFの生成と表示が正常か
    • Excel出力 - Excelファイルの生成が正常か
    • バッチ処理 - 定期実行ジョブが正常に動作するか
  • セキュリティ

    • CSRFトークン - フォーム送信時のトークン検証が機能するか
    • セッション管理 - セッションの作成と破棄が正常か
    • 権限制御 - 適切なアクセス制御が機能するか

3.2 デプロイ戦略

アップグレードのリリースには、慎重な準備と計画が必要でした:

  1. リバートプランの作成

    git checkout -b revert/rails-7-upgrade
    git revert [upgrade-commit-hash]
    

    何か問題が発生した場合に、すぐに元に戻せる準備をしておくことが重要です。

  2. ステージング環境での検証

    • 全機能テスト - すべての機能が正常に動作することを確認
    • パフォーマンス計測 - レスポンスタイムなどのパフォーマンス変化を計測
    • セキュリティ確認 - 新しいセキュリティ機能が正しく動作することを確認
  3. 段階的デプロイ

    • まず第一段階(PR #11244)のデプロイを行い、問題がないことを確認
    • 次に第二段階(PR #11295)のデプロイを行い、完全なRails 7.0環境に移行

3.3 監視とトラブルシューティング

リリース後も注意深く監視を続けました:

  • エラーレート監視 - 通常より多くのエラーが発生していないか
  • レスポンスタイム計測 - パフォーマンスの低下がないか
  • メモリ使用量確認 - メモリリークなどの問題がないか

発生した問題と対応策をチーム内で共有し、アップグレードに関する知見を蓄積しました。

よくある問題と解決策

アップグレード後によく発生する問題とその解決法:

  1. セッション関連の問題

    • 症状: ユーザーのセッションが突然切断される
    • 原因: CSRFトークン設定の変更によるセッション検証の厳格化
    • 解決: クッキーローテーターを設定し、セッションストアの設定を確認する
  2. STIモデルの読み込み問題

    • 症状: NameError: uninitialized constantエラーが発生する
    • 原因: autoload設定の不備によるモデルの未読み込み
    • 解決: preload_stis.rbの設定を確認し、必要なモデルがすべて読み込まれるようにする

4. まとめ:二段階アプローチの効果と学び

Rails 7.0へのアップグレードを二段階に分けて行うことで、次のような効果がありました:

  1. リスクの分散 - 一度に全ての変更を行わないことで、問題発生時の影響範囲を限定できました
  2. 問題の特定が容易 - 発生した問題がGemの更新によるものなのか、デフォルト設定の変更によるものなのかを切り分けやすくなりました
  3. チーム内の理解促進 - 各PRの目的が明確になり、チームメンバーの理解と協力を得やすくなりました
  4. コードレビューの効率化 - 変更範囲が限定されることで、レビューが効率的に行えました

Rails 7.0へのアップグレードは単なる技術的な作業ではなく、アプリケーションの将来への投資です。セキュリティの強化、パフォーマンスの向上、開発者体験の改善など、多くのメリットがあります。

計画的なアプローチと徹底的なテスト、そして継続的な改善によって、アップグレードを成功させ、アプリケーションの品質と持続可能性を高めることができました。二段階アプローチは、大規模なフレームワークアップグレードを安全に行うための効果的な方法であり、今後の同様のアップグレード作業にも活用していきたいと思います。

株式会社Grooves

Discussion