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

# はじめに

こちらは**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は非同期で実行させます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545707/b160a951-ae6c-aa72-88e2-851d047a1b77.png)

## 作ったSAMテンプレート

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

```yaml
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 関数を呼び出すにはどうすればよいですか?」](https://repost.aws/ja/knowledge-center/api-gateway-invoke-lambda)の記載の通り、API Gatewayから非同期でLambdaを呼び出すには統合リクエストを選択し、HTTPヘッダー`X-Amz-Invocation-Type`を追加し、値に`'Event'`を指定すればよいです。

SAMでの統合リクエストの設定はOpenAPIの拡張`x-amazon-apigateway-integration`に定義します。
SAMのプロパティである[DefinitionBody](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-resource-api.html#sam-api-definitionbody)を使用しているため、上記OpenAPI仕様が使用できるわけです。

[x-amazon-apigateway-integration.requestParameters](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-swagger-extensions-integration-requestParameters.html)で、統合リクエストのヘッダーやクエリがプロパティとして定義でき、ここに非同期起動を指示するHTTPヘッダーを指定します。

```yaml
              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](https://stackoverflow.com/a/41389858)に対処方法を書いてくださっている方がいました。

> 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公式ドキュメント](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-integrations.html
)にも同様のことが記載されていました。

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

統合リクエストについて、[AWS公式ドキュメント](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/integration-request-basic-setup.html)から抜粋します。

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

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

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

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

## 実際に試す

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

```python
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秒もかからずレスポンスが返ってきていることがわかります。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545707/ac69ffcd-d51e-ee48-718e-037ed705d332.png)

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

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545707/c4b04b20-2d16-1f11-f7ea-33b9c0bdf701.png)

## おわりに

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

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