LoginSignup
1
1

API GatewayからLambda非同期呼び出しをSAMで書く

Last updated at Posted at 2023-12-20

はじめに

こちらはQiita Advent Calendar AWS Lambda と Serverless 21日目の記事です。

サーバレスで外部からAPIを呼び出したい時、API Gateway + Lambdaは鉄板構成です。
ただ、Lambdaの処理時間が長いとAPI Gatewayのタイムアウトの最大である29秒(RESTの場合)を超えてしまい、APIのリクエストが失敗してしまいます。
そのため、API側でLambdaを突き放して実行する構成が取られますが、今回それを実現するAWS Serverless Application Model(AWS SAM)のテンプレートについて主に説明します。

管理コンソールから上記実現する方法はググればたくさん見つかります。
SAMについては情報が少なかったため、記載することにしました。

実現したかったこと

当時(2年前ぐらい?)、比較的小規模の会社様の請求システムの構築のプロジェクトに参画しました。
お客様からの要望は、以下のようなものでした。

  • 請求書の作成と請求先へのメール送信を自動化したい
  • メール送信まで終わったら通知してほしい

AWSでシステムを構築することはすでに決まっており、運用コストの上限もなぜかすでに決まっていました。
このような状況下でシステム設計を担当し、結果として以下の構成でシステムを構築することに決めました。
色々省略している構成図ですが、API Gateway + Lambda、かつ、Lambdaは非同期で実行させます。

作ったSAMテンプレート

早速ですが、非同期でLambdaをAPI Gatewayから実行するSAMテンプレートは以下です。
重要な箇所、詰まった箇所についてこの後説明します。

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Resources:
  SampleLambda:
    Type: AWS::Serverless::Function
    Properties:
      PackageType: Zip
      Runtime: python3.11
      CodeUri: src/sampleLambda
      Handler: app.lambda_handler
      Timeout: 300
      Architectures:
        - x86_64
      FunctionName: sampleLambdaFunction
      Policies:
        - AWSLambdaBasicExecutionRole
      Events:
        sampleLambda:
          Type: Api
          Properties:
            Path: /sampleLambda
            Method: get
            RestApiId: !Ref SampleApi
    Metadata:
      Dockerfile: Dockerfile
      DockerContext: ./src/sampleLambda
      DockerTag: prod
  SampleApi:
    Type: AWS::Serverless::Api
    Properties:
      EndpointConfiguration:
        Type: REGIONAL
      Name: sampleApi
      StageName: Prod
      DefinitionBody:
        swagger: "2.0"
        schemes:
          - "https"
        paths:
          /sampleLambda:
            get:
              responses:
                "202":
                  description: 202 response
                  schema:
                    $ref: "#/definitions/Empty"
              x-amazon-apigateway-request-validator: all
              x-amazon-apigateway-integration:
                requestParameters:
                  "integration.request.header.X-Amz-Invocation-Type": "'Event'"
                httpMethod: POST
                type: aws
                passthroughBehavior: "when_no_match"
                responses:
                  default:
                    statusCode: "202"
                uri:
                  !Join [
                    "",
                    [
                      "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/",
                      !GetAtt SampleLambda.Arn,
                      "/invocations",
                    ],
                  ]
                StageName: Prod
        x-amazon-apigateway-policy:
          Version: "2012-10-17"
          Statement:
            - Effect: Allow
              Principal: "*"
              Action: execute-api:Invoke
              Resource:
                - "execute-api:/*"
        definitions:
          Empty:
            type: object
            title: Empty Schema

x-amazon-apigateway-integration.requestParameters

「Amazon API Gateway API から非同期に Lambda 関数を呼び出すにはどうすればよいですか?」の記載の通り、API Gatewayから非同期でLambdaを呼び出すには統合リクエストを選択し、HTTPヘッダーX-Amz-Invocation-Typeを追加し、値に'Event'を指定すればよいです。

SAMでの統合リクエストの設定はOpenAPIの拡張x-amazon-apigateway-integrationに定義します。
SAMのプロパティであるDefinitionBodyを使用しているため、上記OpenAPI仕様が使用できるわけです。

x-amazon-apigateway-integration.requestParametersで、統合リクエストのヘッダーやクエリがプロパティとして定義でき、ここに非同期起動を指示するHTTPヘッダーを指定します。

              x-amazon-apigateway-integration:
                # ↓ここ↓
                requestParameters:
                  "integration.request.header.X-Amz-Invocation-Type": "'Event'"

x-amazon-apigateway-integration.httpMethod

詰まった箇所について紹介します。
APIをリクエストするとAccessDeniedException: Unable to determine service/operation name to be authorizedというエラーが発生し、Lambda関数が起動できないという事象が発生しました。

調べてみるとStackoverflowに対処方法を書いてくださっている方がいました。

I think you are using "GET" for your Lambda function endpoint on your GET method as well. Please change it to use "POST" for the Lambda integration HTTP method.

Lambda関数を起動させるためには統合リクエストのメソッドをPOSTにしなければならないようです。
AWS公式ドキュメントにも同様のことが記載されていました。

当時、統合リクエストのメソッドが何であるのか特に気にせずAPIのHTTPメソッドと同じGETにしていたため、本事象が発生してしまいました。

統合リクエストについて、AWS公式ドキュメントから抜粋します。

統合リクエストは、API Gateway がバックエンドに送る HTTP リクエストであり、クライアントから送信されたリクエストデータを渡し、必要に応じて変換します。
統合リクエストの HTTP メソッド (つまり動詞) と URI は、バックエンド (つまり統合エンドポイント) が指定します。
HTTP メソッドと URI はそれぞれ、メソッドリクエストのものと同じ場合もあれば異なる場合もあります。

統合リクエストのメソッドはバックエンドのAWSサービスを呼び出す際に使われるものであり、APIのHTTPメソッドと異なってもよいものということです。

今回バックエンドはLambdaを呼び出します。
LambdaのAPIリファレンスを確認したところ、HTTPメソッドはPOSTであり、非同期呼び出しの場合、X-Amz-Invocation-TypeヘッダーにEventを指定しろと記載されていました。
これら設定値が統合リクエストから渡されるため、Lambdaが非同期で呼び出されるのだなと理解することができました。

              x-amazon-apigateway-integration:
                requestParameters:
                  "integration.request.header.X-Amz-Invocation-Type": "'Event'"
                # ↓ここ↓
                httpMethod: POST

実際に試す

SAMテンプレートについて説明は終わりです。実際にリソースを構築し動かしてみます。
なお、実行するLambda関数を以下に示します。処理が開始して、30秒経ったら終了するものです。

import json
import time

def lambda_handler(event, context):
    print("処理を開始")
    time.sleep(30)
    print("30秒経過しました。")
    return {
        'statusCode': 200,
        'body': json.dumps('OK')
    }

API Gatewayのテスト機能からAPIを実行します。
ログの時間に注目してください。1秒もかからずレスポンスが返ってきていることがわかります。

Lambda関数の実行結果をCloudWatch Logsから確認します。
こちらも時間に注目していただくと、ちゃんと30秒経過してから処理を終了しています。
APIはLambdaの処理時間に引きずられずレスポンスを返却できていることがわかりました。

おわりに

最後まで読んでいただきありがとうございました。
テーマがLambdaとサーバレスなのにAPI Gatewayが中心の内容となってしまったことに執筆途中で気がついてしまいました。(モウシワケナイ)

ただ、私にとって今回取り上げたシステムは本格的にAWSを触り始めた時のものだったので、今回書く機会ができてよかったです。
この記事が誰かのお役に立てば嬉しいです。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1