トレタ開発者ブログ

飲食店向け予約/顧客台帳サービス「トレタ」、超直前予約アプリ「トレタnow」を開発・提供するスタートアップ企業です。

PoCのために限られたリソース・時間でありもののツールを組み合わせてリリースした話

*この記事は トレタ Advent Calendar 2020 の14日目 の記事です。

前回のアドベントカレンダーから早くも1年、今年も寒さとともに腰が軋むサーバーサイドエンジニア 石谷です!

今年はいろいろと激動の年でした。 トレタはというと、コロナ禍のど真ん中なプロダクトを提供するなかで自分たちに何ができるのか、東奔西走した1年だったかなぁと思います。 またプライベートはというと、Youtuberを見てFortniteにハマり、Youtuberを見てデッドバイデイライトにハマる。 (そうじゃない、もっとあるだろ。あ、今年の頭に結婚したんだった。ちなみに結婚式はコロナで延期しました 泣)

はじめに

そんな中で、僕が今年の最後に担当したのは「席指定予約」を実現するバックエンド、「汎用空き時間枠管理システム」という基盤部分になります。 「席指定予約」というのはタイトルにあるPoCプロダクトで、例えば映画館のようにご予約されるお客様が席を直に選択して予約ができるサービスです。詳しくはトレタ代表 仁さんのnoteをご覧ください。 また「汎用空き時間枠管理システム」というのは、お店にあるテーブルだったりコース料理だったり、そういったものを等しく空き時間を管理すべきリソースとして抽象化した概念で管理できるようにしようよ。という思想のもと、CTOであるハオさんを主導として立ち上がったプロジェクトです。「席指定予約」における「テーブルの確保」をこの「汎用空き時間枠管理システム」が担っています。

この「汎用空き時間枠管理システム」は単体では機能しません。現実世界のドメインをそのままモデリングする具象化レイヤーと連携する必要があり、そのドメインにおける「空き時間枠の管理」だけを担います。このように抽象化レイヤーとして「空き時間枠の管理」という責務のみを全うすることで、ドメインが変わっても同じく「空き時間枠の管理」については担当することができます。

f:id:issuy:20201209142924p:plain
テーブルをリソースとして扱うシステム

このプロジェクトについては、私がメディア連携開発チームにいた頃からの課題である「予約台帳のバックエンドが、空席にまつわる機能のエンハンス・API連携先の拡張のサイクルが鈍化している」の解決を狙ったアプローチでもあります。 プロジェクトは今年の頭ごろから議論が始まっていましたが、抽象化されているが故に社内への説明もしづらく、かつそこに集まったメンバーは他のタスクも並行していたため進みが悪い状態でした。どこか目に見えるプロダクトとして一度作ってみんなに見てもらいたいよね、という話をしつつ設計についてある程度深まったタイミングで一度凍結。

そんなプロジェクトがふたたび日の目を浴びたのが10月頃、豚組しゃぶ庵βに向けた席指定予約にこの基盤を入れられないかという話が浮上しました。 大枠の設計はできていたので、これをどのように今回の要件の中で実装するかを会話しはじめました。 だいたいこういう形で実装していけばいいよねというのが固まってきた10月半ば頃、リリース日が明らかになりました。それが12月7日。 実装期間が1ヶ月というスケジュールの中で、要件にない仕様を削ぎ落とし「技術的な検証」や「構想していたものを形にする」という部分を開発的な目標としました。 もちろんユーザー(この場合は豚組しゃぶ庵で働くみなさま、予約してご来店くださるお客様)にご迷惑をかけないことが大前提で、これをスケジュール内で担保できない場合は「席指定予約」の開発を見送るという選択肢も残されていました。

そんなこんなで慌ただしく過ぎ去った11月。前置きが長くなりましたが、今回ご紹介するのは「汎用空き時間枠管理システム」を取り巻くアーキテクチャについてです。実際は理想に対して実装できたのは2,3割というところですが、アーキテクチャとしては当初の構想を反映できたと考えているので、現時点でのアーキテクチャについて記載します。

Contentful→Hasura→Algoliaを連携させるシステム

「汎用空き時間枠管理システム」を取り巻くアーキテクチャとして、まずは関連する主要なサービスを紹介します。それがContentfulHasuraAlgoliaです。それぞれに「汎用空き時間枠管理システム」を成り立たせるための役割を持たせたので、後ほど説明します。

「Contentful→Hasura→Algoliaを連携させるシステム」と矢印を書いていますが、これは「汎用空き時間枠管理システム」におけるデータの流れを表しています。これについても後ほど説明していきますが、「汎用空き時間枠管理システム」という抽象化した概念を成り立たせるためのデータフローになります。

また今回これらのXaaSを採用したのは、限られたリソース・時間での実装、および検証という都合がありました。

Contentful、Hasura、Algoliaのそれぞれの役割

Contentful

飲食店の方に使ってもらうCMSという位置づけで、お店に存在するテーブルや営業日などの設定値を管理するのがContentfulに込めた役割です。そのため、ここは先述の具象化レイヤーにあたります。今回のプロジェクトでは「席指定予約」というドメインのための設定値を管理するレイヤーです。

Hasura

「空き時間枠の管理」というのが、Hasuraに込めた役割です。ここが「汎用空き時間枠管理システム」のコア部分になります。「席指定予約」における「テーブル」のような具象化した概念では扱わず、「リソース」という抽象化した概念で扱う抽象化レイヤーです。

「空き時間枠の管理」には大きく2つの重要な役目があって、1つは「リソース×日」でデータを表現することです。これをstockと呼びます。これは具象化すると「テーブル×営業日」にあたり、例えば「12月24日に1卓空いてる?」のような問い合わせに対して回答するためのデータです。もう1つは「stockを確保すること」で、これをstockのlockと呼びます。これは具象化すると、例えば「12月24日に1卓を確保したい」という問い合わせに応じる処理になります。また、このとき複数人が同時にstockのlockを問い合わせたとしても、確保できるのは1人だけという排他制御の仕様を守ります。

Algolia

「空き時間枠の検索」というのが、Algoliaに込めた役割です。「汎用空き時間枠管理システム」における空き時間枠の検索は全てAlgoliaが担います。Algoliaも抽象化レイヤーに分類されます。

ここでは「12月24日に1卓空いてる?」のような単純な問い合わせへの回答だけでなく、より複雑な「あらゆる検索軸への対応」を実現します。例えば「席指定予約」においてお客様は「12月24日に2名で禁煙席は空いてる?」のように問い合わせ、「汎用空き時間枠管理システム」は「その条件に合致する1卓が空いてますよ」と応答する必要があります。これが「席指定予約」ではなく「コース料理指定予約」だとどうでしょう?「12月24日に5000円〜6000円のコースは2名分空いてる?」のように検索軸が変わります。

これをHasuraのレイヤーで対応するとしたら「席指定予約」や「コース料理指定予約」またはその他のドメインで必要なあらゆる検索軸に対して素早くデータ取得するための機構(DB側でインデックス張るなど)が必要になります。つまり具象化レイヤーの要件が抽象化レイヤーである「汎用空き時間枠管理システム」に染み出します。これは「空き時間枠の管理」という責務のみを全うする上で致命的な問題になります。

このような問題を起こさないためにAlgoliaが「空き時間枠の検索」という役割を担っています。Hasuraでは具象化レイヤーにおける検索軸を「stockに対するメタデータ」として保持するだけで、特に活用しません。これらのメタデータはHasuraからAlgoliaへデータを受け渡す際に、検索軸として利用可能な項目としてデータ注入されます。ここでは「汎用空き時間枠管理システム」において基本となる「店舗、日、リソース」という概念はrequiredなものとして注入されますが、それ以外はメタデータが存在したらAlgoliaにもそのデータを持ちます。これで特別な対応なく、あらゆる検索軸に対応できるようになります。

※ただしAlgoliaで文字列を検索軸にする場合はFacetによるフィルタリングの設定が必要なので注意

Contentful→Hasura→Algoliaの連携

このような役割をもたせた各システムが「汎用空き時間枠管理システム」を成り立たせるために、それぞれがどのように連携しているか、その裏側の技術がどうなっているかを説明します。

「席指定予約」を成り立たせるための連携

まずは「空き時間枠」というデータが生成されるまでの過程です。Hasuraは抽象化した概念しか持っていないため、「席指定予約」という具象化レイヤーのドメインを抽象化した状態で受け取る必要があります。なので具象化レイヤーであるContentfulからテーブルや営業日といったデータを受け取り、stockとして扱えるようにします。次にAlgoliaですが、さきほども説明したように検索のためにHasuraからデータを受け取ります。

f:id:issuy:20201210173917p:plain
Contentful→Hasura→Algoliaの連携

これで「汎用空き時間枠管理システム」としては空き時間枠を管理するためのデータが揃いました。ですが実際に予約するお客様を忘れてはいけません。予約時の流れは以下のようになります。

f:id:issuy:20201210175040p:plain
予約時の流れ

  1. お客様は席指定予約Clientへ検索条件を入力
  2. 席指定予約ClientはAlgoliaで空き時間枠を検索
  3. 席指定予約Clientは日付とテーブルを指定して予約の作成を席指定予約BFFへ問い合わせ
  4. 席指定予約BFFはHasuraへstock(具象化すると12月24日の1卓)の確保を問い合わせ
  5. Hasuraはstockの確保に応じてAlgoliaへstockの確保を反映

このようにAlgoliaへデータが反映されれば、他のお客様が空き時間枠を検索したとしても確保済みのテーブルは表示されなくなります。また、ここでのHasura→Algoliaの処理はstockの確保状態が異なるだけで「空き時間枠」と共通のものを使用しています。

このような連携を実現するために、Contentful→Hasura→Algolia というデータフローを実装する必要があります。例えばContentful→Hasuraであれば、Contentfulで定義したデータ構造をHasuraで定義したデータ構造へ変換して置き換えるための「置き換えロジック」が必要です。しかし今回使用した各サービスだけではそういったロジックを実装することはできません。

幸い各サービスにはWebhookが実装されているため、これを利用することにしました。「置き換えロジック」をAWSのLambdaで実装し、APIGatewayが提供するエンドポイントへのリクエストでこの処理を呼び出すようにします。Webhookでは下流のシステムへデータを流すEventが発生した際にこのAPIGatewayにリクエストします。

f:id:issuy:20201214165402p:plain
Contentful→Hasura→Algoliaの間を取り持つシステム

以下ではそれぞれのWebhookについてどのような処理が呼び出されるかを紹介します。

ContentfulのWebhook

ContentfulのWebhookは、テーブルや営業日などの店舗の設定値を入力するフローの最後に「設定完了」を示すデータが更新されたタイミングでEventを発行するようにしています。

このEventを受けてLambdaで「置き換えロジック」が呼び出されるのですが、Eventによるリクエストボディに含まれるデータだけでは処理に必要なContentful側の情報が不足しています。そこでContentfulのGraphQL APIを使い、テーブルや営業日といったデータをLambda内で取得します。これをHasuraのデータ構造であるstockとして変換し、HasuraのGraphQL APIで送信します。

これでContentful→Hasuraが連携しました。

HasuraのWebhook

HasuraのWebhookでは「空き時間枠」にまつわるデータの更新を検知してEventを発行します。ここではテーブルや営業日などの設定の変更、および予約に伴うstockの確保時に漏れなくAlgoliaを更新するためにあらゆるデータ更新を検知する様にしています。ただしデメリットとして、Algolia上で同一のデータに対する無駄な更新Eventが発行される場合があります。本来であればこういった無駄のないようコントロールしたいところですが、今回は使える時間が限られているため後々の対応としました。

このEventを受けてLambdaで「置き換えロジック」が呼び出されるのですが、ここでもEventによるリクエストボディに含まれるデータだけでは処理に必要なHasura側の情報が不足しています。そこで先程も紹介したHasuraのGraphQL APIを使い、stockやそのlock状態、メタデータなどを取得します。 LambdaのロジックではHasuraから取得したデータをAlgoliaのデータ構造に変換し、AlgoliaのRestAPIで送信します。

ここでもAPIを叩くための認証が必要で、Hasuraへは先程と同様にAuth0のJWTを、AlgoliaへはAPI Keyを利用しています。

これでHasura→Algoliaが連携しました。

Hasura GraphQL APIの認証

ちなみにこれらのAPIを叩くためには、それぞれ認証を通す必要があります。Contentful, AlgoliaではAPI Keyによる認証をサポートしていて、API Keyを管理画面から取得してリクエストヘッダに付加します。ただしHasuraではMachine to Machineの通信のためにAPI Keyは提供していないようで、Auth0のJWTによる対応が必要です。そのためAuth0のMachine to Machine Tokenに対応しました。

これはHasuraのAPIを叩く際にAuth0からJWTを取得し、このJWTをリクエストヘッダに付加してHasuraのGraphQL APIを叩くという処理になります。このJWTを取得するためにAuth0のAPIを叩くのですが、ここでもやはり認証が必要です。Auth0のAPI KeyはAuth0の管理画面から取得し、リクエストヘッダに付加してAuth0のAPIを叩きます。

Contentful→Hasura→Algoliaを連携させるシステムの全体像

このようにContentful→HasuraとHasura→Algoliaのそれぞれの「置き換えロジック」を実装することで「Contentful→Hasura→Algoliaを連携させるシステム」が実現できました。この連携の流れをシステム間のリクエストのフローとして表現すると、このような形になります。

f:id:issuy:20201211190937p:plain
Contentful→Hasura→Algoliaを連携させるシステム(リクエストフロー)

まとめ

少々長くなってしまいましたが、「Contentful→Hasura→Algoliaを連携させるシステム」の話は以上です。

やってみた感想として、今回採用したサービスはどれも便利なものでした。とはいうものの「限られたリソース・時間」という制限に対してこれらのサービスがすべて解決してくれるかというと、やはり難しい。

もちろん実装期間を大幅に縮小してくれるのは間違いないです。ですがそれぞれのサービスへの習熟度が低いと、それに起因する不確実性を抱えながら進むことになります。特に期限が決まっている場合、これらの不確実性がそのまま遅延リスクになります。今回はある程度先回りしておくことでリスクを軽減できたかなと思いますが、この不確実性に起因する問題にはいくつか直面しました。そんな中で期限通りリリースできたのは、一緒に開発するメンバーに恵まれたからだと思っています。みなさんに感謝!!

また今回は技術検証の意味も込めて各サービスの検証や「汎用空き時間枠管理システム」の構築を行いました。これから開発していくプロダクトや技術的な将来像、そういったものを考える際には大きな不確実性に直面します。これに対して一歩、不確実性を解消するアクションが取れたのではないかと思います。

終わりの終わりに

トレタに少しでも興味を持っていただいた方がいれば、ぜひ遊びに来てくださいヽ(*´∀`)/
仲間も募集しています!

© Toreta, Inc.

Powered by Hatena Blog