Upgrade to Pro — share decks privately, control downloads, hide ads and more …

現実のRuby/Railsアップグレード

 現実のRuby/Railsアップグレード

Railsのアップグレードにおいては、公式のアップグレードガイドに従って作業するのが基本となります。しかしながら現実のプロジェクトではRubyとRailsの互換性、gemの互換性、開発が終了したgem・・・といった問題に直面し、一筋縄ではいきません。

この発表では、Rails 5.0 だったアプリケーションを Rails 7.1 にアップグレードした事例を中心に、得られた知見を次のように章立てしてご紹介します。

当時の課題
アップグレードのための準備
アップグレードの手順
発生した問題とその解決
得られたもの
アップグレードしていくために
対象者は、『課題を感じているけどどのように対処すればよいかわからない中級者』並びに『課題に気づいていない初級者』です。それぞれ解決の糸口と、放置による問題の気づきに役立てていただけると思っています。上級者は「あるある」を楽しんでいただけることでしょう。

Railsは今後も素晴らしいフレームワークとして進化を続けていきますが、その恩恵を受け続けるにはアップグレードから逃れることはできません。この発表が皆さんのプロジェクトの一助となれば幸いです。

Yuichi Takeuchi

October 25, 2024
Tweet

More Decks by Yuichi Takeuchi

Other Decks in Technology

Transcript

  1. 古すぎるRUBYとRAILS 2023年秋時点(当時の最新は ruby 3.2 + Rails 7.1) • Ruby 2.4(EOL:

    2020年3月) • Rails 5.0(EOL: 2018年4月) 問題 • セキュリティ:更新が受けられない • 依存: gemの要求バージョンを満たせない • 不便:新しい便利な書き方や機能を使いたい… 当時の課題 複数DB 自力でがんばる CurrentAttributes 的なやつ(自作) CVE? なにそれ このgem 新しすぎて 無理 AppleSiliconで bundle install できない
  2. テストカバレッジ0% • すべて目視確認 ◦ 変更の影響範囲調査の工数増大 ◦ ビジネス側への受入テストの負荷 • 壊れても気づけない ◦

    動いているコードは触ら(れ)ない ◦ リファクタリングできない ◦ アップグレードでは必ず壊れる=できない 当時の課題 他に影響出そうな 変更はしません! 触らなければ 安全
  3. エラーに気づけない本番運用 • エラーは rescue ◦ production.log に書き出す ◦ 「問題が発生しました。◦◦にご連絡ください。」と表示する •

    指摘があったら ◦ サーバにログインしてproduction.log を探す ◦ 調査報告書を提出する 当時の課題
  4. RAILSとRUBYをセットでデプロイできるインフラ • Rubyのバージョンをアプリケーションチームのコントロール下に ◦ 別々にする必要ある? ▪ Rubyの更新申請 ▪ Railsの更新申請 •

    RailsとRubyのバージョンの依存関係は強い ◦ 一緒に管理するのが自然では? • アプリと一緒に指定されたRubyが入ってくれればよい ◦ rbenv install ◦ Docker Image アップグレードのための準備
  5. テストの整備とテストを根付かせる仕組みづくり • RubyやRailsのアップグレードは基本壊れる ◦ 非互換の変更 ◦ バグ • 壊れたらわかるようにする ◦

    テストやファクトリを書いて見せる ◦ コードレビュー担当者に見てもらう ◦ コードレビュー時にテストを書くよう依頼する ◦ テスト観点の助言をする ◦ CIによる自動テストをパスしない限りレビューに進めない ◦ CIによってテストカバレッジを計測 アップグレードのための準備
  6. 非推奨警告をなくし、発生を通知する仕組みづくり • 非推奨警告(DEPRECATION WARNING) • Sentryに記録 • 将来壊れることがわかっている • 原則すぐ対処する

    アップグレードのための準備 # 非推奨警告をSentryに送る ActiveSupport::Notifications.subscribe("deprecation.rails") do |*args| event = ActiveSupport::Notifications::Event.new(*args) message = event.payload[:message] Sentry.capture_message(message, level: :warning) end
  7. ビジネス側との関係を密にし理解を得る • アップグレード作業は問題発生リスクもある ◦ エラー ◦ パフォーマンス特性の変化 • 関係性が悪いと針の筵 •

    日頃から彼らと良い関係構築を心掛ける ◦ 業務・やりたいことの理解 ◦ 積極的なコミュニケーション アップグレードのための準備 動かない 遅くなった 怒られたく ない
  8. RUBYとRAILSのバージョン依存関係を確認 • Railsのバージョンごとに、サポートしているRubyのバージョンの範囲がある ◦ Ruby & Rails Compatibility Table -

    FastRuby.io | Rails Upgrade Service • 現在のバージョンから目標とするバージョンまでの計画を立てる ◦ Ruby 2.5に上げる前に、Rails 5.1にアップグレードして、それからRuby 2.5にして… ◦ 最低でもEOL前のバージョン ▪ Ruby ブランチごとのメンテナンス状況 (ruby-lang.org) ▪ Ruby on Rails — Maintenance policy アップグレードの手順 Railsのバージョン Rubyのバージョン 5.0.x 2.2.2 <= x < 2.5.0 5.1.x 2.2.2 <= x < 2.6.0 最低でも Ruby 3.1.x Rails 7.0.x
  9. 段階的なアップグレード(RAILS①) Railsガイドのアップグレードガイドの通りに進める 1. bundle update rails - bundle update rails

    xxxx yyyy … 依存関係を確認しながら足していく < コミット! 2. bundle exec rails app:update - application.rb などフレームワークが作るファイルを作り直す - すべて上書き - 差分を確認して取り込み < コミット! 3. テスト実行 - ……EEEEEE..EEE… - エラー・警告を消す< コミット(繰り返し) 4. 運用に回して様子を見る アップグレードの手順 エラーは 怖くない!
  10. 段階的なアップグレード(RAILS①) Railsガイドのアップグレードガイドの通りに進める 1. bundle update rails - bundle update rails

    xxxx yyyy … 依存関係を確認しながら足していく < コミット! 2. bundle exec rails app:update - application.rb などフレームワークが作るファイルを作り直す - すべて上書き - 差分を確認して取り込み < コミット! 3. テスト実行 - ……EEEEEE..EEE… - エラー・警告を消す< コミット(繰り返し) 4. 運用に回して様子を見る アップグレードの手順 エラーは 怖くない!
  11. 段階的なアップグレード(RAILS①) Railsガイドのアップグレードガイドの通りに進める 1. bundle update rails - bundle update rails

    xxxx yyyy … 依存関係を確認しながら足していく < コミット! 2. bundle exec rails app:update - application.rb などフレームワークが作るファイルを作り直す - すべて上書き - 差分を確認して取り込み < コミット! 3. テスト実行 - ……EEEEEE..EEE… - エラー・警告を消す< コミット(繰り返し) 4. 運用に回して様子を見る アップグレードの手順 エラーは 怖くない!
  12. 段階的なアップグレード(RAILS①) Railsガイドのアップグレードガイドの通りに進める 1. bundle update rails - bundle update rails

    xxxx yyyy … 依存関係を確認しながら足していく < コミット! 2. bundle exec rails app:update - application.rb などフレームワークが作るファイルを作り直す - すべて上書き - 差分を確認して取り込み < コミット! 3. テスト実行 - ……EEEEEE..EEE… - エラー・警告を消す< コミット(繰り返し) 4. 運用に回して様子を見る アップグレードの手順 エラーは 怖くない!
  13. 段階的なアップグレード(RAILS①) Railsガイドのアップグレードガイドの通りに進める 1. bundle update rails - bundle update rails

    xxxx yyyy … 依存関係を確認しながら足していく < コミット! 2. bundle exec rails app:update - application.rb などフレームワークが作るファイルを作り直す - すべて上書き - 差分を確認して取り込み < コミット! 3. テスト実行 - ……EEEEEE..EEE… - エラー・警告を消す< コミット(繰り返し) 4. 運用に回して様子を見る アップグレードの手順 エラーは 怖くない!
  14. 段階的なアップグレード(RAILS②) - アップグレード後しばらく運用してからフレームワークのデフォルト設定に対応する - config.load_defaults X.X(前のバージョン) - application.rb にある -

    new_framework_defaults_X_X.rb (新しいバージョン) - config/initializers/ にある - new_framework_defaults_7_2.rb 1. 新しいバージョンのデフォルトにするための設定がコメントアウトされている 2. それぞれの設定を調べてコメントアウトを外してテストする 3. すべての設定を変更したら ▪ config.load_defaults “7.2" ▪ new_framework_defaults_7_2.rb削除 アップグレードの手順
  15. 段階的なアップグレード(RAILS②) - アップグレード後しばらく運用してからフレームワークのデフォルト設定に対応する - config.load_defaults X.X(前のバージョン) - application.rb にある -

    new_framework_defaults_X_X.rb (新しいバージョン) - config/initializers/ にある - new_framework_defaults_7_2.rb 1. 新しいバージョンのデフォルトにするための設定がコメントアウトされている 2. それぞれの設定を調べてコメントアウトを外してテストする 3. すべての設定を変更したら ▪ config.load_defaults “7.2" ▪ new_framework_defaults_7_2.rb削除 アップグレードの手順
  16. 段階的なアップグレード(RAILS②) - アップグレード後しばらく運用してからフレームワークのデフォルト設定に対応する - config.load_defaults X.X(前のバージョン) - application.rb にある -

    new_framework_defaults_X_X.rb (新しいバージョン) - config/initializers/ にある - new_framework_defaults_7_2.rb 1. 新しいバージョンのデフォルトにするための設定がコメントアウトされている 2. それぞれの設定を調べてコメントアウトを外してテストする 3. すべての設定を変更したら ▪ config.load_defaults “7.2" ▪ new_framework_defaults_7_2.rb削除 アップグレードの手順
  17. 段階的なアップグレード(RAILS②) - アップグレード後しばらく運用してからフレームワークのデフォルト設定に対応する - config.load_defaults X.X(前のバージョン) - application.rb にある -

    new_framework_defaults_X_X.rb (新しいバージョン) - config/initializers/ にある - new_framework_defaults_7_2.rb 1. 新しいバージョンのデフォルトにするための設定がコメントアウトされている 2. それぞれの設定を調べてコメントアウトを外してテストする 3. すべての設定を変更したら ▪ config.load_defaults “7.2" ▪ new_framework_defaults_7_2.rb削除 アップグレードの手順
  18. 段階的なアップグレード(RUBY) • Railsが対応しているなるべく新しいバージョンにする ◦ マイナーバージョンから。 ▪ 現状2.6.xで3.0.xにするなら 2.6.10 => 2.7.8

    => 3.0.7 のように段階を踏む 1. Dockerfileや.ruby-versionを更新 2. bundle install ▪ インストールできないgemがあったら • gemの更新を確認 (新しいRubyに対応していない) • rubygemsのバージョン 3. テスト実行 ▪ エラー・警告を消す 4. 運用に回して様子を見る アップグレードの手順 # 非推奨警告をSentryに送る例 Warning[:deprecated] = true module Warning class DeprecationWarning < StandardError; end alias_method :original_warn, :warn def warn(msg) return original_warn(msg) unless msg.include?("deprecated") Sentry.capture_exception(DeprecationWarning.new(msg), lavel: :warning) original_warn(msg) if Rails.env.test? || Rails.env.development? end end
  19. 問題と解決 問題 • Gemの更新が止まってる • 新しいバージョンで 動かない • 依存関係を解決できない 解決

    • Forkして自分で保守する • 別のgemに置き換える 保守されていないGEM refile globalize
  20. RAILSをモンキーパッチしているGEM 問題と解決 問題 • 特定のバージョンの 実装に依存 • 簡単に壊れる • 修正難しい

    解決 • 使わない • 動いているうちに やめる努力 • 入れる前にコードに目を通し ておく
  21. RUBYの非互換な変更 問題と解決 問題 • 文法の変更 • 標準ライブラリ、 標準gemの変更 • 標準ライブラリ仕様変更

    解決 • 警告を有効にする • 新しいRubyを想定した gemにアップグレード • テストコード
  22. RAILSの非互換な変更 問題と解決 問題 • デフォルトの変更 • スタイルの変更 • 仕様の変更 •

    内部の変更 解決 • Railsアップグレードガ イド • github.com/rails/rails
  23. RAILSの非互換な変更 BELONGS_TO① - Rails 5.0未満と以降とで、required:オプションのデフォルト値が違う - Rails 5.0未満:required: false -

    Rails 5.0以降:required: true - Railsは互換性を維持できる仕組みがある - framework_defaults - (5.1~) load_config + framework_defaults_x_y - 4.xまでの挙動にできる - belongs_to_required_by_default = false - 古い仕様の互換性はいずれ削除される可能性がある - 甘えず新しい仕様に追従する! 問題と解決 5.0~標準 Belongs_to 先が 存在するか? バリデーション するようになった 設定で 4.Xまでの挙動に できる Rails 5.1 アップグレード前に 5.0標準にしたい
  24. RAILSの非互換な変更 BELONGS_TO② belongs_to の変更 • 4.x ◦ オプション無し:nilを許容する ◦ required:

    true:nilを許容しない • 5.0以降 ◦ オプション無し:nilを許容しない ◦ required: false:nilを許容する(非推奨) ◦ optional: true:nilを許容する 問題と解決 class Post < ApplicationRecord end class Comment < ApplicationRecord belongs_to :post end # Rails 5.0以降でバリデーションエラー Comment.new(post: nil).save!
  25. RAILSの非互換な変更 BELONGS_TO③ 一度に大きな変更が起きないようにする! 1. belongs_to_required_by_default = false を外してテストを失敗させる 2. すべての

    belongs_to に required: true を付ける ◦ belongs_to :user, required: true 3. テストを成功する 運用しながら、適切なオプションに修正していく • belongs_to :user • belongs_to :user, optional: true 問題と解決 いったん これまで通りの 挙動を優先 テストで これまで通りを 保証 非推奨の required: true 未確認の目印に
  26. 新しい作法 SPROCKETS から PROPSHAFT① 問題と解決 Sprockets •JavaScriptやCSSのロードパス設定、プリコンパイル、bundling、minifyなど多機能 •ミドルウェアによる拡張機能の追加 •プリプロセッサ •トランスフォーマー

    •コンプレッサー Propshaft •プリコンパイル、bundling、minifyなどの加工はしない •それぞれのツールに任せる •万能な解決策はない •アセットファイルをどう扱うかのみに集中 •ロードパス上のファイルの参照用ヘルパー、ダイジェスト付与とURL変換、開発サーバー •Rails 8 のデフォルトのアセットパイプライン
  27. 新しい作法 SPROCKETS から PROPSHAFT② 本件プロジェクトの状況 - Sprockets 3(アセットパイプライン) - bootstrap-sass

    - jquery-rails - sassc-rails - webpack + 自作のヘルパー(アセットパイプラインを使わない) - TypeScript, JSX (React) - Workbox (PWA化支援) 問題と解決
  28. 新しい作法 SPROCKETS から PROPSHAFT③ JavaScriptビルドをSprocketsから独立させる 1. Sprockets 3 (assets/javascripts)+ 自作のヘルパー

    + webpack 2. 自作のヘルパー + webpack 3. Sprockets 4 + jsbundling-rails + webpack 4. Sprockets 4 + jsbundling-rails + esbuild ※bootstrap-sass gem jquery-rails gemは npmに移行 ※各段階ごとにリリースし、本番運用後次の段階へ 問題と解決 ビルド時間 2分→2秒
  29. 新しい作法 SPROCKETS から PROPSHAFT② CSSのプリコンパイルをSprocketsから独立させる 1. Sprockets 3 + sassc-rails

    assets/stylesheets/**/*.sass 2. Sprockets 4 + assets/stylesheets/**/*.sass 3. Sprockets 4 + cssbundling-rails + Dart Sass ※各段階ごとにリリースし、本番運用後次の段階へ 問題と解決
  30. 新しい作法 SPROCKETS から PROPSHAFT③ Sprockets から Propshaftへ! • Propshaft +

    jsbundling-rails + cssbundling-rails ◦ JavaScript, CSSのビルドはそれぞれのツールに任せる(下記package.json) ◦ rails assets:precompile すると yarn build:css と yarn build が実行され、その後propshaftがアセットを public/assets にコピーする • sprockets-rails依存のgemがあったら、その解決が必要… ◦ アップデート or fork or 削除 "scripts": { "build": "npx npm-run-all build:frontend build:serviceworker build:css", "build:frontend": "npx esbuild ./app/javascript/user/index.ts ./app/javascript/admin/index.ts ./app/webcomponents/index.ts --bundle --minify - -asset-names=[name]-[hash].digested --chunk-names=[name]-[hash].digested --log-level=info --outdir=app/assets/builds --public-path=/assets -- splitting --inject:react-shim.js --platform=browser --format=esm --target=es6", "build:serviceworker": "npx esbuild ./app/javascript/serviceworker/index.js --bundle --minify --log-level=info -- outdir=app/assets/builds/javascript/serviceworker/ --public-path=/assets --platform=browser --format=iife", "build:css": "npx npm-run-all build:css:tailwindcss build:css:sass", "build:css:tailwindcss": "npx tailwindcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css", "build:css:sass": "npx sass ./app/assets/stylesheets/entrypoints:./app/assets/builds --no-source-map --load-path=node_modules" }, 問題と解決
  31. 新しい作法 ACTIVEJOB ADAPTER • shoryuken gem ◦ ActiveJob Adapter •

    Rails 7.2 ◦ NoMethodError: undefined method `enqueue_after_transaction_commit’ ◦ Adapterの基底抽象クラスが追加された ▪ IMPLEMENT ACTIVE JOB ENQUEUE_AFTER_TRANSACTION_COMMIT · RAILS/RAILS@E922C59 ▪ ActiveJob::QueueAdapters::AbstractAdapter • enqueue_after_transaction_commit? • とりあえず ActiveJob::QueueAdapters::ShoryukenAdapter def enqueue_after_transaction_commit? true end end
  32. 発生した問題とその解決 まとめ • 保守されていないgem ◦ 自分で保守するか使うのをやめるか ◦ gem選びが大切 • Railsをモンキーパッチしているgem

    ◦ やめた方がいい • Rubyの非互換な変更 ◦ 非推奨警告を有効にする(2.7.2以降) • Railsの非互換な変更 ◦ Railsアップグレードガイドが大変よい ◦ 変更前挙動を維持するよう修正の後、新しい挙動に寄せていく • 新しいRailsの作法の登場 ◦ 置き換えられることが決定している機能はなるべく早く対応しておく ◦ 置き換え方法のドキュメントはあるので頼りにする
  33. 書き味向上 得られたもの Rails • 同じことを書くにもこれまでよりも簡単にできるやり方、書き方 • 新しいDSL • ActiveRecordクエリーメソッド など

    Ruby • 書きやすさ・読みやすさを向上する言語機能 • キーワード引数 など • デバッガの進歩 • 静的解析 など
  34. パフォーマンス向上 • Rails 7.2 により Ruby 3.3 でYJITがデフォルトで有効になった ◦ 少なくともグラフで違いが判る程度には効果があった

    • Railsはオブジェクト数を減らすリファクタリングもやってる! • アセットのコンパイルも速くなった 得られたもの
  35. ビジネス側からの信頼 自信をもって変更できることでビジネス側との関係が円滑に • リードタイムの向上 ◦ 広すぎる影響調査 ▪ 仕様がコード化されている ◦ 多すぎる目視確認

    ▪ 正常系のパスは自動テストである程度確認できる ◦ 自信のない変更 ▪ 壊れたことに気づける可能性 • 業務知識の理解が深まった ◦ ビジネス側と同じ言葉でコミュニケーションできるように 得られたもの
  36. GEMの選び方 • 自分が保守できるか? ◦ 多機能ではない ◦ メタプログラミングはほどほどに ◦ 標準の動きを変えない •

    保守できないなら ◦ 継続性はありそうか? ▪専門知識が必要なgemは自分で保守が難しい ▪コントリビューションがありそうか ▪完成していて更新の必要がないものはなくてもok ◦ 置き換えできそうか ▪更新が止まっても代替手段へ切り替えでしのげること ▪シンプルであること アップグレードしていくために gemの 依存gem も注意 開発に専門知識が 必要なgemは置換え しやすそうな作りが 大切
  37. コードの書き方 • 賢”そう”なコードを書かない ◦ 実装に依存した何かをすると、実装が変更されたときに死ぬ ▪昔々のRailsではたまにモンキーパッチも見られたがもれなく死んだ ▪そういうgem入れるな • 依存関係を閉じ込める ◦

    Gemの入れ替えも想定 ◦ 変更箇所を最低限にできるよう、依存をカプセル化しておく ◦ アプリ全体に依存が発生するコードを書くなら 自分で保守する覚悟で書け アップグレードしていくために Gemの例外クラスが アプリケーション中の 各所にあるようだと危 険 実装を読んで理解を 深めるのは良いが、 実装を利用して技巧 を凝らすのはNG
  38. 日々コツコツと • Dependabot ◦ 依存関係の更新PRを自動で作ってくれる • 修正や追加のついでにテストを書く ◦ 特にバグ修正はバグを再現するテストを追加してから修正する ◦

    いつの間にかテストが増えていく! • 情報収集 ◦ 新しいRailsやRubyでできることを知る ◦ アップグレードのモチベーションになる アップグレードしていくために 多すぎると 疲れちゃう。 僕は月1設定
  39. アップグレードしていくために:まとめ • GEMの選び方 ◦ 更新が止まったとき、自分で保守できるか、置き換えが可能なもの • コードの書き方 ◦ 自分でコントロールできないものは使わないか影響を閉じ込める •

    日々コツコツと ◦ 依存関係の更新PRは自動で作る仕組みを入れる ◦ もののついででテストを書く • リソースの確保 ◦ チームに余裕がないなら外部のリソースを使うのもアリ • エンジニアの説明責任 ◦ 信頼して話を聞いてもらえるよう努力する
  40. 現実の RUBY/RAILSアップグレード:まとめ • 当時の課題 ◦ 変更に耐えうる土壌がなかった • アップグレードのための準備 ◦ 壊れることを恐れない環境づくり

    ◦ チーム内外を説得する • アップグレードの手順 ◦ RAILSガイド ◦ 段階的に実施する • 発生した問題とその解決 ◦ いろんな非互換問題が出てくるので、やっていく • 得られたもの ◦ 変更に耐えうる土壌ができた • アップグレードしていくために ◦ 日々の積み重ねこそ大切