今回はAPI GatewayでWebSocketを構築して、簡単なLambdaとの紐付けを行います。 さらにReactアプリケーションからWebSocketへの接続を行い、導通テストをしてみたいと思います。
下地として、下記記事を参考にしています。
WebSocket
に関する部分はほとんど引用になりますが、作成したWebSocket
に対して導通を行う箇所をwscat
ではなくReact
アプリケーションから接続する形になります。
ざっくり説明すると、
DynamoDb
でコネクションIDを管理Web
今回使用するLambdaは3種類あります。
2種類はAPI Gateway
で作成したWebSocket
の$connect
と$disconnect
のルーティングに対応したLambda
です。
残りの1つは、WebSocket
に接続したReact
アプリケーションに対してメッセージを送信する用のLambda
です。これに関しては実際に作りたいアプリの形に合わせて柔軟に内容を変えていくかと思います。
ここでは先に$connect
と$disconnect
のLabmda
を作成していきます。
const AWS = require("aws-sdk");
exports.handler = async (event, context) => {
// DynamoDBのクライアントを定義
const client = new AWS.DynamoDB.DocumentClient();
// DynamoDBテーブルに保存する
const result = await client
.put({
TableName: process.env.TABLE_NAME || "",
Item: {
connectionId: event.requestContext.connectionId,
},
})
.promise();
return {
statusCode: 200,
body: "onConnect.",
};
};
const AWS = require("aws-sdk");
exports.handler = async (event, context) => {
// DynamoDBのクライアントを定義
const client = new AWS.DynamoDB.DocumentClient();
// DynamoDBテーブルから削除する
await client
.delete({
TableName: process.env.TABLE_NAME || "",
Key: { [process.env.TABLE_KEY || ""]: event.requestContext.connectionId },
})
.promise();
return {
statusCode: 200,
body: "onDisconnect.",
};
};
続いて、先ほどのLambda
を用いてAPI Gateway
やDynamoDB
を構築していきます。
CDK
を見ていきましょう。
import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as apigateway2 from 'aws-cdk-lib/aws-apigatewayv2';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
export class TestStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id, props);
// Lambdaに割り当てるロール
const lambdaRole = new iam.Role(this, 'LambdaRole', {
roleName: 'lambda-role',
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
'service-role/AWSLambdaBasicExecutionRole'
),
iam.ManagedPolicy.fromAwsManagedPolicyName(
'AmazonDynamoDBFullAccess'
),
iam.ManagedPolicy.fromAwsManagedPolicyName(
'AmazonAPIGatewayInvokeFullAccess'
),
],
});
// DynamoDBのテーブルを作成
const webSocketConnection = new dynamodb.Table(this, 'WebSocketConnection', {
partitionKey: {
name: 'connectionId',
type: dynamodb.AttributeType.STRING,
},
tableName: 'webSocketConnection',
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
});
// WebSocketのAPI Gatewayを構築
const api = new apigateway2.CfnApi(this, 'Api', {
name: 'WebSocketApi',
protocolType: 'WEBSOCKET',
routeSelectionExpression: '$request.body.action',
});
// $connectルート用のLambda
const connect = new lambda.Function(this, 'connectLambda', {
runtime: lambda.Runtime.NODEJS_14_X,
code: lambda.Code.fromAsset('lambda/connect'),
handler: 'index.js',
role: lambdaRole,
environment: {
TABLE_NAME: 'webSocketConnection'
},
timeout: cdk.Duration.minutes(1),
});
// $disconnect用のLambda
const disconnect = new lambda.Function(this, 'disconnectLambda', {
runtime: lambda.Runtime.NODEJS_14_X,
code: lambda.Code.fromAsset('lambda/disconnect'),
handler: 'index.js',
role: lambdaRole,
environment: {
TABLE_NAME: 'webSocketConnection',
TABLE_KEY: 'connectionId'
},
timeout: cdk.Duration.minutes(1),
});
// API GatewayのInregration用のロール・ポリシー
const integrationPolicy = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
resources: [connectLambda.functionArn, disConnectLambda.functionArn],
actions: ['lambda:InvokeFunction'],
});
const integrationRole = new iam.Role(this, `integration-role`, {
assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
})
integrationRole.addToPolicy(integrationPolicy)
// APIにIntegrationを追加(=Lambdaと紐付け)
const integrationConnect = new apigateway2.CfnIntegration(this, `connect-lambda-integration`, {
apiId: api.ref,
integrationType: 'AWS_PROXY',
integrationUri: `arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/${connect.functionArn}/invocations`,
credentialsArn: integrationRole.roleArn,
});
const integrationDisconnect = new apigateway2.CfnIntegration(this, `disconnect-lambda-integration`, {
apiId: api.ref,
integrationType: 'AWS_PROXY',
integrationUri: `arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/${disconnect.functionArn}/invocations`,
credentialsArn: integrationRole.roleArn,
});
// ルートの作成
const routeConnect = new apigateway2.CfnRoute(this, `connect-route`, {
apiId: api.ref,
routeKey: "$connect",
authorizationType: 'NONE',
target: 'integrations/' + integrationConnect.ref,
});
const routeDisconnect = new apigateway2.CfnRoute(this, `disconnect-route`, {
apiId: api.ref,
routeKey: "$disconnect",
authorizationType: 'NONE',
target: 'integrations/' + integrationDisconnect.ref,
});
}
}
これを実行することで、AWS
にコネクション管理用のDynamoDB
と、API Gateway
でWebSocket
が構築できるかと思います。
後で使用するので、作成したAPI Gateway
のURL
は控えておきましょう。
仮のフロントエンドとしてReact
を用います。
WebSocket
との通信にはWebSocket APIを利用するため、新たにライブラリは使わないです。
基本はuseEffect
内でWebSocket
を用いて通信を管理し、受信したメッセージを画面に表示しています。
import { useEffect, useState } from "react";
const App = () => {
const [status, setStatus] = useState<string>("none");
const [messages, setMessages] = useState<string[]>([]);
useEffect(() => {
// WebSocketと接続
const socket = new WebSocket(
"wss://{WebSocket API URL}"
);
// 接続時の処理
socket.addEventListener("open", (event) => {
setStatus("connected");
});
// 切断時の処理
socket.addEventListener("close", (event) => {
setStatus("closed");
});
// WebSocketからメッセージ受信時処理
socket.addEventListener("message", (event) => {
setMessages((current) => [...current, event.data]);
});
return () => {
// アンマウント時に接続を切断
socket.close();
};
}, []);
return (
<>
<p>ConnectionStatus:{status}</p>
<ul>
{messages.map((msg) => {
return (
<li>{msg}</li>
)
})}
</ul>
</>
)
}
上記のReact
のソースを起動することで、WebSocket
との通信は確立できるかと思います。
DynamoDB
のテーブル内を確認してみると、connectionId
が格納されており、フロントエンド側を止めるとconnectionId
が消えるかと思えていれば成功です。
今度は接続しているアプリケーションに対してメッセージを送るLambda
を作ります。
下記のLambda
を作って手動で実行してみましょう。
※ロールは先ほどのCDK
で作成したロールを使います。
const AWS = require("aws-sdk");
exports.handler = async (event, context) => {
const endpoint =
"https:{Web Socket URL}";
const apiGateway = new AWS.ApiGatewayManagementApi({ endpoint });
const client = new AWS.DynamoDB.DocumentClient();
// DBからコネクションIDを取得
const result = await client
.scan({ TableName: process.env.TABLE_NAME || "" })
.promise();
for (const data of result.Items ?? []) {
const params = {
Data: "テスト",
ConnectionId: data.connectionId,
};
try {
await apiGateway.postToConnection(params).promise();
} catch (err) {
// 対象が既に切断していたら削除する
if (err.statusCode === 410) {
await client
.delete({
TableName: process.env.TABLE_NAME || "",
Key: { [process.env.TABLE_KEY || ""]: data.connectionId },
})
.promise();
}
}
}
return {
statusCode: 200,
body: "onDisconnect.",
};
};
React
側でメッセージを受信して、それが画面に反映されたかと思います。
postToConnection
に渡す内容を変えることで、フロントエンドに通達するメッセージの内容を変えることができます。
今回はAPI Gateway
を使って簡単なWebSocket
を作成し、React
アプリケーションから通信を行うサンプルについて紹介しました。
ここに色々と肉付けしていくことで、例えばチャットアプリや掲示板のようにリアルタイムで情報をやり取りするような仕組みを構築できるかと思います。
今回の内容が役立ちましたら幸いです。