「estie、Rustで新プロダクト作るってよ」イベントレポート

イベントの概要


2022年2月16日(水)に行われた「estie、Rustで新プロダクト作るってよ 」のイベントの様子をお届けします!
本記事では、ハイライトを中心に紹介させていただきます。
イベントのLTの内容に興味ある方は、ぜひ資料estie、Rustで新プロダクト作るってよ - 2022/2/16 - Speaker Deckや、イベントアーカイブ(estie、Rustで新プロダクト作るってよ - YouTube)もご覧ください。

経緯

estieは2022年1月12日、約10億円のシリーズA資金調達を行いました。
この大きな資金調達を経て主力サービスである「estie pro」を拡張するマルチプロダクト戦略のもと、新しいプロダクトをどんどん開発しています。

本イベントは、新規プロダクトの開発言語にRustを採用したestieでの活用例を紹介する目的で、開発に従事している2名が登壇してLTを行いました。

▼LT内容

  • kenkoooo: RustでWebアプリ事始め
  • Tomu Hirata : Rust x Next.js x Auth0でユーザ認証

kenkoooo: RustでWebアプリ事始め

実践Rustプログラミング入門 - 秀和システム あなたの学びをサポート! の著者の1人であるkenkooooさん。
estieでは”アルバイト”としてプロダクト開発に参加しています。

今回は「RustでWebアプリ事始め」ということで、Rustの世間のイメージから実際のestieでのコードを元に開発しやすくなっている点まで幅広く紹介するLTからスタートしました。

【プロフィール】
kenkoooo 「アルバイト」
東京大学理学部化学科を卒業後、自宅でニートに従事。生活費のためにソフトウェアエンジニアとして就職し、国立情報学研究所、リクルート、SoundHoundを経て、現在はIndeed Japanにて機械学習・自然言語処理を用いてデータからデータを生み出すシステムの開発に取り組む。estieではアルバイトとして Rust を用いたウェブアプリ開発に取り組んでいる。

Rustとは

Rustは上に挙げられるような特長があり、多くのユースケースで利用されています。
一方で、世間的にはRustが難しくて「コンパイラのチェックが厳しすぎて一生コンパイルが通らない... 」「所有権?知らない概念がたくさん出てくる...」といったイメージがあるといいます。
kenkooooさん曰く、これはRustが難しいのではなく「そもそもプログラミングが難しい」だけ。

このようなプログラミングの難しさを説明する例として、JavaのListを受け取って要素を追加するような“よくある”関数の例をあげていました。

void listOperation(List<String> list) {
  list.add("Awesome!");
}

この関数は実は List がミュータブルであることを暗黙に仮定していて、Javaの有名な3rd partyのライブラリ”Guava”に含まれているImmutableList を渡すと Runtime Exceptionを起こしてしまいます。
このため、この関数の呼び出し元がlistOperationの処理内容を把握している必要があります。
一般には、関数名やコメントなどでこのような制約を把握しやすくするように対応することになりますが、このような方向では限界があります。

一方Rustでは、listを引数として渡す際に以下のように複数の記法を区別しています。

これらを区別しないとコンパイラが通らないので、「一生コンパイルが通らない」など初めて触れる方からすると、他の言語よりもハードルが高いと感じるかもしれません。
ですが、これは上記のような例を考えると、プログラミングの難しさをきちんと捉えてコンパイラにチェックしてもらうことを可能としているということでもあります。

また、Rustは低レイヤーやシステムプログラミングに使えることがよく知られているため「Linuxなんて作らないので」と自分には関係ないものだと思われることも多いですが、kenkooooさんとしては、汎用プログラミング言語として便利な機能が多く普段使いにもおすすめの言語とのことです。


Rustでのweb開発

Rustでは、JavaScriptにもあるような非同期構文 async/awaitが2年前に安定化し、古参のライブラリたちもこの記法に対応するようになって簡単に非同期処理を書きやすくなったとのことです。
estieでは以下のようなフレームワークを利用しています。

ここからestieの実際のコードを挙げながらのRustで楽にAPIが書けることの具体的な説明に進みました。

#[post("/api/buildings")]
pub async fn create_building(
  request: Json<CreateBuildingRequest>,
  pool: Data<MySqlPool>,
  auth0_id: ReqData<Auth0Id>,
) -> Result<HttpResponse> {
  let building = Building::from(request.building);
  verify_user(&pool, &auth0_id.0).await?;
  let id = Building::insert(&pool, &building).await?;
  Ok(Json(CreateBuildingResponse { building_id: id }))
}

この中で

let value = f()?;

のように ? を用いて記載することで、Goなどの他の言語では以下のように表現される、エラーハンドリングの処理を簡潔に記載することができます。

value, err := f()
if err != nil {
  return nil, err
}

また、先頭行の #[post("/api/buildings")] のようにactix-webのマクロを付与することで、このエンドポイントに対応したPOSTの関数としてコンパイラに必要なコードの生成を任せることができます。

sqlxとRustマクロ

RustのSQL環境としては、Dieselという古参ORMがあるのですが、非同期処理に対応していません。
そのため、今回のプロダクトではsqlxという非同期処理に対応したSQLライブラリを採用しました。
クエリを直接記載する必要があるのですが、強力なマクロの機能によりクエリを直接記載することの怖い部分をカバーすることができるのが魅力です。

async fn find_by_bid(pool: &MySqlPool, bid: i64) -> Result<Vec<RawUnit>> {
  let units = sqlx::query_as!(
    RawUnit,
    "SELECT * FROM units WHERE building_id=?",
    bid
  )
  .fetch_all(pool)
  .await?;
  Ok(units)
}

上記のようなコードの例を元に説明を進めています。

query_as!として記載されているのはマクロで、SQL文の実行結果を以下のようなRawUnitという構造体に詰めて返すようなコードになっています。

struct RawUnit {
  unit_id: i64,
  area: Decimal,
  floor: u8,
  building_id: i64,
}

このような処理を書くと、テーブルのareaカラムに実はnullが入っている場合にランタイムエラーが発生してしまうので、テーブル構造がきちんとマッチしているか怖くなってしまうと思います。

sqlxではコンパイル時にDBに型を問い合わせてRustの構造体の型とSQLのテーブルの型の整合性をチェックしてくれるので、実はこのような心配は必要ないです。

この「SQLクエリの返り値の型をコンパイル時にチェックする」機能は他の言語/FWではあまり一般的ではないアプローチということもあり、発表後にも多く質問が集まったトピックでした。

Tomu Hirata : Rustでauth0を使ってユーザ認証を作ってみた

続いてSWEとして同じプロダクトの開発に関わっているtomuさんからユーザ認証周りの設計について話していただきました。


【プロフィール】
平田東夢 ソフトウェアエンジニア
東京大学情報理工学研究科に属し、動画像処理や自然言語処理を研究テーマとする。以前はfreee株式会社でSREチームに所属。estieでは会社登記前のアルバイトから従事し、estie proアプリの開発、データパイプラインのプロダクトオーナー、新規プロダクトの立ち上げなどに取り組んでいる。

背景

estieでは、今回Rustで開発を進めているプロダクトを含めて複数のプロダクトを同時開発する、マルチプロダクト化を進めています。
これらのサービスは将来的に1つのエコシステムとして統合される構想があるため、全社的な認証基盤の検証の意味も含めてIDaaSサービスであるAuth0の新規プロダクトへの導入を決定しました。

プロダクトの構成と認証フロー

今回説明するプロダクトの構成と認証のフローは上記のようになっています。
このような構成でAuth0を含めて実現している認証のフローについて、それぞれポイントを解説していく流れでLTが進んでいきました。

Auth0 × Next.js

Auth0 と FE側のサーバであるNext.jsでは以下のことを達成する構成としています。

  • クライアント側で認証情報を保持したい
  • トークンの偽装を難しくしたい

これらを実現するために、以下のような処理を構築しています。

  • Next.jsのフロントエンドサーバからID, passwordでログインする
  • Auth0からID tokenが返却される
  • このID tokenをクライアントサイドではsessionのcookieとして保持しAPIとの通信時に利用
  • その際に、Next.jsのサーバ側でtokenを暗号化することでクライアントサイドでこの内容を変更することは出来ないようにしている


Auth0 × Actix_web

続いてRustで構築されたバックエンド側とAuth0の結合について、以下を実現する構成としています。

  • リクエストしているユーザーの認証を行いたい
  • リクエストに付属したトークンの検証を行いたい

大まかな流れとしては以下のようになっています。

  • FEサーバからID Tokenが渡される(Auth0 × Next.js の項で述べたもの)
  • BEサーバがAuth0にJWKを要求する
  • Auth0がBEサーバにJWKを返却する
  • BEサーバでJWTの検証を行う
  • 検証の結果渡されたID tokenが正しければ、レスポンスをFEサーバに返却する

ここから、上記の流れを実装したRustのバックエンドのコードを解説する流れでLTが進んでいきました。
本記事では、このRustの実装自体の細かい解説は省略します。実際のコードベースで解説があるため、ご興味ある方は是非、登壇資料をご覧ください。

Q&A

最後に当日参加いただいた方から、Q&Aセッションがありました。
いくつか抜粋してご紹介します。

  • Q: Rustを採用して生産性はどうですか?
    • tomu: 自分は本プロダクトで初めてRustを製品開発に利用したのですが、最初はやはり「一生コンパイルが通らない状態」と感じることもあったが、少し時間をかけて慣れてきた。慣れてくると、コンパイラがきちんとチェックしてくれるのでランタイムエラーは減っていて中期的には生産性があがるようになった。
  • Q: Rustを触ってどれくらいの期間で開発に参加できるようになりましたか?
    • kenkoooo: estieのメンバだといろいろ書き始めるまでは1週間くらいで参加できるようになった。

最後に

estieでは今回ご紹介したバックエンドにRustを用いたサービスを含め、マルチプロダクトで製品開発を進めています。
今回LTしたチームのより詳しい働き方や、どんなプロダクトを作っているのかなど、ご興味ありましたらお気軽に声がけください。

カジュアル面談も大歓迎です。
hrmos.co

© 2019- estie, inc.