AppSync Eventsの何がいいのか
AppSync Events 使ってますか?!
AppSyncと聞いて、ピンと来る方は世の IT エンジニアの2割くらい、そしてその大半の方が「AWSの GraphQL サービス」と認識しているのではないでしょうか。ましてや "Events" ってなんでしょうか?
AppSync Events はクライアント/サーバー間でイベントをやり取りするための新しい API の仕様です。大事な点として GraphQL は一切出てきません。
こちらが AppSync Events のリリースに関するAWS公式のブログです。
今回の記事では、私の推しサービスである AppSync Events について、その良いところを紹介します。ちなみに普段は AWS Japan でお客様に技術的なご支援を提供しています。
AppSync Events を 3 行で
- クライアント/サーバーでの双方向通信を簡単にするサーバーレスサービス
- GraphQL の Subscription に似た WebSocket ベースの薄いラッパー
- でも GraphQL とは無関係
AppSync Events 登場の背景
ベースとなる WebSocket という技術は、そもそも、クライアント、サーバー間での双方向通信に向けて作られた仕組みです。
しかし、現実的にはサーバーは常にHTTP Requestを待ち受けており、クライアントとしてもWebsocketにメッセージを送るよりも、HTTP Requestを送る方が一般的です。
そこで、サーバー->クライアントのデータ送信にだけWebSocketを採用したのがGraphQLのSubscriptionという機能です。[1]
このSubscription が使いたいがためにGraphQLの採用が検討されるケースは少なくありませんでした。一方で、APIとしてはRESTがいいのに、GraphQL特有のResolver, Schemaなどの概念がついてきて、学習コストが高いという課題がありました。
そこで、AppSync Events という、GraphQLのSubscriptionの仕組みだけをシンプルに切り出したようなサービスが登場しました。
AppSync Events の使い方
AppSync Event API のデプロイ
AppSync Events のリソースとしては、以下の二つが存在します。
- Event API
- API の実態です。エンドポイントドメイン名が一つ払い出されます。
- 例:
https://sample.appsync-api.us-east-1.amazonaws.com/event
- Channel Namespace
- 1つのEvent APIに複数のChannel Namespaceを追加します。
- Channel Namespaceは、エンドポイントの一番初めのパスとなります。
-
default
を追加した場合: https://sample.appsync-api.us-east-1.amazonaws.com/event/default
-
- Channel Namespaceは認証やハンドラーの設定を共通化するためのグループです。
そして、Channel Namespace の中で、任意の名前でPub/Subを行うことができます。例えば、
/default/room1
のような形で、誰かがSubscribeしているところに、他の誰かがPublishすれば送受信が成立します。
リソースとしては、単に
- Event API を作成
- Channel Namespace を追加
これだけで AppSync Events を使い始めることができます。
送受信のやり方
送受信には、AWS Amplifyのライブラリを使うのが最も簡単です。
送信側
await events.post(`default/${roomId}`, JSON.stringify(message))
受信側
const channel = await events.connect(`default/${conversationId}`);
channel.subscribe({
next: data => {
// メッセージ受信時の処理
setMessages(prev => [...prev, data]);
},
error: error => {console.error(error)}
});
エンドポイント設定(送受信側共通)
Amplify.configure(
{
"API": {
"Events": {
"endpoint": `https://${process.env.APPSYNC_EVENTS_DOMAIN}/event`,
"region": "us-west-2",
"defaultAuthMode": "apiKey",
"apiKey": process.env.APPSYNC_EVENTS_API_KEY
}
}
}
);
たったこれだけで双方向での通信が可能です。
AppSync Events の良いところ
サーバーレス、ステートフル
抽象的な見出しとなってしまいましたが、重要なポイントだと考えています。
サーバークライアント間の双方向通信の方式としてよく比較対象に挙がるのが、SSE (Server Sent Events)です。AWSでは Lambda Function URLsがこれに対応しています。SSE は Transfer-Encoding: chunked
という分割してHTTP レスポンスを返せる仕組みの上に成り立ち、一度コネクションを確立すれば、サーバー側からpush型でクライアントにメッセージを送ることができます。非常に似た仕組みですね。
また、SSE にもチャンネルという概念が存在し、一度接続が途絶えてしまっても、再接続できる仕組みがあります。(HTTP リクエスト/レスポンスでこれができるのもすごい話ですが)
Lambda 単体でもSSEで似たことができるのにAppSync Events を使うメリットはなんでしょうか?
筆者の考える一番のメリットは、AppSync がサーバーレスでありながらステートフルである点です。
Lambda の SSE での問題点について考えてみましょう。
複数ユーザーのチャットのシナリオ
- ユーザーAが
foo
チャンネルにメッセージを投稿します。 - ユーザーBが同じ
foo
チャンネルをサブスクライブします。 - 本来ならBはAのメッセージを受信できるはずですが、AとBを処理する Lambda インスタンスが異なるため、B側のインスタンスにはAが開いた
foo
チャンネルの状態が存在しません。 - その結果、Bはメッセージを受信できず、チャットが成立しません。
シングルユーザーで再接続のシナリオ
生成AIチャットbotのストリーミング応答を受け取る状況を想定します。
- クライアントはまず Lambda インスタンスAへ接続し、
bar
チャンネルでストリームの受信を開始します。 - 通信の途中でネットワークが切れ、接続が途切れます。
- クライアントが再接続を試みると、今度は別の Lambda インスタンスBにルーティングされます。
- Bは
bar
チャンネルの過去状態を保持しておらず、Aが持っていたストリームの続きを再開できません。 - その結果、クライアントはストリームの続きではなく“新しい”
bar
チャンネルに接続してしまい、途中からの受信ができなくなります。
どちらのシナリオでも共通して言えるのは、Lambdaがステートレスゆえに、同じインスタンスに接続されることが保証できないことです。この解決には裏側にデータベースを持ってステートフルにするか、トピック名ごとにルーティングする(スティッキーセッション的な)アプローチですが、後者はLambdaおよびサーバーレスの世界では実現が難しいです。
AppSync Eventsはこの問題を解決します。
「どのクライアントがどのチャンネルにサブスクライブしているか」「どのチャンネルにメッセージが来たらどのクライアントに流すべきか」といった情報をサービス側で保持しています。この手のステートフルなアーキテクチャでは、スケーラビリティがボトルネックとなることがありますが、AppSync Event はサーバーレスサービスであり、利用者、および下流のLambdaやFargateといったコンピュート層は AppSync Events のスケールに関して特別な対応を求められません。
多様な認証認可のサポート
AppSync Events も GraphQL API と同様に複数の認証方式をサポートしています。イベントをやり取りするだけのシンプルな API とはいえ、社内チャットや機密データのストリーミングではきめ細かなアクセス制御が欠かせません。主な選択肢は以下のとおりです。
-
IAM 認証
AWS といえばの IAM 認証を当然サポートしています。Federated Role を使えば、企業 IdP 由来の SSO 環境からも簡単に利用できます。 -
API キー
検証用やパブリックな PoC で手軽に使える方式です。期限付きキーを発行し、ステージング環境と本番環境でキーを分ければ最低限のセキュリティを確保できます。 -
Amazon Cognito / OpenID Connect
ユーザー単位の認証が必要な SaaS や B2C アプリでは、Cognito ユーザープールや外部 IdP (OIDC) と連携させることで、ログイン状態に応じたサブスクリプションを実現できます。 -
Lambda オーソライザー
「顧客ごとに細かい利用制限をかけたい」「メッセージ内容に応じてサブスクライブ可否を判定したい」といった高度な要件は Lambda オーソライザーで柔軟に実装できます。結果をキャッシュすればレイテンシへの影響も最小限です。
これらの方式はChannel Namespace 単位で独立して構成できるため、同じ Event API 内で「社内向けは IAM」「外部パートナー向けは Cognito」というように運用を分けることも可能です。
AppSync Events のユースケース
ここからは、AppSync Events のユースケースを3つほど紹介します。
チャットアプリなどの双方向通信が求められるアプリケーション
従来、チャットアプリなどはSubscription機能の存在から、GraphQLで実装されることも多くありました。
そこをAppSync Events に置き換えることで、サーバーレス・マネージドで双方向の通信を簡単に実現することができます。
永続化どうする問題
AppSync Event API は、単に HTTP Post で来た内容を、そのチャンネルを見ている他のクライアントに Websocket で送る、リレーサーバー的存在です。よって、メッセージの永続化は行いません。
シンプルにそのまま使用すると、以下の問題が発生します。
- ページをリロードすると履歴が消えてしまう
- 後からチャットルームに入ってきた人が、入ってくる前に送信された内容を確認できない
つまり、メッセージを送信したタイミングでそのチャンネルをサブスクライブしていたクライアントだけがそのメッセージを受信できるのです。過去のメッセージは残りません。
対応策
対応策は2パターン考えられます。
一つは、シンプルにアプリケーション層で AppSync Event に送信しつつ、永続化することです。コードとしては次のようなイメージです。
await Promise.all([
events.post(`channel/${roomId}`, JSON.stringify(message)),
repository.addMessage(message, roomId)
])
この実装の場合、クライアントサイドでは、最初にevents.connect
する前に、過去のメッセージを取得するAPIをコールします。これは別途 REST API として立てましょう。
二つ目としては、Channel Namespace Handler および Data Sourceの機能を使うことです。
この機能は、AppSync GraphQL API のリゾルバーに近い機能で、AppSync Event API が新たなメッセージを受け取った場合や接続の確立時に、追加のアクションを行うことが可能になります。
この機能を使って、データソースとしてDynamo DBやAuroraを接続したり、Lamdbda経由で任意のデータレポジトリに履歴を保存することができます。AppSync GraphQLを使ったことのある方などはこちらの方が馴染むかもしれません。
クライアントからの取得に関しても、Channel Namespace Handler でやろうと思えばできると思いますが、一つ目の方法と同様、こちらは別途fetchしてくる方がシンプルで良さそうです。
生成AIアプリケーション
今ホットな生成 AI アプリケーションにも向いています。ChatGPTなどを使っていて、AIの思考過程が見られるUIを見て、同じことをしてみたいと思った方も多いのではないでしょうか?それ、AppSync Events で実現できます。
アーキテクチャとしてはシンプルにこのようになります。
SSEを使ったResponse Streamingでも実現可能ですが、AppSync Events を使うメリットとしてはサーバーレス、ステートフルで書いた通りです。
AWSイベントダッシュボード
これはアイデアベースで、実際にどんなケースが良いかは検討しきれていませんが、以下のようなアーキテクチャも考えることができます。
任意のAWS イベント -> EventBridge -> HTTP Endpoint -> AppSync Event API
このアーキテクチャでは、AWS アカウント内で発生したイベントをキャッチし、AppSync Event API に流すことで、AWSアカウント内で発生したイベントのダッシュボードを作成できます。
まとめ
AppSync Events という 2024 年の Re:inventで新登場したサービスについて紹介しました。
新サービスとはいえ、AppSync GraphQL API から引き継いでいる要素も多いため、十分実用的なレベルに機能が充実しています。
サーバーからクライアントにメッセージを送りたくなった時にぜひ一度は検討してみてください!
-
(注: 2025/03のアップデートで上りもWebSocketも使えるようになりました) ↩︎
Discussion