APIGatewayのStepFunctions Integration
API Gatewayの統合タイムアウトの最大は29秒なので、バッチ等の時間がかかる処理を行うAPIを作成する場合は統合先にLambdaではなくStepFunctionsを設定し、StartExecutionだけのレスポンスを得て裏で処理を継続させるパターンがあると思います。
参考資料:
【API Gatewayタイムアウト対策】Step Functionsを組み合わせて非同期処理にしてみる
MappingTemplate
StartExecutionのAPIを叩く場合、リクエストのpayloadにはStepFunctionsで使うInputとMachineARNを付与します。
Inputは、エスケープされたJSONが必要ですが、これはMappingTemplateで処理可能です。
公式ドキュメントにサンプルが載っています
必要なリクエスト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がそのまま使われるようです。
{
"input_key": "input_value"
}
{
"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に含めてみましょう。
#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になってしまいます(泣)
さっきと同じようにリクエストしてみます。
{
"input_key": "input_value"
}
{
"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というプラグインを使うことでデプロイ可能です。
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でオーバーライドしましょう。
- 2つめのAPIGatewayリソースを作成した場合1つ目の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をガリガリ書いていたので特に抵抗感や苦労はありませんでした。。。