APIGatewayタイムアウト不可避を回避（神回避）

## APIGateway初心者あるある

「LambdaとAPIGatewayでAPI作ったで！
ちょい処理重めやけどLambdaは最大15分まで動くし、15分もあったら余裕やわ」

ポチっ（実行）

30秒後
~~~bash
{
    "message": "Endpoint request timed out"
    # 訳: いつまでリクエスト待たせんねんワレ
}
~~~
「15分間は耐えるんちゃうんかい」

（ナレーション）
彼はどこで間違えてしまったのでしょうか。
そうです、Lambdaは確かに設定を変えれば15分まではタイムアウト時間を延ばせますが、APIGatewayは最大30秒しか耐えられないのです。

つまりこのタイムアウトはLambdaによるものではなくAPIGatewayによるものなのです。その証拠に、Lambdaのログを見るとタイムアウト後も動作していることが確認できます。

## 解決策：APIを二つに分けて回避
やり方はいくつかあるみたいですが、今回は「Lambda発火API」と「Lambda状態取得API」の二つのAPIを用意することで解決します。
例えば単純にデータを取得するGETメソッドAPIであれば、もともとの設計は、

【これまでのAPI】
~~~mermaid
sequenceDiagram
autonumber
participant front as フロント
participant apigateway as APIGateway
participant start as Lambda発火API
front->>apigateway: 「データちょうだい」 <br> param={param1, param2, ...} 
apigateway->>start: 「Lambda動かしてや」 <br> param={param1, param2, ...} 
start->>apigateway: 「こちらがデータでございます。」 <br> response={executionArn, ...} 
apigateway->>front: 「はいこれデータやで」 <br> response={executionArn, ...} 
~~~

でしたが、タイムアウトを回避するため、

【今回作るAPI】
~~~mermaid
sequenceDiagram
autonumber
participant front as フロント
participant apigateway as APIGateway
participant start as Lambda発火API
participant status as Lambda状態取得API
front->>apigateway: 「データちょうだい」 <br> param={param1, param2, ...} 
apigateway->>start: 「Lambda動かしてや」 <br> param={param1, param2, ...} 
start->>apigateway: 「ご注文承りました。」 <br> response={executionArn, ...} 
apigateway->>front: 「はいこれ整理券やで」 <br> response={executionArn, ...} 
front->>apigateway: 「注文したやつまだー？」 <br> param={executionArn}
apigateway->>status: 「おいまだ出来てへんのか？」 <br> param={executionArn}
status->>apigateway: 「もう少々お待ちください」<br>or <br>「終わってますね、こちらお品物です。」 <br> response={status, output(本来のレスポンス), ...} 
apigateway->>front: 「まだらしいわ」<br>or <br>「はいこれデータやで」 <br> response={status, output(本来のレスポンス), ...} 
~~~

のようにします。
1~4と5~8でそれぞれ別のエンドポイントを叩いています。

もともとのAPIとの違いは、3でデータを返していないということです。
3では整理券なるものを渡し、後でデータが用意できたところを自分で取りにいくというイメージです。
いつデータが完成するかわからないので何度か定期的に訪れる必要があるでしょう。

## 実装

### 前提
- APIで実行したいLambdaは用意されている

### Step FunctionsでLambdaを動かすステートマシンを作成
ここまで、「Lambda発火」や「Lambda動かしてや」などと言ってきましたが、正しくは「ステートマシンを動かす」になります。
どういうことかはステートマシンを作成するとわかります。

AWSマネコンから「Step Functions」を開いて、ステートマシンを作成します。
Step Functionsはあらゆるデータのやりとりを一つのフローとして保存することができるサービスです。詳しくはハンズオンとか見てください。

以下のように定義します。

![スクリーンショット 2023-07-27 14.19.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388259/7eaaa7d9-7b05-48c6-dbcc-69e304022684.png)
現在はステートマシンの設計は「Workflow Studio」というGUI操作でフローを組み立てられる機能があるのでこの画像だけで十分かもしれません。
一応、コードにするとこうなってます。
~~~json
{
  "Comment": "",
  "StartAt": "Lambda Invoke",
  "States": {
    "Lambda Invoke": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "OutputPath": "$.Payload",
      "Parameters": {
        "Payload.$": "$",
        "FunctionName": "arn:aws:lambda:ap-northeast-1:xxx:function:xxx:$LATEST"
      },
      "Retry": [
        {
          "ErrorEquals": [
            "Lambda.ServiceException",
            "Lambda.AWSLambdaException",
            "Lambda.SdkClientException",
            "Lambda.TooManyRequestsException"
          ],
          "IntervalSeconds": 2,
          "MaxAttempts": 6,
          "BackoffRate": 2
        }
      ],
      "End": true
    }
  }
}
~~~
このように、ただLambdaを実行するだけのステートマシンを用意します。なぜわざわざStep Functionsをかまさないといけないかは後ほどわかります。

### API GatewayでStep Functionsと連携するAPIを作成

「Lambda発火API」と「Lambda状態取得API」の二つのAPIを用意すると述べましたが、この二つのAPIをここからは
「 __StartExecution API__ 」と「 __DescribeExecution API__ 」と呼びます。
#### 1. APIGatewayからStep Functionsを呼び出せるようにロールを作成

以下の公式ドキュメントのステップ１を参照してください。

https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/tutorial-api-gateway.html#:~:text=%E3%83%86%E3%82%B9%E3%83%88%E3%81%A8%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4-,%E3%82%B9%E3%83%86%E3%83%83%E3%83%97,-1%3A%20API%20Gateway

#### 2. StartExecution API(Lambda発火API)

これは上記APIフローの2「Lambda動かしてや」にあたります。

APIGatewayにはStartExecutionというアクションが用意されており、このAPIを叩くことでロールで指定したStep Functionsを実行することが可能です。

APIGatewayを開いて、executionというリソースを作成し、そこ __POSTメソッド__ を作成します。
今回は、私がもともと作成していたAPIにリソースを追加する形で作成したので、画像（左部）にはPOSTが３つありますが一番上のPOSTは無視してください。
以下のようにメソッドを設定します。
![スクリーンショット 2023-07-27 14.41.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388259/0b180864-4d79-650e-258d-04c5894bf4e6.png)

### 3. DescribeExecution API(Lambda状態取得API)

これは上記APIフローの6「おいまだ出来てへんのか？」にあたります。

APIGatewayに用意されているDescribeExecutionは、実行タスクの状態を取得するAPIです。
DescribeExecutionは、Lambdaの状態を直接見ることはできませんが、Step Functionsの状態なら見ることができます。
これがLambdaをわざわざStep Functionsにかませた理由になります。

/executionの中にstatusというリソースを作成して同じく __POSTメソッド__ を作成します。
以下のように設定します。「アクション」の部分だけ先ほどと違います。
![スクリーンショット 2023-07-27 15.04.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2388259/2183671c-4e51-efb0-2a5f-74cbf9cedbdf.png)

これで完成です。

## 使い方

まずLambdaを発火させるためにStartExecution APIを叩きます。
/execution のPOSTを投げると、
~~~json
{
    "executionArn": "arn:aws:states:ap-northeast-1:xxx:execution:xxx:xxx",
    "startDate": x.xxx
}
~~~
のようなデータが返ってきます。
この```executionArn```が実行タスクになります。

次にLambdaの状態を取得するDescribeExecution APIを叩きます。
/execution/status のPOSTを投げるのですが、ここでリクエストボディに先ほどの```executionArn```を含める必要があります。
~~~json:リクエストボディ
{
  "executionArn": "arn:aws:states:ap-northeast-1:xxx:execution:xxx:xxx"
}
~~~

これで
~~~json:レスポンス
{
    "executionArn": "arn:aws:states:ap-northeast-1:xxx:execution:xxx:xxx"
    ...（省略）...
    "output": "{\"isBase64Encoded\":false,\"statusCode\":200,\"headers\":{},\"body\":{\"errorCode\":0,\"message\":\"成功\"}}",
    "status": "RUNNING",
    ...（省略）...
}
~~~
のようなレスポンスが帰ってきたら成功です。
```status```を見れば状態がわかるようになっています。
また、```output```の```body```の中に本来のAPIで求めていたレスポンスが含まれています。

## APIのリクエストにパラメーターを含めたい

パラメーターを渡す方法は簡単です。

まずStartExecution APIのマッピングテンプレートを編集します。
APIGatewayの画面から、StartExecution APIの統合リクエストの画面下部の
マッピングテンプレート > マッピングテンプレートの追加 を選択します。

application/jsonタイプで以下のように記述します。
~~~json
{
    "stateMachineArn": "arn:aws:states:ap-northeast-1:xxx:stateMachine:xxx",
    "input": "$util.escapeJavaScript($input.json('$')).replaceAll('\\\\\"', '\"')"
}
~~~
ここで、stateMachineArnには、最初に作成したStep FunctionsのステートマシンのARNを入れます。Step Functionsのマネコンからすぐに確認することができます。

保存したら、あとはStartExecution APIを叩くときにいつも通りパラメーターをリクエストボディに含ませるだけです。

### Lambdaのパラメーターの受け取り方
パラメーターはeventのbodyにあります。したがって、
~~~python
event_body = json.loads(event['body'])
~~~
で受け取れます。

## まとめ

WebAPIというのはすぐに実行結果を返すというのが大切というか前提になっているから、APIGatewayのタイムアウト制限は最大30秒になっているんでしょうか。
とにかく、Lambdaの処理に時間がかかる可能性があるなら非同期でLambdaを動かして、別のAPIで結果を取得する必要があります。これでとりあえず解決です。