亀のコツコツクラウドインフラ

2025年2月1日

API GatewayでWebSocket APIを構築する方法を解説!

はじめに

WebSocket APIは一度コネクションを確立したら同じコネクションでAPI実行を行うことができるAPIです。

以下のような方におすすめの内容です。

  • リアルタイムのAPIを作成したい
  • 同じページで何回も使ってるAPIを最適化したい
  • AWS認定の学習をしている

この記事ではWebSocket APIについて・設定手順を解説していきます。

設定手順

以下の手順で設定していきます。

  1. 接続用Lambda関数作成
  2. 切断用Lambda関数作成
  3. カスタムルート用Lambda関数作成
  4. それ以外の場合のLambda関数作成
  5. WebSocket APIを作成

接続用Lambda関数作成

接続用のLambda関数を作成していきます。

関数名:webSocketConnection
ランタイム:Python
アーキテクチャ:x86_64
実行ロール:デフォルト

接続したときにCloudWatchログに出力するプログラムを書いていきます。

import json

def lambda_handler(event, context):
    print('Connection Success!')

    return {
        'statusCode': 200,
        'body': json.dumps('Connection Success!')
    }

切断用Lambda関数作成

切断用のLambda関数を作成していきます。

関数名:webSocketDisconnection
ランタイム:Python
アーキテクチャ:x86_64
実行ロール:接続用と同じロール

切断したときにCloudWatchログに出力するプログラムを書いていきます。

import json

def lambda_handler(event, context):
    print('Connection Finish…')
    return {
        'statusCode': 200,
        'body': json.dumps('Connection Finish…')
    }

カスタムルート用Lambda関数作成

クリックしたら実行されるカスタムルート用Lambda関数を作成していきます。

関数名:webSocketDisconnection
ランタイム:Python
アーキテクチャ:x86_64
実行ロール:接続用と同じロール

クリックしたときにjsonデータを返すプログラムを書いていきます。

import json
import boto3

def lambda_handler(event, context):
    connection_id = event['requestContext']['connectionId']
    
    response_message = {
        'statusCode': 200,
        'body': json.dumps('Click Handler!')
    }
    api_gateway_manage = boto3.client(
        'apigatewaymanagementapi',
        endpoint_url='https://'+event['requestContext']['domainName']+'/'+event['requestContext']['stage']
    )

    api_gateway_manage.post_to_connection(
        ConnectionId=connection_id,
        Data=json.dumps(response_message['body'])
    )

    return response_message

それ以外の場合のLambda関数作成

それ以外の場合のLambda関数を作成していきます。

関数名:webSocketDisconnection
ランタイム:Python
アーキテクチャ:x86_64
実行ロール:接続用と同じロール

土の場合でもなかった場合のjsonデータを返すプログラムを書いていきます。

import json
import boto3

def lambda_handler(event, context):
    connection_id = event['requestContext']['connectionId']

    response_message = {
        'statusCode': 200,
        'body': json.dumps('No Processing')
    }

    api_gateway_manage = boto3.client(
        'apigatewaymanagementapi',
        endpoint_url='https://'+event['requestContext']['domainName']+'/'+event['requestContext']['stage']
    )

    api_gateway_manage.post_to_connection(
        ConnectionId=connection_id,
        Data=json.dumps(response_message['body'])
    )

    return response_message

WebSocket APIの作成

WebSocket APIの作成を行っていきます。

API名とルート選択式を入力していきましょう。

ルート選択式は「request.body.*」の形式である必要があります。

次へ行くとルートの設定を行う画面に遷移します。

どのタイミングでトリガーさせるかという設定になります。

  • $connect:接続時
  • $disconnect:切断時
  • $default:それ以外の時
  • カスタムルート:メッセージの内容に応じてルーティング

ルートの追加のボタンがあるので、クリックして追加していきます。

作成したルートにそれぞれ実行したいLambda関数を割り当てていきます。
APIがある場合はそれを設定してもOKです。

今回は、先ほど作成したLambda関数をそれぞれに割り当てていきます。

ステージは本番環境では、以下のように任意の命名をしていきましょう。

開発環境等、操作がめんどくさければ$defaultで問題ありません。

設定が完了したらWebSocket APIを作成していきます。

作成ができたら双方向通信を有効にしましょう。

動作確認

動作確認を行っていきます。

まず、Lambda関数がAPIGatewayの返却データに書き込みを行えるようにする必要があります。

Lambda関数に使用しているIAMロールに以下のポリシーを追加します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "execute-api:ManageConnections",
            "Resource": "arn:aws:execute-api:ap-northeast-1:[アカウントID]:[WebSocket APIのID]/[ステージ名]/POST/@connections/*"
        }
    ]
}

次にクライアントサイドで実行するプログラムを書いていきます。

コピペをしてWebSocketAPIのエンドポイントの編集だけ行って、表示して実行してください。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WebSocket API</title>
</head>
<style>
  p {
    text-align: center;
  }
</style>
<body>
  <h1>WebSocket API サンプル</h1>
  <p><button id="send_button">WebSocket APIを実行</button></p>
  <p><button id="send_else_button">WebSocket APIを実行(予定外)</button></p>
  <p><button id="disconnect_button" style="margin-left: 10px;">WebSocketを切断</button></p>

  <strong>レスポンスデータ:</strong>
  <pre id="response_data"></pre>

  <script>
    // WebSocket APIのエンドポイントを設定
    const ws_endpoint = "wss://[WebSocket APIのID].execute-api.ap-northeast-1.amazonaws.com/[ステージ名]/";

    // WebSocketの接続を初期化
    let web_socket;

    // ページがロードされたときにWebSocketに接続
    window.addEventListener("load", () => {
      connectWebSocket();
    });

    function connectWebSocket() {
      // WebSocketのインスタンスを作成
      web_socket = new WebSocket(ws_endpoint);

      // WebSocketが開いたとき
      web_socket.onopen = () => {
        console.log("WebSocket接続が確立しました。");
        document.getElementById("response_data").textContent = "WebSocket接続が確立しました!";
      };

      // サーバーからメッセージを受信したとき
      web_socket.onmessage = (event) => {
        const response = event.data; // Lambdaからのレスポンスデータ
        console.log("サーバーからのレスポンス:", response);

        // レスポンスデータを画面に表示
        document.getElementById("response_data").textContent = response.replace(/</g, "&lt;").replace(/>/g, "&gt;");
      };

      // WebSocketが閉じられたとき
      web_socket.onclose = (event) => {
        console.log("WebSocket接続が切断されました:", event.reason);
        document.getElementById("response_data").textContent = "WebSocket接続が切断されました!";
      };

      // エラーが発生したとき
      web_socket.onerror = (error) => {
        console.error("WebSocketエラー:", error);
        document.getElementById("response_data").textContent = "WebSocketエラーが発生しました!";
      };
    }

    // ボタン押下時の処理: メッセージ送信
    document.getElementById("send_button").addEventListener("click", () => {
      // メッセージをWebSocketに送信
      if (web_socket && web_socket.readyState === WebSocket.OPEN) {
        const message = JSON.stringify({ action: "customClickRote", data: "Click WebSocket!" });
        console.log("メッセージを送信:", message);

        web_socket.send(message);
      } else {
        console.error("WebSocket接続が開いていません。");
      }
    });

    // ボタン押下時の処理: メッセージ送信
    document.getElementById("send_else_button").addEventListener("click", () => {
      // メッセージをWebSocketに送信
      if (web_socket && web_socket.readyState === WebSocket.OPEN) {
        const message = JSON.stringify({ action: "sendMessage", data: "No Handler" });
        console.log("メッセージを送信:", message);

        web_socket.send(message);
      } else {
        console.error("WebSocket接続が開いていません。");
      }
    });

    // 切断ボタン押下時の処理
    document.getElementById("disconnect_button").addEventListener("click", () => {
      // WebSocket接続を切断
      if (web_socket && web_socket.readyState === WebSocket.OPEN) {
        web_socket.close(); // 接続を閉じる
        console.log("WebSocket接続を手動で切断しました。");
      } else {
        console.warn("WebSocket接続が既に切断されています。");
      }
    });
  </script>
</body>
</html>

表示やコンソールで以下のようにAPIが実行されたらOKです。

接続できているかは、Lambda実行時点ではWebSocketの接続ができていない影響でレスポンスできません。
onopenでWebSocketが開いたときの処理なので、それで判断しましょう。

もし、Lambda関数実行されてるか不安な場合は、CloudWatchログにも出力するようにしているので、そちらを確認しましょう。

IAMロールは最初に作成したIAMロールのCloudWatchログのIAMポリシーに切断用のLambda関数も入れましょう。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:ap-northeast-1:897729135778:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:ap-northeast-1:[アカウントID]:log-group:/aws/lambda/webSocketConnection:*",
                "arn:aws:logs:ap-northeast-1:[アカウントID]:log-group:/aws/lambda/webSocketDisconnection:*"
            ]
        }
    ]
}

CloudWatchログで以下のように出力されてたら、Lambda関数が実行されています。

WebSocket APIについて

WebSocketは一度コネクションを確立してしまい、コネクションを毎回接続する必要がないので、REST APIやHTTP APIと比べると、接続時間分レスポンスが短縮されます。

そのため、何度も同じAPIを実行する場合はWebSocketにすると、パフォーマンスのいいシステムが構築できます。

例を挙げると、チャットアプリやオンラインゲームが代表例でよく挙げられます。

LambdaとAPIエンドポイントを使用するか

Lambda関数とREST APIやHTTP APIのURLエンドポイントのどちらから実行したほうがいいかですが、Lambda関数を実行したほうがいいでしょう。

コスト的にもWebSocketAPI分とHTTP APIやREST API、二重でコストが発生してしまいます。
また、パフォーマンスはWebSocketAPI→HTTP API→Lambdaと実行されるので、少し遅延が発生します。

APIの管理機能を活用したい場合は、URLエンドポイントから実行した方がいいです。

まとめ

WebSocket APIは、リアルタイム通信や頻繁なデータ送受信が必要なシステムに最適で、チャットアプリやオンラインゲームなどで効率的な通信を行えます。

REST APIやHTTP APIと比べてレスポンス時間を短縮できる一方、常時コネクションを維持するためコスト管理をしなければいけません。

用途に応じて適切なAPIを選択し、AWS LambdaやAPI Gatewayを組み合わせて最適なシステムを構築しましょう。

この記事を書いた人

コツコツ亀

コツコツ亀

Webエンジニアとして活動中
AWSを使用してWebアプリケーションを作ったり、サーバーを構築したりしています。

関連記事

新着記事