Slide 1

Slide 1 text

© SmartHR, Inc. rack-attack gemによる リクエスト制限の失敗と学び @pndcat 2025/12/06 北陸Ruby会議01

Slide 2

Slide 2 text

自己紹介 ● なっちゃん / pndcat ● 本業:SmartHRで労務の基本機能を開発 ● 社外活動:カンファレンスの運営 ○ RubyKaigi ○ 関ケ原Ruby会議01 ○ 東京Ruby会議12 2

Slide 3

Slide 3 text

テーマ「みんなの Ruby の使い方」 ● 普段、身近に使っている rack-attack gemの 話をします ● Rubyでリクエスト制限に興味がある人に届い てほしい!! 3

Slide 4

Slide 4 text

今日扱う「リクエスト制限」とは ● 対象 ○ Rails / Rackミドルウェアでやる範囲 ○ APIの安定運用を目的としたリクエスト制限 ■ ユーザーに正しくAPIを利用してもらうための制限 ● 対象外 ○ インフラレイヤのリクエスト制限 ○ DoS攻撃を防ぐためのセキュリティ対策 4

Slide 5

Slide 5 text

SmartHRと リクエスト制限 5

Slide 6

Slide 6 text

Rails / Rack のリクエスト制限の代表 ● ActionController::RateLimiting ● rack-attack 6

Slide 7

Slide 7 text

ActionController::RateLimiting ● Rails 8からリクエスト制限が標準機能に ● シンプルに実装できる ○ サッとリクエスト制限を実装したい方におすすめ 7 class SessionsController < ApplicationController rate_limit to: 100, within: 1.minute, by: -> { request.domain }, only: create end

Slide 8

Slide 8 text

rack-attack ● Rackでリクエスト制限をする定番gem ● 柔軟なリクエスト制限を実現できる ○ throttle (レート制限) も柔軟に定義できる ○ 怪しい挙動のときだけBANする ○ 制限に引っかかったときの情報をレスポンスヘッ ダーで返せる 8

Slide 9

Slide 9 text

SmartHRのリクエスト制限のルール ● トークンごとのリクエスト制限 ○ 1時間で5000回までリクエストできる ● サブドメインごとのリクエスト制限 ○ 1分間で50000回までリクエストできる 9

Slide 10

Slide 10 text

10 class Rack::Attack class ::Rack::Attack::Request # Authorization ヘッダーからトークン文字列を取得 def token_string get_header("HTTP_AUTHORIZATION") end end throttle("token", limit: 5000, period: 1.hour) do |req| req.token_string end throttle("subdomain", limit: 50000, period: 1.minute) do |req| req.host end end SmartHRで動いているコードを発表用に加工しています config/initializers/rack_attack.rb

Slide 11

Slide 11 text

リクエスト制限の情報をヘッダーに返す ● ユーザーがAPIを使いやすいようにリクエスト 制限の情報をヘッダーに返している 11 x-rate-limit-limit 期間内でリクエストできる最大回数 x-rate-limit-reset 制限がリセットされるまでの秒数 x-rate-limit-remaining リクエストできる残り回数

Slide 12

Slide 12 text

レスポンスヘッダ(抜粋) x-rate-limit-limit: 5000 x-rate-limit-reset: 433 x-rate-limit-remaining: 0 x-intensive-rate-limit-limit: 50000 x-intensive-rate-limit-reset: 13 x-intensive-rate-limit-remaining: 44999 12 トークンに関する情報 サブドメインに 関する情報

Slide 13

Slide 13 text

13 self.throttled_responder = ->(request) { throttle_data = request.env["rack.attack.throttle_data"] if throttle_data.key?("token") counts = throttle_data["token"] reset = counts[:period] - (Time.now.to_i % counts[:period]) remaining = (counts[:limit] - counts[:count]).clamp(0..) header["X-Rate-Limit-Limit"] = counts[:limit].to_s header["X-Rate-Limit-Reset"] = reset.to_s header["X-Rate-Limit-Remaining"] = remaining.to_s end if throttle_data.key?("subdomain") # 略 end [429, header, ["Throttled\n"]] } 残り回数などを計算 headerに詰める 制限を超過したらthrottled_responderが 実行されるので、headerに情報を詰める

Slide 14

Slide 14 text

ある日、大量の リクエストが飛んで きた 14

Slide 15

Slide 15 text

● 想定を上回るリクエストがあり、障害が発生 ● 実装やインフラ構成も修正をしたが、リクエ スト制限も見直すことになった 15 障害が発生した

Slide 16

Slide 16 text

新しいリクエスト制限 ● トークンごとのリクエスト制限 ○ 1時間で5000回までリクエストできる ○ 1秒間で10回までリクエストできる ● サブドメインごとのリクエスト制限 ○ 1分間で50000回までリクエストできる 16

Slide 17

Slide 17 text

17 throttle("token", limit: 5000, period: 1.hour) do |req| req.token_string end throttle("token_second", limit: 10, period: 1.second) do |req| req.token_string end throttle("subdomain", limit: 50000, period: 1.minute) do |req| req.host end 追加 throttleの定義を追加

Slide 18

Slide 18 text

18 self.throttled_responder = ->(request) { throttle_data = request.env["rack.attack.throttle_data"] if throttle_data.key?("token") # 略 end if throttle_data.key?("token_second") # 略 end if throttle_data.key?("subdomain") # 略 end [429, header, ["Throttled\n"]] } 追加 headerに情報を詰める

Slide 19

Slide 19 text

リクエスト制限の追加をサッと対応 ● 1秒間で10回までのリクエスト制限ができた! 19

Slide 20

Slide 20 text

「レスポンスヘッダの 情報が足りない」と お問い合わせが入る… 20

Slide 21

Slide 21 text

throttleの順番が良くなかった ● 途中で制限を超えると、それ以降の throttle は評価されない ● 後続の throttle_data が存在しないので、 ヘッダー情報が欠けた 21

Slide 22

Slide 22 text

修正1. throttleの順番を変更 ● 制限が引っかかりづらい順番で定義した ○ サブドメインごとのリクエスト制限 ■ 1分間で50000回までリクエストできる ○ トークンごとのリクエスト制限 ■ 1時間で5000回までリクエストできる ■ 1秒間で10回までリクエストできる 22

Slide 23

Slide 23 text

修正2. テストを書く ● gemのコアな機能を使っているだけなので、テ ストは書いていなかった ● 複数の異なる条件のthrottleを定義して、複雑 な状態になっていた ○ 最初からテストを書くべきだった 23

Slide 24

Slide 24 text

学び:複雑なリクエスト制限はやめよう ● 複数のリクエスト制限を組み合わせると、運用・ テストの複雑さが増す ● シンプルなリクエスト制限にしよう ○ トークンごと、サブドメインごとはややこしい 24

Slide 25

Slide 25 text

まとめ ● rack-attack gemは、柔軟なリクエスト制限 を実現できる ● throttleを複数書くときは、緩い定義から書く ● 複雑なリクエスト制限にはせず、シンプルなリク エスト制限からはじめよう 25

Slide 26

Slide 26 text

LTで話せなかった補足 ● リクエスト制限の情報をヘッダーに返す話 ○ throttled_responderは制限を超過したときのみ 実行される ○ 通常時にもヘッダー情報を返したいなら、コントロー ラ側でもヘッダーを足す必要がある 26