6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

WebSocket ミニマム実装 (TypeScript x Serverless x AWS API Gateway)

Last updated at Posted at 2021-07-04
  • なんか古いやり方の記事しかなかったので、自分で調べたことを書き残しておくよ

前提・読者の想定

  • 普段 Serverless x Lambda x Node.js x TypeScript が扱えていること
    • そこの詳しい説明は無い

やりたいこと

  • WebSocket サーバーを TypeScript で書くよ
  • AWS API Gateway で serve するよ
    • Serverless でデプロイするよ
  • ローカル環境でも簡単にデバッグしたいよ
  • 以上のことをシンプルに実現したいよ
    • 公式サンプルだとDynamo使ったりしてるけど、Dynamo触りたくないし。。。

メリット

  • AWS でオートスケールする WebSocket サーバーが書けるよ
  • とってもお手軽だし、そのうえランニングコストも低い
    • (比較対象: GCP CloudRun しか見てないけど。)

やったこと

準備

# in your lambda project
npm i aws-lambda-ws-server

# for testing websocket
npm i -g wscat

他のドキュメントでよく見る serverless-websockets-plugin本日時点で不要

aws-lambda-ws-server

  • メリット
    • ローカル起動と、Serverless でのデプロイがサポートされているよ
    • ちゃんと動いたし、中身がシンプルでバグる余地も少なそうだから採用したよ
  • デメリット
    • Starが少ないよ
    • README がいろいろ説明不足だよ
    • 型定義もないよ 😇
  • 注意点
    • MAPPING_KEY 環境変数で route を分岐するために使うプロパティ名の指定が必要
    • ローカル起動のデフォルトでは (message json).message が参照される (なんでやねん)
    • デプロイ後のデフォルトは (message json).action
    • このサンプルでは環境変数を使ってローカルの挙動を action に合わせている

src/handler.ts

const ws = require("aws-lambda-ws-server");

export const websocketApp = ws(
  ws.handler({
    // 接続時に通る
    async connect(event: WebSocketEvent) {
      console.log("connection %s", event.id);
      return { statusCode: 200 };
    },

    // 切断時に通る
    async disconnect(event: WebSocketEvent) {
      console.log("disconnect %s", event.id);
      return { statusCode: 200 };
    },

    // 未定義の action を指定すると通る
    async default(event: WebSocketEvent) {
      const {
        id: connectionId,
        message: { body },
        context: { postToConnection },
      } = event;

      console.log("default message", connectionId, body);

      await postToConnection({ action: "default", echo: body }, connectionId);

      return { statusCode: 200 };
    },

    // "myAction1" アクションのハンドラ
    async myAction1(event: WebSocketEvent) {
      const {
        id: connectionId,
        message: { body },
        context: { postToConnection },
      } = event;

      console.log("myAction1", connectionId, body);

      await postToConnection({ action: "myAction1", echo: body }, connectionId);

      return { statusCode: 200 };
    },

    // "myAction2" アクションのハンドラ
    async myAction2(event: WebSocketEvent) {
      // 省略: myAction1 と同等の実装
    },

    // 動かないサンプル (ローカルでは動くが、デプロイ後は動かない)
    // なぜなら serverless.yml の route 定義が漏れているから
    async myAction3(event: WebSocketEvent) {
      // 省略: myAction1 と同等の実装
    },
  })
);


/**
 * aws-lambda-ws-server に型定義がないので書いておく
 */
export type WebSocketEvent = {
  id: string;
  event: {
    requestContext: {
      routeKey: string;
      eventType: "CONNECT" | "DISCONNECT" | "MESSAGE";
      connectionId: string;
    };
    multiValueHeaders: { [key: string]: string[] };
    body: string;
  };
  context: {
    postToConnection: (body: any, connectionId: string) => Promise<void>;
  };
  // message は any json だが、このプロジェクトでは常にこの形として定義する
  message: {
    action: string;
    body: any;
  };
};

serverless.yml

※追記したところだけ抜粋


provider:
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - "execute-api:ManageConnections"
          Resource:
            - "arn:aws:execute-api:*:*:**/@connections/*"

functions:
  websocketApp:
    handler: src/handler.websocketApp
    events:
      - websocket:
          route: $connect
      - websocket:
          route: $disconnect
      - websocket:
          route: $default
      - websocket:
          route: myAction1
      - websocket:
          route: myAction2

ローカルデバッグ

console 1 (server)

# 先述の注意点。route分岐のために使うプロパティ名を環境変数で指定
$ export MAPPING_KEY=action
$ npx ts-node-dev --inspect=9229 --clear --respawn src/handler.ts

# 以下 client からのリクエストに対するログ出力
connection NB+jzmXmqOtPpnCa8eNVDw==
myAction1 NB+jzmXmqOtPpnCa8eNVDw== { hello: 'websocket1' }
myAction2 NB+jzmXmqOtPpnCa8eNVDw== { hello: 'websocket2' }
myAction3 NB+jzmXmqOtPpnCa8eNVDw== { hello: 'websocket3' }
default message NB+jzmXmqOtPpnCa8eNVDw== { hello: 'websocket4' }
disconnect NB+jzmXmqOtPpnCa8eNVDw==

console 2 (client)

$ wscat -c ws://localhost:5000
Connected (press CTRL+C to quit)
> {"action":"myAction1","body":{"hello":"websocket1"}}
< {"action":"myAction1","echo":{"hello":"websocket1"}}

> {"action":"myAction2","body":{"hello":"websocket2"}}
< {"action":"myAction2","echo":{"hello":"websocket2"}}

> {"action":"myAction3","body":{"hello":"websocket3"}}
< {"action":"myAction3","echo":{"hello":"websocket3"}}

> {"action":"myAction4","body":{"hello":"websocket4"}}
< {"action":"default","echo":{"hello":"websocket4"}}

デプロイ後の挙動

& wscat -c wss://{****}.execute-api.ap-northeast-1.amazonaws.com/{****}
Connected (press CTRL+C to quit)
> {"action":"myAction1","body":{"hello":"websocket1"}}
< {"action":"myAction1","echo":{"hello":"websocket1"}}

> {"action":"myAction2","body":{"hello":"websocket2"}}
< {"action":"myAction2","echo":{"hello":"websocket2"}}

# ⭐ serverless.yml に "myAction3" route 定義がないため action が認識されていない
> {"action":"myAction3","body":{"hello":"websocket3"}}
< {"action":"default","echo":{"hello":"websocket3"}}

> {"action":"myAction4","body":{"hello":"websocket4"}}
< {"action":"default","echo":{"hello":"websocket4"}}
6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?