日々のこと

まいにちの暮らしをつれづれ書きます

Kaigi on Rails 2024 Day 1 レポート

去年、Kaigi on Rails 2023にはじめて現地参加してとてもたのしかったので、今年も行ってきました。
今年の Kaigi on Railsも、一層たのしさが増していました。(語彙力)
行ってきて感じたことは、こちらの記事に書いています。
この記事には、Day 1 に聞いたセッションについて書きます。
改めて発表資料を見つつこの記事にまとめていると、曖昧だった理解が進みました。

kaigionrails.org

スポンサーセッション

現地に着くのが少し遅れてしまって、オープニングとスポンサーセッション3つあるうちの1つ目は聞くことができませんでした。

minne の Shoryuken 活用

GMOインターネットグループGMOペパボ)のスポンサーセッションから。
前職ご一緒していた yumu さんの Shoryuken のお話でした。間に合って良かった・・・(ゆりかもめの駅からダッシュした) Sidekiq と Shoryuken の使い分けについてわかりやすく説明してくださっていました。
Shoryuken は、Amazon SQSをバックエンドに動きます(Sidekiq は Redis)。  スポンサーセッションですが、がっつり技術のお話でした。

speakerdeck.com

基調講演 Rails way, or the highway(Rails の道か、さもなくば帰り道はあっちだ)

Palkan san

Keynote: Rails Way or the highway by Evil Martians speakerdeck.com

Rails Way の原則に沿って、どのようにアプリケーションを拡張するかについて。抽象化するコード例を示しながらの話してくれました。
Rails Wayの乗っ取って拡張されたコードが美しかったです。素晴らしかった。
(英語での講演でしたが、スライドに字幕がついていたのでどうにか理解できました)

  • Rails way は Rails アプリケーションを構築する方法論、哲学
    • 生産性と幸福のための最適化
    • 意思決定からの解放(Omakase)
    • スケーラビリティを考慮した設計
  • Rails は "Hello World" から IPO までをサポートする
  • 「ゼロ」から「イチ」への最適化
  • 「イチ」を超えることへの課題
  • Model View Controller、アプリケーションが大きくなるにつれて、3つの箱では足りなくなる
  • 成長するにつれRAILS WAYから外れてくる
  • おまかせは完全ではなく、埋めるべき隙間がある
  • 隙間を埋めることでフレームワークを歪めてしまう 怪獣 on Rails になってしまう
  • Rails フレームワークは構造を与えてくれる
  • Rails Way はあなたの導きの星
  • Rails Way にしたがって隙間を埋めるガイド(英語のみ)

  • Railsを習得し、拡張する(Master & extend Rails

Master RailsRailsを習得する)

Extend RailsRailsを拡張する)

  • Railsを他のものと混ぜ合わせるのではなく、拡張しよう
  • 担当分野の分離には新しい抽象化が必要
  • Railsアプリケーションに新しい抽象化レイヤーを導入する際に、私が従う4つのルール

1. Rails らしい開発者体験とコアコンポーネントとの互換性を提供する

2. 抽象化レイヤの間に明確な境界線を設ける

  • Layered Auchitectureの原則
  • データは上から下へ流れる、各抽象化は自分より下のものだけ知ることができる
  • どこまでデータが流れるか制御することも
  • それぞれのRailsの抽象化を、特定のアーキテクチャそうに対応づけられる
    • Presentation / Controoer, Channels. Views
    • Application / Jobs, Mailer
    • Domain / Models
    • Infrastructure / Adapters(DB, Mail), API clients

3. 介入より抽出 コード内で既存の抽象化を見つけよう

4. 関心事と複雑さのレベルを分離する

  • 学習曲線をなだらかに保ちつつ、新しい知識を得る機会も残す

原則を実践してみる

  • 複数ステップがあるウィザート方式の入力フォームの例
  • 1つのフォームのために7つの新しいメソッドがあった(next_step, previous_step...)
  • Controllerのback アクション、Rails らしくないアクション
  • Rails は標準で複数ステップ機能を提供してくれない
  • 担当分野を分離するために新しい抽象化を追加!
  • Form Object の担当分野
    • コンテキストよりのバリデーション
    • ユーザー入力の変換
    • ユーザへフィードバックを提供
    • UIに基づくカスタムロジック
  • ベテラン火星人開発者のコード、すごく整理されていた(スライド内のコードを参照)
    • class CreateForm < ApplicationForm
    • class Wizard < ApplicationWorkflow
  • 全ての抽象化にはベースクラスが必要
  • フォームオプジェクトをモデルオブジェクトの代わりにform_forで利用できる
  • Controllerのコードは rails g scaffold で自動生成されたコードに似てる
  • hidden_field で wizard_stateを持っていた
    • <%= f.hidden_filed :wizard_state %>
  • フレームワークの一部となる抽象化を導入する例だった
  • フレームワークと戦わなければ、Rails Way で成長することは可能
    1. Railsに似た抽象化を導入
    2. 明確な境界を引く
    3. 担当分野と複雑性を分離
    4. アプリケーションに属する
  • アプリケーションのパズルとピースをうまく組み合わせましょう!

Railsの仕組みを理解してモデルを上手に育てる

  • モデルを見つける、モデルを分割する良いタイミング -
    五十嵐邦明 さん

speakerdeck.com

Rails アプリの実装に慣れた後、メンテしやすいコードを書く技術を身につけたい人向けに、モデルを上手に見つけて、フォームオブジェクトを実装できるような知識を持ち帰ってもらうことが目標、ということでした。
最初の基調講演にも近い部分のあるお話でした。とても整理されていてためになりました。
igaigaさんの本は、Railsの道を歩くときの道しるべとなってありがたいです。

前編: モデルの見つけ方

イベント型モデル

  • イベント型モデルは「行為を記録するモデル」、見つけるコツが必要なテーブルの1つ
  • 見分け方は、名詞であるモデル名に「〜する」をつけると行為になるもの
    • Order(注文)、Shipment(出荷)、Payment(支払い)
  • 例)入荷処理
    • 在庫を増やす(Stock)
    • 支払った代金を銀行口座(BankAccount)から減らす
    • StockとBankAcocuntのどちらのモデルに入荷処理を書くか迷う
    • イベント型モデル「Arrival」を作る -> Arrivalモデルのreceivedメソッド
    • 複数モデルで実装場所に迷った処理をイベント型モデルに書ける 🥳
    • Rails wayに乗った設計方法になる

イベント型モデルに関する参考資料

PORO(Plain Old Ruby Object)を作る

  • 何も継承していないただのクラス、ARを継承していない
  • app/models配下におくのがおすすめ
  • ActiveRecordを継承はしていないけどビジネスロジック置き場
  • POROにルールを作る
    • Rails wayに外れそうな境界にあるのでチームでルールを育てるのがおすすめ
    • チームメンバーが同じ実装を答えられる状態にするのがルール作りの理想「迷うことを減らす」
    • 例)クラス名には返すオブジェクトの名前を名詞で付ける
    • もし将来、データを記録したくなったら、POROからモデルに変更しても良い

Service層を入れるのはできるだけやめてほしい

  • Railsの特徴は、レイヤーを減らして書くコードの量を減らす方針(密結合な設計)
  • 層を増やすのはRailsの良さを消しかねない、Service層を入れるのは相当な覚悟を持って挑む作業!!
  • Service層を入れるかわりにおすすめしたいこと
    • イベント型モデルを丁寧に探していく
    • POROのようなシンプルな考え方から育ててく
  • Service層の困りごと
    • ビジネスロジックをモデルとServiceのどちらに書くか迷いやすい
    • Serviceオブジェクト間でのメソッド共有が難
    • Service層はみんなの理解や認識がばらばらになりがち

後編: モデルを分割するタイミング

  • モデルが太っているかどう判断する?コード行数が多すぎるは気にしない
  • そのまま書き続けるのがしんどくなる時 & そのタイミングで良い分割方法がある時

バリデーションを条件分岐したくなったとき

  • モデルのバリデーションは、DB用とフォーム用を共有してる
  • ここで Form Object!
  • Form用とDB用のバリデーションをそれぞれの仕事に分離するイメージ
  • 時間経過とともに良い設計が変わっていくことがある
  • アプリが成長してFormとDBのバリデーションの共有がしんどくなったら分割する

フォームオブジェクトの作り方

  • ActiveModel::Model, ActiveModer::Attributesを使う
  • ActiveModer::Attributes
    • 型を持つ attributes(カラム的なもの)を簡単に定義できるようにする
  • ActiveModel::Model
    • モデルのように振る舞う機能各種を使えるようになる、form_withとやりとりする機能
  • ActiveModel::Model = ActiveModel:API + ActiveMode::Access
    • Accessにはsliceメソッドとvalue_atメソッドがある
  • app/forms/user_name_form.rb を作る例
    1. 基礎工事
    2. UserモデルをForm Objectへ渡す
    3. saveメソッドをForm Objectに実装
    4. initializeメソッドをForm Objectに実装
    5. コントローラの変更
    6. form_withでフォームオブジェクトを使う
  • 最終形:

GitHub - igaiga/rails_form_object_sample_app

class UserNameForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  # Userモデルに仕事を委譲するため
  attr_accessor :user


  attribute :name, :string
  validates :name, format: { with: /\A\p{Hiragana}+\z/ }, presence: true

  # Form Object から Model へattributesをセット
  def transfer_attributes
    user.name = name
  end

  def initialize(model :nil, ++attrs) # **はキーワード引数をHashで受け取る
    attrs.symbolize_keys!
    if model
      @user = model
      attrs = { name: @user.name }.merge(attrs)
    end
    super(**attrs) # 親クラスのinitializeメソッド呼び出し
  end

  # DB保存機能をsaveメソッドとして実装(モデルと似た書き味を目指す)
  def save
     transfer_attributes
     if valid?
       user.save(...)
     else
        false
     end
  end
end

参考資料

そのカラム追加、ちょっと待って!カラム追加で増えるActiveRecordのメモリサイズ、イメージできますか?

Asayama Kodai さん

speakerdeck.com

integerのカラム追加で、どれだけのメモリが増えるのかということを調べた内容でした。メモリがどれだけ増えてしまうのか気になり、聞きました。
メモリがどのようなオブジェクトに増えるのか、調査していく手法にうなりました。

  • 目的
    • Railsで開発していて、メモリ効率・使用量を意識してARインスタンスを生成するようになること、CRubyの仕組みに興味を持つきっかけになること

integerを add_columnすると何バイト増えるのか

  • 224バイト、これは多い?少ない?
  • オブジェクトのサイズを見るのにObjectSpace.memsize_ofを使うと、インスタンス変数までは追えない
  • ObjectSpace.reachable_objecs_fromを使う
    • 1階層しか辿れないので、探索していく必要がある

増えたオブジェクトを調べる

  • ActiveModel::Attribute::FromDatabase
    • DBからの結果をラップする

CRubyのメモリレイアウトの話

  • CRubyではオブジェクトごとにRVALUEと呼ばれる固定長のメモリを割り当て(例外あり)
  • Ruby3.2から埋め込み表現可能な範囲が広がっている
    • Size Poolという新しい概念が増えた
  • Rubyのウラガワ 笹田さんのWeb+DB Press連載記事を読み漁る

まとめ

  • 224バイトというのはRVALUEが4個 ivptrでVALUE 4*2個
    • ActiveModel::Attribute::FromDatabse(40+32)
    • ActiveModel::Type::Integer(40+32)
    • String(40)
    • Range(40)

モノリスでも使える!OpenTelemetryでRailsアプリのパフォーマンス分析を始めてみよう

ymtdzzz さん

speakerdeck.com

OpenTelemetryの名前はよく聞くのですが、具体的にどういうものなのか知りたかったのと、どのように Rails アプリケーションに導入すればいいのか知りたくて聞きました。
現在は数年前に比べて、OpenTelemetry の導入がしやすくなっていて、トレースによる実装見る前にひと目でボトルネックや見るべき場所が特定しやすくなるのはとても便利だと思いました。

調査の難しさ

  • 調査はエンドツーエンドの粒度、ユーザーから見た遅さ・使いにくさから始まる、調査開始時点ではなにもわからない
  • コードを見る前にどれだけ可能性を潰せるかが勝負

テレメトリ分析の難しさ

  • ログとメトリクスで実装の粒度までブレークダウンするのは難しい
  • ログ + メトリクス + トレース
  • 分散トレーシングとは、1つのリクエストが、アプリケーションを構成する様々なサービスによって処理される過程を追跡する方法
  • 図示されるボックスのことを「スパン」と呼んでいる
  • アプリケーション側の計装(テレメトリを出力・収集できるようにすること)
    • 手動計装: SpanのStart, Endを実装
    • 自動計装: gemで自動出力 -> この方法でデモ

他ツールとの違い

  • Profiler(rack-mini-profiler, stackprof)
  • N+1 Detection(bullet)
  • 分散トレーシングの強みは、テレメトリの関連付け(correlation)
    • ログとの紐付け、メトリクスとの紐付け
  • 1クリックで関連するログを引っ張ってこれたり
  • オブザーバビリティの入り口として、トレースを紹介
  • トレースが調査の入口になる(汎用的、直感的)
  • 分散トレーシングは、正しく実装するのは非常に難しく時間がかかる by 入門監視
  • 2024年になってどうなのよ?

分散トレーシングの導入ハードルの高さ

  • 数年前まで 導入・運用コスト > メリット の感覚
  • 2019.5 OpenTelemetryの登場(OTel)
  • オブザーバビリティの実現に必要なほぼ全てを提供するもの
    • OpenTelemetryという標準仕様の登場
  • 導入・運用コスト < メリットになってきたのでは?

OpenTelemetryのはじめかた

  • やること
    • Railsアプリケーションを計装する (トレースを出せるように)
    • バックエンドを用意する(SaaS or OSSを組み合わせて自前で構築)

Railsアプリケーションを計装する

  • gemを入れる opentelemetry-sdk etc
  • initializerを書く
  • 環境変数を設定してサーバーを起動
  • デモ、架空のECサイト
    • Grafannaでログやトレースの確認(コードをみる前に)
  • 一目でボトルネックがわかる、トレースにはログと紐づいているので見ることができる

バックエンドの選択肢

構成における選択肢

  • バックエンド: すでに使っているSaaS系があるならそれを使おう
  • 計装
    • OpenTelemetry
    • SaaSベンダー独自形式
  • 鉄則: 導入と運用が一番楽な構成を選ぶ

まとめ

  • トレース
    • ログ、メトリクスを補完する情報
    • 関連付けにより、テレメトリ全体を強化
  • OpenTelemtryにより導入ハードルが下がった

カスタムしながら理解するGraphQL Connection

yana-gi さん

speakerdeck.com

元同僚の yana-gi さんの登壇でした。
新しい検索エンジンの導入をチームで進めていて、そのとき紐解いた GraphQL の知見をシェアしてくれました。
(自分が在籍していたときに、yana-giさんたちのチームは新検索エンジンの導入を進めていて、チームで色々コードリーディングなどして奮闘されているご様子だったのですが、今回、改めてその内容を知ることができてうれしかったことと、こうした発表に結実されていて本当にすごいと、発表を聞きながら胸が熱くなりました)

GraphQLとは

  • クライアントが必要な情報だけ指定して取得できるデータクエリ言語 及び ランタイム

新しい検索エンジンの導入

ページネーション

  • オフセットページネーション
    • データの開始位置を指定して取得する offset と limitで取得
    • SELECT * FROM products LIMIT 10 OFFSET 30;
  • カーソルページネーション
    • 特定のデータポイント(カーソル)を基点にデータを取得
    • 時系列などの特定のデータをキーとして検索する
    • SELECT * FROM products WHERE id > 30 LIMIT 10;

GraphQL Connection

  • GraphQLでページネーションを簡単に実装できる
  • Connection: ノード(データ)をリスト形式で取得
  • Edge: ノード間の関係性や付加情報
  • Node: 実際のデータ本体
query {
  products(first: 10) { # ProductConnection Type
  edges {  # ProductEdge Type
    cursor
    node {  # Product Type
    },
    ...
}
  • Connection のノード取得クエリは、開始位置を指定
team {
  member(first: 10, after: "abc") {
    nodes {
      id
      name
    }
  }
}
  • Connection Typeの実装
    • fieldのTypeをconnection_typeに指定
    • ActiveRecord Relationなど、サポートされているクラスの場合、要素を全て渡す
field :items, Types::ItemType.connection_type, null: false

def products
  object.products # ActiveRecord Relationの場合
end

改めて今回やりたいこと

  • 検索エンジンREST API で提供されている、ページネーションは offset のみ
  • 結果を取得したデータを扱うクラスは ActiveRecord ではない!つまり Connection Typeでサポートされていないクラスになりそう
  • サポートされていない場合はどうすれば・・・ Custom Connection!

Custom Connection

  • Custom Connectionを実装する
  • (通常のConnectionでは対応できない特別なページネーションなどに対応するために使用するものらしい)
def products
  # ActiveRecord のリレーションを取得
  relation = object.items
  # 作成したCustom Connection Typeのインスタンスを渡す
  Connections::ProductsConnection.new(relation)
end
  • 独自のコネクションTypeを作るには、GraphQL::Pagenation::Connectionを継承
class Connections::ProductsConnection < GraphQL::Pagination::Connection
  def nodes ... end
  def has_next_page ... end
  def has_previous_page ... end
  def cursor_for(item) ... end
end

疑問に思ったこと 🤔

  • Connectionのoffset(開始位置)はどうやって決まるか

真相を知るためにクエリを実行してから値が返却されるまでを追う

  • Connectionのoffset(開始位置)は、ConnectionClassでcursorから計算していた
  • resolverを通った後に Connection Class で offset が決まる

さらなる疑問 🤔

  • resolver の時点では offset と limit が決まっていない
  • resolver の段階で APIリクエストするにも、offset と limit が決まっていない

答え

  • Promiseクラスで遅延評価を行う! 💡

まとめ

  • GraphQL でページングを行うには Connectin を使う
  • 独自のモデルで Connection を使うには Custom Connectin を定義する
  • Connection Type Class で offset が決まる
  • offset を利用するために resolver では Promise クラスを利用して遅延評価を行う必要があった

リリース8年目のサービスの1800個のERBファイルをViewComponentに移行した方法とその結果

Naoyuki Kataoka さん

speakerdeck.com

シロクCTOの方の発表でした。スキンケアブランド N organic を販売されているサービスとのことでした。
ERBの複雑度が上がってしまって保守しにくくなるというのはあるよなあと思い、こちらの発表を聞きたいと思いました。自動生成で一気に移行したとのこと、すごいです・・!

ビューレイヤーが抱えていた課題

  • 1,800以上のerbファイル
  • パラメータ定義の曖昧さ、一貫性のないパラメータの渡し方、テンプレート内のロジックの多さなどによる、想定外のバグの発生
  • erbには、インスタンス変数もローカル変数も渡せるので混在
  • 再利用されるビュー要素はローカル変数に限定すると良い
  • 複雑なロジックがテンプレート内に埋め込まれ、可読性・保守性が低下 -> 特定の条件でエラー発生
  • ビュー要素のロジックをテストすることは容易ではない
    • 手動テスト・実装コストの高いE2Eテスト・・・

解決策として、ViewComponent

# app/components/message_component.rb
class MessageComponent < ViewComponent::Base
  def initialize(name:)
    @name = name # 必要なパラメータを初期化引数で渡す
  end
end

# app/conponents/message_component.html.erb
<h1>Hello, <%= @name %>!</h1>
<%= render(MessageComponent.new(name: "World")) %>

ViewComponent のメリット

  • ロジックの分離
  • テストの容易さ
  • インタフェースの明示
    • コンストラクタ引数でインタフェースがわかる!
  • パフォーマンスの向上
    • 大量のビュー要素を表示する計測結果、ERBに比べてViewComponentはレンダリング時間が大幅に短縮(401ms -> 57ms)

どうやって移行するか?

  • 移行期間中も他の開発は止められない
  • 移行完了しない場合、中途半端に2つの実装が混在する
  • 全てのViewComponentをerbを元に自動生成する!!!
    • 移行期間中の追加・更新も自動生成を再実行すれば反映できる状態にする
  • 自動生成スクリプトの実装 & 新しいビュー実装のリリース

実装

  • 解析のフェーズ -> 生成のフェーズ

解析

  • erb内でrenderメソッドを呼ぶ位置を決める
  • 正規表現きびしかったので、Ruby Parserを使って解析
    • erbそのものはHTMLタグがあるのでRubyコードとしてparseできないので、HTMLタグを除去して Rubyコードのみ取り出す
  • 抽象構文木からrenderの位置を特定、抽象構文木のノードを再起的に探索してrenderを見つける
  • erbの依存関係の把握
    • renderを呼び出す側、呼び出される側を把握してツリーを作成
    • どこからも呼ばれていないerbを削除
  • ViewComponentの初期化引数に渡すパラメータを特定する必要

生成

  • 解析結果を元に、rubyファイルの作成
    • 必要なパラメータをキーワード引数として持ち、インスタンス変数に代入するメソッド
  • erbファイルの作成
    • 元のerbを持ってきて、renderメソッドをViewComponentの呼び出しに書き換え
    • ローカル変数は全てインスタンス変数に書き換え
  • テストの内容は、エラーはなくレンダリングができるかどうかのみ、カバレッジは目指さない
  • 自動生成スクリプトとテストデータの微調整が作業工程の8割・・・大変
    • ViewComponentのコードを生成してrspec実行、失敗したらスクリプトorテスト修正、Rspecが通るまで繰り返す
  • これにより、PRは20万行の差分!

安全なリリース

  • 段階的にリリースする方法を検討
  • 別系統の生成 app/view: ERB Partials app/view2: ViewComponent
  • view_pathの分岐
    • Railsがビューを解決する場合、view_pathsを順に探索していくことを利用
    • view_pathの操作は、append_view_pathm prepend_view_pathメソッドを呼んで ViewComponent版の実装を優先させる(app/viewよりapp/view2を優先)
  • フィチャーフラグによる検証をした
    • まず社内で動作を確認
    • Cookieの有無でレスポンスを取得してdiffを確認
before_action -> { prepend_view_path 'app/view2' if cookies[:feature_flag] == 'view_component' }
  • カナリアリリース: 一部のユーザーのみに先にリリースする
    • 1%から開始してだんだん上げていく
before_action -> { prepend_view_path 'app/views2' if rand < 0.1 }

ViewComponent導入の効果

  • コンポーネント呼び出しのインタフェースがわかりやすくなった
    • コンストラクタを見ればわかる
  • コードジャンプがしやすくなった
    • クラスとしてコードジャンプが可能に
  • ビューのテストが可能になり、実装ミスに気づきやすく
    • 全ViewComponentのrenderのテストが可能
  • 期間1年半、実装時間120時間、自動生成する戦略は成功
  • パフォーマンスは、ABテストした結果、実計測での差はわずか
    • ActiveRecordのクエリ呼び出し時間が支配的なため

Rails APIモードのためのシンプルで効果的なCSRF対策

corocn さん

speakerdeck.com

APIモードでのCSRF対策についてのセッションでした。リーナーテクノロジーズの方で、岐阜県の nagara.rb 運営されている方の発表でした。
自分は、CSRFについてはRailsが基本的な対策をしてくれる認識でいたのですが、APIモードでどんな考慮が必要なのかなどについて知りたくてお聞きしました。
とてもまとまっていてわかりやすかったです。

RailsCSRF対策

  • Railsトークン方式で対策、CSRFトークンを発行してチェックする
  • Rails API + SPAのプロダクト開発によく出会うようになった、CSRFトークンの対策をどう組み込むのかベストなのか
  • Rails wayに乗っかる場合
    • ViewとControllerで数行書けば対策が終わる、初学者に優しい
  • Rails API + SPA の場合、Rails wayから外れるのでトークン送受を自前で書くことになる
  • トークン方式にこだわりはない、もっとシンプルに対策できない?
  • 発表者の方が3年前にトークン形式でご自分で対策した記事がヒットするので、もっとシンプルな方法を伝えたかったのが今回の発表のモチベーションになったとのこと(ありがたい)

対策

  • 方向性1: リクエストの出どころを確認する
    • 許可したサイトからのリクエストのみ受け入れる
  • 方向性2: Cookie が自動で送られないようにする

具体的なCSRF対策3つ

  • 基本
    • リクエストの出どころを確認する Originヘッダーの確認
  • より安全に
    • リクエストの出どころを確認する Fetch Metadataの確認
    • Cookie が自動で送られないようにする SameSite 属性の指定

1. Originヘッダーの確認

  • Originはプロトコル × ホスト × ポート番号 https://kaigionrails.org部分
  • ブラウザから飛んでくるので想定されるOriginのみ許可して開ければ良い
  • なぜ既存の実装はトークンなの?Originの方がシンプルでは?
    • ブラウザが Origin 送ってくれないことがあった(Form POST)、今は送られてくると思って問題ない
    • 現在も送られないケースはは、GET | HEAD かつ同一オリジンのとき、GETで副作用を発生させないように作ればOK

2. Fetch Metadata

  • 不正なリクエストを弾くための情報を提供するヘッダ、4種類あるが、Sec-Fetch-Siteを使うのが良い
  • same-site, same-origin, cross-site, noneが取りうる値
  • 基本は、same-site, same-originを見れば防御できる
    • same-origin: オリジン完全一致
    • same-site: サイト内(サブドメインはcross-originだがsame-site)
  • Fetch Metadataの良いところは、Originの使いにくい点を解消してくれる
  • わりと新しい仕様なので、ヘッダがあればチェックする方針になりそ

3. SameSite属性の指定

  • Cookieの属性、クロスサイトでのCookie送信の挙動を制御するもの
  • 3rd Party Cookieの制御のために作られた、CSRF対策で使えるのはおまけ、広告・プライバシー文脈
  • SameSite属性の種類 -> Lax or Strictを指定すれば良い
    • None: 全てのリクエストでCookie送信
    • Lax: 同一サイトからのリクエスト、一部のクロスサイトリクエスト(トップレベル+GET)に対して送信
    • Strict: 同一サイトからのリクエストのみ
  • 2分ルールがある
    • Laxの場合、セットされた2分間はPOSTリクエストでもCookieが送信される
    • CSRF対策としては片手落ちなので、他のことも併せて対応する
  • 現行ブラウザはすべて対応しているが、デフォルトNoneのブラウザがあるので、明示的なLaxの指定が必要

Railsでの実装

  • SameSite指定は、config.action_dispatch.cookies_same_site_protection、6.1以降はデフォルトLaxなので指定不要

まとめ

  • CSRF対策はリクエストの出どころを確認することが大事
  • RailsのViewを使う前提ならトークン形式を選択すれば良い
  • Rails APIであれば、Originチェックをベースラインとしよう

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

Yuichi Takeuchi さん

speakerdeck.com zenn.dev

RubyRailsのアップグレードを多く経験された発表者の方が、アップグレードの進め方、発生した問題、アップグレードし続けるために必要なことを実体験を基に発表してくれました。
現実問題にまみれながら、Ruby/Railsアップグレードを地道に進めていかれている様子に感動を覚えました。特に、リソースの確保と、エンジニアの説明責任の話が刺さりました。「余裕ができたらやる」は永遠にやらない、本当にそれはそう・・・。

当時の課題

  • 古すぎるRubyRails、テストがない(怖い・・・)、壊れても気づけない
  • アプリとRubyのバージョン管理が別々
  • 非推奨警告を放置した開発

アップグレードのための準備

  • RubyRailsをセットでデプロイできるインフラ
  • テストの整備と、テストを根付かせるしくみづくり
  • エラーを通知するしくみづくり、Sentry通知
  • 非推奨警告をSentryに記録、将来壊れることがわかっているので原則すぐ対処、見つけ次第殺していきましょう
  • ビジネス側との関係を密にし理解を得る

アップグレードの手順

  • RailsのバージョンごとにサポートしているRubyのバージョンの範囲がある
  • 現在のバージョンから目標とするバージョンまでの計画を立てる

段階的なアップグレード Rails1

  • Railsガイドのアップグレードの通りに進めましょう
  • bundle update rails、依存関係を確認しながら足していく、ここでコミット
  • bundle exec rails app:updateで、application.rbなどフレームワークが作るファイルの作り直し
    • 全て上書きでOK
  • テスト実行、必ず失敗する
    • エラー・警告を消す、コミット(gemもなぜ失敗したか確認する)
  • 運用に回して様子を見る

段階的なアップグレード Rails2

  • アップグレード後、しばらく運用してからフレームワークのデフォルト設定に対応
    • デフォルトでの挙動が変わることがある
    • 原則、新しい状態で動くようにすべき
    • config_load_defaults X.X (前のバージョン)、application.rbにある
    • new_framework_defaults_X_X.rb(新しいバージョン)config/initializers/にある

Rubyのバージョンアップ

  • Railsが対応しているものになったら、Rubyをその中の一番新しいバージョンにする
  • 依存関係を確認して、段階的なアップグレードをする

発生した問題とその解決

  • 保守されていないgem
    • formして自分で保守、別のgemに置き換える
  • Railsをモンキーパッチしているgem
    • 使わない、動いているうちにやめる努力、入れる前にコードに目を通しておく
  • Rubyの非互換な変更(文法の変更、キーワード引数の変更など)
    • 警告を有効にする、新しいRubyを想定したgemにアップグレード、テストコード
  • 標準ライブラリ仕様変更でOpenSSL周りが壊れたので、テスト書いてよかったと思った
  • Railsの非互換な変更
    • 基本的にはRailsアップグレードガイドに書いてあるのでみましょう
    • belongs_toの挙動変更
      • Rails5.0未満と以降で、belongs_toのデフォルトの挙動変更、requiredのデフォルト値が変わった
      • Railsは互換性を維持できる仕組みがあるが、古い仕様の互換性はいずれ削除される可能性あり、甘えず新しい仕様に追従する
      • フレームワークに追従するための変更と、アプリケーションの変更は一緒にやらないことが大切
    • SprocketsからPropshaftへの移行
    • ActiveJob Adapter
      • これまでなかった基底抽象クラスが追加された

得られたもの

  • 書き味の向上、デバッグgemが進化した
  • 脆弱性修正がされるバージョンになって安心 Apache log4jとか
  • パフォーマンス向上
    • Rails7.2よりRuby3.3でYJITに
  • テスト書く文化
  • 業務領域に対する理解、アップグレードのための調査でコードリード
  • ビジネス側からの信頼

アップグレードしていくために

  • gemの選び方
    • コードを眺めて、保守が止まっても自分が保守できるかをみる
    • 代わりのgemと切り替えられるようなインタフェースになっているか
    • 特定のgem依存をなるべく局所化したい
  • コードの書き方、賢そうなコードを書かない、依存関係を閉じ込める
  • 日々こつこつやっていきましょう
    • dependabotで月1アップグレードするのを自動化して面倒にならないように
  • リソースの確保が必要
    • 余裕ができたらやるは永遠にやらない、計画に入れる
  • エンジニアの説明責任
    • アップグレードはコストはかかるが、やらないリスクは確実にある
    • 信頼して説明を聞いてもらえるように努める

Hotwire or React? 〜Reactの録画機能をHotwireに置き換えて得られた知見〜

Haruna Tsujita さん

speakerdeck.com

フィヨルドブートキャンプ出身の方の発表でした。
ある機能を React から Hotwire に置き換えた経験を通して、Hotwire と React のどちらを選択するか考える基準を示してくださった発表でした。
プロポーザルを提出した時点からさらに深掘りして得た知見を共有してくださっていて、素晴らしかったです。

このくらいの規模のサービスに本当にReactは必要?

  • 英語塾キャタルに通う生徒に提供している小規模なアプリケーション、部分的に React で SPA を実現
  • やりたいことはHotwireで十分できるのでは?でもHotwireに移行するのはちょっと怖い
  • Stimulus の情報があまりなくて未知数、技術検証する価値はありそう!
  • 録画機能を置き換えてみる

Hotwire/Stimulusとは?

  • Turbo と相性の良い JavaScript フレームワーク
  • Stimulus自体に書きづらさはない、でもしっくりこない
  • ごりごりにJS書くことになってしまう
  • プロポーザル提出時の結論
    • Stimulusのコントローラに過度なJSロジックが集中、それならReactが良いよね、となった
  • 書きづらさがない、の正体は
    • React HooksなどReactに関する知識が必要ないが、その分、JSのコードを書く必要があった
  • 一方、Turboはチームで好評
  • 仮説1: HotwireのうまみはTurboなのでは?
    • Turboに寄せて、JSを減らせるのでは
  • Hotwier/Turboは、CRUD操作を中心としたSPAをJSを書かずに実現できる
  • Yasuko Ohvaさんの去年の Kaigi on Railsでの発表が大きなヒントに
  • SPAとHotwireでは、アプリを構成するためのものの考え方が大きく違う
  • 画面の流れに沿って実装するのはだめだった
    • CRUD操作を基点に切り替えるところはTurboを利用できそう
    • 「紙芝居の仕掛け」として捉える
  • PRはマージされた

Hotwire or React?

  • CRUD操作が主体 + 補助的なJSが必要な画面
    • 紙芝居 + ちょっとした仕掛けがある場合、Hotwireで十分対応できる
  • CRUD操作を伴わずリッチなインタラクションが必要な画面
    • Reactの得意分野
  • CRUDを含む高度なSPA実装が必要な画面、CRUDなしでシンプルなSPA実装が必要な画面
    • 意見が分かれる
  • React と Hotwire 、どちらも一長一短
  • Hotwireで実装できそうか?とい視点の場合、実装をCRUDに集約できるかどうかが重要
  • Hotwire的な設計ができれば適用可能な範囲は広がる

このあとは懇親会をたのしみました。
また2日目の聞いた内容もレポートを書きたいと思います。