はじめに
で作成したソースコードをベースにAPI Gatewayを使ってWebSocketを使用するシンプルなアプリケーションを作成する。
1. Amazon API Gateway を用いてLamda関数を呼び出す
下記の公式ドキュメントを参考にして作業を進めた
作成したソースコードはこのリポジトリで確認できる
1.1 Lambda関数の作成
CDK プロジェクト内で、新しい hello.js ファイルを含む lambda
ディレクトリを作成する。
cd <プロジェクトのトップディレクトリ>
mkdir lambda && cd lambda
touch hello.js
Lamada関数を lambda
ディレクトリにファイルを作成する。
hello-cdk
└── lambda
└── hello.js
exports.handler = async (event) => {
return {
statusCode: 200,
headers: { "Content-Type": "text/plain" },
body: JSON.stringify({ message: "Hello, World!" }),
};
};
1.2 Lambda 関数リソースの定義
Lambda 関数リソースを定義するには、 AWS コンストラクトライブラリから aws-lambda L2 コンストラクトをインポートして使用する。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
// Import the Lambda module
import * as lambda from 'aws-cdk-lib/aws-lambda';
// Import API Gateway L2 construct
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
export class HelloCdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Define the Lambda function resource
const helloWorldFunction = new lambda.Function(this, 'HelloWorldFunction', {
runtime: lambda.Runtime.NODEJS_20_X, // Choose any supported Node.js runtime
code: lambda.Code.fromAsset('lambda'), // Points to the lambda directory
handler: 'hello.handler', // Points to the 'hello' file in the lambda directory
});
cdk.Tags.of(helloWorldFunction).add('WPS', 'cdktest_rairaii');
// Define the API Gateway resource
const api = new apigateway.LambdaRestApi(this, 'HelloWorldApi', {
handler: helloWorldFunction,
proxy: false,
});
cdk.Tags.of(api).add('WPS', 'cdktest_rairaii');
// Define the '/hello' resource with a GET method
const helloResource = api.root.addResource('hello');
helloResource.addMethod('GET');
}
}
AWS CDK (Cloud Development Kit) を使って AWS Lambda と API Gateway を設定する。このプログラムをデプロイすると、/hello
というエンドポイントが作成され、GET リクエストを受け取ると、Lambda関数 hello.handler が実行されるようになります。
- Lambda関数の作成
-
lambda/hello.js
というファイルにある handler 関数を実行するLambda関数 (HelloWorldFunction) を作成 - 実行環境として Node.js 20.x を使用
- Lambdaのコードは lambda ディレクトリから取得
-
- API Gateway の作成
- HelloWorldApi というAPI Gatewayを作成し、Lambda関数 (HelloWorldFunction) にリクエストを転送するように設定
- proxy: false により、手動でエンドポイントを設定する
- APIのエンドポイント設定
-
/hello
というパスを作成し、GET メソッドでリクエストを受け付けるようにする
-
1.3 アプリケーションをデプロイする
TypeScriptの場合には下記を実行してから、
npm run build
次のコマンドを実行してデプロイする
npx cdk synth
npx cdk deploy
1.4 Lambda 関数の動作確認
$ curl https://<Lambda関数のURL>/prod/hello
{"message":"Hello, World!"}%
CDK で API Gateway を作成する際、特に stageName を指定しない場合、デフォルトの prod ステージが作成されるため、/prod/hello
となる。
ステージ名を明示的にdev
や staging
などを指定したい場合には、stageName を明示的に指定 する必要がある。
ステージ名 | 用途 |
---|---|
dev | 開発環境 |
staging | 検証環境 |
prod | 本番環境 (デフォルト) |
new apigateway.LambdaRestApi(this, 'HelloWorldApi', {
handler: helloWorldFunction,
deployOptions: {
stageName: "dev" // ステージ名を明示的に設定
}
});
1.5 アプリケーションの削除
cdk destroy
2. API Gateway で WebSocket
AWS CDK を使って WebSocket API → Lambda でメッセージをエコーするシステムを構築する
使用したソースコードは下記で確認できる
2.1 プログラム全体の流れ
-
WebSocket API の作成
- WebSocketApi リソースを作成し、WebSocket のルート($connect, $disconnect, $default)を定義する
- 今回のコードでは、先に空の WebSocket API (HelloWebSocketApi) を作成し、あとでルートを追加する
-
WebSocket ステージの設定
- WebSocket API に対して、ステージ を作成する
-
Lambda 関数の定義
- WebSocket から 呼び出される Lambda 関数 を定義する
- WEBSOCKET_ENDPOINT を環境変数として設定することで、Lambda 内で WebSocket API のエンドポイントを参照できる
-
IAM ポリシーの付与 (execute-api:ManageConnections)
- WebSocket API では、Lambda 関数がクライアントへメッセージを返すときに ApiGatewayManagementApi を使用する
- この操作には execute-api:ManageConnections の権限が必要となる
- Lambda 関数のロールにポリシーを追加することで、メッセージ送信(postToConnection)を許可する
-
WebSocket の各ルートを追加 ($connect, $disconnect, $default)
- WebSocket API の各ルートに上記の Lambda 関数を紐付ける
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
// Import the Lambda module
import * as lambda from 'aws-cdk-lib/aws-lambda';
// Import API Gateway WebSocket module
import * as apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2';
import * as integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
// Import IAM module
import * as iam from 'aws-cdk-lib/aws-iam';
export class HelloCdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// 1. WebSocket APIを先に作成 (ルートは後で addRoute)
const webSocketApi = new apigatewayv2.WebSocketApi(this, 'HelloWebSocketApi');
cdk.Tags.of(webSocketApi).add('WPS', 'cdktest_rairaii');
// 2. WebSocketステージの作成 (任意で 'prod' や '$default' を指定)
const webSocketStage = new apigatewayv2.WebSocketStage(this, 'HelloWebSocketStage', {
webSocketApi,
stageName: 'prod', // 例: 'prod' にした場合の接続URL → wss://{apiId}.execute-api.{region}.amazonaws.com/prod
autoDeploy: true,
});
cdk.Tags.of(webSocketStage).add('WPS', 'cdktest_rairaii');
// 3. WebSocketから呼びだされるLambda関数の定義
const helloWorldFunction = new lambda.Function(this, 'HelloWorldFunction', {
runtime: lambda.Runtime.NODEJS_20_X,
code: lambda.Code.fromAsset('lambda'),
handler: 'hello.handler',
// もしLambdaで endpoint を使うなら、ここでwebSocketApi.apiIdを参照可能
// 例: stageName が 'prod' の場合
environment: {
WEBSOCKET_ENDPOINT: `https://${webSocketApi.apiId}.execute-api.${this.region}.amazonaws.com/prod`,
},
});
cdk.Tags.of(helloWorldFunction).add('WPS', 'cdktest_rairaii');
// 4. すでに作成済みの Lambda 関数 (helloWorldFunction) に対してポリシーを追加
helloWorldFunction.addToRolePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['execute-api:ManageConnections'],
resources: [
// WebSocket API の ARN を指定
// stageName: 'prod' → "prod/POST/@connections/*"
`arn:aws:execute-api:${this.region}:${this.account}:${webSocketApi.apiId}/${webSocketStage.stageName}/POST/@connections/*`,
],
}));
// 5. WebSocket APIの各ルート ($connect, $disconnect, $default) を addRoute で定義
webSocketApi.addRoute('$connect', {
integration: new integrations.WebSocketLambdaIntegration('ConnectIntegration', helloWorldFunction),
});
webSocketApi.addRoute('$disconnect', {
integration: new integrations.WebSocketLambdaIntegration('DisconnectIntegration', helloWorldFunction),
});
webSocketApi.addRoute('$default', {
integration: new integrations.WebSocketLambdaIntegration('DefaultIntegration', helloWorldFunction),
});
}
}
2.1 Lambda関数の実装
AWS Lambda 関数として実行される Node.js コードで、API Gateway (WebSocket) からのイベント(接続・切断・メッセージ送信)を処理しつつ、エコー(echo)メッセージをクライアントに返す.
wss と https
AWS API Gatewayの WebSocketエンドポイント は、クライアント側から接続するときは wss:// ですが、Lambda から ApiGatewayManagementApi を呼び出すときは HTTPS (REST形式) でアクセスする必要がある。したがって、プログラム上では「WebSocket用のエンドポイント (wss) と、Lambda 内で管理APIを呼び出すときのエンドポイント (https)」を使い分ける というポイントがある
environment: {
WEBSOCKET_ENDPOINT: `https://${webSocketApi.apiId}.execute-api.${this.region}.amazonaws.com/prod`,
},
hello-cdk-stack.ts では WEBSOCKET_ENDPOINT に https://${webSocketApi.apiId}.execute-api.${this.region}.amazonaws.com/prod
を設定している
Lamda関数では、endpoint に process.env.WEBSOCKET_ENDPOINT を設定しつつ、wss:// → https:// に変換している。WebSocket へのメッセージ送信は内部的に HTTPS リクエストになるため、wss:// を https:// に置き換える 必要がある
-
クライアント視点
- wss://{apiId}.execute-api.{region}.amazonaws.com/prod で WebSocket 接続
-
Lambda 内部
- https://{apiId}.execute-api.{region}.amazonaws.com/prod を使い、ApiGatewayManagementApiClient 経由でメッセージ送信
Lambda Handler (exports.handler)
- event は API Gateway WebSocket から渡されるオブジェクト
- routeKey
- $connect の場合 → 新規接続時
- $disconnect の場合 → 切断時
- その他は $default ルート → 任意のメッセージ受信時を示す
- connectionId は WebSocket の接続を一意に識別する ID。メッセージをクライアントに送り返す際に必要となる
sendMessageToClient 関数
- PostToConnectionCommand は @aws-sdk/client-apigatewaymanagementapi のクラス
- connectionId: 送信先の WebSocket 接続を指定し、Data に送りたいメッセージ(Buffer 形式)を設定
- apiGateway.send(command) で実際に API を呼び出し、メッセージをクライアントに返します
- execute-api:ManageConnections の IAM ポリシーがないと AccessDeniedException が発生する
const { ApiGatewayManagementApiClient, PostToConnectionCommand } = require("@aws-sdk/client-apigatewaymanagementapi");
// WebSocket API のエンドポイントを HTTPS に変換
const apiGateway = new ApiGatewayManagementApiClient({
endpoint: process.env.WEBSOCKET_ENDPOINT.replace(/^wss:\/\//, "https://"),
});
exports.handler = async (event) => {
console.log("Received event: ", JSON.stringify(event, null, 2));
const { requestContext, body } = event;
const connectionId = requestContext.connectionId;
try {
switch (requestContext.routeKey) {
case '$connect':
console.log(`New connection: ${connectionId}`);
return { statusCode: 200, body: 'Connected' };
case '$disconnect':
console.log(`Disconnected: ${connectionId}`);
return { statusCode: 200, body: 'Disconnected' };
default:
console.log(`Received message: ${body}`);
await sendMessageToClient(connectionId, `Echo: ${body}`);
return { statusCode: 200, body: `Echoed: ${body}` };
}
} catch (error) {
console.error('Error: ', error);
return { statusCode: 500, body: 'Error processing request' };
}
};
// メッセージをクライアントに送信する関数(修正済み)
const sendMessageToClient = async (connectionId, message) => {
try {
const command = new PostToConnectionCommand({
ConnectionId: connectionId,
Data: Buffer.from(message),
});
await apiGateway.send(command);
console.log(`Sent message to ${connectionId}: ${message}`);
} catch (error) {
console.error(`Failed to send message to ${connectionId}:`, error);
}
};
2.2 エコーサーバーの動作確認
以下のコマンドを実行すると、API Gateway の WebSocket エンドポイントを取得できる
$ aws apigatewayv2 get-apis --query "Items[?Name=='HelloWebSocketApi'].ApiEndpoint" --output text
https://abcdef1234.execute-api.us-east-1.amazonaws.com
API Gateway に接続する
wscat -c https://abcdef1234.execute-api.us-east-1.amazonaws.com/prod
Connected (press CTRL+C to quit)
> hello, world
< Echo: hello, world
> hello, aws lambda
< Echo: hello, aws lambda
> %
3. API Gateway (WebSocket) のログ確認とトラブルシューティング
API Gateway で WebSocket を利用するプログラムを動作させるまでに複数の不具合に遭遇したのでトラブルシューティングのためのメモを記載しておく。
3.1 API Gateway の状況を確認する
デプロイした API Gateway WebSocket が正しく作成されているかを CLI で確認する
aws apigatewayv2 get-apis --query "Items[*].[ApiId, Name]" --output table
出力例
-------------------------------------
| GetApis |
+-------------+---------------------+
| abcdef1234 | HelloWebSocketApi |
+-------------+---------------------+
ApiId と Name が表示されれば、WebSocket API は正常に登録されている
3.2 統合設定 (Integrations) を確認する
aws apigatewayv2 get-integrations --api-id abcdef1234
出力例
Items:
- ConnectionType: INTERNET
IntegrationId: 3jslwg4
IntegrationMethod: POST
IntegrationType: AWS_PROXY
IntegrationUri: arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789123:function:HelloCdkStack-HelloWorldFunctionB2AB6E79-fQTmWA2xxyzJ/invocations
...
- IntegrationUri に 正しい Lambda 関数 ARN が入っているかを確認する
- IntegrationType: AWS_PROXY であれば、Lambda へのプロキシ統合が正しく設定されている
3.2 Lambda の CloudWatch Logs を確認する
aws logs tail "/aws/lambda/HelloCdkStack-HelloWorldFunctionB2AB6E79-fQTmWA2xxyzJ" --follow
ログの例
2025-02-16T11:25:04.577Z undefined ERROR Uncaught Exception {
"errorType":"Runtime.ImportModuleError",
"errorMessage":"Error: Cannot find module 'aws-sdk'\nRequire stack:\n- /var/task/hello.js\n...
}
このように、Cannot find module 'aws-sdk' のエラーが記録されています。
修正例
Node.js 20.x では aws-sdk がデフォルトで含まれていないため動作しない という問題があります。修正方法するには aws-sdk を @aws-sdk/client-apigatewaymanagementapi に変更する
const AWS = require('aws-sdk');
const { ApiGatewayManagementApiClient, PostToConnectionCommand } = require("@aws-sdk/client-apigatewaymanagementapi");
4. WebSocketを使う場合の注意点
IAM ポリシー execute-api:ManageConnections の付与
- Lambda が WebSocket 接続先にメッセージを返すには、execute-api:ManageConnections が必須です
- ARN の形式 は
arn:aws:execute-api:{region}:{account}:{apiId}/{stageName}/POST/@connections/*
- ポリシーがないと AccessDeniedException が発生する
Node.js ランタイムと AWS SDK
- Node.js 20.x 以降では、aws-sdk v2 が含まれないため、今回は @aws-sdk/client-apigatewaymanagementapi (v3) を使用した
- postToConnection メソッドではなく、PostToConnectionCommand を使う必要がある
参考資料