Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Terraformと AWS Lambdaで WebSocket通信
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
おがどら
March 10, 2024
Programming
270
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Terraformと AWS Lambdaで WebSocket通信
おがどら
March 10, 2024
More Decks by おがどら
See All by おがどら
任意のドメインを破壊
ogadra
0
65
LambdaとSQLiteでシステム構築
ogadra
1
650
hono-remix-adapter使ってみた
ogadra
0
420
Other Decks in Programming
See All in Programming
自作Discord音声botをLambda MicroVMsに移行した話 / Migrating My Discord Voice Bot to Lambda MicroVMs
akihisaikeda
1
300
AIで開発が速くなった先で、開発チームは何を目指すか
shihochan
1
2k
AI時代のFlutter品質設計 — アプリ品質を守るQuality Layers
omatty198
0
360
Snowflake Openflow Connector利用の勘所 / 20261024developersio2026tokyo-snowflake-openflow
kasacchiful
PRO
0
310
AIが生成したコードは読むべきか?抽象化設計とコードの品質保証 / abstraction-design-and-quality
minodriven
16
8.8k
クラウドエージェントは、 何がそんなに便利なのか / what makes cloud agents so useful
rkaga
5
800
[DroidKnights 2026] Workflow AX: From Auto-Documented Policies to E2E Testing
raeminkang
0
540
Vapor Mode でアクセシビリティは 壊れないか検証した話
fuqda
1
330
Lambda durable functionsでHuman in the Loopを楽に…楽に……
har1101
2
290
TanStack Vue Query と Pinia Colada から学ぶ非同期状態の型設計
aoshi
0
630
graphql-ruby を Spinel で速くしようとした話
mtsmfm
0
140
Spec-driven Development with AI
simas
PRO
0
2.3k
Featured
See All Featured
Rails Girls Zürich Keynote
gr2m
96
14k
Jamie Indigo - Trashchat’s Guide to Black Boxes: Technical SEO Tactics for LLMs
techseoconnect
PRO
0
740
AI in Enterprises - Java and Open Source to the Rescue
ivargrimstad
0
1.6k
Exploring the relationship between traditional SERPs and Gen AI search
raygrieselhuber
PRO
4
4.4k
Efficient Content Optimization with Google Search Console & Apps Script
katarinadahlin
PRO
1
970
A Tale of Four Properties
chriscoyier
163
24k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
367
27k
Tips & Tricks on How to Get Your First Job In Tech
honzajavorek
1
830
The AI Search Optimization Roadmap by Aleyda Solis
aleyda
2
6.4k
10 Git Anti Patterns You Should be Aware of
lemiorhan
PRO
659
62k
Music & Morning Musume
bryan
48
7.5k
The Cost Of JavaScript in 2023
addyosmani
56
10k
Transcript
Terraformと AWS Lambdaで WebSocket通信
まずはこちらにアクセス! QRコードは本番デモ用になりますので、 資料には掲載しておりません。
ご覧のような*アプリを作成しました • フロントエンド、バックエンド、Chrome拡張の3部分からなる • Next.js (フロントエンド。メッセージ送信を行う) • Go (バックエンド。WebSocketサーバ) •
React (Chrome拡張。スライドにメッセージを表示する) • フロントエンドでコメントをすると、Chrome拡張に表示される * QRコードを読み取るとチャットアプリライクなUIが表示され、 そこでテキストを送信するとスライドにメッセージが流れるアプリ。
ogadra (おがどら) TypeScript / Python / Go / Terraform 大学1年の頃,
スクレイピングをしたところどっぷりハマ り, それ以来プログラミングに勤しんでいる . 最近の趣味はCiv6, AWS, ネットワーク. M3 MacBook Air 欲しい. 自己紹介
Agenda • システム構成図 • 今回話すこと • WebSocketとはなにか • LambdaでWebSocketを使うやり方 ◦
API Gateway ◦ Lambda ◦ DynamoDB • 感想
システム構成図
システム構成図
今回話すこと・話さないこと • WebSocketについて • Lambdaについて • DynamoDBについて 話すこと 話さないこと •
フロントエンドについて • Chrome拡張について
プレゼンの目的 • WebSocket通信について知る • AWS Lambda, API Gatewayを知る • WebSocketアプリケーションをLambdaで作れることを知る
そもそもWebSocket通信とは クライアント、サーバーの双方向で通信するプロトコル HTTP通信ではレスポンスという形でサーバーはメッセージを送信 → 任意のタイミングでサーバーからメッセージを送れるようになる
WebSocketサーバを建てるには 以下の要件を満たす必要がある 1. 接続要求に応じ、クライアントの接続情報を保持 2. 送信要求に応じ、クライアントからメッセージを受信 3. 接続情報を用い、クライアントにメッセージを送信 4. 切断要求に応じ、クライアントの接続情報を破棄
WebSocketサーバを建てるには 以下の要件を満たす必要がある 1. 接続要求に応じ、クライアントの接続情報を保持 2. 送信要求に応じ、クライアントからメッセージを受信 3. 接続情報を用い、クライアントにメッセージを送信 4. 切断要求に応じ、クライアントの接続情報を破棄
API Gatewayを用いて識別・対応するLambdaを発火
WebSocketサーバを建てるには 以下の要件を満たす必要がある 1. 接続要求に応じ、クライアントの接続情報を保持 2. 送信要求に応じ、クライアントからメッセージを受信 3. 接続情報を用い、クライアントにメッセージを送信 4. 切断要求に応じ、クライアントの接続情報を破棄
API Gatewayを用いて識別・対応するLambdaを発火 DynamoDBに保存
WebSocketサーバを建てるには リクエストを識別 connectionIdを付与 接続情報を 永続化
WebSocketサーバを建てるには (API GW) resource "aws_apigatewayv2_api" "websocket_api" { name = "ws-samidare"
protocol_type = "WEBSOCKET" route_selection_expression = "$request.body.action" } resource "aws_apigatewayv2_route" "connect" { api_id = aws_apigatewayv2_api.websocket_api.id route_key = "$connect" target = "integrations/${aws_apigatewayv2_integration.connect.id}" } resource "aws_apigatewayv2_integration" "connect" { api_id = aws_apigatewayv2_api.websocket_api.id integration_uri = aws_lambda_function.connect.invoke_arn }
WebSocketサーバを建てるには (API GW) resource "aws_apigatewayv2_api" "websocket_api" { name = "ws-samidare"
protocol_type = "WEBSOCKET" route_selection_expression = "$request.body.action" } resource "aws_apigatewayv2_route" "connect" { api_id = aws_apigatewayv2_api.websocket_api.id route_key = "$connect" target = "integrations/${aws_apigatewayv2_integration.connect.id}" } resource "aws_apigatewayv2_integration" "connect" { api_id = aws_apigatewayv2_api.websocket_api.id integration_uri = aws_lambda_function.connect.invoke_arn } WebSocket 呼び出す関数 ルートキー($connect, $disconnect, message)
WebSocketサーバを建てるには (Lambda) resource "aws_lambda_function" "connect" { package_type = "Image" image_uri
= "${aws_ecr_repository.ogadra_websocket.repository_url}:latest" image_config { command = [] entry_point = [ "/functions/connection", ] } depends_on = [ aws_iam_role_policy_attachment.dynamodb, ] }
WebSocketサーバを建てるには (Lambda) resource "aws_lambda_function" "connect" { package_type = "Image" image_uri
= "${aws_ecr_repository.ogadra_websocket.repository_url}:latest" image_config { command = [] entry_point = [ "/functions/connection", ] } depends_on = [ aws_iam_role_policy_attachment.dynamodb, ] } entry_pointを分けることで 同一イメージを使いまわした DynamoDBに対する権限をロールに付与
WebSocketサーバを建てるには (DynamoDB) resource "aws_dynamodb_table" "connections" { name = "samidare-wss-connections" hash_key
= "roomId" range_key = "connectionId" global_secondary_index { name = "connectionId-index" hash_key = "connectionId" } }
WebSocketサーバを建てるには (DynamoDB) resource "aws_dynamodb_table" "connections" { name = "samidare-wss-connections" hash_key
= "roomId" range_key = "connectionId" global_secondary_index { name = "connectionId-index" hash_key = "connectionId" } } connectionIdから逆引きできるようにする roomIdでレコードをまとめて取ってくるため ※分かりづらいですが、 hash_keyがパーティションキー、 range_keyがソートキーのことです。
WebSocketサーバを建てるには (DynamoDB) パーティションキー roomId ソートキー connectionId hoge foo hoge bar
hoge baz hoge qux fuga foobar fuga foobaz piyo fooqux piyo barfoo パーティションキーがroomId, ソートキーが connectionIdのテーブルを作成する。 →roomId, connectionIdの複合プライマリーキー GSI(グローバルセカンダリインデックス)に connectionIdを指定し、connectionIdでも検索できるよ うにしておく。
WebSocketサーバを建てるには (DynamoDB) パーティションキー roomId ソートキー connectionId hoge foo hoge bar
hoge baz hoge qux fuga foobar fuga foobaz piyo fooqux piyo barfoo 接続時にはroomIdとconnectionIdを指定し、その情報 をDynamoDBに保存する。 接続時の処理
Lambda関数 (Connection) func Connection( ctx context.Context, req *events.APIGatewayWebsocketProxyRequest ) (Response,
error) { data, _ := attributevalue.MarshalMap(ConnectionRecord{ roomId: req.QueryStringParameters["roomId"], ConnectionId: req.RequestContext.ConnectionID, }) db.PutItem(context.TODO(), &dynamodb.PutItemInput{ TableName: aws.String(os.Getenv("TABLE_NAME")), Item: data, }) return generateResponse( fmt.Sprintf("connected to %s", req.QueryStringParameters["roomId"])), nil }
Lambda関数 (Connection) func Connection( ctx context.Context, req *events.APIGatewayWebsocketProxyRequest ) (Response,
error) { data, _ := attributevalue.MarshalMap(ConnectionRecord{ roomId: req.QueryStringParameters["roomId"], ConnectionId: req.RequestContext.ConnectionID, }) db.PutItem(context.TODO(), &dynamodb.PutItemInput{ TableName: aws.String(os.Getenv("TABLE_NAME")), Item: data, }) return generateResponse( fmt.Sprintf("connected to %s", req.QueryStringParameters["roomId"])), nil } クエリパラメータで roomIdを指定 (メッセージには含めない) roomIdとconnectionIdを Dynamoに保存
WebSocketサーバを建てるには (DynamoDB) パーティションキー roomId ソートキー connectionId hoge foo hoge bar
hoge baz hoge qux fuga foobar fuga foobaz piyo fooqux piyo barfoo connectionIdのみをリクエストに含める。 そのままではメッセージを送信する先のconnectionId がわからないので、GSIを用いてconnectionIdが含まれ るroomIdを特定する。 例えば、リクエスト元のconnectionIdが”foo”の場合 は、roomIdが”hoge”のセッションにメッセージを送れば 良い。 メッセージ受信時の処理①
Lambda関数 (Messaging) webhookMessage := ReceiveWebhookMessage{} json.Unmarshal([]byte(req.Body), &webhookMessage) filter := expression.Name("connectionId").
Equal(expression.Value(req.RequestContext.ConnectionID)) expr, _ := expression.NewBuilder().WithFilter(filter).Build() // GSIを使ってPrimary Keyを指定し, RoomIDを取得する out, err := db.Query(context.TODO(), &dynamodb.QueryInput{ TableName: aws.String(os.Getenv("TABLE_NAME")), IndexName: aws.String(os.Getenv("DB_GSI_NAME")), ExpressionAttributeNames: expr.Names(), ExpressionAttributeValues: expr.Values(), KeyConditionExpression: expr.Filter(), })
Lambda関数 (Messaging) webhookMessage := ReceiveWebhookMessage{} json.Unmarshal([]byte(req.Body), &webhookMessage) filter := expression.Name("connectionId").
Equal(expression.Value(req.RequestContext.ConnectionID)) expr, _ := expression.NewBuilder().WithFilter(filter).Build() // GSIを使ってPrimary Keyを指定し, RoomIDを取得する out, err := db.Query(context.TODO(), &dynamodb.QueryInput{ TableName: aws.String(os.Getenv("TABLE_NAME")), IndexName: aws.String(os.Getenv("DB_GSI_NAME")), ExpressionAttributeNames: expr.Names(), ExpressionAttributeValues: expr.Values(), KeyConditionExpression: expr.Filter(), }) メッセージ時にはroomIdを 含めていないため、 DynamoDBから取得
WebSocketサーバを建てるには (DynamoDB) パーティションキー roomId ソートキー connectionId hoge foo hoge bar
hoge baz hoge qux fuga foobar fuga foobaz piyo fooqux piyo barfoo 送信すべき対象のroomIdを特定出来たら、その roomIdに紐づけられているconnectionId全てに対して メッセージを送信する。 このとき、メッセージを送信できなかったconnectionId はDynamoDBからレコードを削除する。(切断扱い。) メッセージ受信時の処理②
Lambda関数 (Messaging) // RoomIDを使って, ルーム内のConnectionIDを取得する out, err = db.Query(context.TODO(), &dynamodb.QueryInput{
TableName: aws.String(os.Getenv("TABLE_NAME")), ExpressionAttributeNames: expr.Names(), ExpressionAttributeValues: expr.Values(), KeyConditionExpression: expr.Filter(), }) // ルーム内のConnectionIDに対して, メッセージを送信する for _, item := range out.Items { _, sendErr := svc.PostToConnection( ctx, &apigatewaymanagementapi.PostToConnectionInput{ ConnectionId: &record.ConnectionId, Data: responseMessage, })
Lambda関数 (Messaging) // RoomIDを使って, ルーム内のConnectionIDを取得する out, err = db.Query(context.TODO(), &dynamodb.QueryInput{
TableName: aws.String(os.Getenv("TABLE_NAME")), ExpressionAttributeNames: expr.Names(), ExpressionAttributeValues: expr.Values(), KeyConditionExpression: expr.Filter(), }) // ルーム内のConnectionIDに対して, メッセージを送信する for _, item := range out.Items { _, sendErr := svc.PostToConnection( ctx, &apigatewaymanagementapi.PostToConnectionInput{ ConnectionId: &record.ConnectionId, Data: responseMessage, }) 取得した接続情報全てに対し、 For文でメッセージを送信
Lambda関数 (Messaging) if sendErr != nil { // 送信に失敗した場合は, DynamoDBからレコードを削除する(切断扱い)
targetKey := map[string]string{ "roomId": record.roomId, "userId": record.UserId, } key, _ := attributevalue.MarshalMap(targetKey) db.DeleteItem(context.TODO(), &dynamodb.DeleteItemInput{ TableName: aws.String(os.Getenv("TABLE_NAME")), Key: key, }) }
Lambda関数 (Messaging) if sendErr != nil { // 送信に失敗した場合は, DynamoDBからレコードを削除する(切断扱い)
targetKey := map[string]string{ "roomId": record.roomId, "userId": record.UserId, } key, _ := attributevalue.MarshalMap(targetKey) db.DeleteItem(context.TODO(), &dynamodb.DeleteItemInput{ TableName: aws.String(os.Getenv("TABLE_NAME")), Key: key, }) } 通信に失敗したレコードを DynamoDBから削除
WebSocketサーバを建てるには (DynamoDB) パーティションキー roomId ソートキー connectionId hoge foo hoge bar
hoge baz hoge qux fuga foobar fuga foobaz piyo fooqux piyo barfoo connectionIdのみをリクエストに含める。 メッセージ受信時と同様に、GSIを用いてパーティション キーを特定し、プライマリーキーでレコードを削除する。 切断時の処理
Lambda関数 (Disconnection) selectedKeys := ConnectionRecord{} targetKey := map[string]string{ "roomId": selectedKeys.roomId,
"userId": selectedKeys.UserId, } key, _ := attributevalue.MarshalMap(targetKey) db.DeleteItem(context.TODO(), &dynamodb.DeleteItemInput{ TableName: aws.String(os.Getenv("TABLE_NAME")), Key: key, }) return generateResponse("disconnected"), nil プライマリーキーを指定 (パーティションキー + ソートキー)
ディレクトリ構成 . ├── Dockerfile ├── functions │ ├── connection │
│ ├── functions.go -> ../../functions.go │ │ └── main.go │ ├── disconnection │ │ ├── functions.go -> ../../functions.go │ │ └── main.go │ └── messaging │ ├── functions.go -> ../../functions.go │ └── main.go ├── functions.go ├── go.mod └── go.sum
ディレクトリ構成 . ├── Dockerfile ├── functions │ ├── connection │
│ ├── functions.go -> ../../functions.go │ │ └── main.go │ ├── disconnection │ │ ├── functions.go -> ../../functions.go │ │ └── main.go │ └── messaging │ ├── functions.go -> ../../functions.go │ └── main.go ├── functions.go ├── go.mod └── go.sum connectionのENTRYPOINT disconnectionのENTRYPOINT messagingのENTRYPOINT シンボリックリンクを用いて 同一ファイルで複数のLambda関数を作成
感想 Lambdaは常時起動し続けるわけではないので、WebSocket通信のようなコネク ションを維持しなければならないサーバは作成が難しいと思っていたが、 DynamoDBを上手く使うことによって作成することができた。 今回の構成では、常時起動しているリソースはなく、完全に従量課金で事足りるた め、個人開発としてもお財布に優しい構成にできた。 これからもコスト意識を徹底した個人開発に勤しみたい。
Thanks Twitter : @const_myself