🗺️

AWS API Gatewayのwebsocket APIでできることとできないこと

2023/10/22に公開

※この記事はサーバレスなオンラインじゃんけんアプリを作ってみたのサブ記事です。

今回開発したオンラインじゃんけんアプリのAWS側の構成図はこんな感じになっており、Clientからのリクエストを受け付けるためにAPI Gatewayを使っており、バックエンドにはLambdaを使っています。

今回はこの構成でwebsocket通信を使ったサーバからのデータ送信や、クライアント間の同期などの処理を実装しました。その中でできたことややりたかったけどできなかったことを残しておきたいと思います。

できたこと

Websocket APIの作成

AWS API Gatewayでは、簡単にWebsocketで接続するAPIが作成できます。マネージドサービスなので、冗長化構成も勝手にやってくれているので、とても簡単にWebsocketAPIを作成することができます。細かい設定は慣れる必要があるのですが、使っていくうちに慣れると思います。
今回はバックエンド側にLambdaを使っていますが、どのLambda関数を使うのかという設定も簡単に行えます。APIを作るのとLambda関数を定義する順番はどちらでも大丈夫でした。
https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api.html

AWS CDKでのAPIの作成例

AWS CDK(javascript)での書き方の例を紹介します。

まずはWebsocketAPIの作成は以下のようにして書きます。

    const api = new apigateway.CfnApi(this, name, {
      name: "JankenGameAPI",
      protocolType: "WEBSOCKET",
      routeSelectionExpression: "$request.body.action",
    });

作成したAPIにLambda関数を紐づける方法は以下のようにします。それぞれで定義している内容は次のとおりです。

  • connectionIntegration: API Gatewayが受信したデータをメタデータを付与してLambda関数に連携する設定
  • connectRoute: routeKeyが$connectのリクエストとconnectionIntegrationを紐づける設定
  • deployment.addDependency: 構築時の依存関係の設定
    const connectIntegration = new apigateway.CfnIntegration(this, "connect-lambda-integration", {
      apiId: api.ref,
      integrationType: "AWS_PROXY",
      integrationUri: "arn:aws:apigateway:" + config["region"] + ":lambda:path/2015-03-31/functions/" + connectFunc.functionArn + "/invocations",
      credentialsArn: role.roleArn,
    });

    const connectRoute = new apigateway.CfnRoute(this, "connect-route", {
      apiId: api.ref,
      routeKey: "$connect",
      authorizationType: "NONE",
      target: "integrations/" + connectIntegration.ref,
    });
    
    deployment.addDependency(connectRoute);

できなかったこと

Pub/Subパターンの実装

AWS API GatewayのWebsocketAPIでは、Socket.ioやAzure Web PubSubサービスのようなPub/Subパターンは使えませんでした。
具体的には、トピックというキーを指定してメッセージを送信したり、特定のトピックにきたメッセージを受信したりするということができませんでした。AWS API GatewayのWebsocketAPIでサーバから接続されているクライアントにメッセージを送る場合には、2つの方法があります。1つ目は、普通のHttpAPIのようにリクエストに対してレスポンスを送る方法で、もう一つはバックエンド側で@connectionコマンドを、クライアントのコネクションごとに発行されているconnectionIdを指定して実行する方法です。そのため、クライアント側で特定のトピックをSubscribeしてサーバからのメッセージを受信するということがそもそもできないのです。
そのため、あるクライアントから来たメッセージを他の特定のクライアントに送る場合は送信先のconnectionIdを特定する必要があるので、例えばDynamoDBで管理するなどアプリ側での設計が必要になります。

今回はどう対応したのか

今回は、ユーザーがマッチングしたときにgameIdという識別子を発行して、それをキーにDynamoDBにそれぞれのconnectionIdを登録して、クライアント側にデータ送信が必要な場合はDynamoDBからconnectionIdを取得するという処理をLambda関数で実装しました。難しい処理ではないものの、DBの設計やDBからデータを取得して送信先を設定する処理など、必要な設計が増えたのでPubSubを使えたらアプリの設計がもっとシンプルになったと思います。

AWS SDKを使った実装例を紹介します。endpointは、WebsocketAPIを作成したときに発行される@connectionコマンド用のURLです。テキストデータだけではなくバイナリデータも送ることができますが、今回紹介しているコードはjsonデータをテキストデータにして送信しています。

import { ApiGatewayManagementApiClient, PostToConnectionCommand } from "@aws-sdk/client-apigatewaymanagementapi";

const sendMessageToClient = async (endpoint, data, receiver) => {
  const client = new ApiGatewayManagementApiClient({
    endpoint: endpoint
  });
  const input = { // PostToConnectionRequest
    Data: JSON.stringify(data),
    ConnectionId: receiver,
  };
  const command = new PostToConnectionCommand(input);
  await client.send(command);
};

まとめ

API Gatewayではサーバを立てることなく簡単にWebsocketAPIを作成できるメリットがある一方で、純粋なWebsocketになるため便利な機能などは別途自分で設計する必要があることが実際に使ってみてわかりました。慣れてみると自由度が高いので設計次第でいろんな機能を実現できそうだと感じましたが、特定のデザインパターン(Pub/Subなど)を使いたい場合は、自分でサーバを立てたり、他のサービスを使用する方が良いと思います。
ただ、Websocketを使うかどうかによってクライアント側の実装にも変わるため、途中からの変更は結構難しく、最初の段階でしっかり吟味する必要がありますね。

Discussion