自己紹介

松田圭紀氏(以下、松田):キャディ株式会社 バックエンドエンジニアの松田と申します。私からは「Rust製の業務WebアプリケーションをRustでリプレイス」というテーマでお話したいと思います。

まずはじめに自己紹介です。私は1年半ほど前にキャディに入社して、バックエンドエンジニアをしています。主に受発注プロダクトのバックエンドをRustで開発したり、たまにTypeScriptでフロントを書いたりといった仕事をしています。

キャディに入社する前は別の都内のスタートアップ企業にいて、Goでバックエンドの開発をする仕事をしていました。技術的な趣味としては、開発環境をけっこういじくり回すのが好きで、今はWezTermというターミナルエミュレータの上でNeovimを起動して開発をしています。

リプレイス対象となったプロダクト

それでは本題に入ります。今回リプレイスしたものとしてお話しするプロダクトは、(この)画面自体はリプレイス前の画面になりますが、受発注管理プロダクトというものです。

機能としては、お客さまから受注をいただいたいろいろな製品を、最適な加工会社さまに発注をするというものです。加工会社さまの選定をする機能とか、あるいは加工会社さまから我々の拠点に納品をいただいて、そこで検品をして顧客にお届けするという一連のサプライチェーンを可視化したり設計したりといった機能を担っているプロダクトとなっています。

なぜリプレイスするのか

なんでリプレイスするのかですが、一言で言うと、ドメインモデルの前提が大きく変わってしまったという事情があります。もともとは多品種少量・受注生産というビジネスを扱っていましたが、キャディのビジネスが進化していくに従って、そこだけでなく中量産・見込み生産へとビジネスが拡張していきました。

どういうものかと言うと、(スライドを示して)左のこの図が多品種少量の図なんですが、もともとのドメインモデルとしては、基本的にまずお客さまから受注をいただいて、それから生産を開始します。それから、1つの製品を繰り返し何度も生産することも基本的にはあまりない前提でドメインモデルを組んでいたので、受注をいただく度に案件という箱が立ち上がって、その中に製品を登録したり、それぞれの製品をどの加工会社さまに発注するのかという情報を登録していったりするようなドメインモデルを構築してきました。

(スライドの)右側の図が中量産・見込み生産というものに対応しますが、中量産の世界では同じものを継続的に何度も発注をいただくことがあります。それから、受注をいただいてから発注をするというだけではなくて、需要を先読みして製品を作って在庫を溜めておいて、受注をいただく度にその在庫を出していくということも行うようになってきました。

そうすると、案件の中に製品があるというかたちではなくて、部品表のマスタというものを持ってそこに製品のマスタを管理しておく。その上で、製品マスタを見積であったり受注であったり発注だったりに紐づけていくというようなかたちのドメインモデルが求められるようになってきました。

「こういったことは既存のコードベースの拡張では対応できなかったんですか」という疑問が出てくると思います。こちらに関しては、既存のコードベースの拡張でも対応できなくはありませんでした。ただ、Entity間の関係性を全面的に見直す必要があるので、相当な工数がかかるということがあり、工数を考えると全面リプレイスしてしまったほうがお得だと今回は判断をしました。

全面リプレイスすることによって現在のビジネスモデルを前提とした最適な設計ができるというところもあるし、ついでにいろいろ溜まっていた技術的な負債を返済ができるといったところのメリットを考えると、リプレイスしたいということになりました。

互換性についても社内ユーザーのみが触っているところは必ずしも確保しなくてもいいという判断をして、社外ユーザーさま、具体的には加工会社さまが関わってくる部分のみ互換性を確保する方針でリプレイスをしています。

リプレイスの全体像とアーキテクチャ図

ここからはリプレイスの全体像をお話していきたいと思います。まず今回のリプレイスにあたって、開発チーム内で大きく3つの方針を最初に設定しました。まず1つ目ですが、YAGNIを重視しようと。YAGNIというのは「必要になるまで作らない」ということです。リプレイス前のシステムではスケールをした先を見据えていろいろと複雑な機構を作り込んでいた部分もありましたが、結果としては早すぎる抽象化・最適化になっていた感覚が否めなかった。

かなり複雑なものを作ってしまった結果、保守にコストがかかってしまっていたところがあるので、今回は早すぎる抽象化をなるべく避けていこうと思っていますというのが1つです。

2点目は学習コストの削減というところです。ここも1つ目と被るところがあります。複雑化してしまった結果、新しくジョインされた方のキャッチアップにやや時間がかかる状況が生じてしまっていたので、リプレイス後はなるべく一貫性のある規約に従ってコードベースを構築して、新しく入られた方が速やかにキャッチアップをして、ドメインロジックの実装に集中できる状態を作っていきたいと思っています。

3点目はテスタビリティを向上したいというところです。もともとのアーキテクチャでは内製したO/R(Object-Relational)マッパー的なライブラリを使っていた事情もあって、外部I/O(Input/Output)のモック化がしづらい状況になってしまっていました。なので、新しいアーキテクチャではテスタビリティも高めていきたいと思っています。

(スライドを示して)こちらが全体のアーキテクチャの図となっています。まずUI、BFF(backend for frontend)、マイクロサービスの3層構成となっていて、UIがNext.js、BFFがNestJS、バックのマイクロサービス群がRustで書かれている構成になっています。マイクロサービス群はドメインのコンテキストに従って分割をしていて、具体的には部品表であったり見積、受注、発注などのコンテキストごとにサービスが立っているかたちになっています。

特徴的なところをいくつか説明します。まず、マイクロサービスであるにも関わらず、DBが1つということになっています。1つのDBを共用するのはアンチパターンと言われることも多いですが、今回私たちはあえてこういう選択をしました。

その理由としては、マイクロサービスあるあるのような話で、マイクロサービスの境界の切り分けを間違えて、あとで辛くなってしまったという話はよく聞くと思っています。そういった「サービスの切り方を間違えたな」という時に、なるべく早めに軌道修正ができるようにということで、最初は1DBからスタートをしていくことを考えています。ただし、将来的に分割しやすいように、論理的なスキーマを分けて運用をしていくとしています。

なぜRustを使うのか

こちらの図で「マイクロサービスは全部Rustです」と紹介しましたが、なぜRustを使うのかについて説明をします。キャディでは以前からRustを使ってきましたが、Rustを使っている中で一番強く感じていたのは、型の表現力の高さと堅牢性があるところが非常にありがたいと思っています。こういったRustの長所を引き続き活かしていきたいということで、Rustを選択しました。

組織面で言うと、現状もRustを使っているので当たり前ではありますが、キャディの中ではRustがもっとも利用経験者が多い言語であるということで、Rustを使うことによってチーム間の人員の流動性を確保できる。それから、社内に蓄積された知見を利用できることも、Rustを引き続き使っていく決め手となりました。

逆に、今回検討する中で、Rustを使うことに対する懸念もいくつか出てきました。そこについて説明します。まず1つは、すでに入社しているメンバーはともかく、新規に入るメンバーにとって、学習コストが大きいのではといったところが一番の懸念として上がりました。しかし、こちらについては前提として、キャディでは特定の言語の経験を採用の条件とはしていないところがあり、どの言語を選択したとしても、その言語が未経験の新規メンバーの方は必ず発生します。

そういう時に、「どの言語であっても一定の学習コストが発生するところはそんなに変わらないよね」といった議論がありました。それから、これまでに入社したRust未経験メンバーも無事にキャッチアップできているので、ここについてはそんなに大きな問題ではないだろうと判断をしました。

それからもう1点として、外部SDK(Software Development Kit)が提供されないなどで困るケースがあるのではないだろうかということがありますが、こちらについても少なくともこれまでの経験では大きな問題は生じていません。仮にそういったケースが発生した場合は、そういった部分だけ別のマイクロサービスに切り出して別の言語で開発する選択肢もあるというところで「こちらも大丈夫だろう」という判断に至りました。

リプレイスで大きく変わったこと:モノレポを導入

それでは、今回のリプレイスについてもう少し細かい具体的な点を紹介していければと思っています。まず今回大きく変わったこととして、モノレポを導入することにしました。これは、1つのGitHubのリポジトリ上にUIとBFFとすべてのマイクロサービスを共存させるというものです。

ディレクトリ構成としては、(スライドの)下に書いてあったディレクトリの図のようになっています。トップレベルがprotoであったりrustであったり、typescriptと言語別に切られていて、rustディレクトリの中にはマイクロサービスごとのディレクトリがあります。typescriptの中にはuiとbffがあります。こういった構成を採用しました。

いち開発者として、こういう構成を採用して非常に開発生産性が高まってよかったなと思っています。これを実現するためにCircleCIのpath-filteringという仕組みを使っていて、PR(Pull Request)ごとにdiffが立っているディレクトリのパスに応じて、必要なCIのジョブだけを起動することをしています。

また、ここに並んでいるマイクロサービス群を自動生成するスクリプトを用意していて、新しいサービスを作る時もコマンド1つでサービスの雛形を作れるようになっています。

クリーンアーキテクチャの採用

こちらはバックエンドの各マイクロサービスのファイル構造ですが、アーキテクチャとしていわゆるクリーンアーキテクチャを採用しました。クリーンアーキテクチャの(スライドの)右のような同心円の図を見た方も多いと思いますが、我々のアーキテクチャは3層構成になっていて、内側がドメイン、その外側がユースケース、そのさらに外側がインフラというかたちになっています。ドメインロジックはドメイン層に集約をします。

インフラ層、例えばDBアクセスなどに依存する場合は、ドメイン層の中に定義されたtraitを経由してインフラ層に依存するかたちを取っています。ちなみにtraitと言うのは、他の言語で言うところのインターフェースに対応するものになっています。

この具体的なディレクトリ構成に関してもdomain、usecase、infraというように、クリーンアーキテクチャのレイヤーの構造に従ってディレクトリを切ることによって、新規で参画されたメンバーもすぐにキャッチアップできるように配慮しています。

ドメインロジックの設計はドメイン駆動設計を採用

それからドメインロジックの設計に関しては、ドメイン駆動設計を参考として取り入れています。ドメイン駆動設計はいろいろな概念の集合体のようなところがありますが、一番大きなものとしては“集約”という概念があると思っていて、部品表や見積などのそれぞれのコンテキストの中に、いくつかの集約と呼ばれる単位を定義しています。

集約は1つまたは複数のエンティティから構成されていて、データの更新を行う場合、必ず集約単位で行うルールを定めています。これによってそれぞれの集約がその集約の内部のデータの整合性を担保しているということで、それぞれの集約が守るべきルールを確認したい時は、集約の定義を確認するとそれがわかるようになっているアーキテクチャを取っています。

マイクロサービスの内部を小さなcrateに分割

こちらはRust固有の話になりますが、Rustはcrateと呼ばれる他の言語でいうところのパッケージやモジュールに相当する単位を持っていて、このcrate単位でビルドが走るようになっています。今回、ビルド時間を短縮するために、1つのマイクロサービスの内部を(スライドの)下の図のようなかたちで多数の小さなcrateに分割することを行っています。

依存関係の矢印がありますが、こうすることによって依存関係がないcrate同士は並列にビルドでき、大きくビルド時間を短縮することが可能になっています。

テスタビリティの向上

最後にテスタビリティの向上についてです。こちらは外部I/Oをモック化するために、Rustではもっとも定評のあるmockallというcrateを利用しています。mockallのcrateでリポジトリのtraitをモック化することで、ユースケース層に関しても単体テストをかけるようにしています。

今回は結合テストの自動化にもチャレンジしていますが、現状は結合テストのコードもRustで書くことをしています。gRPCのリクエストを飛ばすための専用のcrateを定義して、そのcrateをcargo testで走らせることによって、実際に立ち上がったサーバーのgRPCのエンドポイントを直接叩きにいくかたちで結合テストを行うようになっています。

リプレイスでもっとも大変なことはドメインモデリング

まだ取り組み始めたばかりなので苦労していることもいろいろあり、やはり一番苦労が大きいのはドメインモデリングです。事業規模が拡大していく中で、案件担当者であったり検品拠点スタッフであったり会計担当者であったり、ユーザーごとのニーズの多様性が増大してきたところが1つあります。

それから見込み発注、在庫引当、装置一式組み立てといった、いろいろな概念が新しく発生しているので、日々それらをどんどんシステムに取り組んでいくところで苦労しながら開発をしているところです。

それから、マイクロサービスに関しても本格的なマイクロサービスの導入はキャディでは始めてになるので、サービス間通信であったり分散トレーシングであったりサービスメッシュであったりの面でもいろいろ検討している状況です。

それから、テストQAに関してもこれからどんどん品質を高めていかないといけないと思っています。

といったかたちで駆け足になってしまいましたが、私たちの今回取り組んでいるリプレイスの概要は以上となります。

こういったリプレイスを含め、開発に一緒に取り組んでくださる方々を募集しています。複雑な業務ドメインのモデリングが好きな方、それからマイクロサービスに精通されている方がいましたら、ぜひカジュアル面談でお話できればと思っています。

ご清聴いただきどうもありがとうございました。