AppSync に新しく WebSocket による Publish/Subscribe (Pub/Sub) API として AppSync Events がリリースされました。公式ドキュメントでは Amplify クライアントを使った接続のサンプルが紹介されていますが、今回 Amplify を使用しない接続方法を試してみました。
(あとから気づいたのですが、AWS 公式ブログでも Amplify を使わない方式で紹介されていました… 🙇♂️)
AppSync Events とは?
公式ドキュメントでは、コネクションやリソースのスケーリングを管理することなく、リアルタイムのイベントデータを何百万ものサブスクライバーにブロードキャストできる、安全でパフォーマンスの高いサーバーレス WebSocket API を作成できるとあります。
これまでも AppSync ではリアルタイムなイベントを扱うことができましたが、GraphQL でのやり取りが前提でした。また、API Gateway でも WebSocket を扱うことができましたが、コネクションの管理など実装側で考慮すべき点がいろいろありました。今回リリースされた AppSync Evnets は両者からいいとこ取りをしたようなシンプルな WebSocket のマネージドサービスとして利用でき、簡単に WebSocket での Publish / Subscribe が実装できそうです。
公式ドキュメントはこちら : https://docs.aws.amazon.com/appsync/latest/eventapi/event-api-welcome.html
クライアントからの接続方法
公式ドキュメントの Getting started ではクライアントとしては Amplify を使った例が示されています。Amplify を使うことで WebSocket を使った Pub / Sub がシンプルに実装できるのですが、このためだけに Amplify を導入するのは躊躇われるケースもあるかと思い、Amplify を使わずにネイティブな WebSocket(url, protocols)
で AppSync Event に接続してみようと思ったのが今回のモチベーションです。
動作確認したアーキテクチャ
絵にするほどではないですが今回は以下のような構成を作って動作を確認してみました。ブラウザ側の React アプリとマネジメントコンソールの間でメッセージをやり取りしてみます (マネジメントコンソールでも Publish / Subscribe のテストが IoT Core と同じようにできます)。
動作確認環境の構築
動作確認のためのデモ環境を作りました。
1. Event API の作成
まずはじめに、AppSync Events の API を作っていきます。マネジメントコンソールから作成する場合は API の名前を指定するだけで作成できます。自動的に default
という名前空間が作成され、API のデフォルトの認証として API Key が設定 & 作成されます。
作成できたら、API 一覧のリストから作成した API の詳細を開き、“設定” タブを選択後、以下の値をメモっておきます。
- DNS エンドポイントの “HTTP”
- DNS エンドポイントの “リアルタイム”
- “認可設定"のセクションから API キー
AWS CLI で API を作成する場合
CLI で作成する場合は以下のようになります。
エラーになる場合は新しい機能なので AWS CLI がバージョンアップされているか確認してみてください。私はバージョン “2.19.1” で実行しました。
# API の作成
aws appsync create-api --name "My AppSync App" --event-config '{
"authProviders": [ { "authType": "API_KEY" } ],
"connectionAuthModes": [ { "authType": "API_KEY" } ],
"defaultPublishAuthModes": [ { "authType": "API_KEY" } ],
"defaultSubscribeAuthModes": [ { "authType": "API_KEY" } ]
}'
# 名前空間の作成 (`default` という名前で作成する場合)
# ${API_ID} は `create-api` の結果で得られる `api.apiId` の値
aws appsync create-channel-namespace --api-id ${API_ID} --name default
# 認証方法を API Key とした場合、API Key を作成する
# ${API_ID} は `create-api` の結果で得られる `api.apiId` の値
aws appsync create-api-key --api-id ${API_ID}
1つ目の create-api
の実行結果である JSON から以下をメモっておきます。
api.dns.HTTP
: HTTP エンドポイントapi.dns.REALTIME
: リアルタイムエンドポイント
3つ目の create-api-key
の実行結果である JSON から以下をメモっておきます。
apiKey.id
: API キーの値
API Key 認証の場合、最後の create-api-key
コマンドで --expires
オプションで API Key の有効期限を設定できます。現在時刻から Epoch Unix タイムスタンプで指定し、最低でも 1日以上有効期間を設ける必要があります。指定しない場合のデフォルトの有効期間は 7日間なので注意しましょう。
また、API キーは複数発行することができるのでローテーションなども対応しやすいのではないかと思います。
ログ出力や WAF による保護
今回設定しませんが、CloudWatch Logs へはリクエストレベルのログやハンドラーのログを出力することができます (ドキュメント)。また、AWS WAF による保護も可能です (ドキュメント)。
補足: 名前空間と認証方法
API を作成すると、デフォルトの認証モードが、接続時、Publish 時、Subscribe 時 それぞれ用に設定できます。名前空間を作成するとそれぞれの認証モードを引き継ぎますが、上書きすることもできます。名前空間は Publish や Subscribe 時のチャネル名 (トピック名) のプレフィックスとして設定されます。
例えば、名前空間 guest
と user
を作成し、未ログインユーザは API Key 認証で /guest/hoge
が Subscribe できる、ログイン済みユーザは Cognito ユーザプール認証で /user/fuga
が Subscribe できる、といった分け方ができるのではないかと思います。
ついでに、AppSync Events API で使える認証方法は以下が使えるようです (ドキュメント)。
- API Key
- Lambda
- IAM (SigV4)
- OpenID Connect
- Amazon Cognito User Pool
2. クライアント側 (React アプリ) の作成
demo-app
というフォルダを作成し、Vite で React プロジェクトを作っていきます。途中の選択肢は “React” と “TypeScript + SWC” を指定しました。
mkdir demo-app && cd $_
pnpm create vite@latest . -- --template react-ts
必要なものをインストールし、ローカルで React アプリを実行します。
pnpm install
pnpm run dev
続いて、src/App.tsx
を以下のように書き換えます。
|
|
以上で確認環境としては準備完了です。解説の前に先に動かしてみたい方は下の方の動作確認をご覧ください。
(解説) Amplify を利用しない AppSync Events の利用方法
Amplify を使わずにネイティブな WebSocket で AppSync Events に接続する方法を順を追って解説したいと思います。
1. WebSocket オブジェクトの生成
WebSocket オブジェクトを生成する際、必須 1つ、任意 1つの引数を指定することができます (参考: MDN)。
ws = new WebSocket(url, protocols);
url
には AppSync Event API のリアルタイムエンドポイントを使った、wss://${AppSyncRealTimeEndpoint}/event/realtime
を指定します。/event/realtime
のところは固定値ですprotocols
にはプロトコル文字列を設定できます。AppSync Event API では以下の 2つを設定します。aws-appsync-event-ws
- 認証情報などをまとめ、Base64URL 形式でエンコードした文字列
サブプロトコルに指定する認証情報
protocols
に指定する認証情報は認証モードに応じて JSON オブジェクトから作成します。例えば API Key 認証の場合は下記になります。その他の認証モードについてはドキュメントを参照ください。host
は HTTP エンドポイントである点に注意してください。 (行番号は前述の src/App.tsx
での行番号と一致しています。)
|
|
ドキュメントによると、この JSON オブジェクトを Base64URL 形式でエンコード (さらにいくつかの文字を置換)し、、、
|
|
さらに header-
を前に付与したものをサブプロトコルとして指定する必要があるようです。
まとめると、前述の src/App.tsx
では以下のようになります (getWebSocketInstance()
の中で new WebSocket(url, protocols)
を呼び出しています。)
|
|
2. 初期化
接続を確立するだけでは AppSync Events を利用できません。いくつかの手続きを踏む必要があるので、ドキュメントに倣って実行していきます。
まず初期化として init メッセージを送信します。動作確認では、WebSocket 接続確立後、onopen
のタイミングで送信しました。
前述の src/App.tsx
ですと以下の箇所です。
|
|
init メッセージのレスポンス
初期化が成功すると、以下のようなメッセージが返ってきます。今回実装していないですがクライアントが接続タイムアウト期間内 (connectionTimeoutMs
) にキープアライブメッセージを受信しない場合、クライアント側から接続を閉じてあげる必要があるとドキュメントにはあります。
{
"type": "connection_ack",
"connectionTimeoutMs": 300000
}
動作確認では init のレスポンスに対しては何もしていませんが WebSocket.onmessage
で、受信したメッセージの event.data.type
が connection_ack
の時として判別することができます。
前述の src/App.tsx
ですと以下の箇所でハンドリングできます。
|
|
3. Subscribe
初期化が完了すると、Subscribe が可能になります。
Subscribe するには以下のような JSON を WebSocket から送信することで可能になります。
{
"type": "subscribe",
"id": "abcdef12-abcd-1234-abcd-1234567890ab",
"channel": "/YOUR_NAME_SPACE/SUB_A/SUB_B",
"authorization": {
"x-api-key": "da2-12345678901234567890123456",
"host": "example1234567890000.appsync-api.us-east-1.amazonaws.com"
}
}
id
はクライアントコネクションごとに一意である必要があります。重複するとエラーが返ります。また、Subscribe を解除 (Unsubscribe) する時に必要になります。authorization
は WebSocket の接続時の認証情報と同じものを指定します (src/App.tsx
の 37 - 40行目)authorization.host
はリアルタイムエンドポイントではなく HTTP エンドポイント である点にご注意ください
今回の動作確認では、ボタンクリック時に Subscribe を開始するようにしました。
前述の src/App.tsx
ですと以下の箇所です。
|
|
Subscribe に成功すると event.data.type
に subscribe_success
が設定されたメッセージが返ります。event.data.id
は Subscribe する際に指定した ID と同じで、Subscribe を解除 (Unsubscribe) する時に必要になります。エラーが発生すると subscribe_error
が返されます (メッセージフォーマットなどはドキュメントを参照してください)。
前述の src/App.tsx
ですと以下の箇所でハンドリングできます。
|
|
4. データメッセージの受信
Subscribe が成功し、Subscribe しているチャネルにイベントが Publish されると、データメッセージとして以下のフォーマットで受信できます。
{
"type": "data",
"id": "abcdef12-abcd-1234-abcd-1234567890ab",
"event": ["\"my published message \""]
}
前述の src/App.tsx
では event.data.type
が data
の時にハンドリングできます。
今回の動作確認では受信したメッセージを単純にリストに追加していきました。
|
|
ブロードキャストエラーが発生すると event.data.type
に broadcast_error
が設定されてクライアント側に届くそうです (メッセージフォーマットなどはドキュメントを参照してください)。
前述の src/App.tsx
ですと以下の箇所でハンドリングできます。
|
|
5. Subscribe 解除 (Unsubscribe)
Subscribe を解除するには Subscribe 時に指定した ID (= Subscribe が成功した時の応答メッセージの ID) を指定する必要があり、以下のようなフォーマットになります。
{
"type": "unsubscribe",
"id": "abcdef12-abcd-1234-abcd-1234567890ab"
}
今回の動作確認では、“Unsubscribe” ボタンを押した際に Unsubscribe するようにしてみました。
|
|
Unsubscribe に成功した場合は、event.data.type
に unsubscribe_success
が設定されたメッセージが届きます。今回の動作試験では保管しておいた Subscription ID をリセットするようにしました。
前述の src/App.tsx
ですと以下のようにしています。
|
|
Unsubscribe にエラーがあった場合、event.data.type
に unsubscribe_error
が設定されたメッセージが届きます (メッセージフォーマットなどはドキュメントを参照してください)。
前述の src/App.tsx
ですと以下の箇所でハンドリングできます。
|
|
6. Publish
Publish は HTTP のエンドポイントから POST メソッドで実行するようです。ドキュメントはこちら。
今回の動作確認では以下のようにしてみました。
|
|
リアルタイムエンドポイントから Publish はできないのか
ドキュメントによるとリアルタイムのエンドポイントでも対応してそうに読み取れたのですが、UnknownOperation となってしまい Publish できませんでした (私の指定の仕方が悪かったのかもしれません…)。
type:
The type of operation being performed. Supported client operations are subscribe, unsubscribe, publish. The property is a string and must be one of the message types defined in the next section, Configuring message details.
リアルタイムエンドポイントを使って以下のように Publish してみたのですが (いろいろ試してみたのですが)…
wsRef.current.send(JSON.stringify({
"type": "publish",
"id": crypto.randomUUID(),
"channel": channel,
"events": [ "message" ],
"authorization": {
"host": AppSyncHttpEndpoint,
"x-api-key": AppSyncApiKey,
}
}));
以下のようなエラーになってしまいました。
{
"id": "abcdef12-abcd-1234-abcd-1234567890ab",
"type": "publish_error",
"errors": [
{
"errorType": "UnknownOperationException",
"message": "Unknown Operation Request."
}
]
}
7. Keep-Alive の受信
AppSync Events から 60秒間隔で Keep-Alive メッセージが届きます。event.data.type
が ka
の時として受信できます。
src/App.tsx
では以下の箇所でハンドリングできます。
|
|
今回は実装していないのですが、2. の初期化時で得られる connectionTimeoutMs
のタイマーをリセットする実装を入れると良さそうです。
動作確認
それでは実際に動かして確認してみましょう (動作確認の環境構築 が完了しており、pnpm run dev
でローカルのサーバが起動していることが前提です)。
1. React アプリ (Subscribe) <- AppSync <- マネジメントコンソール (Publish)
まずは React アプリから Subscribe し、マネジメントコンソールから Publish したメッセージが届くか確認してみます。
ブラウザを開き、http://localhost:5173 に接続すると以下のようになるかと思います。“Subscribe” ボタンを押して Subscribe します。
続いてマネジメントコンソールから作成した API の詳細を開き、“Pub / Sub エディタ” タブを選択します。
“パブリッシュ” セクションで認証タイプを合わせ、“チャンネル” を React アプリと合わせます (React アプリの Channel が /default/test
であれば、名前空間に default
、次のテキストボックスに /test
と入れます)。
テキストエリアに適当にメッセージを入れ “パブリッシュ” します。
以下のように React アプリ側でメッセージが受信できていれば OK です。
“Unsubscribe” ボタンを押すと、Subscribe を解除します。
2. マネジメントコンソール (Subscribe) <- AppSync <- React アプリ (Publish)
続いて逆向き、マネジメントコンソールから Subscribe し、React アプリから Publish したメッセージが届くかも確認してみます。
マネジメントコンソールで作成した API の詳細を開き、“Pub / Sub エディタ” タブを選択後、下の方に “サブスクライブ” セクションがあります。
“チャンネル” はこちらではワイルドカード (/*
) を指定してみます。“接続” 後、“サブスクライブ” します。
続いて React アプリ側から Publish します。
マネジメントコンソール側でもメッセージが受信できていれば OK です。
まとめ
AppSync Events という使いやすい WebSocket のマネージドサービスが登場しました。公式で案内されている利用方法のサンプルが Amplify を利用する方式でしたのでネイティブな WebSocket で利用する方法を確認してみました。Publish に関しては HTTP エンドポイントを使った POST になるというところはポイントかもしれません。
ご参考になれば幸いです。
最後に・・・
この投稿は個人的なものであり、所属組織を代表するものではありません。ご了承ください。