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
おがどら
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
63
LambdaとSQLiteでシステム構築
ogadra
1
630
hono-remix-adapter使ってみた
ogadra
0
410
Other Decks in Programming
See All in Programming
コードで差別化できない時代の ソフトウェアエンジニアリング / Software Engineering in an Era Where Code Doesn’t Differentiate
kishida
2
170
ActionController meets Protocol Buffers
sorah
0
230
Everything will be SERVERLESS — 信じて運用した10年の経験値 / Everything Will be Serverless — Lessons Learned from 10 Years of Operational Experience
seike460
PRO
1
1k
見えないものを探る要求要件定義に必要な基本的思考 / invisible-requirement-thinking
minodriven
15
8.3k
sbt 2.0: takeoff
eed3si9n
0
150
C#の現在地 進化の歴史と、AI時代の.NET Everywhere
neuecc
5
5.1k
気持ちぃ〜角丸 Squircle
ryu1013
0
290
AI主導のプログラミング学習アプリを作ってみた
econanringo
0
170
golang-bob.pdf
iwatsukayura
0
130
UnityでSystem.Net.WebSocketsなWebSocketサーバが動かないのでUnity Monoのコードを覗いてみた / about implementing websocket server with unity mono
drumath2237
1
590
wkhtmltopdfの次どうするか問題2026
willnet
2
1.9k
コーディング自動化の次へ。人の介入をゼロにするファクトリーとサポートAIによる実践
timakin
2
1.8k
Featured
See All Featured
Why Our Code Smells
bkeepers
PRO
340
58k
4 Signs Your Business is Dying
shpigford
187
23k
The Cult of Friendly URLs
andyhume
79
7k
<Decoding/> the Language of Devs - We Love SEO 2024
nikkihalliwell
1
350
SEO for Brand Visibility & Recognition
aleyda
0
4.8k
How to Create Impact in a Changing Tech Landscape [PerfNow 2023]
tammyeverts
56
3.5k
How Software Deployment tools have changed in the past 20 years
geshan
2
35k
Unlocking the hidden potential of vector embeddings in international SEO
frankvandijk
0
980
Git: the NoSQL Database
bkeepers
PRO
433
67k
Leo the Paperboy
mayatellez
10
2.4k
Gemini Prompt Engineering: Practical Techniques for Tangible AI Outcomes
mfonobong
2
590
The Success of Rails: Ensuring Growth for the Next 100 Years
eileencodes
47
8.4k
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