こんにちは。サーバーサイドエンジニアの三村(@t_mimura39)です。
こちらでご案内した通り、弊社で新しくリリースした「ClinPeer」の裏側をご紹介します。
今回はClinPeerのバックエンドについての簡単なシステム概要と選定技術の紹介編です。
2024-2025年にrails newをした新鮮なRailsプロジェクトの様子をお楽しみください。
目次
- システム概要
- 技術選定
- Ruby
- Ruby on Rails
- Puma
- ActionPack::CloudfrontViewerAddress
- Trilogy
- SolidQueue
- SolidCache
- Kaminari
- Async::HTTP::Faraday
- Ueki
- Jb
- Nokogiri
- Blazer
- ActiveHash
- Flipper
- Lograge
- MaintenanceTasks
- ReciteCSV
- UserAgent
- AnnotateRb
- RBS系各種
- Prosopite
- Traceroute
- RuboCop
- CiLogger
- Committee / Committee::Rails
- RSpec::Snapshot
- SuperDiff
- その他
- おわり
システム概要
細かな選定技術の紹介の前に簡単なシステム概要をご紹介します。
弊社は主にRuby on Railsを採用しており、今回も特別な技術的な要件がなかったためRuby on Railsを採用しました。
また、社内標準的にクラウドはAWS環境が採用されています。
「Amazon CloudFront -> ALB -> AWS Fargate(puma -> Rails)」で構成されるごく一般的なWebシステムです。
rails stats
次にRailsシステムの規模感のご紹介です。
定量的な指標としてrails statsを記載します。
bin/rails stats +----------------------+--------+--------+---------+---------+-----+-------+ | Name | Lines | LOC | Classes | Methods | M/C | LOC/M | +----------------------+--------+--------+---------+---------+-----+-------+ | Controllers | 2982 | 2207 | 106 | 296 | 2 | 5 | | Helpers | 4 | 2 | 0 | 0 | 0 | 0 | | Jobs | 239 | 162 | 16 | 20 | 1 | 6 | | Models | 10463 | 6557 | 230 | 714 | 3 | 7 | | Mailers | 685 | 611 | 9 | 17 | 1 | 33 | | Views | 3904 | 3662 | 0 | 0 | 0 | 0 | | Libraries | 634 | 451 | 5 | 33 | 6 | 11 | | Config specs | 49 | 39 | 0 | 0 | 0 | 0 | | Controller specs | 17 | 13 | 0 | 0 | 0 | 0 | | Form_builder specs | 1059 | 855 | 0 | 24 | 0 | 33 | | Job specs | 577 | 439 | 0 | 0 | 0 | 0 | | Lib specs | 404 | 350 | 0 | 1 | 0 | 348 | | Mailer specs | 608 | 500 | 0 | 1 | 0 | 498 | | Model specs | 15063 | 12527 | 2 | 14 | 7 | 892 | | Request specs | 5319 | 4560 | 0 | 45 | 0 | 99 | | System specs | 3339 | 2588 | 0 | 3 | 0 | 860 | | Task specs | 165 | 132 | 0 | 0 | 0 | 0 | | Validator specs | 278 | 214 | 0 | 0 | 0 | 0 | +----------------------+--------+--------+---------+---------+-----+-------+ | Total | 45789 | 35869 | 368 | 1168 | 3 | 28 | +----------------------+--------+--------+---------+---------+-----+-------+ Code LOC: 13652 Test LOC: 22217 Code to Test Ratio: 1:1.6
statsの結果から分かる通りまだまだ規模が小さいシステムですね。
Modelsが230とありますが、この内ActiveRecordなクラス(= テーブル数)は87個とこれまた両手に収まる程度の規模感です。
RSpecを採用している点以外はRails標準のディレクトリ構成となっています。
Gemfile
「技術選定」と称していますが、特殊な技術を採用しているわけではないため実質利用しているGemのご紹介となります。
熟練のRubyistはGemfileを見るだけでそのRailsシステムの全容が分かると聞いたことがある(要出典)のでそのまま公開します。
gem "rails" # Rack gem "puma" gem "actionpack-cloudfront_viewer_address" # DB gem "trilogy" # Authn / Authz gem "bcrypt" gem "jwt" # Job gem "mission_control-jobs" gem "solid_queue" # Cache gem "solid_cache" # ActiveRecord / ActiveModel Extension gem "kaminari" # Frontend gem "propshaft" gem "vite_rails" # Network gem "async-http-faraday" gem "faraday" gem "ueki" # View Templating Engine / Serializer gem "jb" # SDK gem "aws-sdk-cloudfront" gem "aws-sdk-s3" gem "datadog" gem "googleauth" gem "rollbar" gem "sendgrid-ruby" gem "zendesk_api" # XML gem "nokogiri" # bi gem "blazer" # Other gem "active_hash" gem "bootsnap", require: false gem "flipper" gem "flipper-active_record" gem "flipper-ui" gem "fugit" gem "lograge" gem "maintenance_tasks" gem "recite_csv" gem "repl_type_completor", require: false gem "rqrcode" gem "useragent" gem "validate_url" gem "warning" group :development, :test do gem "annotaterb" gem "debug", require: "debug/prelude" gem "factory_bot_rails" gem "rbs_rails", require: false, github: "pocke/rbs_rails", branch: "master" gem "prosopite" gem "steep", require: false gem "traceroute" end group :ruby_lint do gem "brakeman", require: false gem "erb_lint", require: false gem "rubocop", require: false gem "rubocop-capybara", require: false gem "rubocop-factory_bot", require: false gem "rubocop-performance", require: false gem "rubocop-rails", require: false gem "rubocop-rspec", require: false gem "rubocop-rspec_rails", require: false gem "rubocop-thread_safety", require: false end group :test do gem "capybara" gem "capybara-playwright-driver" gem "ci_logger" gem "committee-rails" gem "database_rewinder" gem "rspec-rails" gem "rspec-snapshot" gem "simplecov", require: false gem "super_diff" gem "test-prof" gem "webmock", require: false end group :development do gem "letter_opener_web" gem "web-console" gem "ruby-lsp", require: false gem "ruby-lsp-rails", require: false gem "ruby-lsp-rspec", require: false end
少しだけ社内用Gemを取り除いていますが、それ以外は実際のGemfileの内容と同等です。
特別な技術は利用しておらず、素直なrailsシステムであることが分かるかと思います。
それではこれらから抜粋して、もう少し深掘って紹介させてください。
技術選定
Ruby
もちろん最新です。
$ cat .ruby-version 3.4.2
ちなみに、最近話題のdevin.aiにv3.4.2にアップデートしてもらいました。
最近のRubyは破壊的なアップデートがほとんどないため、積極的に安心してバージョンアップすることができて素晴らしいですね。
Ruby on Rails
こちらも最新の「v8.0.1」です。
2024年7月に以下のコマンドでrails newを実行しました。この時点でrails v8のリリースが予想できていたため最初からbeta版を採用していました。
rails _7.2.0.beta3_ new ./ --name=clinpeer --database=trilogy --skip-action-mailbox --skip-action-text --skip-active-storage --skip-action-cable --skip-asset-pipeline --skip-javascript --skip-hotwire --skip-jbuilder --skip-test --skip-system-test --devcontainer
ClinPeerでは最新のrailsに追従できるように Rails Edge(mainブランチ)をCIで回しています。 そのおかげでrailsの最新バージョンがリリースされたら数日後にはすぐに更新対応できるような体制が築けています。
Rails EdgeをCIで回す方法はタイミーさんのブログが参考になるかと思います。
Puma
https://github.com/Shopify/pitchfork も気になるところですが、今回は素直にRails標準のPumaを採用しました。
特別な使い方は特にしていないため次にいきましょう。
ActionPack::CloudfrontViewerAddress
自作Gemです。
元々ClinPeerの開発で本対応が必要になったため先行してGemとして公開しました。完全新規のプロジェクト立ち上げはこういった機会にもなるので良いですね。
Gemの詳細はこちらのブログ記事をご参照ください。
Trilogy
ClinPeerはMySQLを採用しました。
「PostgreSQL vs MySQL(vs SQLite)」の話もありそうですが、これだけで重厚なブログネタになりそうなため今回は割愛します。
これまでRailsでMySQLといえば https://github.com/brianmario/mysql2 でしたが、昨今は次世代のMySQLアダプタとして「Trilogy 」が目立っています。
ネイティブライブラリへの依存が少なく、今後のメンテナンス性の観点からもtrilogyが優れていると判断し迷いなくこちらを採用しました。
今のところは不具合・デメリットなど特になく安定して稼働しています。
SolidQueue
こちらは今回の技術選定の目玉ポイントの一つ目です。 ActiveJobのアダプタとしてSolidQueueを採用しました。
弊社では https://github.com/sidekiq/sidekiq の採用実績が多く知見も溜まっている一方で、SolidQueueは国内外問わずまだまだ採用実績が少ないためチーム内から若干の不安の声も挙がっていました。
しかし、Rails v8から標準アダプタになることは分かっており今後のデファクタスタンダードになるだろうという期待も込めて先陣切って採用することにしました。
実際にコードを読んでみると、ActiveRecordを活用したアプリケーションとして実装されているため今となってはSidekiqよりも親近感があるほどです。
SolidQueueを採用するにあたり執筆した解説ブログも公開しているのでこちらも良ければ目を通してください。
SolidQueueを採用する上で問題視される点の一つが「パフォーマンス」ですが、幸い(?)現在のClinPeerでは大量で尚且つ即時性が求められるようなJobがないためこの点は現在問題となっていません。今後運用する中で課題が見えてきたら随時ご報告します。
SolidQueueについて語りたいことはまだまだありますが、長くなってしまうため次の機会でということで。
SolidCache
導入はしているものの実は使っていません(正直)
「Redisを使わない宣言」という意味合いで先行してSolidCacheのセットアップをしましたが、Rails内でのキャッシュを活用する場面に遭遇していない現状です。
パフォーマンスやDB負荷などに課題があり、CloudFrontでのキャッシュで対応しきれないようなケースが発生したら利用する想定です。
導入されていて負債になっているわけでもないためそれまで眠らせてあります。
ちなみに、SolidQueue同様にActiveRecordな実装なため内部処理を読み解くのは容易であり学びもありました。
Kaminari
王道のページネーションGemですね。
https://github.com/ddnexus/pagy と少し悩みましたが、使い勝手の面でKaminariの方が好みでした。
Pagyの方が軽量で高速と評されていますが、正直ページネーション処理のパフォーマンスはリクエスト全体と比較すると軽微なためこの点は重視しませんでした。
Async::HTTP::Faraday
Firebase Cloud Messaging(以後FCM)のAPIを並列で500件ほど呼び出すために利用しています。
FCMはHTTP/2に対応しているため、クライアントもHTTP/2に対応しているものを探しました。
https://honeyryderchuck.gitlab.io/httpx/wiki/Faraday-Adapter や https://github.com/dleavitt/faraday-typhoeus なんかも候補でしたが、今後のメンテナンス性も考慮した結果 https://github.com/socketry/async を採用することとしました。
Ueki
Ruby, Rails, SolidQueue と並ぶようなGemではありませんが紹介です。 HTTP API Clientを自作するためのGemです。
詳細はこちらをご参照ください。
はい、ただの宣伝でした。次いきましょう。
Jb
A simpler and faster Jbuilder alternative.
このコンセプトが大好きなので愛用しています。
が、ClinPeerは諸般の事情によりそこまで活用していません。
jbにデメリットがあるため活用していないわけではなく、APIのレスポンスに特殊な形式を採用しているが故の結果です。
この点については本連載記事の中で後日ご紹介します。
Nokogiri
王道中の王道ですね。
XMLファイルをパースする処理を実装する必要がありそこで利用しています。
弊社社員がメンテナでもある https://github.com/ruby/rexml を利用しても良かったのですが、Railsの依存で結局Nokogiriはインストールされるためパフォーマンス面で有意なNokogiriを利用することとしました。
Blazer
正式にサービスがリリースされるまでの簡易的なBIツールとして利用していました。
弊社ではBIツールとして Redash がよく使われていましたが、それと比べてRailsエンジンとしてお手軽に導入できる点が魅力的でした。
ActiveHash
区分値管理Gemとして古くから使われてきたGemですね。
これに代替するようなGemを知らないため採用しましたが、もし他におすすめのGemがあれば教えてください。
Flipper
王道なフィーチャーフラグGemです。
Flipperを採用している方は多くいると思いますが、ClinPeerでは少し使い方を工夫しています。
こちらに関しても別途本連載記事にてご紹介予定ですのでお楽しみに。
Lograge
こちらも古くから使われているGemですね。Railsのリクエストログを構造化するものです。
最近だと https://github.com/reidmorrison/semantic_logger なんかも注目を集めていますが、Logrageが手に馴染んでいるためこちらを採用しました。
Rails本体で構造化ログに関して動きがありそう(最近は停滞していそう)なので楽しみにしています。
ログの構造化に関しても一工夫入れているためまとめ記事を執筆予定です。
MaintenanceTasks
今回の技術選定での注目ポイント二つ目です。
開発初期は特にデータマイグレーション作業など必要になる場面が多く、ついrails consoleでデータ更新をしたくなるものです。
MaintenanceTasksはrails consoleの甘い誘惑からの脱却を手助けしてくれるGemです。
じわじわと知名度が上がってきていて、最近ではohbaryeさんのプレゼンで知った方も少なくないのではないでしょうか。
MaintenanceTasksが何を解決してくれるのか、などを歴史的経緯も含め丁寧に解説されているためぜひご覧ください。
さりげなく私も「MaintenanceTask利用者」として紹介していただきました。めっちゃMaintenanceTask推している人みたいに見える。
ReciteCSV
私のお気に入りGemの一つなのでご紹介します。
CSVを読み取る処理を実装することはありますか?ありますよね。
標準のCSVライブラリは row[0]
や row["col"]
のように使う必要があり可読性の点で問題が生じることが多々あります(個人の感想です)。
そういった時に便利なGemでして、CSVの行を1行1行 POROなオブジェクトとして row.col
のように取り扱えるようにしてくれるGemです。
私の恩師が作者なこともあり手によく馴染んでいるという理由もありますが、これ以上にCSV読み取り処理を綺麗に実装するのを助けれくれるGemはないのではないかと私は思っています。
UserAgent
UserAgentをパースするGemってたくさんあって迷いますよね。
正直なところそこまでリッチな機能は求めていなかったためどのGemでも良いかなと考えていました。
が、そういえば最近ActionPackの依存にuseragent Gemが追加されていることを思い出したため素直にこれを利用することとしました。
AnnotateRb
長年 https://github.com/ctran/annotate_models を愛用していましたが、ここ最近メンテナンスが停滞しつつあるためアクティブにメンテされているforkされた annotaterb を採用しました。
基本的な機能は annotate_models と同等ですが、table commentなど若干の追加機能が実装されていて嬉しいですね。
RBS系各種
まだ私は頑張っていません
Prosopite
ActiveRecordのN+1検知Gemです。
N+1検知といえば https://github.com/flyerhzm/bullet が有名ですが、Bulletは誤検知(偽陽性・偽陰性)や最新バージョンRailsへの追従の遅さが課題となることが多々ありました。
BulletはActiveRecordにモンキーパッチを当ててN+1を検知するアプローチなのに対して、ProsopiteはActiveRecordが発行したSQLを監視するというアプローチを採用しています。
ActiveSupport::Notifications.subscribe 'sql.active_record'
を活用したシンプルな作りなので、Railsのバージョンアップの影響を受けにくい利点があります。
zero false positives / false negatives.
をアピールしていますが、判定がシンプルなためどうしても実装次第では誤検知は生じてしまっています。
誤検知といっても発行されるSQLを確認すると「まぁ仕方ないよね」と素直に受け入れることができるため、Bulletの誤検知の理不尽さ?はないように感じています。(Bulletは賢い判定な分誤検知発生時に原因の特定が難しいのですよね)
また、他にもRails標準のStrictLoadingに関しても採用を検討しましたがガチガチに利用しようとすると開発体験の悪化につながりそうと判断し今回は採用を見送りました。
Traceroute
「ルーティング定義されているがアクションメソッドが定義されていない」「アクションメソッドが定義されているがルーティングが定義されていない」の二つを検知するGemです。古くからあるGemなので知っている方も多いのではないでしょうか。
Rails本家に rails routes --unused
という機能が実装されていますが、Tracerouteを完全に置き換えられるものではないため継続利用しています。
RuboCop
昨今の大抵のRailsシステムにはRuboCopが導入されているかと思うので紹介は割愛します。
最近のRailsでは https://github.com/rails/rubocop-rails-omakase が標準となっていますが、肌に合わなかったため採用しませんでした。
Copは最初からガチガチに固めているわけではなく、Metrics系のCopを一律無効化したりとチームメンバーの特性に応じて使い分けています。 深く可読性について考えないままに「Metrics/AbcSizeに指摘されたのでとりあえずメソッド分割しました」のような対応をするストレスフルな開発体験から脱却できていて幸せです。
CiLogger
弊社技術顧問willnetさん作のGemです。
CI環境化でのRSpec実行において、失敗したテストに関するログのみを出力するように制限してくれます。Flaky Test発生時の原因特定などが捗ったりログ出力量が抑えられるため若干のコスト削減に繋がったりします。
導入デメリット特にないのでとにかく入れておいて良いのではないでしょうか。
Committee / Committee::Rails
APIのI/F定義をOpenAPIなYAMLファイルで管理しています。
I/F定義通りにAPIが実装されていることを担保するために、RequestSpecでCommitteeを活用し「テスト時のリクエスト内容」と「レスポンス」の妥当性を検証しています。
RSpec::Snapshot
RSpecのテスト期待値をスナップショットファイルという形で管理してくれるGemです。
JavaScript のテストでJestなんかを利用する方はイメージしやすいのではないでしょうか。
ClinPeerでは主にメールの本文の期待値管理として利用しています。
テキストメールもHTMLメールも真面目に期待値をテストしようとすると管理が煩雑になるためJestっぽいスナップショット管理の仕組みが欲しくなり、少し探してみたら本Gemを見つけました。
メール本文以外にも少し期待値が巨大なテキストファイルになるようなケースでも活用しています。
SuperDiff
RSpecでのdiffをリッチな表示にし差分を分かりやすくしてくれるGemです。
一時期作者のメンテ停止の流れがありましたが、別の方がメンテを引き継ぐことになりました。
その他
認証系Gem
https://github.com/heartcombo/devise や https://github.com/Sorcery/sorcery など著名なGemも検討しましたが、重厚なGemは将来の負債になると考え Rails公式提供のAuthentication Generator をベースに必要最低限の機能を自前実装する方針としました。
実は開発初期の段階では「とりあえずdeviseを入れておこう」くらいの軽い気持ちで技術選定していました。しかし、deviseを使っている内に「deviseの嬉しみ」より「deviseの辛み(というより実装不透明さ)」を感じるようになり、早い段階で脱deviseな対応を入れました。
Ridgepole
開発初期はDBスキーマの更新が活発なためRidgepoleでの気軽なスキーマ更新が魅力的だったため活用していました。
Ridgepoleは発行されるALTERが若干暗黙的になるためサービス実運用が開始する際には一工夫(--dry-runで発行されるALTERを事前確認するなど)が必要となります。サービス実運用が開始するとスキーマ更新の頻度は落ち着いてきたため現在はRails標準のマイグレーションを利用しています。
設定値管理(Rails.application.config_for)
設定値管理系のGemもたくさんありますよね。
- https://github.com/binarylogic/settingslogic
- https://github.com/palkan/anyway_config
- https://github.com/rubyconfig/config
ClinPeerではいずれも利用せずに、Rails標準のconfig_for を利用しています。
少し工夫した使い方をしているためこちらに関しても別途ブログ執筆予定です。
ファイル管理(ActiveStorage)
https://github.com/shrinerb/shrine も選択肢にありましたが、Shrineを採用する理由も特になかったため「Rails標準」という理由(使い勝手なども考慮しつつ)からActiveStorageを採用しました。
ActiveStorageに関してもトレーサビリティを向上させるような工夫を入れたりしているため、機会があればご紹介したいです。
HTMLテンプレートエンジン(ERB)
Slim や Haml なんかも候補にありましたが、シンプルさを重視して素直なERBを採用しました。
個人的には https://github.com/amatsuda/string_template なんかも気にはなっていましたが、ERBに慣れているメンバーも多かったためこれに関しては討議するまでもなくあっさりとERBに決まりました。
ヘルスチェック(Rails標準Health)
弊社内では https://github.com/jphenow/okcomputer を利用しているプロジェクトが多かったのですが、 (Rails標準のHealth)https://github.com/rails/rails/pull/46936 で必要十分そうだったためGemの採用は見送りました。
おわり
ClinPeerのRailsプロジェクトの概要と選定技術について、Gemを中心に紹介してみました。
新しいものも積極的に採用しながら、古き良きものも大事にするという精神で技術選定をしました。一つ二つ発見があれば幸いです。
次の連載記事ではもう少し深いお話を紹介する予定ですので、どうぞお楽しみに!
是非読者になってください!
メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!
■募集ポジションはこちら
■エンジニア紹介ページはこちら
■メドピア公式YouTube
■メドピア公式note