AWS Lambda 関数から別の Lambda 関数を呼び出す処理を実装します。 事情があって、非同期で呼び出す方法を調べてみました。
背景
- Slack の Bot を Lambda で実装していたのですが、少し処理に時間がかかるものだとタイムアウトしてしまい、処理が実行されませんでした。
- なんかしらのイベントを受け取る場合も、Slash コマンドの場合でも3000 ms = 3 秒以内にレスポンスが返ってこないと、タイムアウトになってしまいます。
- Lambda を用いてサーバーレスな運用をしたい Slack Bot の場合、大きな壁となります。
- そこで、以下のように、Slack からのリクエストには 3 秒以内にレスポンスを返却し、処理自体は別の Lambda 関数が行うという構成にすれば、少々複雑な処理であってもタイムアウトせずに実行可能です。
実際の実装方法
- 今回は、Go で実装する方法を紹介します。
呼ばれる関数の実装
- 単純に Slack に投稿するだけのものとします。
- 呼ばれてから投稿するまでに 5 秒スリープさせます。
package main import ( "log" "os" "time" "github.com/aws/aws-lambda-go/lambda" "github.com/nlopes/slack" ) // HandleRequest : リクエストハンドラー func HandleRequest() { time.Sleep(5 * time.Second) // 環境変数 SLACK_ACCESS_TOKEN には、Slack App の「Bot User OAuth Access Token」を設定する api := slack.New(os.Getenv("SLACK_ACCESS_TOKEN")) _, _, err := api.PostMessage("random", slack.MsgOptionText("メッセージが投稿されました", false)) if err != nil { log.Printf("failed to post message: %s", err) } } func main() { lambda.Start(HandleRequest) }
呼び出す側の関数
- Lambda の処理内で別の関数を呼び出すには、
lambda.Invoke
を用います。github.com/aws/aws-sdk-go/service/lambda
のlambda
であることに注意です(github.com/aws/aws-lambda-go/lambda
の方ではありません!)。- 引数である
InvokeInput
のInvocationType
によって、同期呼び出しか非同期呼び出しかが決まります。"Event"
にすると、非同期呼び出しになります。"RequestResponse"
にすると、同期呼び出しになります。
非同期呼び出しの場合
svc := lambda.New(session.New()) input := &lambda.InvokeInput{ FunctionName: aws.String(invokedFunction), Payload: []byte(body), InvocationType: aws.String("Event"), } resp, _ := svc.Invoke(input) fmt.Println(resp.Payload) return events.APIGatewayProxyResponse{ Body: "OK", Headers: map[string]string{ "Content-Type": "text"}, StatusCode: 200, }, nil
同期呼び出しの場合
svc := lambda.New(session.New()) input := &lambda.InvokeInput{ FunctionName: aws.String(invokedFunction), Payload: []byte(body), InvocationType: aws.String("RequestResponse"), } resp, _ := svc.Invoke(input) fmt.Println(resp.Payload) return events.APIGatewayProxyResponse{ Body: "OK", Headers: map[string]string{ "Content-Type": "text"}, StatusCode: 200, }, nil
main.go
の仕上げ
- 上記以外のところは、Slack の Event API を受け取って検証などを行うだけです。
- 最終的な
main.go
はこうなります。
呼び出し側の
main.go
の実装▼
package main import ( "encoding/json" "fmt" "log" "net/http" "os" "github.com/aws/aws-lambda-go/events" l "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/lambda" "github.com/nlopes/slack" "github.com/nlopes/slack/slackevents" ) // HandleRequest Lambda のリクエストをハンドルする func HandleRequest(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { eventsAPIEvent, err := VerifyEvent(request) if err != nil { log.Printf("ERROR: %s", err) return events.APIGatewayProxyResponse{ Body: request.Body, StatusCode: http.StatusInternalServerError, }, nil } log.Printf("EventType: %s", eventsAPIEvent.Type) if eventsAPIEvent.Type == slackevents.URLVerification { response, err := returnChallengeValue(request) if err != nil { log.Printf("ERROR: %s", err) return events.APIGatewayProxyResponse{ Body: request.Body, StatusCode: http.StatusInternalServerError, }, nil } return response, nil } invokedFunction := os.Getenv("INVOKED_FUNCTION_NAME") input := &lambda.InvokeInput{ FunctionName: aws.String(invokedFunction), Payload: []byte(request.Body), InvocationType: aws.String("Event"), } svc := lambda.New(session.New()) resp, _ := svc.Invoke(input) fmt.Println(resp.Payload) response := events.APIGatewayProxyResponse{ Body: "OK", Headers: map[string]string{ "Content-Type": "text"}, StatusCode: 200, } log.Print(response) return response, nil } // VerifyEvent : Slack からのリクエストイベントを検証する func VerifyEvent(request events.APIGatewayProxyRequest) (slackevents.EventsAPIEvent, error) { // リクエストヘッダの検証 if err := verifyRequest(request); err != nil { return slackevents.EventsAPIEvent{}, err } // Token の検証 eventsAPIEvent, err := slackevents.ParseEvent( json.RawMessage(request.Body), slackevents.OptionVerifyToken( &slackevents.TokenComparator{ VerificationToken: os.Getenv("SLACK_VERIFICATION_TOKEN"), }, ), ) if err != nil { return slackevents.EventsAPIEvent{}, err } return eventsAPIEvent, nil } func verifyRequest(request events.APIGatewayProxyRequest) error { body := request.Body headers := convertHeaders(request.Headers) sv, err := slack.NewSecretsVerifier(headers, os.Getenv("SLACK_SIGNING_SECRET")) if err != nil { return err } sv.Write([]byte(body)) return sv.Ensure() } func convertHeaders(headers map[string]string) http.Header { header := http.Header{} for key, value := range headers { header.Set(key, value) } return header } func returnChallengeValue(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { var r *slackevents.ChallengeResponse err := json.Unmarshal([]byte(request.Body), &r) if err != nil { return events.APIGatewayProxyResponse{}, err } return events.APIGatewayProxyResponse{ Body: r.Challenge, Headers: map[string]string{ "Content-Type": "text"}, StatusCode: 200, }, nil } func main() { l.Start(HandleRequest) }
- 呼び出す側の関数には、他の Lambda 関数を呼び出すためのアクセス権限を設定する必要があります。
{ "Effect": "Allow", "Action": [ "lambda:InvokeFunction" ], "Resource": "*" }
- Serverless Framework を用いて設定する場合、
serverless.yml
のprovider
に以下のように記載します。
provider: iamRoleStatements: - Effect: 'Allow' Action: - 'lambda:InvokeFunction' Resource: - '*'