re:Invent 2024: AWSによるEvent-driven architectureのフロントエンド実装パターン
はじめに
海外の様々な講演を日本語記事に書き起こすことで、隠れた良質な情報をもっと身近なものに。そんなコンセプトで進める本企画で今回取り上げるプレゼンテーションはこちら!
📖 AWS re:Invent 2024 - Asynchronous frontends: Building seamless event-driven experiences (API305)
この動画では、Event-driven architectureにおけるフロントエンドとバックエンドの連携パターンについて解説しています。AWS AppSync、AWS IoT Core、Amazon EventBridgeなどを活用した6つの実装パターンを紹介し、それぞれのトレードオフを詳しく説明します。Pollingパターンの課題から、WebSocketを使用したTwo-way通信、GraphQLベースの実装、MQTTプロトコルを用いたアプローチまで、具体的なコード例とともに解説しています。また、30秒以上の処理時間を要する長時間実行プロセスの実装方法や、Push通知の実装についても触れており、それぞれのパターンの特徴や使い分けについて、実際のフライト情報システムを例に挙げながら分かりやすく説明しています。
※ 画像をクリックすると、動画中の該当シーンに遷移します。
re:Invent 2024関連の書き起こし記事については、こちらのSpreadsheet に情報をまとめています。合わせてご確認ください!
本編
Event-driven architectureの概要とセッションの導入
おはようございます。re:Inventの1日目へようこそ。ヘッドフォンをまだ装着されていない方は、ぜひ装着してください。私たちの声を聞きたい方はピンクチャンネルのままで、他の方の声を聞きたい場合は別の色に切り替えていただいて構いません。Event-driven architectureは、クラウドネイティブ開発において非常に注目を集めています。私たちはEDAについてよく耳にしますが、EDAの非常にシンプルな定義は「データが変更されたら、何かが起こり、それに応答する」というものです。その代表的な例がeコマースで、注文から出荷、配送までの流れを調整するための接続組織としてイベントを使用しています。
これらは通常、システム間のやり取りですが、そのシステムが見落としがちな人的要素が必ずと言っていいほど存在します。例えば、私のような人間が荷物の場所やピザがいつ届くのかを執着的に更新して確認しているわけです。今日のセッションでは、シームレスなフロントエンド体験を実現するための技術パターンとそのトレードオフについて探っていきます。このトークは、フロントエンドエンジニア、バックエンドエンジニア、フルスタックエンジニア、そして伝説の10Xエンジニアの方々向けにデザインしています。非常に豊かでインタラクティブなEvent-drivenフロントエンドを作成できるよう、フロントエンドとバックエンドの両方について話していきます。これは本当に、その2つの結びつきについての話なのです。
リアルタイムデータ更新インターフェースの実例
私の名前はJosh Kahnで、AWSのWorldwide Tech Leader for Serverlessを務めています。そして後ほど、AWSのSenior Solutions ArchitectであるKim Wendtが加わる予定です。 このような種類のインターフェースは、私たちが日常的に目にするものです。これは昨日の私のラスベガス行きのフライトを簡単にモックアップしたものです。フライトに関する情報や、「現在搭乗中」といった情報、そして特定のフライトに関する最新情報が表示されています。株価、スポーツのスコア、あるいは配送物の現在地など、同様のインターフェースを想像していただけると思います。
このようなインターフェースは、空港や駅などいたるところで見かけます。これは実際に私が最近フランクフルト経由で搭乗したフライトのものですが、ここには本当にたくさんのデータが表示されています。この例については後ほど何度か触れますが、最初に一括で読み込むデータがたくさんあり、その後、個々の部分を少しずつ更新していきたいというケースです。そのためのパターンについて探っていきます。また、最近では地域のファストフード店やコーヒーショップなどでも、このような表示を目にするようになってきています。
もし既にCertification Loungeに行かれた方、あるいは本日以降の週中にExpoがオープンした際に訪れる予定の方は、Serverless Espressoブースでコーヒーの冒険を体験することができます。ここでは実際に2つの異なるメッセージングパターンが動作しています。右側のバリスタの後ろにある大画面には、作成待ちのコーヒーの全体的な概要が表示されており、これは典型的なブロードキャストメッセージです。一方、左側には注文時に使用したモバイルデバイスに表示される注文の詳細があり、これはより特定の注文状況に関するポイントツーポイントのメッセージです。私が以前Serverless Espressoで働いていた時、多くの人からインターフェースの構築方法や更新の仕組みについて質問を受けました。実際、これはかなり難しい課題なのです。
UIのEvent-driven性質とPollingアプローチの課題
User Interfaceは本質的にEvent-drivenです。ボタンをクリックしたり、スクロールしたり、あるいは一箇所に長く留まりすぎたりした時、それはEventとなります。マーケターが気にするようなEventかもしれませんが、何らかのEvent Handlerが検知しようとするEventなのです。 例えば、このケースではボタンをクリックすると、単純に「You clicked me」というAlertをポップアップ表示させる、というごくシンプルなコードを書いています。これはReactでの例ですが、他にもたくさんのバージョンがあります - Eventが発生し、何かが起こり、それに対して何かをしたい、というのは、先ほど私が説明したEDAの簡略化された定義によく似ていますね。
しかし、もっと多くのことができます。データをロードすることもできます。この場合、ボタンがクリックされた時にデータをロードし、UIフレームワークのReactivityを使ってページ上の要素を更新します。
ただし、これは非常に同期的です。この場合、APIからのデータのロードを待ち、ページ上の要素を更新して、それで静的な状態になります。このシンプルなケースでは、物事を更新するメカニズムは実際にはありません。しかし、私たちは皆、世界が非同期であることを知っています。私たちの周りでは常に何かが起こっています。荷物が私の家に近づいていたり、飛行機が着陸してゲートに向かってタキシングしていたり。状況は常に変化しているのです。
そこで、私たちはさまざまなAWSサービスを使用して、よりインタラクティブで最新の情報を提供するインターフェースを構築する方法を探ることにしました。 リアルタイムデータは、物事をより良くする傾向があります。より最新の情報が得られ、全体的なユーザー体験が向上します。フライトステータスの更新は重要です - 間違ったゲートに行ってしまうと、特に空港の反対側まで走って正しい場所に行かなければならない場合は、おそらく良くない選択となってしまいます。このような体験を構築しようとしたことがある方なら、それが困難であることをご存知でしょう。異なる技術が関係し、多くの場合、異なるチームが関わってきます。
しかし、私たちがお客様から最もよく見る最も直接的なアプローチは、Pollingと呼ばれる手法を使用することです。 Pollingは、フロントエンドが定期的に、つまり一定の間隔でバックエンドを呼び出して更新を確認する方法です。私のような親にとって、これは長距離ドライブ中に子供たちが数秒おきに「もう着いた?」と後部座席で聞いてくるのと同じようなものです。Pollingにはコストがかかる可能性があります。この場合、隠れたコストがあります。親の場合は、それは単なるイライラや煩わしさです。Eventは別の場所で発生していますが、クライアントが再度Pollingを行うまで、インターフェース上では実際にそれが実現されません。誤解しないでください - Pollingが意味を持つ場合もあります。データが非常に急速に変化している場合や、更新される項目の一つ一つを知る必要がない場合もあります。例えば、急成長中のフードデリバリーサービスを運営している場合、注文が入るたびにその日の総注文数を示すダッシュボードを更新する必要はなく、5〜10分ごとに更新すれば十分でしょう。
AWS AppSyncを活用したリアルタイム更新パターン
AWSでPollingを実装するにはどうすればよいのでしょうか? まず、イベントバスであるAmazon EventBridgeがあり、これが継続的にイベントを発行していきます。この構成は、これから何度も出てきます。次に、Event Handlerと呼ぶAWS Lambda関数があります。この関数は、EventBridgeが発行したイベントを受け取り、必要なデータを抽出して、View Modelテーブルと呼ぶAmazon DynamoDBテーブルに書き込みます。クライアントがデータを要求すると、Amazon API Gatewayを通じてリクエストが行われ、API Gatewayが別の関数を呼び出してリクエストを満たすために必要なデータを取得します。
これは多くの人にとって馴染みのある方式です。RESTの意味論を使用し、WebSocketのような特別な仕組みを必要としません。ただし、ここにはいくつかの隠れたコストがあります。まず、ユーザー体験の観点から見ると、Pollingの間隔でしか更新されないため、やや遅くなる可能性があります。また、無駄が多く、通信が頻繁になります。APIを変更されたデータのみを扱うように特別に設計していない限り、余分なネットワークコールが多く発生します。さらに、不要な可能性のあるネットワークコールが多いため、サーバーへの負荷も大きくなります。Serverlessの世界では、制限に達するまでLambda関数を追加できますが、それでもバックエンドへの負荷は増加します。
ここで最も厄介な隠れた問題は、現在のリクエストと前回のリクエストの差分を計算する責任がクライアントに移ることです。複雑なデータの場合、これは面倒で間違いが起きやすくなることがあります。クライアントアプリを開発した経験がある方なら分かると思いますが、アプリの更新は時として難しく、App Storeを通じてアップデートをプッシュする必要があるかもしれません。そのため、できるだけ変更は少なくしたいものです。Pollingを改善するための手法はありますが、私たちの考えでは、これらの課題の多くに対処できる非同期のアプローチがあります。一般的に、これらをSubscriptionベースのアプローチと呼びます。 クライアントはサーバーに対して、何か変更があった場合にだけ通知するよう依頼します。
これは、私が子供たちに「着いたら教えるから」と言って振り向くようなものです。それまでは静かにしていてください、という感じですね。これは一般的に、大量の更新ではなく小さな変更に最適で、これから見ていく中でそれが分かると思います。そのため、空港の出発案内板のような場合、データの大部分は同期的に一括ロードし、その後はSubscriptionベースのパターンを使用して、個々のフライトのデータが変更された場合にのみ更新を行います。今日は主にWebSocketを使用してSubscriptionを実装していきますが、Subscriberを更新する方法としてWeb Hooksというアプローチもありますが、これは今回話題にしているWebやモバイルインターフェースのようなクライアントにはあまり適していません。
技術的な詳細に入る前に、BroadcastとPoint-to-pointというメッセージングパターンについて説明させてください。Broadcastメッセージは多数のSubscriberに向けて送信されます。例えば、Las Vegasの天気はどうか? Chicagoを出発する全てのフライトについて教えて、といった具合です。一方、Point-to-pointメッセージは、特定の消費者、おそらく1人か少数の人々に向けて送信されます。これは個々のフライトの更新や、自分の荷物の現在地といった情報かもしれません。BroadcastとPoint-to-pointの選択は、これから見ていくアーキテクチャによって異なります。ユースケースや目的に応じて、一方の道筋が他方より適している場合があります。両方に優れているものもあれば、一方だけに適しているものもあります。
高レベルのアーキテクチャについて、頭の片隅に置いておいていただきたいと思います。本日、航空業界の方々がいらっしゃいましたら申し訳ありませんが、これは実際のシステムを大幅に簡略化したものです。様々なサービスから発生するイベントがあり、それらがEvent Busに到達し、そこでBackend for Frontend(BFF)と呼ばれるサービスを実装することになります。 このBackend for Frontend、つまりBFFの様々なバリエーションを実装していきます。これは、Webやモバイルなど、フロントエンドアプリケーションのニーズと最適化されたバックエンドAPIを密接に結びつける一般的なアーキテクチャパターンです。各フロントエンドに必要なデータのオーケストレーション、永続化、集約を処理することが私たちの目標です。iOSアプリ用、Androidアプリ用、Webアプリ用など、それぞれのBFFを用意することができます。
通常、分散システムにおいて「結合」という言葉はネガティブな意味合いを持ち、避けるべきものとされています。しかし、このケースでは多くの人がこの方法が有用だと考えています。チームにより多くの自律性を与え、関心事の分離を改善し、システム全体の他の側面の分離をより柔軟にすることができます。後ほどご紹介する私たちのパターンとサンプルコード(Kimが全体像をお見せする予定です)は、クライアントの更新にのみ焦点を当てています。リポジトリをフォークして他の機能を実装することもできますが、私たちはフライトステータスの更新にのみ注目しています。
私たちが構築して皆さんにお見せしているものには、かなりの重複があることに気づきました。優れたアーキテクトとして、あまり同じことを繰り返したくありませんでした。Kimが説明するほぼすべてのアーキテクチャで共通のパターンを目にすることになります。画面上では「Event Processor」と呼んでいます。そこでは、Amazon EventBridgeというEvent Busがあり、必要なイベントタイプ(出発、遅延、搭乗など)のみをフィルタリングするEventBridgeルールを定義しています。Event Handlerと呼ばれるAWS Lambda関数がそのイベントを受け取り、どう処理するかを決定し、Amazon DynamoDBのビューモデルテーブルに書き込み、そしてDynamoDB Streamを使ってそれらの変更をシステムの残りの部分にストリーミングします。残りの部分をStream Handlerと呼びますが、そこで様々なオプションが活用されることになります。補足ですが、サンプルコードを見ると、実際にはDynamoDB Streamではなく、Kinesis Data Streamを使用しています。これは、同じストリームから複数のコンシューマーがデータを取得できるようにするためですが、機能的にはほぼ同じです。
この後の講演では、3つの異なるインタラクションパターンについて見ていきます。Kimは本日の大部分の時間を使って、リアルタイムイベントについて説明します。これについては既にいくつか例を共有しました。私は後半で、長時間実行プロセスについて説明します。これは特に、30秒以上の応答時間がかかる可能性のある生成AIについての議論が増えてきたことで、より一般的になってきています。そして最後に、Pushイベントについて簡単に触れます。Pushイベントは機能的にはリアルタイムイベントパターンと似ていますが、異なるプレイヤーが関与し、異なるセマンティクスを持っています。
では、Kimに引き継ぎたいと思います。
AWS AppSync EventsとChannel Namespaceによる柔軟なPub/Subモデル
Josh、ご紹介ありがとうございます。これから説明する内容について、とても分かりやすく導入していただきました。これからご紹介するパターンについて、5つ半のパターンを見ていきます。各パターンについて、その概要と、Joshが先ほど説明したStream Handler(私たちのModel View Tableにすべてのデータを集約するもの)にどのように統合されるのかをお話しします。そして、すべての設計にはトレードオフがあるものですから、各パターンのトレードオフについてもまとめていきます。
最初に説明するのは、Two-way WebSocketパターンと呼んでいるものです。このパターンは、Amazon API Gateway WebSocket APIsを活用しています。ご存じない方のために説明しますと、これは完全な双方向通信が可能な管理型WebSocket APIです。クライアントがサーバーに情報を送信でき、サーバーもクライアントに情報を送り返すことができます。このイベント処理ワークフローへの統合方法について説明する前に、どのクライアントに更新を送信するかを知るための準備が必要です。画面の左側から見ていきますと、API Gatewayには特別なConnectルートとDisconnectルートがあります。クライアントがAPIに接続すると、このConnectルートにヒットし、そのクライアント固有のConnection IDを割り当てて、DynamoDBのClientsテーブルに保存します。同様に、切断時にはDisconnectルートにヒットし、そのConnection IDをClientsテーブルから削除します。
このパターンでは、コネクション管理は自分で行う必要があることにお気づきかもしれません。クライアントの接続と切断を追跡しているわけです。これがイベント処理ワークフローに組み込まれる仕組みですが、システムにイベントが入ってくると、Stream Handlerがそれらのイベントを処理し、フィルタリングを行うこともあります。同時に、どのクライアントがこれらの更新に関心があるかを確認するため、Clientsテーブルもチェックします。Stream Handlerは更新が必要なクライアントのConnection IDを取得するためのルックアップを行い、接続中の各クライアントに対して、Post-to-Connection APIコールと呼ばれる処理を実行します。
いくつかのトレードオフをまとめてみましょう。コネクション管理は自分で行う必要があり、メッセージのファンアウトもStream Handler内で処理する必要があります。Stream Handlerに何を使用するかの選択が非常に重要になってきます。AWS Lambdaには制限があり、数百万のクライアントが接続している場合、それらすべてを反復処理すると制限に達する可能性があります。良い点としては、これは非常に柔軟なソリューションです。基本的なWebSocketを使用しており、完全な双方向通信が可能なため、チャットアプリケーションなどに適しています。
さらにトレードオフとして、これは単なるWebSocket APIなので、リアルタイム更新の受信を開始する前にすべての情報を取得するには、別のAPIエンドポイントが必要になります。RESTやAmazon API Gatewayを使用したHTTPエンドポイントを使用することも可能ですが、リアルタイム更新用のWebSocket APIとは別になります。各パターンについて、AWSではセキュリティが最優先事項であるため、サポートされている認証モードについても説明しています。API Gateway WebSocket APIでは、IAMとAmazon Cognito Identityによる認証、カスタムニーズに対応するLambda、そして特徴的なのは、パブリックWebSocketsをサポートしていることです。デモでもお見せしますが、これは真のパブリックAPIオプションを持つ唯一のパターンです。さて、次のパターンについて説明していきましょう...
次のパターンでは、私が個人的に大好きなサービスであるAWS AppSyncを使用します。 このパターンでは、AWS AppSyncがフロントドアとして機能します。AppSyncは、フルマネージド型のGraphQLおよびWeb APIサービスです。GraphQLは定義言語で、APIからデータを読み取るためのクエリ操作と、データを変更するためのミューテーションを定義することができます。リアルタイム機能は、Subscriptionを通じて提供され、このSubscriptionはデータのミューテーションの結果としてトリガーされます。ミューテーションが発生すると、Subscriptionを購読しているすべてのクライアントに対して、そのミューテーションが発生したことを通知します。
これは非常に重要なポイントです。なぜなら、先ほど説明したイベント処理ワークフローとの関連で考えると、実際にはそのイベント処理ワークフローの早い段階でデータを更新しているからです。DynamoDBの新しいモデルテーブルを更新し、その後DynamoDB Streamsを通じて変更を消費します。ストリームハンドラー関数に到達する時点では既にデータが変更されているため、この問題に対処するためにAppSync特有のNONEデータソースを使用します。NONEデータソースは基本的にパススルーとして考えることができます。入力を受け取り、実際にデータを変更することなくミューテーションを実行しますが、接続されているクライアントにSubscriptionをトリガーすることができます。この解決策が最初の解決策と比べて非常に優れている点は、AppSyncが接続管理を代行してくれることです。そのため、DynamoDBテーブルでクライアントの接続IDを追跡し、更新を送信するために反復処理を行う必要がなくなります。
このアプローチは非常に拡張性が高いものです。これまでのイベント処理ワークフローでは、DynamoDBとそのChange Data Capture機能であるDynamoDB Streamsについて説明してきましたが、同様にストリーム機能を持つマネージド型グラフデータベースのAmazon Neptuneを使用することも可能です。これはこのアーキテクチャにぴったりと当てはまり、接続されたクライアントへの同じSubscription機能を実現できます。また、ストリームハンドラー関数で必要に応じてストリームをフィルタリングすることも可能です。AWS AppSyncを使用するため、GraphQLの学習曲線は存在します。GraphQLは新しい言語であり、従来のAPIで慣れ親しんだRESTのセマンティクスとは異なるため、チームの導入の障壁となることがあります。しかし、GraphQLを活用することのメリットは一般的にトレードオフを上回ります。その一つのメリットは、単一のAPIエンドポイントを通じて、クエリ、ミューテーション、リアルタイム更新のすべてを利用できることです。
認証の観点からも非常に柔軟です。IAM、Amazon Cognito、カスタム認証用のLambda、そしてAppSync特有のOpenID Connectとの直接統合をサポートしています。発行者URLをAPIに提供すると、トークンを渡した際に、カスタムコードを必要とせずにそのトークンを検証してくれます。 私がAppSyncを非常に気に入っているため、次のパターンでもAppSyncを使用します。このパターンは「Direct to GraphQL」パターンと呼んでいます。Amazon EventBridgeとAWS AppSyncのミューテーションの間の直接統合を活用します。イベント処理やストリームハンドラーのステップはもはや不要で、基本的にAmazon EventBridgeからのイベントとAppSyncのミューテーションの間のマッピングを使用します。
イベント処理フローを使用しなくなったため、上流にあったDynamoDBのビューモデルテーブルの位置を変更する必要があります。AppSyncとDynamoDBの直接統合を使用して、AppSyncから直接データを永続化することができます。これはAppSyncで、リゾルバーとデータソースと呼ばれるものを使用して実現します。リゾルバーは直接接続され、TypeScript、JavaScript、またはApache Velocity Template Languageで記述されます。
このResolverでは、基本的にイベントをデータソースに永続化するためのコードを少し書くことになります。Direct Integrationを使用する場合のトレードオフについて、いくつか重要な点があります。2つのシステム間で直接統合を行うため、実装がとても簡単です。フィルタリングを行うLambda関数が多数必要なく、更新を取得するためのDynamoDB Streamsも必要ないため、動作部分が少なくなります。また、前回のパターンではSubscriptionを実行するために非データソースを使用して変更をトリガーする必要がありましたが、このアプローチではデータの変更とSubscriberへの更新を1つのステップで行うことができます。
前回のパターンと同様に、Subscriberへの更新送信に関して高い柔軟性があります。また、このアプローチでもAWS AppSyncを使用したプライベート接続をサポートできます。これは前回のパターンでも同様でした。AWS AppSyncのプライベートAPIを使用することで、VPC内だけに通信を制限することができます。もしワークフローに適している場合は、一方向のSubscriptionを持つGraphQLを使用することができます。Amazon EventBridgeとAWS AppSync間の接続は非常にシンプルですが、デモの構築中に私たちが実際に経験した潜在的な落とし穴がいくつかあり、その解決策についてお話ししたいと思います。
ここで、AWS AppSyncでのBroadcastとPoint-to-Pointの実装がいかに簡単かについてお話ししましょう。前述したように、これらは全て自動的に処理されるため、クライアントに更新を送信するためのカスタムイテレータを作成する必要はありません。実際、スキーマの一部として定義し、クライアント側で実装するだけで済みます。右側から見ていくと、これはGraphQLスキーマの一部で、2つのMutationを示しています。Mutationはデータ修正操作で、フライトの作成と更新のための2つのMutationがあります。下部にはSubscription typeがあり、このSubscription typeはAWS Subscribeディレクティブを通じて両方のMutationに紐付けられており、これによってMutationとSubscription操作を連携させることができます。
BroadcastとPoint-to-Pointに関して重要な点は、Subscriptionのflight IDパラメータのオプションです。クライアント側でこのflight IDパラメータを指定すると、「このflight IDの更新だけを受け取りたい」とクライアントが表明していることになります。Subscriptionでflight IDを指定しない場合、クライアントは「すべてのフライトの更新を受け取りたい」と表明していることになります。これがAWS AppSyncにおけるBroadcastとPoint-to-Pointの違いです。クライアント側とサーバー側の両方に関係しますが、AWS AppSyncはバックエンドで全てを賢く処理してくれます。Subscription確立時にフィルタリングのためのパラメータを提供するだけです。コード実装の観点からは、Point-to-Point Subscriptionの場合は変数を渡すだけで、Broadcastの場合は変数を渡さずに全てのflight IDの更新を受け取るだけなので、非常に簡単です。これにはAWS Amplify APIクライアントライブラリを使用していますが、これはAWS AppSyncやその他のサービスの実装に非常に便利です。ただし、基本的にはWebSocketを使用しているため、Apolloのような他のライブラリやGraphQLライブラリを使用してWebSocketとインターフェースすることもできます。
このアプローチには落とし穴があると述べましたので、Mutation Selection Setsについて簡単に説明します。Selection Setsは、GraphQLに特有のもので、クライアントが要求している情報を自由に選択できるという点でGraphQLの強みとなっています。Mutationを実行する際、そのリクエストから実際に返してほしいフィールドを全て指定し、それがSelection Setを構成します。右側には Create Flight Mutationがあり、下部のチャンクで返してほしいパラメータを全てリストアップしている部分が、Selection Setです。これがEventBridgeとDirect Integrationで重要な理由は、もしflight IDのような項目を省略してしまい、Subscriberがその情報を期待している場合、Mutation側でその情報を提供していないため、Subscription側ではnull値が返されてしまうからです。
Point-to-pointとBroadcastに基づくフィルタリングを行う際、これは重要になってきます。一般的な経験則として、このインテグレーションを使用する場合や、アウトオブバンドの更新を行う場合は、作業を簡単にするために、すべてのSelection Setを含めることをお勧めします。トラブルシューティングについては、Amazon EventBridgeのターゲットにはDead Letter Queueを付加できる機能があり、これはターゲットに配信できないイベントの対応に非常に役立ちます。AWS AppSyncにも様々なレベルで有効にできるロギング機能があり、トラブルシューティングに役立ちます。私自身、自分のコードのトラブルシューティングで常にこれらを使用しているので、よく知っています。
次のパターンに移りましょう。これまでGraphQLを使用する AWS AppSyncについて説明してきましたが、このパターンは「No GraphQLイベントパターン」と呼ばれるものです。これはAWS AppSyncの新機能であるAWS AppSync Eventsを使用するもので、GraphQLを必要としない純粋なPub/Subモデルです。このイベントバスアーキテクチャ、つまりイベント処理アーキテクチャに統合する際、Stream Handlerは基本的にHTTP POSTオペレーションを実行して、作成した特定のChannel Namespaceをサブスクライブしているサブスクライバーに更新を通知します。パブリッシャーはHTTP POSTオペレーションを介してAWS AppSync Eventsと通信し、サブスクライバーはWebSocketを使用してそれらの更新をリアルタイムで受け取ります。
トレードオフの観点から見ると、これは真のPub/Subモデルです。GraphQLスキーマの要件がなくなり、送信するメッセージやイベントをセグメント化する強力な機能を持っています。Channel Namespaceを使用しますが、これについては後ほど詳しく説明します。基本的には、APIを通過するイベントやメッセージを整理するための論理的な構造だと考えてください。私自身はGraphQLが好きですが、習得の負担が大きいことは確かなので、このパターンではGraphQLは不要です。トレードオフとして、現在のSubscription WebSocketは一方向の接続であり、サブスクライバーはそのWebSocket経由で通信を返すことができません - 依然としてHTTP POSTを介して発行する必要があります。Catch-up APIが必要な場合は、REST HTTP APIや純粋なGraphQL APIを通じて実装する必要があります。
認証の観点からは、これは内部的にはAWS AppSyncを使用しています。GraphQLとAppSyncを動作させるために構築したインフラストラクチャの多くを活用しており、同じ認証モードがすべてサポートされています。ここまで説明してきたすべてのイベントパターンには、データ永続性という概念がありました - 後でイベントのキャッチアップが必要になる場合に備えて、イベントを永続化しています。しかし、すべてのパターンでそれが必要というわけではありません。そこで、ボーナスパターンとして「Fire-and-Forgetパターン」があります。このパターンでは、更新を送信し、その時点で接続しているユーザーだけが更新を受け取ります。接続していない場合は更新を見逃すことになります - これが適している場合もあれば、適していない場合もあります。
AWS IoT CoreとMQTTを用いたメッセージングパターン
Amazon EventBridgeの機能であるAPI Destinationsを活用して、HTTP POSTオペレーションに接続・統合し、AppSync Event API経由でパブリッシュを行います。AppSync Eventsを通じてオプションのEvent Handlerを呼び出すことができ、このEvent Handlerは、パブリッシュされたイベントが接続されたサブスクライバーに送信される前に、イベントのエンリッチメントやフィルタリングなどを行うJavaScriptで書かれたコードとして考えることができます。接続されたサブスクライバーについても同様で、サブスクライバーが特定のChannel Namespaceに対してSocket APIへの接続を確立する際に、オプションでUnsubscribe Handlerを呼び出すことができます。これにより、きめ細かな認証や、Channel Namespace内の特定のチャンネルへのサブスクリプションの制限などを行うことができます。これらのイベントは、接続されたサブスクライブ済みクライアントにブロードキャストされます。Channel Namespaceとその認証制御について、もう少し詳しく説明しましょう。これまでフライトステータスについて話してきましたので、以下のような例を考えてみましょう。
このフライトシナリオのイベントを整理する方法として、flight statusという名前空間を定義することができます。これを事前に定義し、その名前空間に特化した設定を行います。パブリッシュとサブスクライブのために、どのような接続方法を使用できるかを決定するマルチ認証モードを定義します。また、オプションのイベントハンドラーを追加することもできます。
具体的なチャンネルについては、フライトIDをチャンネルとして使用し、パブリッシュ時に動的に作成することができます。ここに示されているRVI123というチャンネルは、事前に作成する必要はなく、バックエンドで何かが発生した結果として作成され、更新をパブリッシュしたり、サブスクライバーに情報を送信したりすることができます。同様に、座席割り当てのための別の名前空間を持ち、フライトIDやユーザーIDを含むチャンネルを設定することもできます。
これらの異なる名前空間のユニークな点は、パブリッシャーとサブスクライバーのニーズに基づいて、完全に異なる認証設定を持つことができることです。フライトステータスのフローでは、API destinationsはAPI keyかOpenID Connectのいずれかを使用できます。パブリッシャーはOpenID Connect認証を使用してAPI destinationsを介してインターフェースを取り、一方でサブスクライバーは空港の表示板やユーザーベースのデバイスなので、AWS IAM認証が最適かもしれません。座席割り当ての場合、バックエンドシステムはIAMを使用して更新をパブリッシュし、サブスクライバー側では、ユーザーがアプリケーションで認証に使用しているAmazon Cognito user poolsを使用することができます。
これらは、名前空間レベルで定義されているイベントハンドラー内で見られる例の一部です。左側には、onPublishハンドラーのサンプルがあり、ここではイベントエンリッチメントを行っています。1つまたは最大5つのバッチで処理される各イベントに対して、基本的に追加のメタデータを付加します。クライアントやパブリッシャーからその責任を取り除き、イベントハンドラーに組み込んで、イベントが入ってきた時間、どのイベントか、今日のどのセッションの一部かといった追加のメタデータを付加します。右側では、より細かい認証を強制するonSubscribeハンドラーのサンプルを示しています。自分がメンバーであるグループを含むチャンネルにのみサブスクライブできるようにしています。これは、特定のグループ内のユーザーやメンバーに特定の更新を配信し、ブロードキャストやポイントツーポイントの観点から強力な認証制御を実施する必要がある場合に非常に効果的です。
これは実際には、パブリッシュするチャンネルを変更するだけのシンプルな作業です。ポイントツーポイントの場合、特定のユーザーベースのチャンネルまたは一意の識別子を提供してサブスクライブできるようにし、ブロードキャストの場合は、誰でもサブスクライブできるキャッチオールメッセージにブロードキャストするだけです。ここではAWS Amplify APIクライアントを使用していますが、このおよそ20行のコードを見る限り、実装はかなり簡単そうです。ただし、お好みのWebSocket APIライブラリを使用してサブスクリプションを確立することもできます。パブリッシュ後の観点からは、どのようなHTTPクライアントでも使用できます。
もう1つのパターンについてお話ししましょう。今回はGraphQLと先ほどのGraphQLイベントパターンから離れて、AWS IoT CoreでのMQTTについて説明します。MQTTをご存知ない方のために説明すると、これは軽量で、IoTデバイスでよく使用される認証モードの多くをサポートしている、もう1つのパブリッシュ-サブスクライブ型メッセージングプロトコルです。このパターンがイベント処理ワークフローに組み込まれる仕組みとしては、Stream Handlerがイベントを受け取り、フィルタリングを行った後、特定のIoTトピックにパブリッシュします。トピックは、先ほど説明したChannelと非常によく似ていますが、名前付けや定義の方法が少し異なります。
このフローは非常に柔軟で、必要な数だけトピックを作成できます。ブロードキャストとポイントツーポイントの実装も、名前空間の例と同じくらい簡単で、単にブロードキャスト用とポイントツーポイントワークフロー用に異なるトピックを定義するだけです。このアプローチにはいくつかのトレードオフがあり、その1つは、リージョンごとのAWSアカウントで単一のエンドポイントしか持てないということです。
これまで説明してきた他のパターンでは、作成するAPIの数に応じて、それぞれ独自のエンドポイントを持つことができました。しかし、AWS IoT Coreでは、アカウントごと、リージョンごとに1つのエンドポイントしか持てません。これは複数のアプリケーションを設計する際の潜在的なトレードオフとなる可能性があります。さらに、このアプローチでサポートされる認証モードは、IoTベースのデバイスに重点を置いています。IAM、Amazon Cognito Identities、そしてIoTデバイスでよく使用されるX.509証明書による認証をサポートしています。
これはクライアント側での実装例です。正直に言うと、これは画面で見るよりもずっと実装が難しいものです。おそらく、これまでの中で最も実装が困難なクライアントでした。ただし、朗報として、このパターンをさらに探求したい場合は、デモの中に実行可能なサンプルコードを用意しています。ブロードキャストとポイントツーポイントの違いは、実際には定義するトピックの違いだけです。
もし写真を撮るべきスライドがあるとすれば、おそらくこれでしょう。これは、これまで説明してきたパターンのスナップショットです。このスライドが示そうとしているのは、今日お話ししたパターンのそれぞれにトレードオフがあるということです。それぞれに得意な部分があり、自分で実装しなければならない部分もありますが、結局のところ、実装しようとしているユースケース次第なのです。アプリケーションでは1つまたは複数のパターンを使用できますが、説明したすべてのパターンにはそれぞれ長所と短所があります。
リアルタイム更新パターンのデモンストレーション
これからデモに移って、これらの機能を実際にお見せしたいと思います。その前に、今日お話しする内容についてご説明させていただきます。 左側に軽量なReactアプリケーションがありますが、これは先ほど説明した6つのパターンと連携しています。Pollingもその中に含めていますが、これはPollingと非同期のリアルタイムの例との間のトレードオフを強調するためです。Step Functionsワークフローを使って、フライトステータスの変更をシミュレーションしています。このStep Functionsワークフローでは、数週間前にリリースされた新しいJSON機能を使用しており、後ほどソースコードでご確認いただけます。そして、これらすべてのパターンはEventBridge接続を通じて連携しています。
すべてのデータは事前にロードされています。最新の情報を取得するというコンセプトがありますので、それではStartをクリックしてみましょう。うまく動くことを祈っています。画面に表示されている内容をご説明します。上部では、Point-to-Pointと呼ばれる方式を示しています。特定のフライトを指定してそのステータスを購読しています。また、非同期イベントとは別の名前空間を使用して座席割り当てのワークフローを実装し、API DestinationsとAsync Eventsの連携をお見せしています。更新が始まると、データが変更された行が緑色でハイライトされるのが見えてきます。
右側にEvent Ageという追加フィールドがありますが、これはイベントが作成されてからフロントエンドがそのイベントを受信するまでの時間を表しています。すでにお気づきかもしれませんが、右中央にある直接GraphQLパターンが最も速い傾向にあります。 これは、EventBridgeと更新を配信するAppSyncサービスの間の移動部分が少ないためです。他のパターンでは、 更新は通常、イベントが作成されてから数秒以内に到着します。
特に注目していただきたいのがPollingです。Event Ageが数秒から、クライアント側で設定した15秒のポーリング間隔まで、さまざまな値を示すことがわかります。 これは、Pollingがバックエンドへの問い合わせ頻度に応じてしか更新を取得できないため、アプリケーションに追加の遅延が発生する可能性があることを示しています。最新の更新でも8秒、6秒、さらには11秒かかっているのが分かります。 これでデモは以上です。スライドに戻って、
Joshに残りのセッションを進行してもらいましょう。 ありがとう、Kim。ライブデモは常に冒険で、Wi-Fiがどう動作するか分からないものですからね。皆さんが同時にモバイルデバイスを使用しなかったことに感謝します。
長時間実行プロセスとPush通知の実装アプローチ
次は、一般的に30秒以上の応答時間を要する長時間実行プロセスについてお話しします。生成AIの台頭に伴い、この話題に関する質問が増えていますが、今回はそれについては触れません。以前お話しした、PollingとSubscriptionの考え方に立ち返って、別のユースケースについて見ていきましょう。これらの長時間実行プロセスは、これまでご紹介したパターンのどれを使っても実装可能です。個人的には、このユースケースでもSubscriptionアプローチを好んで使います。というのも、必要に応じて10秒でも10分でも対応できる柔軟性があるからです。確かに技術的な複雑さは増しますが、これらは十分に理解された設計パターンであり、Kimがお見せしたように、Amazon EventBridgeとAWS AppSync Eventsだけでも実現できるのです。
フライトの例をもう少し掘り下げてみましょう。フライト検索で考えてみます。私は数年前に、AWS AppSyncからAWS Step Functionsを直接呼び出すような仕組みを実装しました。ここでは、利用可能なフライト特典を探すために、複数の在庫プロバイダーを検索するかもしれません。このワークフローの各ステップの結果が返ってくるたびに、並行して実行されている検索の結果を個別に公開できます。すべてのプロバイダーからの応答が揃ったら、それらの結果を集約して、これがリストの終わりであることを示すイベントをフロントエンドに発行できます。このようなScatter-Gatherアプローチは、このケースでも非常にうまく機能します。
実は、AWS re:Inventで2年連続でこの全く同じアーキテクチャ図を使用しています。昨年の講演は今回とは全く異なる内容で、AWS AppSyncやその他のトピックに関するものでしたが、セッション後に6人ほどの方が画面右側にあった小さな図の実装方法について質問に来られました。ここで行われているのは、AWS AppSyncを使用していますが、他にも多くの選択肢があります。AWS AppSyncがAmazon Athenaでクエリを開始し、AthenaがAmazon S3バケット内のデータに対してクエリを実行し、完了すると結果を別のS3バケットに書き込みます。これが発生すると、S3がイベントを発行し、Lambda関数をそのイベントにSubscribeできます。必要に応じてEventBridgeを経由することもできます。そして、Lambda関数がこのクエリが完了したことを通知し、AWS AppSync、あるいは先ほど説明したパターンのいずれかを通じて、クライアントに結果が準備できたことを知らせます。そうすることで、クライアントはS3のPresigned URLを使用して結果セットをダウンロードできます。
最後に簡単にPush通知について話しましょう。機能的には、Push通知はリアルタイム更新によく似ていますが、サードパーティが関与し、考慮すべき異なるセマンティクスがあります。また、これらのフィルタリングをより厳密に行う必要があるでしょう。フライトステータスが変更されるたびにPush通知を受け取っていたら、おそらくそれらをオフにしてしまうでしょう。そのため、デモコードでは、遅延や特に重要な事項に限定してフィルタリングしており、これは実装が非常に簡単です。
これはWeb Pushを使用した簡単な例です。はい、デスクトップのブラウザでもPush通知が可能です。最初のPush通知を送信する前にいくつかの前提条件があります。まず、クライアントがSubscribeする必要があり、ブラウザによってはSubscriptionオプションを取得するためにUI要素をクリックする必要があるかもしれません。Subscribeすると、トークンやその他の情報がサーバーに送信されます。この場合、Lambda関数がそのデータをSubscriptionsテーブルに書き込みます。これは、Kimが先ほど共有した双方向WebSocketパターンに似ていますが、若干の違いがあります。そして、ユーザーが特定の注文やフライトステータスの更新をSubscribeしたい場合、この2つの要素、つまり注文情報とSubscriberをSubscribersテーブルで結び付ける必要があります。
これにより、Event Handlerは最終的にどのSubscriberがアップデートの通知を受けたいのかを確認できるようになります。Event Busからイベントが発生すると、Event Handlerはサブスクリプションテーブルをチェックして、誰に通知する必要があるかを判断します。その後、Apple、Google、Firefoxなどの通知サービスにリクエストを送信します。この時点で配信の制御は私たちの手を離れ、さらにメッセージを受信するためにはService Workerのような仕組みが必要になります。これは今日のトピックの範囲を少し超えていますが、これから数分後にお見せするデモコードの古いバージョンでその方法を確認できます。そちらも参考にしてみてください。
モバイル通知に関しては、AWS End User Messagingのようなサービスを利用することで、多くのサブスクリプション機能を任せることができるため、少しシンプルになります。End User Messagingでデバイストークンを保存することもできますが、どのSubscriberがどのタイプのイベントに関心があるかを紐付ける必要は依然としてあります。この場合、End User Messagingを通じてサブスクライブします。Event Busからイベントが発生すると、Notification Handlerがそれを検査し、どのSubscriberにどのアップデートを送信するかを判断します。その後、End User Messagingを使用して通知を行いますが、End User MessagingはAppleやGoogleなどの通知サービスを経由して、ベストエフォートでモバイルデバイスに配信します。仕組みは似ていて、達成しようとしていることも似ているので、この部分は詳しく説明しませんが、他にも考慮すべき点やサービスが関係してきます。
以上が今日お伝えしたい主な内容です。リアルタイムイベント、その実装のための6〜7つのパターン、長時間実行プロセスの実装方法、そしてPushイベントの実装方法と、これらのインタラクション全体で同じパターンを活用する方法について説明してきました。サンプルコードはすべてGitHubで公開しており、QRコードまたは短縮URLからアクセスできます。どちらを使っても同じところにたどり着けます。
re:Inventの期間中、他にもいくつか参考になるセッションがあります。Event-Driven Architectureの構築に関するセッションは素晴らしい内容です。また、Amazon API Gateway、AWS AppSync、ALBのどれを使うべきか迷っている方向けのChalk Talkも人気があります。そして、宣伝になりますが、私は今週後半にセキュリティに関する別のトークも行います。1時間お付き合いいただき、ありがとうございました。セッションのアンケートにご協力ください。もちろん5点満点の評価をいただけると嬉しいですが、今後このセッションをより良くするために、皆様からの率直なフィードバックを心からお待ちしています。ありがとうございました。残りの週もお楽しみください。
※ こちらの記事は Amazon Bedrock を利用することで全て自動で作成しています。
※ 生成AI記事によるインターネット汚染の懸念を踏まえ、本記事ではセッション動画を情報量をほぼ変化させずに文字と画像に変換することで、できるだけオリジナルコンテンツそのものの価値を維持しつつ、多言語でのAccessibilityやGooglabilityを高められればと考えています。
Discussion