API Gateway から Lambda 関数を非同期で呼び出す

Serverless Framework を使い、AWSAPI Gateway から Lambda 関数を非同期で呼び出す方法を調べてみました。

目次

背景

最近、クライアントからのイベントを受け取るエンドポイントを設置する機会があり AWSAPI Gateway + Lambda を使用しました。
サービスの品質アップやクライアントの仕様によって、いち早くレスポンスを返したいケースがあります。

具体的な例としてはこちらです。 blog.linkode.co.jp

Slack のイベントを受け取り、3秒以内にレスポンスをするために Lambda 関数から別の Lambda 関数を非同期で呼び出す方法を紹介していますが、今回は API Gateway から非同期で Lambda 関数を呼び出す方法を調査したので、サービス構築の一例を紹介します。

f:id:ken01-linkode:20200930175329p:plain

環境

API Gateway、Lambda 関数のデプロイに Serverless Framework を使用しています。
Serverless Framework はNode.jsで作られたCLIツールで AWS LambdaAzure FunctionsGoogle Cloud Functions等の Serverless Applicationを構成管理、デプロイするためのツールです。
また、Lambda関数のランタイムとしてGo言語を、goのビルドにmakeを使用しました。
各ツールのインストールや環境構築については割愛します。

プロジェクトの作成

まず、Serverless Flamework のコマンドでプロジェクトのテンプレートを作成します。 Lambdaのgoランタイムテンプレートは aws-goaws-go-depaws-go-mod の3種類から選択できますが、モジュール名を明示的に指定したいので aws-go を選択しました。

$ sls create --template aws-go

モジュール名を linkode.co.jp/TestAsync として、プロジェクトを初期化します。

$ go mod init linkode.co.jp/TestAsync

Lambda関数のビルド

フォルダー構成

次に、テンプレートで生成されたファイルとフォルダーを以下のように配置し直します。

./
│  .gitignore
│  go.mod
│  Makefile
│  serverless.yml
└─handler
    └─TestAsync
            main.go

フォルダー構成はプロジェクトやチームの方針によってさまざまですが、今回の例では TestAsync というLambda関数名を意識した配置にしています。

main.go

Lambda関数本体は時間のかかる処理を想定して、5秒間のスリープを行います。

package main
 
import (
    "log"
    "time"
 
    "github.com/aws/aws-lambda-go/lambda"
)
 
// Handler is lambda handler
func Handler() {
    log.Printf("Start processing.")
 
    time.Sleep(time.Second * 5)
 
    log.Printf("Process is finished.")
}
 
func main() {
    lambda.Start(Handler)
}

Makefile

Makefile は次のように編集します。

.PHONY: build clean deploy
 
build:
    env GOOS=linux go build -ldflags="-s -w" -o bin/TestAsync/app.lambda_handler handler/TestAsync/main.go
 
clean:
    rm -rf ./bin
 
deploy: clean build
    sls deploy --verbose

これで、makeコマンドでビルドが行える状態になりました。

デプロイの設定

API Gateway からの Lambda関数の非同期呼び出しは、serverless.yml で設定を行います。
全体の内容は次の通りです。

service: testasync
frameworkVersion: '>=1.28.0 <2.0.0'
 
provider:
  name: aws
  runtime: go1.x
  stage: dev
  region: us-east-2
  iamRoleStatements:
    - Effect: Allow
      Action:
        - logs:CreateLogGroup
        - logs:CreateLogStream
        - logs:PutLogEvents
      Resource:
        - "*"
 
package:
  individually: true
  exclude:
    - ./**
 
functions:
  TestAsync:
    package:
      include:
        - ./bin/TestAsync/**
    handler: bin/TestAsync/app.lambda_handler
    events:
      - http:
          method: get
          path: testasync
          async: true # 非同期呼び出しを有効化
          response:
            statusCodes:
              202:
                pattern: '' # デフォルトのレスポンス

TestAsync 関数の eventsasync: true (デフォルトはfalse)とすることで、API Gateway からの Lambda の呼び出しが非同期になります。
また、response は何も指定しない場合、200 OKがデフォルトとなりますが、今回は非同期でLambdaを呼び出すため、202 Acceptedとしています。

ステータスコードのカスタマイズ詳細は Serverless Framework のドキュメントを参照してください。
https://www.serverless.com/framework/docs/providers/aws/events/apigateway#custom-status-codes

あとは、Serverless Framework のコマンド sls deploy を実行することでデプロイが行なえます。

AWS マネジメントコンソール

余談ですが、マネジメントコンソールからも同じ設定が行なえます。
マネジメントコンソールでの設定は、次の記事が参考になりました。

【AWS】API GatewayからLambdaを非同期で実行する

動作確認

ビルドとデプロイを行い、動作確認をします。
curl コマンドで、レスポンスのステータスコードを表示させて、timeコマンドでその実行時間を計測します。
※URLのRESTAPIIDの部分は適宜置き換えてください。

$ time curl https://RESTAPIID.execute-api.us-east-2.amazonaws.com/dev/testasync -o /dev/null -w '%{http_code}\n' -s
202
 
real    0m0.904s
user    0m0.016s
sys     0m0.016s

202 Acceptedが返却されて、1秒以内にレスポンスされている事がわかります。

一方 Lambda 関数のログを確認すると、5秒の間隔を開けてログが出力されていることがわかりました。

2020/09/30 07:28:53 Start processing.
2020/09/30 07:28:58 Process is finished.

今度は試しに以下のように serverless.ymlfunctions を書き換えて、同期呼び出しを確認してみます。

functions:
  TestAsync:
    package:
      include:
        - ./bin/TestAsync/**
    handler: bin/TestAsync/app.lambda_handler
    events:
      - http:
          method: get
          path: testasync
          integration: lambda
          async: false
          response:
            statusCodes:
              202:
                pattern: ''

integration: lambdaasync: false で明示的にLambda非プロキシ統合で同期呼び出し、となるように指定しています。
これをデプロイしてみて、非同期と同様に動作確認してみます。

$ time curl https://RESTAPIID.execute-api.us-east-2.amazonaws.com/dev/testasync -o /dev/null -w '%{http_code}\n' -s
202
 
real    0m5.802s
user    0m0.016s
sys     0m0.063s

コマンドの実行全体で5秒以上経過していることがわかります。

まとめ

今回の例では呼び出した Lambda 関数が5秒間スリープするだけでしたが、サーバー側の処理内容にかかわらず同じ構成が使えます。
クライアントに対して即座にレスポンスを返すだけで良いケースでは、シンプルな方法かと思います。

参考資料