バックエンドをRustに書き換えた実例報告

松本健太郎氏:私からは、まさにバックエンドをRustに書き換えるということで、実例報告的なことをやりたいと思っています。よろしくお願いします。

本日は、どういうことをしたかというところと、それをやるにあたっての意思決定。実際にやってみてどういうところが問題になったか。あとは、特にTypeScriptとの連携周り、どういう工夫をしたかをお話しできればと思います。

自己紹介です。松本健太郎と言います。

アプリケーションの概要と書き換えが決定した経緯

今回は、まだプロトタイプ開発中で、たくさんのお客さんがついているアプリケーションではありません。もともとNext.jsでアプリケーションが書かれていて、バックエンドがMySQLというものを、APIの部分だけactix-webを使った、Rust実装に変えました。

APIの本数は10本ぐらいで、そんなに複雑なロジックがあるものではありません。データベースからデータを取ってきたり、データベースにデータを入れたりするシンプルなものでした。

「そもそもなんで書き換えるの?」という話が出てきたのか。初期開発をしていたNode.jsに詳しい業務委託のエンジニアの方がチームを抜ける時に、次はどういうメンバーを入れて、どういう言語で開発していくのかを話すタイミングがありました。

estie(※株式会社estie)としてはRustに力を入れていきたいタイミングでもあったし、そもそもRustが書けるエンジニアのkenkooooさんが社内にいました。私もわりとリソースを入れられる状態だったので、候補に上がってきたのかなと思っています。

一応他の言語応検討はしたのですが、TypeScriptでいくのか、Rustで書き直すのかを中心に議論を進めました。estieでは、各プロダクトでどんな言語を使っていくか、けっこう現場のメンバーに権限委譲されています。チームのメンバーを中心に、メリット・デメリットを整理して、CTOや他のチームのエンジニアからも意見をもらいながら整理していきました。

最終的には、長期的な視点を持って意思決定をしました。特にアプリケーションの開発効率や安全性では、Rust優位の面もあります。また、エンジニア採用の観点から見ても、Rustのほうが興味を持ってもらえるのではないか。TypeScriptが書けるエンジニアはたくさんいるけれど、Rustが書ける職のほうが採用しやすいのではないかという仮説もありました。

書き換えにおける懸念点3つ

主に3つ、これが解決できるだろうかという観点がありました。1つ目が、型定義が二重管理になってしまうのではないか、という問題です。Rustで実装されたAPIをTypeScriptから呼ぶと、両方の言語で型定義が必要になります。Rust側をエンジニアが実装をして、TypeScript側でもエンジニアが実装するということは避けたいと思っていました。

これはRustの構造をベースにTypeScript側のコードを自動生成できる仕組みを入れることで解決しました。後半で説明します。

次に、Rust用のコンテナが必要になる問題です。Node.jsだと1つのコンテナでよかったのですが、Rust用のコンテナがもう1個必要になるのではないかという指摘がありました。これは、kenkooooさんのチームでも運用が始まっていて、それと同じようにいけるという判断をしました。

もう1つ、Rustが書けるエンジニアをアサインできるのか、という問題がありました。これは、比較的興味を持っている人がアサインできればキャッチアップしてもらえそうだな、という感覚がありました。

前職でも、興味のある方に入ってもらえれば、キャッチアップしてすぐに立ち上がってもらえていたし、estieのslackにはRustの質問に答えるチャンネルがあるので、そういうところから進めていってもらえるのではということで、ここもクリアになりました。

実稼働3日で再実装は完了

「実際に書き換えてみましょう」というフェーズをお話しします。実際のところ、実稼働丸3日ぐらいで再実装できました。着手前は、5日〜7日ぐらいかかるかなと思っていたのですが、思いのほかkenkooooさんのチームのノウハウを活かすことができ、同じようなスタックで進められました。

すでに「この組み合わせでけっこういい感じに動くぜ」というのが明らかになっていて、必要なものが実装されていたので、それを流用できるなど、メリットが受けられる状態になっているな、と感じました。

一方で、プロトタイプをRustで書くこと自体の是非がまだわかっていない状態です。これは時間をかけて実際に運用を回してみないとわからないのかな、と思っています。

保守しにくいコードを書きにくくする効果はあると思うのですが、何回も書き直したり、微修正を繰り返していったりする中で、どれぐらいそこが守られていくのかを、今後は確かめていきたいと思います。

フロントエンドとの連携での課題と解決策

フロントエンド側との連携についてお話をします。先ほどもお伝えしたとおり、Rust側でAPIを実装すると、リクエストの型とレスポンスの型を定義する必要があります。

TypeScript側でもAPIのリクエストとレスポンスの型を書く必要がありますが、両方人手でやるのはなかなか大変なので、なにかうまい方法を入れたいなと思いました。

別のプロジェクトではts_rsというモジュールを使っていたのですが、クライアントのコードを生成するOpenAPIを使うために、今回のプロジェクトではpaperclipというクレートを導入しました。

先にts_rsの説明をします。非常にシンプルなモジュールで、ts_rsの「ts」を融通してアトリビュートを書くと、そのRustで書いた構造体がTypeScriptのコードとして出力される、というものです。

実際に、この左側のRustのコードを書いてcargo testを打つと、bindings/User.tsというファイルが自動で生成されて、RustのstructがTypeScriptのコードとして右図の内容が書き込まれます。簡単に動かせるので、興味ある方はお手元で動かしてみてもらえればと思います。

ただ、もうちょっと、いろいろとやってほしいんだよなあというところがあります。OpenAPIの定義ファイルを作れば、OpenAPIの定義ファイルからTypeScript側でやりたいことができるということで、今回はpaperclipという別のクレートを使用しました。

先ほどkenkooooさんの発表の中で、「actix-webは非常に少ないコード量で実装ができていいね」と話されていましたが、そのactix-webの、マクロを置き換えるようなかたちでpaperclipのものを使うと、最終的にOpenAPI形式の定義が自動生成されるというものです。

上から順番に見ていくと、paperclip由来のマクロや関数を融通して、構造体にはこれがAPIのものであるという印を付けていきます。関数はこちらですね。構造体と関数で付けるものが違いますが、それを付けていきます。

これまではサービスを書くだけでしたが、その前後にAPIをラップしてやるのと、それをどのエンドポイントで出力するのかを書きます。(スライドを示して)これぐらいの差がありますが、だいたいactix-webの記法のまま、同じように書けます。

これをビルドすると、先ほどのエンドポイントでswagger、OpenAPIの定義ファイルを出力できます。これは非常に便利かなと思います。

もうちょっと大きくなって、クレートを分割した時とかにどう書けるのかを、この先調査して使っていくことになるのかな、と感じています。

他のチームへ波及効果をもたらしていきたい

では今回のまとめです。私たちのチームは将来にベットするかたちで、まずはRustで書いてみるという実験のコストを支払ったと言えます。

今回のイベントは、多くの方に集まってもらっていて、仕事でRustを書きたいエンジニアもけっこういるのかなと思っているので、そういう方はぜひチームに入ってもらえればうれしいです。

社内にいくつもRustで開発しているチームがあるのも魅力的だと思います。スタックが共有できたり、ノウハウを共有できたりというところで、効果がすでに現れてきていると思っています。

後発の私たちのチームは、まだ恩恵を受けているだけの状態なので、これから他のチームに波及効果を出していけるようにがんばりたいです。

最後に。チームメンバーを募集しています。MeetyのURLを作っているので、ご興味を持った方がいれば申し込んでほしいと思っています。以上です。ありがとうございました。