API Gateway + WebSocket の基本的な使い方を調べてみた
西田@大阪です。
API Gateway で WebSocket を利用する場合の基本的な使い方や料金、Serverless Framework でのデプロイ方法について調べてみました
API Gateway で WebSocket を利用する場合、まずクライアントとAPI Gateway がステートフルなコネクションを確立します。
そして、リクエストの内容や、特殊なイベントに応じてルーティングされ、ルーティングに応じた Lambda を起動します。
起動されたLambdaの処理の中でクライアントを指定しメッセージを送信します
料金
必要な料金は以下の3つです
項目 | 料金 |
---|---|
API Gateway でコネクションを保持し続ける料金 | 100万分あたり $0.315 |
メッセージの送受信回数 | 100万メッセージあたり $1.26 |
Lambdaの利用料金 | 1msあたり $0.000001667 ※メモリ1Gの場合 |
※ 東京リージョンの料金です。参考: https://aws.amazon.com/jp/api-gateway/pricing/
1000人の非常にアクティブユーザーがいると仮定して、それぞれ1日に2時間接続し1000メッセージをやり取りするとして
360万分(約 $1
) + 3000万メッセージ(約 $30
) + Lambdaの利用料金
月$50程度となるので、常時接続のためのEC2を起動しておくことに比べてだいぶ低価格で構築できるのではないでしょうか
※ 常時接続のコネクションはユーザーが意図せずに保持されることも多いので注意は必要です
ルーティング
ルーティングは ルート選択式(RouteSelectionExpression)
と呼ばれる、メッセージ内に含める文字列によるルーティングと 特殊なルート
によってルーティングされます
ルート選択式(RouteSelectionExpression)
ルート選択式にはJSON形式で送られてるくるメッセージのJSONPathを指定します
例をあげますと、以下のJSONがクライアントから送られてきたとします
{ "action": "actionRoute", "service": "serviceRoute", "data": { "parameter": "aabbcc" } }
この場合ルート選択式が以下の場合actionRoute
にルーティングされます
$request.body.action
また、以下のルート選択式の場合 serivceRoute
にルーティングされます
$request.body.service
複数の変数を設定することも可能で、以下の場合 actionRoute
、 serviceRoute
両方にルーティングされます
${request.body.action}/${request.body.service}
参考: ルート選択式
特殊なルート
キー | 説明 |
---|---|
$connect | クライアント接続時にルーティングされます。認証・認可、また接続ユーザーを管理したい場合に使えます |
$disconnect | クライアント切断時にルーティングされます。リソースの後処理などが必要な場合に使えます |
$default | すべてのルートにはずれた場合にルーティングされます。エラー処理やフォールバック、また別のバックエンドに流すためのプロキシとして使いたい場合に使えます |
クライアントとの通信時のフローは以下の通りです
- クライアントから API Gateway に接続時に
$connect
に設定されたLambdaが起動します - メッセージ内のボディに含まれるキーをもとにルーティングされ、ルーティングに応じたLambdaが起動します
- クライアントから切断、もしくはタイムアウトによる切断が行われたときに
$disconnect
に設定された Lambdaが起動します
Serverless Framework を使ってデプロイ
今回は Serverless Framework で python を使ってのデプロイする例をご紹介します
※ サンプルとしてコードはGithubにアップしております
serverless.yml
provider.websocketsApiRouteSelectionExpression
にルート選択式を指定します
provider: name: aws runtime: python3.8 # ルート選択式 websocketsApiRouteSelectionExpression: $request.body.action
続いてLambdaを起動するルーティングの設定をします
- 接続時のルーティング($connect) => handler.connect_handler
- ルート選択式で指定された値が
actionRoute
の場合 => handler.action_handler - 切断時のルーティング($disconnect) => handler.disconnect_handler
- デフォルトのルーティング($default) => handler.default_handler
functions: coonectHandler: handler: handler.connect_handler events: - websocket: $connect actionHandler: handler: handler.action_handler events: - websocket: actionRoute disconnectHandler: handler: handler.disconnect_handler events: - websocket: $disconnect defaultHandler: handler: handler.default_handler events: - websocket: $default
参考: Serverless Framework - AWS Lambda Events - Websocket
handler
サンプルのhandlerの一部をご説明します
クライアントから接続があった場合にルーティングされる connect_handlerです。サンプルは statusCode
に200を返しておりますが、接続を拒否したい場合は500等を返すようにします
def connect_handler(event, context): return { "statusCode": 200 }
次にリクエストで送られてきたメッセージの action
要素のValueに actionRoute
という文字列が設定されていた場合に起動するaction_handlerです。
メッセージを送信してきたクライアントに対し {"message": "sample"}
というメッセージを送信するといった単純な処理をおこなっています
具体的な手順としては以下です
- eventに渡されてくる
RequestContext
からconnectionId
を取得します - API Gatewayに接続されているクライアントにメッセージを送信するためboto3の ApiGatewayManagementApi を操作するためのインスタンスを生成します。その際に
endpoint_url
が必要となるので、requestContext
に含まれるドメイン名とstage
を使って生成します - ApiGatewayManagementApi の
post_to_connection
メソッドでクライアントにメッセージを送信します
def action_handler(event, context): connection_id = event["requestContext"]["connectionId"] apigw = get_apigw_management_client(event) apigw.post_to_connection( ConnectionId=connection_id, Data=json.dumps({ "message": "sample" }) ) return { "statusCode": 200 } def get_apigw_management_client(event): domain = event["requestContext"]["domainName"] stage = event["requestContext"]["stage"] return boto3.client('apigatewaymanagementapi', endpoint_url=f'https://{domain}/{stage}')
client.html
WebSocket 通信用のインスタンスを生成します
const ws = new WebSocket("wss://${YOUR-WEB-SOCKET-ENDPOINT}")
生成したインスタンスにメッセージ受信のハンドラを設定し、その中でJSON文字列をパースしています
ws.onmessage = function (e) { const data = JSON.parse((e.data)) // ... }
action
属性にactionRoute
を設定したメッセージを送信しています
const message = { "action": "actionRoute", "service": "serviceRoute", "data": { "parameter": "aabbcc" } } ws.send(JSON.stringify(message))
最後に
常時接続のサーバーを管理するのは大変なためWebSocketの利用を諦めた方も多いのではないでしょうか?お手軽に試せる上に、サーバーレスで管理もだいぶ楽になったので、これを気に試してみると良いかもしれません。