イベントソーシングとWasmは相性がいい: SekibanWasmRuntimeが目指す多言語ES+CQRS
株式会社ジェイテックジャパン CTOの高丘 @tomohisaです。
Sekiban という、C# のイベントソーシング / CQRS フレームワークを作っています。
今回の記事で扱う SekibanWasmRuntime は、こちらのリポジトリで公開しています。

この記事では、Sekiban を Wasm で動かすために作っている SekibanWasmRuntime について書きます。最初に考えていたのは「Rust でも書けるようにしたい」ということではありませんでした。多言語で書けることは、Wasm を選んだ結果として後から見えてきた利点です。
なぜ作ったのか
最初の問いは、Sekiban をサービスとして提供するにはどうしたらいいか、でした。イベントソーシングのバックエンドを、利用者が毎回自分で立てなくてもよい形にしたい。できれば、Actor Model を使った安定した実行環境も含めて、サービスとして使えるようにしたい。そこから考え始めました。
イベントソーシングでは、ドメインコードが中心になります。Command を受けたときに何を Event として出すか。Event からどう Projection を作るか。そこは利用者ごとの業務ルールそのものです。つまり、Sekiban の実行基盤をサービスとして提供したいなら、利用者ごとのドメインコードをどうサーバー側で動かすかを避けて通れません。
最初に頭に浮かんだのは、「サーバーをアップデートする」のではなく、「プログラムをアップロードする」形でした。利用者がドメインコードをアップロードする。Sekiban 側のサーバーは、そのコードを読み込んで実行する。そうすれば、Sekiban の実行基盤はサービスとして提供しつつ、業務ルールは利用者が持てるかもしれない。
ただ、外からアップロードされたコードをそのままサーバー上で動かすわけにはいきません。ファイルシステムに触られたら困ります。ネットワークに勝手に出られても困ります。CPU やメモリを食い潰されても困ります。このあたりを考えると、Wasm が自然な選択肢に見えてきました。
Wasm は、ブラウザで使うためだけの技術ではありません。サーバー側で、制限された実行環境として使えます。ホスト側が許した機能だけを渡して、ドメインコードを小さな module として動かせます。SekibanWasmRuntime は、この考えから始まっています。
いま公開しているもの
ここは最初に分けておきます。今公開している SekibanWasmRuntime は、アップロード型のマルチテナントサービスそのものではありません。公開済みなのは、セルフホストできる runtime、public container、NuGet packages、Rust crates、samples です。
つまり、まず公開したかったのは、Sekiban を Wasm で動かし、イベントソーシングのドメインコードを module として渡せる実行環境です。まだ preview 段階ですが、試験用のデモ専用ではなく、ライセンスの範囲でセルフホストや自分のプロダクションコードに組み込める形で公開しています。
public container を使えば、まずローカルで runtime と Postgres を立てて動かせます。さらに自分でサーバーを立てるなら、C# / Orleans の構成を持ってセルフホストすることもできます。Orleans の設定には難しい部分がありますが、SekibanWasmRuntime はそこを隠したサービスだけを前提にしたものではありません。この記事では、この実行環境をなぜ Wasm で作ったのか、そして C# 以外の言語でドメインコードを書けることがなぜ自然に出てきたのかを書きます。
Actor Model と managed な実行環境
サービス化を考えるうえで大きかったのが Actor Model です。イベントソーシングのバックエンドをちゃんと動かそうとすると、インフラ構築が少し面倒になります。イベントを保存するだけなら分かりやすいのですが、実際には、特定の集約やタグに対する Command をどう並べるか、Projection をどう動かすか、同じデータに対する処理が並行して壊れないようにどう守るか、という話が出てきます。
Actor Model は、データごとに処理を1つずつ順番に扱うことで、並行処理を安全にする考え方です。Actor は、状態を持つ実行単位です。特定のデータに関する処理を一つの Actor に寄せることで、そのデータに対する処理を扱いやすくします。同じ対象への処理をむやみに並列で走らせない。これによって、安定性と一貫性を確保しやすくなります。
Sekiban では、Actor Model を C# 製のフレームワーク Orleans を使って実現しています。ただ、Actor Model を自分で運用するのは簡単ではありません。ステートフルな実行環境なので、配置、スケール、再起動、永続化、並行実行の扱いを考える必要があります。イベントソーシングのよさを使いたいだけなのに、先に分散実行環境の構築で疲れてしまう。これはもったいないと思っています。
最近、Cloudflare Durable Objects の話をよく目にするようになりました。Cloudflare のドキュメントでは、Durable Objects は stateful serverless applications を可能にするものとして説明されています。また、Durable Object は globally-unique な single-threaded instance として、自分の persistent storage を持つ、と説明されています。
これは、Actor Model 的で状態を持つ実行を、managed な仕組みとして使いやすくする流れの一つだと見ています。もちろん、Cloudflare Durable Objects と Orleans は同じものではありません。Sekiban Cloud が Cloudflare Durable Objects の上に作られている、という話でもありません。ただ、ステートフルな実行単位を managed にして、開発者がそのコンセプトを使いやすくする、という方向には近いところがあります。
Actor Model の価値は分かりやすいのに、使える環境を自分で作るのは面倒です。Cloudflare は自分たちの技術力とインフラで、その面倒な部分を managed にして見せました。Sekiban でも、イベントソーシングと Actor Model のよいところを、もっと簡単に使えるようにしたい。その思いが、SekibanWasmRuntime と Sekiban Cloud の両方につながっています。
Wasm を安全な実行境界にする
Wasm は WebAssembly の略です。名前には Web と入っていますが、いまはブラウザだけのものではありません。サーバー側で、制限された実行単位として使えます。
SekibanWasmRuntime では、Wasm を「アップロードされたドメインコードを安全に実行するための境界」として使います。たとえば Rust で Decider を書き、それを Wasm にコンパイルします。SekibanWasmRuntime は、その Wasm module を C# / Orleans のランタイムから呼びます。
このとき、Wasm module はホストから渡された入口を通してしか外側とやり取りできません。ホスト側である SekibanWasmRuntime が、何を渡すかを決めます。Wasm にすればどんなコードでも完全に安全になる、という話ではありません。それでも、ホスト側が渡す機能を絞り、サンドボックスの中で動かせることは、この用途には大きいです。
ここで初めて、多言語の話が出てきます。Wasm を境界にすると、ドメインコードは Wasm に対応した言語で書けます。C# でも Rust でも書けますし、WASI / Wasm に対応した言語なら、将来的に同じ考え方で載せられます。多言語対応は最初の目的ではありませんでしたが、Wasm を選んだ結果として得られた良い副産物でした。
現時点での対応状況は、次のように分けて考えています。
| 言語 | 現在の位置づけ | 備考 |
|---|---|---|
| C# | 公開パッケージあり | primary supported sample |
| Rust | 公開 crate あり | primary supported sample |
| Go | reference sample | experimental。C# / Rust と同じ公開パッケージや CI gate の支援レベルではありません |
| TypeScript | reference sample | experimental。C# / Rust と同じ公開パッケージや CI gate の支援レベルではありません |
| MoonBit | reference sample | experimental。C# / Rust と同じ公開パッケージや CI gate の支援レベルではありません |
| Swift | reference sample | experimental。C# / Rust と同じ公開パッケージや CI gate の支援レベルではありません |
Wasm の境界そのものは C# と Rust だけに閉じていません。ただし、いま公開パッケージとして整備しているものと、reference sample として同じ考え方を動かしているものは分けて見ています。
SekibanWasmRuntime の境界
SekibanWasmRuntime は、Sekiban のイベントソーシング / CQRS の実行基盤に Wasm の境界を足す runtime layer です。図にすると、分けたい場所はこうです。

左側は C# で実装されている Sekiban core です。イベント保存、Command 実行、Projection の管理、Orleans による Actor Model の実行はここにあります。右側が Wasm module です。ここに、利用者ごとの Decider や Projection を置きます。
この図で言いたいのは、Sekiban core まで多言語にするという話ではありません。core runtime は C# / Orleans のままです。Wasm にするのは、利用者ごとのドメインコードです。そして、Wasm にした結果として、C# 以外の言語でも書けるようになります。
イベントソーシングとCQRS
イベントソーシングでは、現在の状態そのものを主データとして保存しません。保存するのは、何が起きたかです。天気予報のサンプルなら、WeatherForecastCreated のようなイベントを保存します。現在の状態が欲しいときは、イベントを順番に適用して作ります。
このやり方だと、後から別の読み取りモデルを作りやすいです。イベントが残っているので、Projection を追加して再構築できます。その代わり、Decider と Projection が重要になります。Command を受けたときに、どの Event を出すか。Event から、どう読み取り用の状態を作るか。ここにアプリケーションごとの業務ルールが入ります。
CQRS は、書き込みと読み取りを分ける考え方です。Command は変更するためのリクエストです。Query は状態を読むためのリクエストです。イベントソーシングと組み合わせると、Command を受け、Decider が Event を出し、Event を保存し、Projection が読み取り用の状態を作り、Query で読みます。
この流れを見ると、Decider と Projection は runtime core から分けやすい場所にあります。Command を受ける API、イベント保存、Orleans の実行管理は C# 側に置く。ドメインの判断や Projection のロジックは Wasm module に置く。もともと境界が多いアーキテクチャなので、Wasm module という境界を入れても無理が少ない。ここが、SekibanWasmRuntime を作っていて一番納得したところです。(ここでは概念上の境界の話です。実際の command 実行の置き場所は、serialized runtime contract と sample の経路に従います。)
Sekiban Cloud との関係
SekibanWasmRuntime と並行して、Sekiban Cloud も作っています。Sekiban Cloud は、CQRS/ES の backend を managed にするための Web service として考えています。ただ、この2本の記事の中心は Sekiban Cloud ではなく、公開した SekibanWasmRuntime です。
Sekiban Cloud は、SekibanWasmRuntime の先にある managed な利用形態の一つです。利用者のドメインコードを安全に受け取り、イベントソーシングの backend と Actor Model の実行環境を managed にするには、ここで説明している Wasm runtime の考え方が必要になります。
public な contract は SekibanWasmRuntime 側に置いたままにします。Sekiban Cloud は、その contract に追従する managed 実装です。そこに credential、テナント分離、サービスの登録・管理といった、managed service に必要な仕組みを足していきます。一般提供時期や SLA は、まだここでは書きません。ただ、Sekiban Cloud は、できるだけ早く、まずはプレビューとしてリリースしたいと思っています。
ただし、この記事で紹介している公開済みの内容は、SekibanWasmRuntime の packages、crates、runtime container、samples です。Sekiban Cloud は作成中です。
ライセンス
SekibanWasmRuntime は Elastic License 2.0 で公開しています。利用、変更、再配布、セルフホストはできます。社内利用もできます。一方で、第三者向けの hosted service、managed service、SaaS として SekibanWasmRuntime の主要機能を提供する場合は、別途 J-Tech Japan との商用ライセンスが必要です。
このライセンス境界は、NuGet packages、Rust crates、runtime container にも同じように適用されます。NuGet packages も crates.io の Rust crate も Elastic License 2.0 です。Sekiban 本体は Apache License 2.0 のままです。SekibanWasmRuntime のライセンスとは分けて考えてください。
なのでこの記事では、基本的に「公開」という言葉を使っています。いわゆる OSI approved な open source と同じ意味の OSS ではありません。
イベントソーシングを試しやすくしたい
これによって、イベントソーシングをもっと簡単に、そして C# や Rust など Wasm 対応言語で試してもらえる助けになれば嬉しいです。具体的な起動方法、C# / Rust の sample、public container で確認している command、query、materialized view の流れは、詳細編で扱います。
詳細編
詳細編はこちらです。
Discussion