Blogへ戻る

Language: 日本語

AppSync Eventsを用いたイベントドリブンなUI更新

画面に「処理が終わったよ」「新しいデータがあるよ」という通知をリアルタイムに届けたい場面はよくあります。

画面に「処理が終わったよ」「新しいデータがあるよ」という通知をリアルタイムに届けたい場面はよくあります。

ただ、これを真面目に作ろうとすると途端にインフラと実装の負担が増えます。WebSocketやSSE(Server-Sent Events)のサーバーを自前で立ててコネクションの状態を管理し、切断時の再接続や複数インスタンス間のfanout、認証・認可の引き継ぎまで面倒を見る必要があります。やりたいことは「更新されたこと」をブラウザに伝えるだけなのに、通知のパイプラインを作るだけでかなりの工数を取られてしまいます。

この記事では、AWS AppSync Eventsを使って、バックエンドで起きたイベントをブラウザまで届ける「イベントドリブンなUI更新」の設計と実装についてまとめます。

AppSync Events とは

AWS AppSync と聞くと、多くの方は「AWSのマネージドなGraphQLサービス」を思い浮かべるのではないでしょうか。

しかし、2024年の AWS re:Invent で発表された AppSync Events(Event API)には、GraphQLは一切登場しません

従来、クライアントとサーバー間でリアルタイムなpush通知を行う手段として、AppSync GraphQLのSubscription機能がよく使われていました。しかし「ただ通知やイベントをやり取りしたいだけなのに、GraphQLのスキーマ定義やリゾルバーの学習コストを背負わなければならない」という課題がありました。

AppSync Events は、まさにその 「GraphQLのSubscription機能」からGraphQLを完全に取り除き、WebSocketによる購読・配信の仕組みだけをシンプルに切り出したサーバーレスサービス です(AWS公式の解説記事でも詳しく紹介されています)。「GraphQLは使わないけれど、AppSyncの強力なSubscription機能だけが欲しい」という開発者にとって、まさに待望の仕組みと言えます。

基本構造

AppSync Events を構成する要素は主に2つです。

  1. Event API: 払い出されるエンドポイントの実体(例: https://xxxx.appsync-api.ap-northeast-1.amazonaws.com/event
  2. Channel Namespace: エンドポイントの第一パスとなるグループ(例: /default

認証・認可(APIキー、IAM、Cognito、OIDC、Lambdaオーソライザー)やハンドラーは、このChannel Namespace単位で設定できます。

クライアントやサーバーは、Namespace配下の任意のチャンネル(例: /default/sessions/{sessionId})に対してメッセージをPublishし、購読側はWebSocketでSubscribeします。

送信(Publish)はHTTP POSTとWebSocketのどちらからでも行え、受信(Subscribe)はWebSocketで行います。

Cloud Run で WebSocket を持つ場合との比較

バックエンドが Cloud Run などのコンテナ構成である場合、自前で WebSocket 接続を受ける構成がまず候補に挙がります。

しかし、Cloud Run で WebSocket を持つと、接続が開いている間はずっとアクティブなインスタンスとして扱われ、接続時間に応じた課金が続きます。また、スケールアウト時にインスタンス間でメッセージを同期する仕組み(Pub/Sub や Redis 等)が必要になり、タイムアウトや再接続の制御も含めて管理すべきインフラ面積が一気に広がります。

AppSync Events を通知線として使う場合、料金は Event API 操作(100万回あたり1.00ドル)と connection minutes(100万分あたり0.08ドル)の従量課金です。

接続の維持や fanout のスケール管理をマネージドに任せられるため、「通知線だけが欲しい」という要件に対してインフラの運用負荷を最小限に抑えられます

設計方針: 状態は運ばず、シグナルだけを流す

AppSync Events を使う上で、私が最も意識している設計の原則がこれです。

「Events では本文や最新状態を運ばず、再取得(再フェッチ)のシグナルだけを流す」

[サーバー処理完了]

       ▼ (1) Publish { sessionId, type: "UPDATED" }
[AppSync Events]

       ▼ (2) WebSocket Push (シグナル受信)
[フロントエンド]

       ▼ (3) invalidateQueries (API再取得)
[通常のREST / tRPC API] ──▶ 正本データをDBから取得して描画

DBとAPIを正本(Single Source of Truth)として維持し、AppSync Events はTanStack Queryの invalidateQueries を起動するための通知線に徹します。

シグナル配信に限定する理由は2つあります。

  • セキュリティ境界の維持: payloadに機密データを載せず { sessionId, type } だけに留めることで、万が一イベントが漏れても「更新があった」という事実しか出ない。本文の取得は既存の認可付き通常APIに集約できる。
  • 整合性と自己修復性: WebSocketが一時切断されてイベントが欠損しても、画面リロードや再接続後のAPI再取得で確実に最新状態へ収束する。UI側で複雑な順序制御や配送保証を作り込まずに済む。

実装例: LLM チャットボットでの非同期UI更新

この構成が特にハマるのが、複数モデルへの問い合わせを非同期で行うLLMチャットボットのような画面です。

以前GMOインターネットグループでの就業型インターンで開発したLLMチャットボットのバックエンドでも、まさにこの「非同期生成+シグナル配信+API再取得」の型を採用しました(全体のシステム構成や背景はインターン参加記の記事でも触れています)。

User ─────────▶ Backend API: メッセージ送信

                  ├─▶ DB: ユーザー入力を保存
                  ├─▶ (非同期タスク起動)
                  └─◀ 202 Accepted を即座に返却

[非同期ワーカー]
  ├─▶ 複数LLMへ並列リクエスト & 応答をDB保存
  └─▶ AppSync Events へ Publish: { sessionId, type: "RESPONSE_READY" }

Frontend (WebSocketで購読中)
  ├─◀ AppSync Events からシグナル受信
  ├─▶ Backend API: 一覧を再取得 (refetch)
  └─◀ 最新の会話履歴を取得して描画更新

送受信はお手軽なコード断片だけで完結する

AWS Amplify等のライブラリを使うと、送受信の配線は数行で書けます。

// 送信側 (バックエンド / Node.js): 処理完了時にシグナルを流す
await events.post(`default/${sessionId}`, JSON.stringify({ type: "SESSION_UPDATED" }));

// 受信側 (フロントエンド): シグナルを受け取ったらクエリを破棄して再取得
const channel = await events.connect(`default/${sessionId}`);
channel.subscribe({
  next: () => queryClient.invalidateQueries({ queryKey: ["messages", sessionId] }),
});

バックエンドはHTTP POSTで叩くだけ、フロントエンドはイベントを受け取ったらTanStack Queryのキャッシュを破棄して再取得を走らせるだけです。長いボイラープレートを書く必要はありません。

Outbox パターンとの併用

さらに信頼性を高めたい場合は、Transactional Outbox パターンと組み合わせるのが有効です。

DB書き込みのトランザクション内で outbox テーブルにイベント発行レコードを一緒に保存し、非同期のリレー処理(CDCや定期ポーリング)がそれを読み出して AppSync Events へ Publish します。

[アプリケーション処理]
  └─▶ [DB Transaction]
        ├─ ドメインテーブル更新
        └─ Outboxテーブルにレコード挿入 (status: PENDING)

[リレー処理]
  ├─ Outboxテーブルから未送信レコードを取得
  ├─ AppSync Events へ Publish
  └─ Outboxレコードを SENT に更新

これにより、「DBへの保存は成功したが外部通知の送信に失敗して画面が更新されない」という中途半端な状態を防ぎ、配信の確実性を担保できます。

まとめ

リアルタイム通知の仕組みを検討するとき、各レイヤの役割分担を意識すると構成がシンプルになります。

要件適したサービス
遅延実行・リトライ・レート制御Cloud Tasks / SQS
バックエンド間の疎結合なイベント連携Pub/Sub / EventBridge
ブラウザへのリアルタイム通知(最後の1マイル)AppSync Events
画面の正しい状態データの取得通常の REST / GraphQL / tRPC API

「GraphQLを使わずに、AppSyncの手軽なSubscription機能(サーバーからクライアントへのプッシュ配信)だけを使いたい」、そして通知したい内容が「データの最新化の合図」であるならば、WebSocketに重い状態を持たせる必要はありません。

「AppSync Events は invalidateQueries as a Service」 という合言葉で捉えると、インフラの運用コストとクライアント側の実装の複雑さを両方抑えた、堅牢なリアルタイムUIが作れます。

参考リンク