API GatewayでStepFunctionsと統合する時のTips

## APIGatewayのStepFunctions Integration
API Gatewayの統合タイムアウトの最大は29秒なので、バッチ等の時間がかかる処理を行うAPIを作成する場合は統合先にLambdaではなくStepFunctionsを設定し、StartExecutionだけのレスポンスを得て裏で処理を継続させるパターンがあると思います。

参考資料:
[【API Gatewayタイムアウト対策】Step Functionsを組み合わせて非同期処理にしてみる](https://dev.classmethod.jp/cloud/aws/apigateway-stepfunctions-asynchronous/)

### MappingTemplate
StartExecutionのAPIを叩く場合、リクエストのpayloadにはStepFunctionsで使うInputとMachineARNを付与します。
Inputは、エスケープされたJSONが必要ですが、これはMappingTemplateで処理可能です。

[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/tutorial-api-gateway.html)にサンプルが載っています

必要なリクエストbody

```
{
   "input": "{}",
   "name": "MyExecution",
   "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:HelloWorld"
}
```

公式のサンプルMappingTemplate

```
{
    "input": "$util.escapeJavaScript($input.json('$'))",
    "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:HelloWorld"
} 
```

`$util.escapeJavaScript`を使ってリクエストbodyのJSONをエスケープしています。
また、stateMachineArnを指定しているのでリクエストに含める必要がなくなります。
したがって、このMappingTemplateを適用した場合のリクエストはinputそのものになります。
name属性はExecutionNameなので90日間ステートマシン内で一意である必要があるんですが、
指定しなければAPIGatewayが生成するRequestIdがそのまま使われるようです。

```:RequestBody
{
   "input_key": "input_value"
}
```

```:変換後のbody
{
   "input": "{\"input_key\" : \"input_value\"}",
   "name": "requestId",
   "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:HelloWorld"
}
```

### inputにrequestContextを含みたい！
サンプルの構成のだとステートマシンに与えられるinputはリクエストbodyのみです。
つまりStepfunctionから呼ばれるLambdaはcontextを参照できません。
それでは少し不便なのでMappingTemplateを少し改良してRequestIdをinputに含めてみましょう。

```:MapingTemplate
#set( $body = $util.escapeJavaScript($input.json('$')) )
{
  "input": "{\"input\":$body,\"requestContext\":{\"requestId\":\"$context.requestId\"}}",
  "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:HelloWorld"
}
```
`#set( $body = $util.escapeJavaScript($input.json('$')) )`で`$body`にinput jsonを代入します。
`$context`でリクエスト時のcontextにアクセスできます。
`$context.requestId`とすることで実際のRequestIdを値としてセットしています。
あとは忘れないようにinputの中をエスケープしましょう。
これを忘れるとInternal ServerErrorになってしまいます（泣）

さっきと同じようにリクエストしてみます。

```:RequestBody
{
   "input_key": "input_value"
}
```
```:変換後のbody
{
  "input": "{\"input\":{\"input_key\":\"input_value\"}, \"requestContext\":\"requestId\"}",
  "name": "requestId",
  "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:HelloWorld"
}
```

これでステートマシン内のFunctionからRequestIdが拾えるようになりました。

### Serverless Frameworkでのデプロイ
AWS SAMでも良いのですがdefinitionの記述だけJSONになってしまうのでこちらを選択しました。
[serverless-step-functions](https://github.com/horike37/serverless-step-functions)というプラグインを使うことでデプロイ可能です。


```:serverless.yml
plugins:
  - serverless-step-functions

functions:
  hello:
    handler: handler.hello

stepFunctions:
  stateMachines:
    MyStateMachine:
      name: myStateMachine
      events:
        - http:
            path: /execute
            method: post
            request:
              template:
                application/json: |
                  #set( $body = $util.escapeJavaScript($input.json('$')) )
                  {
                    "input": "{\"input\":$body,\"requestContext\":{\"requestId\":\"$context.requestId\"}}",
                    "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:HelloWorld"
                  }
      definition:
        Comment: "serverless"
        StartAt: HelloWorld1
        States:
          HelloWorld1:
            Type: Task
            Resource:
              Fn::GetAtt: [HelloLambdaFunction, Arn]
            End: true
```

providerは省略しています。
こんな感じで簡単にステートマシンとAPIGatewayを作成できます。
基本的な使い方は大体readmeに書いてあり始めやすいです。
非常に使いやすいのですが、構成次第ではちょっと工夫が必要になります。
例えば、

- StartExecution以外のAPI叩きたいとき

デフォルトがStartExecutionになっているようで、Describeを叩きたい場合はResourcesでオーバーライドする必要があります

- APIKeyRequireがデフォルトでオフ

APIKeyの作成は問題なくできますが、メソッドに対する必須条件をつける際はこれもResourcesでオーバーライドしましょう。

- ２つめのAPIGatewayリソースを作成した場合１つ目のMappingTemplateがコピーされる(気がする)

例えばこのような定義をしたとき、

```
stepFunctions:
  stateMachines:
    MyStateMachine:
      name: myStateMachine
      events:
        - http:
            path: /execute
            method: post
            request:
              template:
                application/json: |
                  #set( $body = $util.escapeJavaScript($input.json('$')) )
                  {
                    "input": "{\"input\":$body,\"requestContext\":{\"requestId\":\"$context.requestId\"}}",
                    "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:HelloWorld"
                  }
        - http:
            path: /describe
            method: post

```

これをデプロイすると、
`/execute`のtemplateが`/describe`にも装備されてしまうような動作をしました。
これもResourcesで上手くやれば解決します。
あとはMethodResponseとかいろいろありますがServerlessFrameworkのいいところは
pluginが対応していなくてもResourcesで定義してやればなんとかなるところですね。
その分記述量は増えてしまいますが、筆者はよく生のCloudformation templateをガリガリ書いていたので特に抵抗感や苦労はありませんでした。。。