この記事はServerless Advent Calendar 2018の 22日目になります。
AWS re:Invent で発表された API Gateway の WebSocket 対応、ついに利用できるようになりました!
WebSocket がサーバーレスで簡単に利用できるようになるとアプリの幅も広がり、いろいろなことができるようになります。
さっそく API Gateway の WebSocket を試してみます。
※ 今回 Serverless Framework の serverless-websockets-plugin を使いますが 2018年12月現在、暫定の実装になるとのことです。API Gateway の WebSocket 対応が AWS CloudFormation で未サポートのため Serverless Framework 本体には含まれず Plugin になっているとのこと。正式版では構文など変わる可能性があることに注意が必要です。
しかしながら “With all that out of the way, play with our new presents!” ということで、楽しんでみましょう!
環境
本記事の開発環境は以下となります。
- Windows 10 64bit + WSL Ubuntu 18.04.1 LTS
- Visual Studio Code
- Node.js 8.10.0
- Yarn 1.12.3
- TypeScript 3.2.2
- Serverless Framework 1.35.1
- Serverless Websockets Plugin 1.0.0
参考情報
- [発表]Amazon API GatewayでWebsocketが利用可能
- Using API Gateway WebSockets with the Serverless Framework
- serverless/serverless-websockets-plugin
WebSocket について
HTTP ベースの Web API ではサーバーに対して接続、リクエストを送り、処理結果のレスポンスを受け取り、切断終了の流れになります。
必要に応じて、このリクエスト/レスポンスのやり取りを繰り返す形になります。
それに対して WebSocket はサーバーに接続後、コネクションを維持したまま双方向でメッセージを送ることができます。
これは通常のリクエスト/レスポンスに当たるやり取りのほかに、サーバー側から任意のタイミングでメッセージを送れます。
たとえば私たちがハッカソンで作った「ラップ、タップ、アップ 🎶」では、演奏者へのフィードバックとして「👍」を送ることができ、その数をリアルタイムでカウントする機能があります。
この「👍」をリアルタイムでカウントする部分を HTTP ベースの Web API で作るとすると、アプリから「👍」の数を取得する API を繰り返し「リクエスト/レスポンス」して受け取る必要が出てきます。
仮に数に変化がなかったとしてもアプリ側は知るすべがないので、繰り返し「👍」の数を取得しに行かなければなりません。
WebSocket が使える場合、コネクションは維持したまま必要な時に「👍」の数をサーバー側がアプリへメッセージを送ることができるようになります。
「👍」の数に変化がなかった場合は維持されているコネクションの中にメッセージが流れないだけでアプリはとくに何もする必要はありません。変化があった時だけサーバーが教えてくれるので、それに応じてアプリの表示を変えるだけになります。
これにより不要なリクエスト/レスポンスを減らせるほか、よりリアルタイムに近い状態で変化を受け取ることができるようになります。
HTTP ベースでは「リクエスト/レスポンス」で受け取り、次のリクエストを出すまでの間にあった変化は結果しか受け取れません。5だった「👍」を次のリクエスト/レスポンスでは 10になっているかもしれません。
WebSocket では状態の変化を断続的に返すことも可能になるので、5、6、7… とメッセージを返せます。
これにより、よりスムーズに描画を変えていくことができるようになります。
これはチャットのようなコミュニケーションや金融などの取引の価格情報などに使うことができます。
今回API Gatewayがこの WebSocket に対応したので、その使い方をチャットを実装することで確認します。
(WebSocket でチャットは定番すぎますが、「👍」のカウントだとさみしいですからね)
※「ラップ、タップ、アップ 🎶」では、API Gateway がまだ WebSocket が扱えなかったので、Firebase Realtime Databaseを使って実現しています。
Node.js プロジェクトの準備
Serverless Framework のボイラープレートを使って、TypeScript の Node.js プロジェクトを作成します。
プロジェクトのディレクトリを作成しプロジェクトのひな型を作ります。
1 | username@pc:~$ mkdir samples-apigateway-websocket-chat |
続いて Serverless Framework をローカルで追加し、AWS SDK と WebSocket クライアント wscat、必要なパッケージを最新化してインストールします。
Serverless Framework はグローバルに追加して使うようですが、複数人開発でグローバルのを使っているとバージョンの違いなどでトラブルがあったのでpackage.json
で明示して、それを使うようにしているためです。サクッと試すにはグローバルでもよいでしょう。
1 | username@pc:~/samples-apigateway-websocket-chat$ yarn add -D serverless serverless-websockets-plugin wscat |
package.json
をプロジェクトに合わせて修正します。
主にname
description
author
あたりを合わせるとよいでしょう。
また、作成しているのはアプリケーションなのでモジュール公開の防止のために"private": true
を設定しておくとよいでしょう。private の詳細は、こちらのpackage.json | npm Documentationをご確認ください。
1 | { |
Serverless Framework の設定
serverless.yml
を編集し Serverless Framework の設定を行います。
1 | service: |
plugins
に API Gateway WebSocket を使うためのserverless-websockets-plugin
を追加します。
iamRoleStatements
では、API Gateway の WebSocket API を呼び出すためのexecute-api:ManageConnections
に許可を与えます。
また WebSocket に接続しているクライアントのコネクションを管理するための DynamoDB が必要なので、DynamoDB に関する許可も与えます。
30行目、31行目のwebsocketApiName
とwebsocketApiRouteSelectionExpression
は、serverless-websockets-plugin の設定になります。websocketApiName
は管理用にわかりやすい名前を設定しておくとよいでしょう。
functions
は WebSocket のイベントとアクションに合わせて定義を行います。events
にwebsocket
が追加でき、websocketApiRouteSelectionExpression
に設定された Key の値とrouteKey
の文字列のマッチングによって起動する Lambda を振り分けています。$connect
$disconnect
は WebSocket の接続と切断に対応します。また$default
はマッチするアクションがない場合に呼び出されます。
最後のsendMessage
が WebSocket で受け取るメッセージのキーになります。具体的には{ "action":"sendMessage", "data":"hello world" }
のような JSON のaction
に入る文字列のマッチングになります。
resources
は WebSocket のコネクション管理用 DynamoDB を定義しています。
今回は最低限のConnectionId
だけを管理します。名前など無しの完全に匿名のメッセージだけのチャットになります。(WebSocket の動きと実装を試すってことで💦)
AWS Lambda の実装
handler.ts
に処理を実装します。
1 | import { APIGatewayEventRequestContext, APIGatewayProxyEvent, APIGatewayProxyHandler, APIGatewayProxyResult } from 'aws-lambda'; |
4行目~10行目の interface 宣言ですが、こちらは API Gateway の WebSocket 対応の TypeScript 型定義がまだ入っていないので補完するために宣言しています。requestContext
に入ってくるdomainName
とconnectionId
が必要なため追加しています。型定義が更新されたら不要となります。
13行目ApiGatewayManagementApi
のインスタンスを取得する関数ですが、API Gateway から WebSocket のメッセージを返す際にendpoint
が必要となり、これがdomainName
とstage
から決まってきます。固定値の場合はよいのですが、今回のように動的に決まってくる場合はrequestContext
から受け取ったdomainName
とstage
が必要なため毎回インスタンスを生成しています。
16行目~58行目でserverless.yml
に定義した Lambda のエントリーポイントと実装があります。
connect
とdisconnect
は WebSocket の「接続/切断」の際に呼び出されます。
接続時にconnectionId
を DynamoDB へ保存し、切断時に削除しています。切断は呼び出されないこともあるので、注意が必要です。
defaultMessage
は WebSocket で受け取ったメッセージに対応するアクションがなかった場合に呼び出されます。ここでは “Error: Invalid action type” をクライアントに返しています。
WebSocket でメッセージを返すにはApiGatewayManagementApi
のpostToConnection()
を呼び出します。その際にクライアントのconnectionId
が必要となります。
この関数のようにメッセージを送ってきたクライアントだけにメッセージを送る場合はrequestContext
のconnectionId
が使えるので、それを使ってエラーメッセージを返します。
最後のsendMessage
は、チャットのコメントを送る処理になります。リアルタイムに複数クライアントへメッセージを送る WebSocket を使う処理の肝となる部分です。
複数クライアントにメッセージを返すには、メッセージを送るべきクライアントすべてのconnectionId
が必要となり、それぞれにApiGatewayManagementApi#postToConnection()
を行う必要があります。
つまり API Gateway が接続中のクライアントを知ってくれてるわけではないので、自前で管理する必要があり、そのために DynamoDB を用意しているものになります。
今回は全員参加の簡単な実装なので、DynamoDB のコネクション管理テーブルをスキャンし、全件に対してApiGatewayManagementApi#postToConnection()
を呼び出しています。(なんか、もっとスマートな実装できないかなぁ)
ApiGatewayManagementApi#postToConnection()
を呼び出す際に、コネクションが切断されているクライアントがありえます。先ほど「切断は呼び出されないこともあるので、注意が必要です。」と書きました通り、クライアントが切断されてもイベントが呼び出されないケースもあります。そのため DynamoDB への削除されずconnectionId
が残るケースもありえます。その場合statusCode
が410
のエラーが投げられるので、そのエラーが投げられた場合は DynamoDB から該当するconnectionId
を削除しておき再発防止しておきます。
デプロイ & 実行!
デプロイは Serverless Framework がしっかりやってくれるので、以下のコマンドで行えます。
あらかじめ AWS の Access Key の用意とプロファイルを設定しておきます。(デフォルト・プロファイルでない場合は--aws-profile [your profile name]
と利用するプロファイル名を--aws-profile
で指定します)
1 | username@pc:~/samples-apigateway-websocket-chat$ yarn serverless deploy |
デプロイが完了すると、コマンドの実行結果に WebSocket の URL が出力されます。
その URL に対してwscat
で接続し API Gateway WebSocket の動きを確認します。
1 | username@pc:~/samples-apigateway-websocket-chat$ yarn wscat -c wss://6ovkt8XX.execute-api.us-west-2.amazonaws.com/dev/ |
接続できると>
で入力待ちになります。{ "action":"sendMessage", "data":"hello world" }
とメッセージを送ると、チャットのコメントが帰ってきます。
1 | username@pc:~/samples-apigateway-websocket-chat$ yarn wscat -c wss://6ovkt8XX.execute-api.us-west-2.amazonaws.com/dev/ |
複数のコンソールから接続すると、送ったコメントがすべてのコンソールに流れてきます。
WebSocket でリアルタイムに複数クライアントへメッセージが遅れていることが確認できます。
チャットとは名ばかりのコメントだけを送りあうだけの実装(しかも wscat によるコマンドライン)でしたが、API Gateway WebSocket の実装がつかめました。
Serverless Framework は今後の AWS CloudFormation 対応によって変わってくる部分がありますが、こんなに簡単に設定できるのでとても助かります。
WebSocket が簡単に利用できるようになったので、いろいろなアプリづくりに生かせそうで楽しみです。
では、ハッピー・サーバーレス・ライフ! ハッピー・ホリデー!!