Next.js + Go + AWS API Gateway ã§ WebSocket API ã䜿ã£ãŠ API ãµãŒããŒããããã³ããšã³ãã«éç¥ãéã ã¯ããã« ããã«ã¡ã¯ ãã¢ãã ã§ããã¯ãšã³ãåšãã®éçºãè¡ã£ãŠãã rymiyamoto ã§ãã ãšããªãŒãšããŠåã®è©Šã¿ãšãªã Tech Blog Advent Calendar 2023 ã® ïŒæ¥ç®ã®èšäºãšããŠåå ãããŠããã ããŸããã æ¯æ¥ä»ã®èšäºãå
¬éãããã®ã§ããã²ãã§ãã¯ããŠã¿ãŠãã ããïŒ tech.every.tv ä»åã§ãã Next.js + Go + AWS API Gateway ã§ WebSocket API ã䜿ã£ãŠã¿ãã®ã§ãã®å
容ã玹ä»ããŠãããŸãã çµç·¯ ãã¢ãã ã§ã¯çŸåšãããã¯ãšã³ã㯠Go ã§ããã³ããšã³ã㯠React(Next.js) ã§éçºãè¡ã£ãŠããŸãã ããã³ããšã³ããšããã¯ãšã³ãã®é信㯠REST API ã§è¡ã£ãŠããŸãããããšã³ããŠãŒã¶ãŒã®è¡åã«å¯ŸããŠããã·ã¥ããŒããå©çšããŠãããŠãŒã¶ãŒã«å³ææ§ã®ããéç¥æ©èœãå®è£
ããå¿
èŠãåºãŠãããããWebSocket API ã䜿ã£ãŠã¿ãããšã«ããŸããã çŸç¶ API ãµãŒããŒã¯ ECS äžã§åããŠãããAPI ãµãŒããŒåŽã§ WebSocket API ãå®è£
ããã®ã¯å°ãæéãããããããAWS API Gateway ã§ WebSocket API ãå®è£
ããããšã«ããŸããã WebSocket API ãšã¯ ãŠãŒã¶ãŒã®ãã©ãŠã¶ãŒãšãµãŒããŒéã§å¯Ÿè©±çãªéä¿¡ã»ãã·ã§ã³ãéãããšãã§ãããã®ã§ãã ãµãŒããŒã«ã¡ãã»ãŒãžãéä¿¡ããããå¿çããµãŒããŒã«ããŒãªã³ã°ããããšãªããã€ãã³ãé§ååã®ã¬ã¹ãã³ã¹ãåä¿¡ããããšãã§ããŸãã developer.mozilla.org ä»åã®å®è£
å
ã
ãã ECS ç°å¢(API ãµãŒããŒã»dashboardã»web)ãã API Gateway ã§ WebSocket API ãå©çšã§ããããã«åçš® Lambda ãäœæããŸããã ãŸãè£åŽã§ã¯ Lambda ãã RDS ãžã®æ¥ç¶ãè¡ããããããRDS Proxy ãå©çšããŠããŸãã æ§æå³ æµããšããŠã¯ä»¥äžã®ãããªãããŒã§ãã dashboard(FE)ã§ WebSocket API ãå©çšããããã®ã¯ã©ã€ã¢ã³ããäœæããŠã³ãã¯ã·ã§ã³ç¢ºç« web(FE)ã§ API ãµãŒããŒã«å¯ŸããŠãªã¯ãšã¹ããéã£ãéã«ãéç¥ãè¡ã Lambda ãåŒã³åºã API Gateway ãéã㊠dashboard ã«éç¥ãé£ã¶ API Gateway ã®èšå® ãšããã WebSocket API ãå©çšã§ããããã« API Gateway ãäœæããŸãã API Gateway ã«ãããŠãã©ã®ãªã¯ãšã¹ãã«å¯ŸããŠã©ã®æäœãè¡ãããæ±ºå®ããã«ãŒãåŒãæå®ããŸãã ä»åã¯ç¹å¥ã«æå®ããããªãã®ã§ $request.body.action ãšããŠãããŸãã WebSocket API ã䜿ãããã® API GateWay ã®äœæ 以éã®éšåã¯ç¹ã«æå®ããªããã°ããã©ã«ãã®ãŸãŸäœæããŠãããŸãã ãã®ãšããã«ãŒãã« $connect ãš $disconnect ã远å ãããŸããããããã¯æ¥ç¶ãšåææã®ã«ãŒããšãªããŸãã IAM Role ã®äœæ å®è¡çšã® Lambda ã® Role API Gateway ã SecretManager(RDS Proxy åšãã®æ©å¯æ
å ±ã®ç®¡ç) ãå©çšããããã« Role ãäœæããŸã (以é web-socket-lambda-role ãšããŸã) ãã®æã«å¿
èŠãšãªãããªã·ãŒã¯ä»¥äžã§ãã { " Statement ": [ { " Action ": " secretsmanager:GetSecretValue ", " Effect ": " Allow ", " Resource ": " * ", " Sid ": " GetSecretValue " } , { " Action ": [ " ec2:DescribeNetworkInterfaces ", " ec2:DeleteNetworkInterface ", " ec2:CreateNetworkInterface " ] , " Effect ": " Allow ", " Resource ": " * ", " Sid ": " ManageNetworkInterface " } , { " Action ": [ " logs:PutLogEvents ", " logs:CreateLogStream ", " logs:CreateLogGroup " ] , " Effect ": " Allow ", " Resource ": " * ", " Sid ": " ManageLogGroup " } , { " Action ": " execute-api:* ", " Effect ": " Allow ", " Resource ": " * ", " Sid ": " ExecuteAPI " } ] , " Version ": " 2012-10-17 " } API ãµãŒããŒãã Lambda ãåŒã³åºãããã® Policy 远å Lambda ãåŒã³åºãããã® Policy ã API ãµãŒããŒã® Role ã«ä»äžããŸãã ãã®æã«å¿
èŠãšãªãããªã·ãŒã¯ä»¥äžã§ãã ( websocket-notification ãåŸã«äœæãããéç¥çšã® Lambda ã®ååã§ã) { " Statement ": [ { " Action ": " lambda:InvokeFunction ", " Effect ": " Allow ", " Resource ": " arn:aws:lambda:ap-northeast-1:111111111111:function:websocket-notification ", " Sid ": "" } ] , " Version ": " 2012-10-17 " } Lambda ã®èšå® ä»å WebSocket API ãå©çšããããã® Lambda ã¯ä»¥äžã® 3 ã€ãšãªããŸãã connect: æ¥ç¶æã« API Gateway ããåŒã³åºããã disconnect: åææã« API Gateway ããåŒã³åºããã notification: API ãµãŒããŒããåŒã³åºãããŠéç¥ãè¡ã connect ããããš API Gateway ã® $connect ã«ãŒããã€ãã³ãããªã¬ãŒãšããŠèšå®ãã ã¯ãšãªã«ãŠãŒã¶ãŒãç¹å®ã§ãããããªæ
å ±ãæž¡ããŠãã WebSocket ã®æ¥ç¶ ID ãååŸã㊠DB ã«æžã蟌ã¿ãè¡ã å
éšã®åŠçã®ã€ã¡ãŒãžã¯ä»¥äžã§ãã package main import ( "context" "log" "net/http" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" ) type Response events.APIGatewayProxyResponse func Handler(_ context.Context, request events.APIGatewayWebSocketProxyRequest) (Response, error ) { log.Println( "Begin WebSocket connect" ) log.Println( "ãŠãŒã¶ãŒç¹å®" ) // ãªã¯ãšã¹ãã®ã¯ãšãªãããŠãŒã¶ãŒãäžæã«ç¹å®ã§ããæ
å ±ãååŸããŸã // ä»åã¯ãŠãŒã¶ãŒã®èå¥ãšãªãããŒã¯ã³ãååŸããŠããŸã(è€æ°ã¿ããèå¥ãããã) token := request.QueryStringParameters[ "token" ] // 以äžã«ããŒã¯ã³ãããŠãŒã¶ãŒIDãååŸ log.Println( "DSNã®ååŸéå§" ) // 以äžã«DNSæ
å ±ãsecret managerããååŸããåŠç log.Println( "DBã®æ¥ç¶" ) // 以äžã«DBã®æ¥ç¶åŠç log.Println( "ã³ãã¯ã·ã§ã³IDã®ä¿å" ) // ãªã¯ãšã¹ãããã³ãã¯ã·ã§ã³IDããšããŸã connectionID := request.RequestContext.ConnectionID // 以äžã«ã³ãã¯ã·ã§ã³IDã®ä¿ååŠç log.Println( "End WebSocket connect" ) return Response{StatusCode: http.StatusOK}, nil } func main() { lambda.Start(Handler) } disconnect ããããš API Gateway ã® $disconnect ã«ãŒããã€ãã³ãããªã¬ãŒãšããŠèšå®ãã WebSocket ã®æ¥ç¶ ID ãååŸã㊠DB ããåé€ãè¡ã å
éšã®åŠçã®ã€ã¡ãŒãžã¯ä»¥äžã§ãã package main import ( "context" "log" "net/http" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" ) type Response events.APIGatewayProxyResponse func Handler(_ context.Context, request events.APIGatewayWebsocketProxyRequest) (Response, error ) { log.Println( "Begin WebSocket disconnect" ) log.Println( "DSNã®ååŸéå§" ) // 以äžã«DNSæ
å ±ãsecret managerããååŸããåŠç log.Println( "DBã®æ¥ç¶" ) // 以äžã«DBã®æ¥ç¶åŠç log.Println( "ã³ãã¯ã·ã§ã³IDã®åé€" ) // ãªã¯ãšã¹ãããã³ãã¯ã·ã§ã³IDããšããŸã connectionID := request.RequestContext.ConnectionID // 以äžã«ã³ãã¯ã·ã§ã³IDã®åé€åŠç log.Println( "End WebSocket disconnect" ) return Response{StatusCode: http.StatusOK}, nil } func main() { lambda.Start(Handler) } notification ããããš åŒã³åºãæã® payload ã«ã¯éç¥ãè¡ããŠãŒã¶ãŒã® ID ãå«ããŠãã ãã®ãŠãŒã¶ãŒ ID ããã³ãã¯ã·ã§ã³ ID ãååŸããŠéç¥ãè¡ã å
éšã®åŠçã®ã€ã¡ãŒãžã¯ä»¥äžã§ãã package main import ( "context" "fmt" "log" "net/http" "os" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/apigatewaymanagementapi" ) type Response events.APIGatewayProxyResponse func sendMessage(ctx context.Context, endpoint, connectionID, message string ) error { cfg, err := config.LoadDefaultConfig(ctx) if err != nil { return err } client := apigatewaymanagementapi.NewFromConfig(cfg, func (o *apigatewaymanagementapi.Options) { o.BaseEndpoint = aws.String(endpoint) }) input := &apigatewaymanagementapi.PostToConnectionInput{ ConnectionId: aws.String(connectionID), Data: [] byte (message), } _, err = client.PostToConnection(ctx, input) return err } type Event struct { UserID uint64 `json:"user_id"` JsonString string `json:"json"` } func Handler(ctx context.Context, event Event) (Response, error ) { log.Println( "Begin WebSocket notification" ) log.Println( "DSNã®ååŸéå§" ) // 以äžã«DNSæ
å ±ãsecret managerããååŸããåŠç log.Println( "DBã®æ¥ç¶" ) // 以äžã«DBã®æ¥ç¶åŠç log.Println( "察象ãŠãŒã¶ãŒã®ã³ãã¯ã·ã§ã³IDãªã¹ã(è€æ°ç«¯æ«ãè€æ°ã¿ãã®éœåäž)ååŸ" ) // lambdaåŒã³åºãæã®payloadã«ã¯ç¹å®ãããããã®ãŠãŒã¶ãŒIDãæž¡ãããã¡ãã»ãŒãžã®jsonå
¥ããŠãããŸã userID := event.UserID // 以äžã«å¯Ÿè±¡ãŠãŒã¶ãŒã®ã³ãã¯ã·ã§ã³IDã®ãªã¹ãååŸåŠç log.Println( "API Gatewayãçµç±ããŠWeb Socketã®ã¡ãã»ãŒãžãéä¿¡" ) endpoint := os.Getenv( "API_GATEWAY_ENDPOINT" ) for _, connectionID := range connectionIDs { err = sendMessage(ctx, endpoint, connectionID, event.JsonString) } if err != nil { return Response{StatusCode: http.StatusInternalServerError}, err } log.Println( "End WebSocket notification" ) return Response{StatusCode: http.StatusOK}, nil } func main() { lambda.Start(Handler) } API ãµãŒããŒã§éç¥ Lambda(notification)ãåŒã³åºã API ãµãŒããŒåŽã§ä»¥äžã®é¢æ°ãäœæãéç¥ãè¡ã Lambda ãåŒã³åºãããã«ããŸãã package aws import ( "context" "fmt" "os" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/lambda" ) func createClient() (*lambda.Client, error ) { cfg, err := config.LoadDefaultConfig(context.Background()) if err != nil { return nil , fmt.Errorf( "load default config on background context failed. err: %w" , err) } return lambda.NewFromConfig(cfg), nil } func Invoke(payload [] byte ) error { funcName := os.Getenv( "WEBSOCKET_NOTIFICATION_FUNCTION" ) client, err := createClient() if err != nil { return fmt.Errorf( "create lambda client failed. lambda function: %s, err: %w" , funcName, err) } input := &lambda.InvokeInput{ FunctionName: aws.String(funcName), Payload: payload, } _, err = client.Invoke(context.Background(), input) if err != nil { return fmt.Errorf( "call %s failed. err: %w" , funcName, err) } return nil } WebSocket API ã䜿ãããã®ã¯ã©ã€ã¢ã³ãã®äœæ WebSocket API ãå©çšããããã®ã¯ã©ã€ã¢ã³ããäœæããŸãã ããã³ããšã³ã㯠Next.js ã§äœæããŠããã®ã§ãuseEffect ã§ã³ãã¯ã·ã§ã³ç¢ºç«ãè¡ãããã«ããŠããŸãã "use client" ; import { useEffect , useState } from "react" ; type ApplicationEvent = { message: string ; } ; export default function EventReceiver ( { token } : { token: string } ) { const [ applicationEvent , setApplicationEvent ] = useState < ApplicationEvent >(); // WebSocketã®ã³ãã¯ã·ã§ã³ã匵ã useEffect (() => { const connectWebSocket = () => { const webSocketURL = process .env.NEXT_PUBLIC_WEB_SOCKET_URL ; if ( ! webSocketURL ) { throw new Error ( "Web Socketã®URLãèšå®ãããŠããŸããã" ); } const ws = new WebSocket ( ` ${ webSocketURL } ?token= ${ token } ` ); ws.onmessage = ( event ) => { const e = JSON .parse ( event.data ); setApplicationEvent ( e ); } ; // åæã«æ¥ç¶åãããšãã®åæ¥ç¶ ws.onclose = () => { setTimeout ( connectWebSocket , 1000 ); } ; return ws ; } ; const ws = connectWebSocket (); return () => { ws.close (); } ; } , [ token ] ); if ( ! applicationEvent ) { return null ; } return < div > { e.message } < /div >; } (Next.js ver.13 æ³å®ã®ããã12 以äžã®å Žå㯠"use client"; ã¯äžèŠã§ãã) ãŸãšã WebSocket API ãå©çšã㊠API ãµãŒããŒããããã³ããšã³ãã«éç¥ãéãæ¹æ³ã玹ä»ããŸããã èªèº«ã§ WebSocket API ãå®è£
ããã®ã¯å°ãæéãããããŸãããAPI Gateway ã䜿ãããšã§ç°¡åã«å®è£
ããããšãã§ããŸãã ä»åã¯è€æ°ãã©ãŠã¶ã®ã±ãŒã¹ãèæ
®ããŠãŠãŒã¶ãŒã«ã³ãã¯ã·ã§ã³ãçŽä»ããããã«ããŸããããããã§ãªãå Žåã¯éç¥éšåã«ã³ãã¯ã·ã§ã³ ID ãæž¡ãããšã§ããå°ãã¹ãããªããããšæããŸãã ãã ã¯ã©ã€ã¢ã³ãåŽã§ WebSocket ã®ã³ãã¯ã·ã§ã³ã匵ããšãã«åæ¥ç¶åŠçãæžãå¿
èŠãããã®ã§ããã¬ãŒã³ã§æžããšå°ãé¢åã§ãã WebSocket API ã¯ä»åã®ãããªéç¥æ©èœã ãã§ãªãããªã¢ã«ã¿ã€ã ãªãã£ãããªã©ã«ãå©çšã§ããã®ã§ãä»åŸãæ§ã
ãªå Žé¢ã§å©çšå¯èœã§ãã åããããªå®è£
ããŠã¿ããæ¹ã®åèã«ãªãã°å¹žãã§ãã