DIGGLE開発者ブログ

予実管理クラウドDIGGLEのプロダクトチームが、技術や製品開発について発信します

RubyKaigi2024 に参加しました

DIGGLEは、2023年に続いて2024年も継続してRubyKaigiスポンサーを努めました。5/15〜5/18に沖縄で開催されたRubyKaigi2024にnikuが参加したので、その際の感想を公開します。

RubyKaigiの発表は「Ruby自体をよくする、Rubyの適応領域を既存から押し広げてゆく」という観点で捉えると私にはすっきりとまとめられるものが多いように感じました。全部面白かったのでどの感想コメントにも「面白かったです」というのがついていると思ってください。 (参加していなかった時間帯もあります)

1日目(5/15)

Writing Weird Code - RubyKaigi 2024

Weirdなコードを書くのは当人は楽しくやっていると思いますが、Rubyのコードってどこまで無茶なことできるの、変わったことできるのという探求は、Rubyのコードの(読み手からみた)整合性や一貫性に大きく影響すると感じました。Weirdなコードを書くときに、普通に書いてからWeirdな形に変えていくの、知らなかったなあ。言われてみれば、なるほどという感じですね。

Unlocking Potential of Property Based Testing with Ractor - RubyKaigi 2024

このトークはタイトルからはRubyを使ったライブラリを作った話という捉え方にもなりそうですが、IOではなくCPUがボトルネックになりやすいPropertyBasedTesting(PBT)をRactorを使うとどうなるか、現在のRactorの現実的なユースケースでの性能評価をするというのが目的に含まれていて、単なるライブラリを作った話ではありませんでした。着想が素敵ですね。もう一点、多くの言語ではExampleBasedTestingが最初にあり、PBTやFuzzingはここから認知されていくという感じだと思います。(Go言語はFuzzingを標準ライブラリとして組み込んでいますね)。PBTは考え方に慣れればそれほど大変ではなく書け、役に立ち、かつメンテナンスコストも現実的かなと思うので、Rubyでもこれから使われていくとどうなるのかなという点で私も興味がありますし、こういった品質担保のやり方が普通なコミュニティとなることでRubyの適用領域がさらに広がっていくといいですね。

Strings! Interpolation, Optimisation & Bugs - RubyKaigi 2024

"#{foo}#{bar}"のような文字列補完を実行するときの処理を8行消すことで、場合によっては2倍速くなる変更を加えたお話でした。Stringは文字列のサイズ(長さ)によって収める領域が異なります。過去にコミットされていた文字列補完最適化の処理が誤っていたので、収める領域が変わるときは遅くなっていた。またその文字列補完最適化は過去は速かったが現在は意義の薄いものだった。といったように複数にこみいった事情をかかえた状況で、最終的には単にその最適化ルートを取り去ることで速くなったそうです。結果は簡単ですが、こういった状況の見つけかた、調査、対処のアプローチといったプロセスが丁寧に解説されていて門外漢にもわかりやすかったです。ObjectSpaceってこうやって使うんだあ。

irb
irb(main):001> require 'objspace'
=> true
irb(main):002> my_str = 'hello'
=> "hello"
irb(main):003> puts ObjectSpace.dump(my_str)
{"address":"0x12035a1f8", "type":"STRING", "shape_id":0, "slot_size":40, "class":"0x10296ec60", "embedded":true, "bytesize":5, "value":"hello", "encoding":"UTF-8", "coderange":"7bit", "memsize":40, "flags":{"wb_protected":true, "old":true, "uncollectible":true, "marked":true}}
=> nil

The depths of profiling Ruby - RubyKaigi 2024

新しくpf2というプロファイラを作った理由と、どのように取り組んだかというお話でした。素朴なプロファイラでは得られない、Rubyの複数Thread動作の記録、C言語のスタックトレースとRuby言語のスタックトレースを並べて記録できるなどが特徴です。プロファイラは観察対象より下の層の情報も集める必要があるということをおっしゃっていて、なるほど確かにそうだが、作るのも大変だし、作ったあと観察対象の(内部構造が変わる)Ruby最新版に追従し続けるのも大変そうだなという気持ちになりました。使うときは今以上に感謝の気持ちをもって利用します。

Vernier: A next generation profiler for CRuby - RubyKaigi 2024

こちらもプロファイラのお話でした。機能としてはpf2とも重なっている部分が多く、Rubyの複数Thread動作の記録やGCでの時間停止、待ち受け時間(idletime)などを記録できます。時間帯が連続しているのはおもしろいですね。どちらの実装でも情報を集める役の(プロファイラ専用の)スレッドはGVLを避けるためC言語側で作っている、結果をビジュアルで見るにはFirefoxProfilerを使っているなど、複数のプロファイラ作りの視点から得られる、いろんな人が試行錯誤した結果今はこれが王道なのかなというものの見方が得られたのがおもしろかったです。今回のRubyKaigiではあともう一つRubyのプロファイラのお話がありました。三人が並んで親交を深めている写真が素敵でした。

Ractor Enhancements, 2024 - RubyKaigi 2024

Rubyの並列処理をいい感じにするRactorの現時点での状況と、今後のお話でした。Ractorの制限によりRubyコードで使うrequireやtimeoutに対応できないところを解決する試み、GCに時間がかかるのを解決する、メモリ管理の改善を挙げられていました。Ractorをよくするのと既存コードをRactor向けにしていくのを進めていくと、どこかのタイミングで違和感少なく特別な心構えなしに使えるようになると嬉しいですね。引き続き応援しています。RubyKaigiで話題になっていたネームスペースがうまくRubyに入ったらRactorはそれぞれ独立したネームスペースで動きますという形にするとRactor同士のメッセージングは難しくなりますがrequireの問題やsharingobjectの問題などの多くが解決しないかなあと妄想しました。

2日目(5/16)

Finding Memory Leaks in the Ruby Ecosystem - RubyKaigi 2024

Rubyのメモリーリークを発見するためのアプローチと、その結果たくさんのメモリーリークを発見して実際に改善できたというお話でした。一般にメモリーリークの発見に使われるValgrindというツールがあるのですが、Rubyに対して適用すると(機械的に見ると)メモリーリークの可能性があるけれど実際には問題ない処理が多量に列挙されてしまうためあまり役立たなかったそうです。そこでRuby3.3からRUBY_FREE_AT_EXITという環境変数を有効化するとValgrindから見てもメモリーリークに見えなくなる処理を足したそうです。そうするとValgrindで列挙されるメモリーリークの可能性は、本当にメモリーリークの可能性のあるものだけに絞られます。実際にこれを使ってたくさんのメモリーリークの発見に役立っているそうです。発想がすばらしいですね!

Optimizing Ruby: Building an Always-On Production Profiler - RubyKaigi 2024

常に本番環境でプロファイラをオンにしておく話です。プロファイラはプログラムの内部状況を取得するオーバーヘッドがあるため、プロファイラを使っているときはプロファイラを使っていないときに比べて処理能力が落ちてしまいます。ですから本番環境で常にプロファイラをオンにしておくというのは難しく、そんな夢のような話があるんだろうかと発表を聞く前は思っていました。発表者はDatadogにお勤めの方で、Datadogへデータを送るのに有用なddtraceという本番環境で常にプロファイラをオンにしても大きく支障ないgemを紹介してくれました。ちなみに本番環境でプロファイラを常にオンにするための秘訣は、データの一部のみを抜きだす(けれど全体像が掴めるようにする)サンプリングでした。ddtraceを使っていればDatadogにデータを送らずともローカルでも使えることをスライドの紹介で知りました。手元で様子をみるのに便利ですね。 https://docs.google.com/presentation/d/1hKiEQvqzuzkXDxhDiH36nc2Eew80_QYBSLp2vaXWurU/edit#slide=id.g1f7f8fc8955_0_313

class ExportToFile
  def export(flush) = !!File.write("hello-profiler.pprof.lz4", flush.pprof_data)
end
Datadog.configure { |c| c.profiling.exporter.transport = ExportToFile.new }
$ bundle add ddtrace && bundle install
$ DD_PROFILING_ENABLED=true bundle exec ddprofrb exec ruby hello-profiler.rb
$ lz4 -d hello-profiler.pprof.lz4
$ go tool pprof -http :8987 hello-profiler.pprof

Unlock The Universal Parsers: A New PicoRuby Compiler - RubyKaigi 2024

PicoRubyという、mruby互換の軽量なRuby実装があります。そちらをコンパイルするにあたって従来のパーサー、Lramaから生成されたパーサー、Prismの3つの候補が選択肢にある中、どれが向いているのかを比較していました。Lrama由来の場合はコンパイル時のメモリ消費やコンパイル結果のファイルサイズが他のものに比べて大きすぎたのですが、CRubyでは使うけれどPicoRubyでは使わない部分が取り外せず含まれているためで、それを外せるようにしてPicoRubyでも実用に近づけたよという話でした。馴染みのない分野だったのですが説明や図がわかりやすく、状況と問題がつかめたのでどうやって解決しているのかなというのをワクワクしながら聞けました。ちなみにPrismのほうは既にかなり完成されていてPicoRuby実用においても問題は少ないようでした。私の誤解があるかもしれませんが、そこからパーサーという領域では重なっているLramaとPrismのRubyKaigi2024時点での成熟度というのを感じとりました。あくまでRubyKaigi2024時点の話で、今後についてはわかっていません。引き続き楽しみですね。

Squeezing Unicode Names into Ruby Regular Expressions - RubyKaigi 2024

Unicodeにはcharacterpropertiesという、文字の種別が定義されています。たとえば「ひらがな」という種別は\p{Hiragana}で表わすことができます。次のように、既存のRubyでも正規表現で多くのものが使えるようになっています。

#2文字目に「に」が含まれている
'Юに코δ'=~/\p{Hiragana}/
=>1

こういったpropertyは既に大量にあり、またユニコードのバージョンが上がるごとに増え続けています。これをRubyの正規表現の中で扱うには、propertyの定義をRubyプログラムの中にデータ構造として持たなければいけません。単純に保持するのでは、データ量が多くなったり、該当の名前を検索する速度が遅くなります。うまく収めるためにRadixtreeという方式をとったというお話でした。大きいデータを、データの特性に着目してコンパクトにかつ使いやすいように収納していくさまを目にする知的興奮がありました。格好よかった。

3日目(5/17)

Turning CDN edge into a Rack web server with ruby.wasm - RubyKaigi 2024

RubyのWebフレームワークであるところのSinatraで作られたアプリケーションは通常PumaやApacheやNginxといったサーバーの上で動きます。アプリケーションとサーバーの間にはRackと呼ばれるミドルウェアがいて、サーバーの種類毎の違いを吸収した統一されたインターフェースを提供してくれています。そのおかげで、アプリケーションに変更を加えずにサーバーの種類を入れ替えることも可能になっています。今回はCDNEdgeでwasmが動くことと、Rubyがwasm化したことの2つを利用して、RackというミドルウェアがCDNEdgeをサーバーとして扱えるようにしてみよう。それが可能だとすると、アプリケーションの変更を行わなくてもCDNEdgeでコンテンツが配信できるという挑戦のお話でした。説明も平易で伝わりやすく、取り組みは簡単ではなかったのだなとわかりました。実際にCDNEdge上でSinatraアプリケーションが動いているのを見せるデモは説得力がありました。

Speeding up Instance Variables with Red-Black Trees - RubyKaigi 2024

Ruby3.2からはObjectShapeというパフォーマンス最適化方式が導入されています。これによりキャッシュしたインスタンス変数を速く読み取れるようになりました。一方でキャッシュしていないインスタンス変数へのアクセス速度に対しては素朴な実装のままでした。Ruby3.3ではキャッシュしていないインスタンス変数へのアクセスを速くするためにデータ構造として赤黒木を導入したそうです。実際にキャッシュミスのときの速度がRuby3.2より上がっていました。すごい。OkasakiStyleという論文の形式で赤黒木を実装したそうで、その論文のなかに「有能なプログラマなら15分未満で実装できる」と書かれていて実装のときに緊張したというくだりが好きです。

ERB, ancient and future - RubyKaigi 2024

RubyのテンプレートエンジンERBの歴史と、25年経った今そのときの判断が概ね正しかったというふりかえり、2012年には試したけれどできなかったRails互換のERBが最近思いついてできるようになりそうだという話でした。25年何かについて考えたり、10年くらい前にできなかったことを思い出して改めて挑戦してみるというのはその年数のあいだ生きていないといけないわけで、素敵だし、すごいことだなと感じました。歴史がある。

The state of Ruby dev tooling - RubyKaigi 2024

Rubyを使った開発で利用するツールの状況や提言のお話でした。Rubyの開発ツールは、選択肢が多く、多様性が高いとは言えるものの、初心者にはどのツールが適していてどれを入れると始めやすいか悩む原因ともなっているということをRustとの比較で示してくれました。発表者はこの状況をfragmentation(断片化)という表現をつかって説明していました。最近だとLanguageServerが開発ツールのハブになりつつあるので、ここをきっかけに状況を改善できるのではないかとお話してくれました。私も統合されたツールを持つ言語として挙げられていたGoやRustでの開発経験はよかったなと感じているので、一定うなずけるところがありました。ただ、めいめいが頑張って作りメンテしていたものを絞りこんで初期設定おすすめを選んでいくのは思い入れや歴史があり困難な道程になりそうだとも感じています。そうなれば、特に開発を始める人にとって便利なのは間違いないと思うんですけれどもね。うまくいくよう応援したいです。

Matz Keynote - RubyKaigi 2024

Matzのキーノートが最終日にあるのは珍しいですね。Ruby4についての言及もあり刺激的な内容でした。パフォーマンスがよくなって文句を言う人はだれもいない、そのとおりだなと思いました。DIGGLEプロダクトもパフォーマンスを上げ続けていますが、まだまだ速くできるところがあるので、新しい機能の導入とパフォーマンス両方をおいかけていきたいです。

DIGGLEは、RubyKaigi 2024にシルバースポンサーとして協賛します!

※ 上記画像中央のロゴはさわらつきさんの著作物です
※ Rubyのロゴはまつもとゆきひろ氏の著作物です

DIGGLEのCTO水上です。

DIGGLE社は前年に引き続き、RubyKaigi 2024のシルバースポンサーとして協賛いたします。微力ながらRubyコミュニティに貢献ができればと思っております!

rubykaigi.org

DIGGLEとRuby

当社サービスは、2016年のローンチ以来、RubyとRuby on Railsをバックエンドの言語、Webフレームワークとして採用しております。私たちが事業として成長を続けられているのは、RubyとRubyコミュニティのおかげです。

特に、サービス初期の立ち上げと、頻繁かつ大胆な変更の繰り返しを乗り越えた上で、大きな負債なく継続できていることは、Rubyという言語の持つ柔軟性と設計思想、充実した周辺ライブラリがあってのことだと考えています。

DIGGLEで活躍するgemの一部

gem 'active_hash'
gem 'activerecord-import'
gem 'acts_as_list'
gem 'clockwork'
gem 'devise'
gem 'factory_bot_rails'
gem 'haml'
gem 'kaminari'
gem 'mini_racer'
gem 'mustache'
gem 'oj'
gem 'paranoia'
gem 'pundit'
gem 'rails'
gem 'ransack'
gem 'ros-apartment'
gem 'rubocop'
gem 'sidekiq'

今こそRuby / Railsを学ぶべき理由

もっとRuby界隈を盛り上げていきたいので、これを機に、特に初学者であったり、サービスを立ち上げる検討をしている方向けに、私の個人的見解でRubyやRailsを選ぶべき理由をつらつらと書いてみます。よければ選定の参考にしてください。

1. MVC Architecture

Ruby on Railsは、いわゆるMVCアーキテクチャです。

MVCは銀の弾丸ではありません。MVCを使っていると、だいたい私の体感的に、8割は非常に簡単に作れて、残り2割はレールから外れる必要があります。私たちが作るサービスの機能は、複雑度の観点でパレートの法則のような形をしているのだと考えられます。大事なことは、8割でしっかり楽して、2割でしっかり考えぬくことです。初めてサービスを作る方にとって、MVCはきっと丁度よいフレームワークです。

2. HTMLを基本として、適応的にJavaScriptを使える

Ruby on Railsは、シンプルなHTMLベースのWebサーバが基本です。

きっと多くの方が、イケてるUIを目指したいので、フロントをリッチにしたいと思うでしょう。しかし、良いフロントエンドを作るには、JavaScriptの前に、HTMLとCSSについてよく知ることが肝要です。特に、CSSはどんどん進化し多くのことはCSSでカバーできるようになっています。JavaScriptより描画パフォーマンスが良い場合がほとんどです。

Railsは、フロントエンドの節度を守るレールを用意しつつも、時々JavaScriptを使いたくなりそうな課題に対して、いくつか丁度よい道具を提供しています。最近では、それらがすべてHotwireという形で集約されました。もちろん、必要に応じて、Reactなども導入できます。

これらをRailsのレールの上で習得するメリットは、うまく手を抜く技術があがることです。スピード感のあるデリバリーが求められる現代において、頑張る必要がない所で手を抜くのは悪いことではなく、むしろビジネスとして適切な選択ができる能力として重宝されます。

3. 圧倒的なコミュニティとその「近さ」

Rubyという国産言語で、ここまで世界的に使われており、毎年カンファレンスで盛り上がり、多くの人に愛され続け、メンテナンスされている言語はRubyしかないでしょう。

RubyKaigiという一大イベントですぐに最高のハッカー達と、日本語で(あるいは英語で)会話だってできます。エンジニアとしての最高の環境がとても近くにあります。

diggle.engineer

あとがき

DIGGLEとしては、Ruby界隈に引き続き貢献していき、Rubyとともに成長していければと感じております!一企業として一つの言語に向き合いつづけることこそが、Rubyと自社の中長期的な成長に繋がると考えています。

RubyKaigi当日は弊社メンバーも参加しますので、ぜひお声がけください!

水上

herp.careers

DIGGLE React SPA開発におけるアーキテクチャの勘どころ

はじめに

初めまして。 2024年2月にDIGGLEにエンジニアとして入社したfujitaです。

私はこれまで、バックエンドの開発が中心で、フロントエンドはVanilla JS、JQueryでの簡単なDOM操作やAjax通信の実装を行った程度の経験しかありませんでした。DIGGLEでは基本的にエンジニアはバックエンド・フロントエンドの両方の開発に携わるため、フロントエンドの開発に取り組むにあたって、いくつかの困難に直面しました。具体的には、DIGGLEではReactを用いたシングルページアプリケーション(以下、SPA)として構築されており、これまで経験したことがないほどにフロントエンドのコードベースの規模が大きく複雑なため、以下のような難しさがありました。

  • 機能を追加・改修するために、どのファイルのどの部分を修正すべきかがわからない
  • コードを修正した際に、その影響範囲を把握することが難しい

しかし、フロントエンドにおけるアプリケーションアーキテクチャ(以下、アーキテクチャ)、簡単にいえば各ディレクトリ、ファイル、コンポーネントの役割・責務を理解していくにしたがって、これらの困難は徐々に解消され、開発スピードも向上してきました。

本記事では、私がReactを用いた大規模なSPA開発に初めて取り組む中で得た知見や、DIGGLEのフロントエンドアーキテクチャがどのように設計されているかについて共有したいと思います。

Reactを用いたSPAのアーキテクチャ

SPAのアーキテクチャ

基本的なSPAのアーキテクチャとしては、バックエンドでも用いられるMVCや、それが発展してできたMVVMがあり、これらは総称してMVW(Model-View-Whatever)と呼ばれます。これらはいずれもアプリケーションのデータやロジックをもつModelと、UIでありModelの視覚的表現であるViewを要素としてもちます。そして、第3の要素(C: ControllerやVM: ViewModel)によってViewとModelを適切に分離しつつ連携できるようにします。この分離によって、コードの可読性、保守性、テスト容易性などが向上します。

React

ReactはUI構築のためのJavaScriptライブラリです。大雑把にいうとReactコンポーネントはMVW のVを担当し、アーキテクチャとは直接関係しません。宣言的にコンポーネントを作成することができ、状態の変化に応じたUIの更新はReactが裏側でやってくれますが、どのようにアプリケーションのデータを取得・管理し、Reactコンポーネントへ渡すかなどは別に考えなければなりません。(公式では、Next.js や Remix などのフレームワークの使用が推奨されています)

さらに、先ほどReactはMVWのViewに相当すると述べましたが、Reactコンポーネントは自身の状態を管理・更新したり、副作用を扱ったりでき、View以上のものといえます。そのため、一般にReactではMVWとは異なるアーキテクチャが採用されます。代表的なアーキテクチャとしてFluxなどがありますが、開発チームの事情に応じて様々な選択肢があるようです。次項で、DIGGLEのアーキテクチャの一部をご紹介します。

DIGGLEのフロントエンドアーキテクチャ

DIGGLEでは、UIとロジックを分離するために Container/Presentationalパターンが、関心の異なるロジックを分離するためにHOCパターンが用いられています。

なお、状態管理については記事がありますので、ぜひご覧ください。

diggle.engineer

Container/PresentationalパターンによるUIと状態・ロジックの分離

各ページのコンポーネントを実装する際、UIに関心をもつ Presentational Componentと、状態やロジックに関心を持つContainer Componentに分割することで、UIと状態・ロジックを分離します。具体的には以下のようになります。

  1. Container Componentは状態管理APIからグローバルな状態(ログインユーザーの状態など)を取得します。
  2. Container ComponentはバックエンドAPIをコールし、リソースのデータを取得します。レスポンスからViewModelを作成し、それを状態として保持します。 ViewModelはMVVMにおけるものとは若干異なり、APIで取得したデータとそれに関連するメソッドを含んだオブジェクトです。

  3. Presentational Componentは表示に必要なデータやロジックをContainer Componentからpropsとして受け取り、UIを構築します。

以下の記事を参考にさせていただきました。

zenn.dev

コードの例を見てみましょう。以下は、レポートの作成年月やタイトルなどを表示するコードです。

ViewModel

ReportsView は、レポートに関するデータとメソッドをもちます。

ReportsView.tsx

import { Report } from 'Models';
import { Record, OrderedMap } from 'immutable';

const ReportsViewRecord = Record<{
  reports: OrderedMap<any, Report> | null;
  // その他のデータ
  ...
}>({
  reports: null,
  ...
});

export class ReportsView extends ReportsViewRecord{
  // jsonデータからViewModelを組み立てる
  load(){ ... }
      
  // その他のメソッド
  ...
}

Container Component

APIをコールしてレポートのデータを取得後、ViewModelを作成し、状態として保持します。

ReportsContainer.jsx

import { ReportsView } from 'ViewModels';
import { ReportsPresentational } from './ReportsPresentational';

const ReportsContainer = () => {
  const [model, setModel] = useState(new ReportsView());
  // その他の状態
  ...
    
  useEffect(() => {
    fetch('https://xxxx/reports')
      .then((res) => res.json())
      .then(data => setReports((model) => model.load(data));
  }, []);

  return (
    <ReportsPresentational
      model={model}
      ...
    />
  );
};

Presentational Component

レポートの作成年月、タイトルを表示します。

ReportsPresentational.jsx

export const ReportsPresentational = ({ model: { reports, ... }}) => {
  return (
    <ul>
      {reports.map(({ id, title, year, month, ... }) => (
        <li key={id}>
          <p>{year}{month}月:{title}</p>
          // その他のレポート情報を表示する
          ...
        </li>
      ))}
    </ul>
  );
};

メリット

前述したアーキテクチャを取り入れることのメリットであるコードの可読性、保守性、テスト容易性などの向上が得られます。特に主要なデータやロジックが、他のコンポーネントと分離された形でViewModelに含まれるため、ViewModelに対するテストを重点的に書くことで、効率的にテストの恩恵を受けられます。

HOCパターンによる関心の異なるデータやロジックの分離

例えば、あるコンポーネントをモーダル表示したい場合、モーダル表示に関連する状態やロジックを高階コンポーネント (HOC、Higher Order Component)に分離します。高階コンポーネントとは、あるコンポーネントを受け取って新規のコンポーネントを返すような関数です。

例を見ましょう。以下のModalizeはモーダル表示に関連するデータや状態をもつコンポーネントです。

import { Modal } from '...';

export const Modalize = (Component) => (props) => {
  const [isOpen, setIsOpen] = useState(false);
  const open = ... // モーダルを開くメソッド
  ...
  return(
    <Modal
      open={open}
      ...
    >
      <Modal.Header>
        ...
      </Modal.Header>
      <Modal.Content>
        <Component {...props} />
      </Modal.Content>
    <Modal />
  );
};

コンポーネント MyComponent をモーダルで表示したい場合は、以下のようにします。

const EnhancedMyComponent = Modalize(MyComponent);

メリット

モーダル表示に関連する状態やロジックが分離されているため、これを様々なコンポーネントをモーダル表示するために再利用することができます。また、複数の高階コンポーネントを自由に組み合わせることもできます。以下は、モーダル表示のための高階コンポーネントである Modalize と非同期処理のための高階コンポーネントである WithAsync の機能を MyComponent に組み込むコードです。

const EnhancedMyComponent = WithAsync(Modalize(MyComponent));

ただし、高階コンポーネントの数が増えるとコードが読みにくくなります。

const EnhancedMyComponent = HOCn(...(HOC2(HOC1(MyComponent)))...);

デコレータを利用すると以下のように書けます。

@HOCn
...
@HOC2
@HOC1
MyComponent

デコレータの詳細は以下をご覧ください。

github.com

www.typescriptlang.org

今後の展望

ここまで紹介したコードを分離するためのパターンは、コンポーネントを利用したものでした。そのため、不必要な再レンダリングを引き起こす可能性があったり、テストが難しかったりする場合があります。これらの問題を解決し、より簡単かつシンプルにコードの分離を行うために、今後、Hooksの活用を積極的に進めていきたいと考えています。

より根本的な目標は、ReactのHooksを活用した宣言的アプローチへの進化に追随することで、Reactの利点を最大限に引き出し、プロダクトの価値を高めることです。

We're hiring!

DIGGLE では共にプロダクトを開発してくれるエンジニアを大募集中です。

少しでも興味があれば、ぜひ下記採用サイトからエントリーください。
カジュアル面談も実施しているので、気軽なお気持ちでご応募いただければと思います!

herp.careers

YAPC::Hiroshima 2024に行ってきたよ

ご無沙汰しています。DIGGLEのhirataです。 今回初めてYAPCに行ってきたのでその時の事を書いてみようと思います。

YAPCとは「Yet Another Perl Conferenceの略で、Perlを軸としたITに関わる全ての人のためのカンファレンスです」(サイトからの引用)でPerlを軸にはしますが、Perlにとどまらず技術的な話題を取り扱うカンファレンスとのことです。

関係ないですがタイトル画像は広島のアーケード街にあったゲームセンターの入口です。手をパチパチ叩くロボットが面白かったので撮ってしまいました。

事の始まり

YAPC::Hiroshimaが開催される数ヶ月前の事。いつものようにデイリーの朝会に参加していると、「YAPCに行きたい人居ないですか?」というCTOのお言葉。 ありがたいことに希望があれば経費で行かせてくれるという事でしたが、そのときはYAPCがPerlに関連するイベントということしか知らなかったために全く関心がありませんでした。

ですが、よく話を聞くとPerlに限ったイベントではなくプログラミングやソフトウェア技術全般のイベントとのこと。後でスケジュールを見てみると面白そうな講演もいくつかあり、折角の機会なので参加させていただくことにしました。やった!会社のお金で旅行だ!

前夜祭

前日2/9の午後から新幹線で東京から広島まで移動して、どうせならということで前夜祭にも参加しました。

受付を済ませて着席し周りを見渡すと、いかにもソフトウェアエンジニアの集まりという雰囲気で、これは期待が持てる感じです。

前夜祭が始まると、まず開発者さん自身によるHonoというJavascriptのフレームワークの熱い紹介がありました。私があまり強くない分野に関連する話題が多かったため内容の理解が難しかったのですが、とにかく開発のスピード感と熱量が凄そうなフレームワークでした。

その後も登壇者の方が各々のテーマでお話いただいていました。会場ではお酒が振る舞われており、登壇者の方のお話を聞きながらお酒を飲むスタイルです。観客の皆さんはお酒が入っているため好きにガヤガヤ話していて、しかも登壇者もそれを気にしないで話し続けるというカオスな状態で盛り上がっており、前夜祭の雰囲気を十二分に味わうことができました。

本編

2/10は朝から参加です。受付を済ませた後、まだ時間があったので協賛企業のブースを廻りました。ここでDeNAさんが朝食用におにぎりを配っていて美味しそうでしたが、ホテルの朝食でたっぷり食べてしまっていたため、お腹いっぱいで食べることができませんでした。

一通りブースを回り、早めに会場に向かうと既に何やら始まっている雰囲気。うっかりオープニングの事を忘れてしまっていた…。折角だから見ておけばよかったです。それはともかく、席に座ると昨日と打って変わってサラリーマンっぽい雰囲気の人が増えてました。

午前の講演で面白いと思ったのは、「VISAカードの裏側と “手が掛かる” 決済システムの育て方」と「My Favorite Protocol: Idempotency-Key Header」の2つでした。 特にVISAカードの方は普段触れることのないカード決済の仕組みを知ることができ、とても興味深かったです。やはり昔から続くサービスはいろいろ泥臭い部分があり、歴史の積み重ねを感じました。ネットもない時代からあるのでどうしても過去の経緯を引きずらざるを得ないのだと思います。こちらはベストLTに選ばれていました。 またもう一つの「My Favorite Protocol」の方はWebでAPIを開発する上でよく遭遇する問題である、「確実に一回だけリクエストを送りたい」問題に対する解の一つだと思いました。

お昼は弁当が配られたのですが、どれも美味しそうでした。私が食べたのは穴子弁当。写真撮ったけど画質が宜しく無いですね。美味しくいただきました。

昼のおべんとう

午後の部では「理解容易性と変更容易性を支える自動テスト戦略」が面白かったです。もしかしたらテストに詳しい方だと当然の内容なのかもしれませんが、テストをネットワークアクセスやDBアクセス等の有無によってlarge, medium, smallに分けてピラミッドにするという話はなるほどと思いました。業務でテストを書く上で単体テスト、統合テスト等は書き分けて意味があるのかと感じることもありましたが、この分類であればsmallは基本的にパッと終わるテスト、largeは時間の掛かるテストになるので、普段はsmallだけテストしてぱぱっと確認し、PRを出す前にlargeも含めたテストを実施してより網羅的に確認といった事ができそうです。登壇者の方が翻訳に参加された「テスト駆動開発」という本も気になって買ってしまいました。

そして最後にあの伝説のとほほさん(敢えてひらがな)の講演がありました。登壇する前に座ってた席が実は近くだったのでびっくりしました。まさかインターネットの黎明期にPerlだったりHTMLでお世話になった伝説の人にお目にかかることができるなんて…。講演の内容もとても面白かったです。

その後は懇親会まで参加しましたが、体調があんまり良くなかったのもあり、最初に近くにいた人としか話せなかったのが心残りです。次に参加する機会があれば体調を整えて交流を楽しめるようにしたいと思いました。

懇親会場

その後

懇親会で人から聞いて知ったのですが、次の日にもいろいろ関連イベントがありました。しかし既に枠が一杯で参加できず。 なので翌日は広島城に行ったり、庭園を散策したり、広島のアーケード街を散歩したりで1日中観光してから帰りました。知らない街を散策するのはとても楽しいです。

途中で食べた備後府中焼きは広島風お好み焼きのバリエーションらしいのですがとても美味しかったです。語彙が少なくて具体的にどこがとは言えないのですが、個人的には普通のお好み焼きよりも好みの味でした。

備後府中焼き

タイミング悪く私自身の体調が良くなかったのが残念ですが、イベントは盛り上がっておりとても楽しめました。オンラインでも見ることはできますが、生の会場の雰囲気はまた違ったものがあってYAPCに参加できて良かったと思いました。

おしまい。

We're hiring!

DIGGLE では共にプロダクトを開発してくれるエンジニアを大募集中です。このように気軽に技術者の集いに参加させてもらえます。

少しでも興味があれば、ぜひ下記採用サイトからエントリーください。
カジュアル面談も実施しているので、気軽なお気持ちでご応募いただければと思います!

herp.careers

パフォーマンス分析基盤の構築。S3やAthenaを活用して柔軟性の高いデータの取り扱いを実現した話

はじめに

DIGGLEではすでにDatadogを導入してパフォーマンス指標のトレースデータ*1は取れていますが、以下の課題がありました。

  • ユースケースを分けてパフォーマンス指標を取得したい
  • DIGGLEがターゲットとしたい企業やペルソナユーザーのデータのみを取り出すのが面倒
  • 開発時点でCIが動いた時に、分析結果をプルリクのコメントに出力したい
  • Datadogに保存されているパフォーマンス指標のトレースデータには有効期限がある

上記に対応する為にまずは調査を行いました。

Datadog API Clientによるデータの取得

Datadogに保存されているデータをAPI経由で取得することによって、必要なデータが得られないかを調べました。

結果としては、RUMデータは取得できそうでしたが、APMのトレースデータは現状ではAPI提供されていませんでした。

そしてCI起点でのパフォーマンス分析を考えると、リアルタイムにパフォーマンス指標データを取れない所からDatadogベースでは厳しいです。 またDatadogを選択すると有効期限の問題や、ターゲットとしたいデータを取り出すのが面倒という問題が解消できません。

上記などの理由によりDIGGLEでは、独自でパフォーマンス指標を取得する方針としました。 DatadogはDatadogで有用なのでそちらの利用は継続し、別途パフォーマンス分析基盤を構築して、そちらに分析用のデータを保存するイメージです。

パフォーマンス分析の概要

対象データ

顧客への価値提供に影響が強い部分の、ユースケース別の実行時間などのデータになります。 特定処理の実行時間から、細かい粒度だとSQLの実行時間まで対象としています。 またDIGGLEがターゲットとする、代表的なペルソナユーザーが処理したデータになります。

分析方法

それぞれのユースケース別の実行時間を分析する形になります。 パフォーマンス改善施策などの効果を測定する為に、リリース対応前後の実行時間をクエリ抽出して効果測定します。 また、代表的なペルソナユーザーのデータを定量データとして保持して、予期せぬタイミングでパフォーマンスが悪化していないかなどの時系列分析も行います。

パフォーマンス分析基盤の構成の検討

データ保存先

トレースデータの保存先としては以下のような選択肢が考えられます。

  • DB
  • NoSQL系
  • Bigquery
  • Redshift
  • S3
  • Athena

どの選択肢も一長一短がありますが、DIGGLEではS3へログデータとして転送する方針となりました。 既存システム性能への影響が小さく、S3から先へもAthenaでデータソース化するなど色々な選択肢が取れるためです。 またS3へ保存することにより、Datadogの有効期限の問題は解消されます。

構成

S3へのデータ保存が決まりましたので、パフォーマンス分析基盤の構成はおおまかに以下のようになりました。 3と4の構成についてはデータ保存後も柔軟に変更可能です。

1のRailsから標準出力への出力先を実ファイルへ変更すると、CIでの分析も可能になります。また独自プログラムで実行するのでユースケース分けや、データのフィルタリングについてもやりやすいという利点があります。

  1. ECSのRailsから標準出力にパフォーマンス指標データをログ出力
  2. Firelensを経由してFluent bitでログをS3へ転送
  3. S3に保存されたJSONデータをAthenaでデータソース化
  4. AthenaをデータソースとしてMetabaseからクエリでデータ分析を実施

対応内容

サーバーサイド

こちらはDatadogのソースなどを参考にしながら、Rails標準のActive Support Instrumentationで取得できるパフォーマンス指標はこちらで取得し、それ以外については独自で対象のメソッドをフックして、パフォーマンス指標をリクエストなどの処理ごとに取得するように対応しました。

インフラ

ECSのRailsからは標準出力でログ出力を行い、それがFirelensからFlunet bitに渡り、Fluent bitのrewrite_tagでS3へ振り分けを行います。

Flunet bitの設定例)

jsonデータのhogeキーの値がhogehogeの場合にS3に転送される設定例になります。

# rewrite tag
[FILTER]
    Name          rewrite_tag
    Match         *-firelens-*
    Rule          $hoge hogehoge target true

# output to s3
[OUTPUT]
    Name            s3
    Match           target
    bucket          target-bucket
    region          ap-northeast-1
    use_put_object  On
    total_file_size 1M
    upload_timeout  1m
    retry_limit     False
    compression     gzip

S3に保存したJSON形式のログデータはAthenaで以下のようなクエリでテーブル化が可能です。

CREATE EXTERNAL TABLE IF NOT EXISTS hoge (
  id int,
  name string
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
WITH SERDEPROPERTIES (
    'null.format' = ''
)
LOCATION 's3://target-bucket/'
TBLPROPERTIES ('has_encrypted_data'='true');

注意点としてはinteger型を定義した場合は、空白データを許容してくれなくなるので、ログ出力側で0などを出力する必要があります。

最終的にDIGGLEではAthenaをデータソースにMetabaseでクエリを実行する形式にしています。

フロントエンド

フロントエンドはサーバーサイドで作った仕組みに乗っかる形で、処理単位で複数のトレースデータをサーバーサイドに送信する形を取りました。

最後に

今回はパフォーマンス分析基盤の構築について検討から実際の構築まで紹介しました。性能観点と後々の柔軟性という部分で今回はS3へデータを保存する形になりましたが、他にも様々な選択肢があると思います。

この記事が、分析基盤構築などの一助となれば幸いです。

DIGGLEのエンジニアのchikugoがお送りしました。

We're hiring!

DIGGLE では共にプロダクトを開発してくれるエンジニアを大募集中です。

少しでも興味があれば、ぜひ下記採用サイトからエントリーください。
カジュアル面談も実施しているので、気軽なお気持ちでご応募いただければと思います!

https://herp.careers/v1/diggle/_dgvcOQcFfeqherp.careers

*1:トレースデータとはアプリケーションやサービスが実行する一連の操作を表す実行時間などを含むデータです

なりすまし/迷惑メールと思われない為のDMARC導入手順

はじめに

DIGGLEエンジニアのaki0344です。
2024/02/01より、Gmailのガイドラインが変更されました。 support.google.com

Gmailに対して1日5000件以上のメールを送信しているドメインはSPF、DKIM、DMARCによる認証を設定していない場合なりすましや迷惑メールと判断されて送信先に届かない可能性があります。
今回、DIGGLEではSalesチームやプロダクトから送信したメールが迷惑メールと判定されないよう、全社のメールでDMARCに対応しました。
本記事ではその際に行った作業についてまとめてみました。
実際の手順のみ知りたい方はDMARC対応の流れからお読みください。

DMARC対応作業を始めるまで

それは一通の問い合わせから始まった

ある日、Salesチームから1通の問い合わせが届きました。

翌日、開発チームのデイリースクラムで問い合わせについて共有され、対応が必要だねという話になりました。
メールのセキュリティについて全く知識が無かった私は静観していたのですが、生憎他メンバーは直近の作業に追われており、急ぎの作業のなかった私に押し付けられましたが対応することにしました。
何のことか良くわからないけど、何を設定すれば良いかは明示されてるしそんなに大変な作業でもないでしょと思っていました。実際に作業を始めるまでは…。

そもそもDMARCって何だ?

さて、DMARCに対応するためには当然DMARCが何者なのかを知らなければいけません。
まずはDMARCで検索をかけてみたのですが、SPF/DKIMというこれまた聞いたことのない言葉が出てきました。
いきなり調べることが増えてしまった…。
様々なサイトを眺めていましたが、『構造計画研究所』さんのブログ記事がとても分かりやすかったです。

sendgrid.kke.co.jp

sendgrid.kke.co.jp

それぞれを一言で表すと以下のようになります。

  • SPF : 正規のサーバから送信されていることを証明する
  • DKIM : ドメイン管理者がメールを送信していることを証明する
  • DMARC : SPF/DKIMで認証失敗した場合にメールをどう扱うかを決める

DMARCに対応するためには、その前段階としてSPF/DKIMに対応しなければいけません。
※DMARCはSPF/DKIMいずれかのみと組み合わせての対応も可能ですが、今回DIGGLEでは全て対応することを前提に作業を進めました。

対応時の注意点

DMARCに対応するにあたり、いくつか注意点があります。

メール送信方法によって対応内容が異なる

メール送信を行うサービスを利用している場合、それぞれに対応が必要となります。
一部設定が漏れていた場合、そのサービスから送信されたメールが送付先でなりすましと判定される可能性があります。

SPF、DMARCの対応は複数サービスで同じ場所に修正を加える必要がある

詳細は後述しますが、DNSに設定するレコードはSPF、DMARCでそれぞれ1行です。
複数サービスでSPF、DMARCに対応する場合、各サービスの案内に記載されている内容をそのまま転記するとそれぞれ複数行追加してしまう可能性があります。
作業完了時、SPF、およびDMARC用のDNSのレコードはそれぞれ1行ずつと覚えてください。
なお、DKIMは各サービスごとにレコードが必要となります。

SPF/DKIM未対応の状態でDMARCに対応してはいけない

先述の通り、DMARCはSPF/DKIMの認証結果を基に次のアクションを決定する機能となります。
SPF/DKIMに未対応の状態でDMARCのみ追加した場合、送信したメールがすべてなりすましと判定される可能性があります。
DMARCは対応が必要な全てのサービスでSPF/DKIMに対応し、最後に設定を行いましょう。

定義を間違えると他のサービスにも影響を及ぼす危険性がある

複数サービスで共通する箇所に修正を入れるため、1か所失敗すると全てのサービスが正常に動作しなくなる可能性があります。
かならずクロスチェックをしましょう。

DMARC対応の流れ

ここからは実際にDMARCに対応する際に行ったことを書いていきます。

自社で保有しているドメイン一覧を特定する

複数のドメインを保有している場合、各ドメインでメールを送信している可能性があります。
今回の対応はドメイン毎に必要となるため、対応漏れが無いよう自社ドメインを全て洗い出します。

メールアドレスに使用されているドメインを特定する

メールの送信元(From)となり得る全てのドメインで対応要否の判定が必要となります。
社内の有識者に確認し、どのドメインがメール送信に使用されているかを絞り込みます。
DIGGLEでは以下の方法で絞り込みを行いました。

  • バックオフィスに確認
    各社員に割り当てられたメールアドレスを管理している部署に、他のドメインを利用しているかを確認します。
  • プロダクト管理者に確認
    プロダクトの機能でユーザーへメール送信する際に利用しているドメインを確認します。

ドメイン毎にDMARC対応が必要か判定する

Googleのガイドラインでは、1日5000件以上のメールを送信する場合にDMARCへの対応が必要となっています。
毎日大量のメールを送信しないことが明らかな場合はDMARCの対応を行わなくてもガイドラインには抵触しません。
今回のDIGGLEの対応ではドメイン毎に送信しているメールの件数について確認はしていませんが、使用用途から現在、あるいは今後1日5000件以上のメールを送信すると判断したドメインに対して対応を行うことにしました。

メール送信を行っているサービスを洗い出す

SPF/DKIMはメールサーバ毎に設定を行う必要があります。
そのため、自社内で先ほど絞り込んだドメインを送信元としてメール送信している外部サービスを確認します。
バックオフィスなど各部署の利用しているサービスを管理している部署に、自社ドメインでメール送信を行っているサービスについて問い合わせましょう。
DIGGLEでは念のためslackの全社員が登録しているチャンネルでも確認を行いました。
※文中にある各個人からのメール送信はGmailのSMTPサーバを利用しており、過去にSPF、DKIM対応済みだったため今回は対応不要としています。

各サービスでSPF、DKIMの対応方法を確認する

SPF、DKIMの対応方法はサービスによって異なるため、各サービスのマニュアルや設定画面から対応方法を特定します。
DIGGLEで利用していたサービスでは大別して4パターンあることがわかりました。

SMTPサーバを指定している

Metabaseのように自分でサーバを用意するシステムの場合、メールはSMTPサーバを指定するのが一般的です。

この場合はSMTPサーバを管理しているサービスで次に記載する内容を確認します。

SPF/DKIMを有効化するレコードをDNSに追加する

最も一般的な対応方法です。 各サービス毎に、SPF、およびDKIMを設定するための値が記載されているページを探します。 SPFは一般に公開されていることが多く、マニュアル等やFAQで記載されている個所を探します。

SPFの設定値サンプル

DKIMで設定する値はドメイン毎に異なるため、管理者画面からのみ確認が可能となっています。
ドメインや配信時のメールアドレスを設定するページで確認できることが多いです。

emaildocs.netcorecloud.com

サービス側のDNSサーバを参照するためのレコードをDNSに追加する

SendGridなどの一部サービスでは、サービスが用意しているDNSサーバがSPF/DKIMの認証を行う機能があります。

sendgrid.kke.co.jp

その場合、自社で用意しているDNSではサービス側のDNSサーバを参照するためのレコードを定義する必要があるため、その値が記載されているページを探します。
レコードの値はドメイン毎に異なるため、管理者画面からのみ確認が可能となっています。
ドメインや配信時のメールアドレスを設定するページで確認できることが多いです。

他サーバのDNSサーバ参照値サンプル

対応方法が何も書かれていない場合

DIGGLEで利用しているサービスの中には、FAQ等のドキュメントや管理者用設定画面のどこを探してもSPF、DKIM、DMARCに関する記載が一切存在しないサービスがありました。
こちらは実際にメールを配信する手順で社員へメールを送信し、ヘッダ情報でSPF、DKIM、DMARCに対応しているかを確認し、未対応の場合は問い合わせる方向で作業を進めました。
※ヘッダ情報の確認方法は後述しています。
結果的には全て対応済みだったため事なきを得ました。

DNSで各ドメインにSPF、DKIM、DMARCのレコードを追加する

各サービスで追加する内容が明確になったので、ドメイン毎にレコードを追加していきます。
ここで2点注意事項があります。
※サブドメイン毎に設定する場合はこの限りではありません。

  • SPFのレコードは1ドメインに1つ
    複数のサービスを利用している場合、『SPF/DKIMを有効化するレコードをDNSに追加する』でサービス毎のSPFを確認していると思いますが、DNSには1つのレコードにまとめて登録する必要があります。
    複数の場合はincludeをサービス数分定義し、各サービスの値を設定します。

    • 誤った設定例
      v=spf1 include:spf.corp01.value ~all
      v=spf1 include:spf.corp02.value ~all
      v=spf1 include:spf.corp03.value ~all
    • 正しい設定例
      v=spf1 include:spf.corp01.value include:spf.corp02.value include:spf.corp03.value ~all
  • DMARCのレコードは1ドメインに1つ
    DMARCはサービス数に関わらず共通の値を使用します。

SPF、DKIMのレコードを追加する

ドメインを管理しているDNSで、ドメイン毎にSPF、DKIMを有効にするためのレコードを追加します。
SPFは1つにまとめた値を、DKIMはサービス毎に指定された値の数だけレコードを追加します。
値に誤りがあると他サービスにも影響する可能性があるため、クロスチェックを行う等、確認は念入りに行ってください。
レコード追加後、各サービスからメールを送信し、ヘッダの内容からSPF、DKIMが有効になっているか確認します。
※有効になるには一定時間を要する場合があります。
Gmailの場合は『メッセージのソースを表示』で確認することが可能です。

DMARCのレコードを追加する

ドメインを管理しているDNSで、ドメイン毎にDMARCを有効にするためのレコードを追加します。
パラメータは必要に応じた内容を設定します。
www.naritai.jp

認証失敗時の動作pについてはいきなりrejectなどの厳しい制限を指定すると今までメールを受け取れていた相手に届かなくなる可能性があります。
一旦noneを設定し、レポートで状況を確認しながら段階的に制限を強めていくのがおすすめです。
以下は設定値の一例です。

v=DMARC1; p=none; rua=mailto:report-rua@my.corp.com; ruf=mailto:report-ruf@my.corp.com;

ruarufを設定しておくとメールでレポートが送られてくるため、後から到達率等の振り返りが可能となります。
レコード追加後、各サービスからメールを送信し、ヘッダの内容からDMARCが有効になっているか確認します。
※有効になるには一定時間を要する場合があります。
SPF、DKIMと同様に、Gmailの場合は『メッセージのソースを表示』で確認することが可能です。

サービス導入時の対応手順を整備する

以上の手順で現在利用しているサービスについてはSPF、DKIM、DMARCが有効になりました。
一方、今後各部署で利用するサービスを追加した場合は、今回と同様の手順でSPF、DKIMの追加が必要な可能性があります。
そのため、社内でまとめているサービス導入時の手続き等の文書にDNS管理者へ設定の依頼を行う手順を追加しておくことをお勧めします。
DIGGLEでは以下の条件を満たすサービスの場合にはDNS管理者へ通知するよう手順をまとめました。

  • 自社ドメインを利用してメールを送信する
  • 初期セットアップで送信メールサーバを指定しない

これで今後の新規サービス導入がスムーズに進むことが期待できます。

おわりに

今回はDIGGLEから送信したメールがGmailでなりすましや迷惑メールと判定されないよう、SPF、DKIM、DMARCを有効化するために行った作業をまとめてみました。
本記事では効率的で漏れのない手順をまとめていますが、実際には私の知識不足により各手順を行ったり来たりしながら無駄の多い作業となってしまいました。
幸い、DIGGLEでは最近立ち上がったSREチームにセキュリティに強い方がいたため、その方に叱られながらアドバイスをいただきながら無事作業を完了させることが出来ました。
本記事が、メールのセキュリティ強化に対応する際の一助となれば幸いです。

We're hiring!

DIGGLE では共にプロダクトを開発してくれるエンジニアを大募集中です。

少しでも興味があれば、ぜひ下記採用サイトからエントリーください。
カジュアル面談も実施しているので、気軽なお気持ちでご応募いただければと思います!

herp.careers

Rustで型付Webアプリケーション開発

この記事は Rust Advent Calendar 2023 25日目の記事です。

お久しぶりです。DIGGLEのhirataです。

今回はDIGGLEとは関係ないですが、個人的にRustの利用を促進すべくRustでのWebアプリケーションの開発経験について書きたいと思います。

所感としては一人で開発するならRustを使うべし、と思える経験でした。やはりあちこちで言われている事ではありますが、静的型とライフタイムチェックによりコンパイルを通ってしまえばほぼバグが無く動くという安心感がとても心地よいものでした。コンパイラが代わりにテストをしてくれているような心持ちです。 また、Rustで作成したサーバはとてもメモリ消費量が小さく、Railsアプリであれば下手をすると1GB程まで行ってしまいますが、数十MBで十分動くというのも個人にとってありがたいものでした。 ちなみにとても安定して動作しており、今までバグやアップデート以外の再起動はありません。

参考までに私は元組み込み系のエンジニアでして、その影響で未だにメモリを沢山使うことに罪の意識を覚えます。 そして現在の仕事のメインはRailsでメモリ潤沢な大富豪バックエンドの開発をしています。その影響で随所にRailsとの比較が出てくるかなと思います。

どうしてRustだったのか。そしてどんなWebアプリケーションか。

それまでRustでいくつか小さなプログラムを書いたことはありましたがWebアプリケーションを書くとどんな感じなのだろうかという好奇心が全てでした。

おかげで少々苦労することにはなりましたが。

プロジェクトとしては個人での副業で、開発者は一人だったのでインフラも含めて全て自由に決めることができました。運用費はできるだけ安く (できればタダ) にしたかったので、Railsだとリソース的に厳しかろうということでRustを選んだ一面もあります。 用途はとある小規模なジムで使う来客管理システムで、会員登録やその事前登録、お客様の入退場、入場料等を管理できるものになっています。 一人で開発するならSPAじゃないほうが開発は速かろうということで、マルチページでのアプケーションとしました。 使用しているクレート (Rustでは公開・共有されているライブラリの事をこう呼びます) は開発開始当初は大まかに以下の通りでした。

後は必要に応じて追加していくことになります。

まずは見切り発車での開発。

とりあえずactix-webはRustではその当時(2年ほど前)一番メジャーで高速そうなクレートだったので採用しました。確かどこかのベンチマークサイトで一番になっていたことも有ったはずです。他のも大体人気順に決めた感じでした。 あまり知識がない状態だとこんなものです。

実際に使ってみるとactix-webやteraは特に問題なく使えたのですが、dieselはなかなか難解でした。JOINの無い状態だと簡単に使えるのですがJOINした場合の型が以下のように大変なことに。。。 これはJOIN有りでSELECTした結果を引数として受け取る関数です。とても自分で書けるものではなかったので、エラーメッセージからコピペしました。

fn admittance_search_query<'a>(params: &CustomerAdmittanceSearchParams)
 -> diesel::query_builder::BoxedSelectStatement<'a, 
        (schema::customer_admittances::SqlType, schema::staffs::SqlType, schema::customers::SqlType),
        diesel::query_source::joins::JoinOn<diesel::query_source::joins::Join<diesel::query_source::joins::JoinOn<diesel::query_source::joins::Join<schema::customer_admittances::table,
        schema::staffs::table,
        diesel::query_source::joins::Inner>,
        diesel::expression::operators::Eq<diesel::expression::nullable::Nullable<schema::customer_admittances::columns::processed_by_id>,
        diesel::expression::nullable::Nullable<schema::staffs::columns::id>>>,
        schema::customers::table, diesel::query_source::joins::Inner>, 
        diesel::expression::operators::Eq<diesel::expression::nullable::Nullable<schema::customer_admittances::columns::customer_id>,
        diesel::expression::nullable::Nullable<schema::customers::columns::id>>>,
        diesel::pg::Pg>

RDBを使っている以上、JOINが無いことは考えられないと思いますのでこれは避けて通れなさそうです。当時はver1.x系しか無くてそれを使っていたのですが、今はver2になっているのでもしかしたら現在は解決されているかもしれません。もしくはもっと良い書き方があるよ!という方は教えていただけると助かります。

初期段階で一番苦労したのは、Railsのように1から10までレールが引かれている訳では無いので、開発の基盤を安定させるところでした。例えば以下の事は自分で決める必要がありました。

  • モジュールをどのような構造で構成するか。MVCにするか、レイヤードアーキテクチャにするか、はたまたオリジナルにするか。そこまで複雑にならない予定だったのでRailsで慣れているMVC構成としました。
  • 構成したモジュール間の初期化処理をどのように繋げるか。簡単なようですがついついエレガントさを求めてしまい、意外に時間を取られます。そして最終的には悩んだ割にあんまりエレガントじゃないという。。。
  • エラーログはどう出力するか。どのライブラリを使うのか。エラーレベルはどう定義するのか。最終的にはenv_loggerとlogというクレートを使いました。
  • 各種設定や開発環境、本番環境の切り分けは環境変数で行うのか、設定ファイルを作るのか。コンテナで動かすつもりだったので環境変数で全て設定するようにしています。
  • JavaScript側の構成はどうするのか?JavaScriptのライブラリを使いたかったので一応webpackも入れました。

このように事ある毎に自分でレールを敷く必要があるので、このやり方でいいのかという自問との戦いでした。下手に悩むくらいなら実は酒でも飲みながら進めた方が捗ったかもしれないですね。

開発を終え保守のステージ。

無事リリースし以降数回の修正を重ねることになりますが、幾度かの修正を重ねる中、HTMLテンプレートのチェックをコンパイル時に行ってくれないteraだと、実際に動かしてみるまで些細なミスが発見できないのが苦痛になってきました。 修正後にサーバを起動しページをレンダリングしようとした段階で、結構な頻度でテンプレート内の式でエラーになってしまいます。

ということで、コンパイル時にテンプレートについても静的チェックを行ってくれるクレートを探していたところ発見しました。

github.com

これがとても良く私の用途にマッチしてくれて、おかげでテンプレート内の式の書き間違いに苦しむ事はなくなりました。一度コンパイルが通ればページレンダリングの段階でエラーになることはありません。

このようにしてHTML出力に関してはほぼ動的にエラーが発生することはなくなり、コンパイル時に気づくことができるようになりました。 ただまだHTML上でシステムにリクエストを行うような下記の部分はサーバを起動して動的にチェックする必要があります。

  • aタグやその他に書かれているシステムのURL
  • formタグの中身のinput

これらをコードで生成するライブラリを作ることができればJavaScirpt以外は全てコンパイル時にチェックされることになり動的なエラーを撲滅できそうです。 実現できれば後はJavaScriptをTypeScript化すれば全てに型が付いてコンパイラがチェックしてくれるWebアプリケーション開発環境のできあがりです。夢が膨らみます。暇が無くてできてませんが。

またコードを書いている中でどうしても、formを受け取るための構造体と、ORMで使うための構造体に差が出てきてしまうので最初は手書きでメンバをコピーするコードを書いていました。 これが苦痛でたまらない上に、ミスに気づきにくかったのですが、構造体のコピーをしてくれるようなクレートが見つからなかったため、自分でマクロを作りました。

https://crates.io/crates/clone_into_derive

これでかなりコードが簡略化できたのと、どうしてもコピーするメンバを書き忘れることがあったのですがそれが無くなって保守性があがりました。

こんなコードが有った場合、直接コピーできないのでどうしても冗長に書く必要があります。わかりやすくするために簡略化しています。

// formから変更内容を受け取る用の構造体
pub struct UpdateCustomer {
    pub second_name: String,
    pub first_name: String,
    ...
    pub blood_type: i16,
    pub memo: String,
    pub monthly_path_limit: Option<NaiveDate>,
}

// ORM用の構造体定義
pub struct Customer {
    pub id: i64,
    pub assigned_id: Option<i32>,
    pub email: String,
    pub sex: i16,
    pub birthday: NaiveDate,
    pub post_number: String,
    pub prefecture: String,
    pub municipalitie: String,
    pub address: String,
    pub tel: String,
    pub created_at: NaiveDateTime,
    pub updated_at: NaiveDateTime,
    pub blood_type: i16,
    pub membered_at: Option<NaiveDateTime>,
    pub memo: String,
    pub second_name: String,
    pub first_name: String,
    pub second_name_furigana: String,
}

pub fn change_it(con: &Connection, customer_id: i64, update_customer: UpdateCustomer) {
    let customer = customer::find(con, customer_id);
    customer.second_name = update_customer.second_name.clone();
    customer.first_name = update_customer.first_name.clone();
    ...
    customer.blood_type = update_customer.blood_type;
    customer.update(con);
}

こうなります (変更部分だけ)。違いはUpdateCustomer#[derive(CloneInto)]というマクロを使用するための記述が追加されたのと、そのマクロupdate_customer_clone_into!chjange_it内でメンバのコピーに使っていることです。かなり短くなりました。

// formから変更内容を受け取る用の構造体
#[derive(CloneInto)]
pub struct UpdateCustomer {
    pub second_name: String,
    pub first_name: String,
    ...
    pub blood_type: i16,
    pub memo: String,
    pub monthly_path_limit: Option<NaiveDate>,
}

pub fn change_it(con: &Connection, customer_id: i64, update_customer: UpdateCustomer) {
    let customer = customer::find(con, customer_id);
    update_customer_clone_into!(param.customer, customer);
    customer.update(con);
}

よし!かなり使えるライブラリだ!、と思ってクレートとして公開したのですが、全然アクセスがありません。私の想定では人気のクレートになってRust界の有名人になる筈だったのですが。

是非使っていただければ。

あとがき。

という訳で、全てでは無いですがHTMLテンプレートの中までコンパイラが型チェック等をしてくれる状態になりました。 一人で開発しているとやることが多いので、動作確認の一部とはいえ肩代わりしてくれる存在が心強いです。

実装面での感想ですが、Rustでは俗に言う継承(inheritance)が無く、代わりに委譲(composition)が推奨されているのでRubyのような純粋オブジェクト指向の世界から来ると戸惑いを覚えます。 共通部分だけベースクラスとして定義して、それを継承するということは結構やってしまいがちだと思いますが、一切使えません。代わりに委譲を使いましょう。

また安心してマクロが使えるというのは特筆すべきことかなと思います。マクロとはコードを生成するコードの事で、Rustではコンパイル時にマクロからコードに展開します。 マクロはデバッグも厄介で時にバグの温床になるのですが、Rustでは生成されたコードが問題ないかどうかを型の整合性の面と変数のライフタイムからコンパイラがチェックしてくれるので、コンパイルが通ればほぼ大丈夫という安心感があります。 私の書いたマクロは単にコピー元の変数の構造体定義にあるメンバをコピー先として指定された変数の同名のメンバに代入するコードを生成するというものですが、このチェックのおかげで問題があればコンパイラが検出してくれます。なので私のクレートではコピー先の型は指定しなくても良いようにしました。 このようにマクロとコンパイラでの静的チェックはとても相性が良さそうに思います。

そしてRustはいい!コンパイルが通れば動くという安心感に加えて、クレート=ライブラリも比較的充実しており、あまり開発時に困ることもなさそうです。 私はCommon Lispも好きなのですが、欲しいライブラリが無いことがままあって悲しい思いをすることが多いです。。。

ただ、マクロが必要になることが結構ありそうなこと、ライフタイムがあること、そもそも型の概念が難しい等々で、やはりRustを扱う人には高い技術が求められそうです。 個人的にはそれに伴うリターンはあると思うので、高い技術力をもったメンバーを集めるのであれば、Rustでの開発も選択肢に入れてもいいのではないかと考えます。

以上RustでSPAでは無いアプリケーションを作ったときのお話でした。 今どきはHTMLを直接出力するWebアプリを作る機会は少ないのかもしれませんが、何かの参考にしていただければ。

We're hiring!

DIGGLE では共にプロダクトを開発してくれるエンジニアを大募集中です。

少しでも興味があれば、ぜひ下記採用サイトからエントリーください。 カジュアル面談も実施しているので、気軽なお気持ちでご応募いただければと思います!

herp.careers