AWS Amplify Advent Calendar 2020 10日目の記事です!
Amplify CLIを使うとAPI GatewayのREST APIをサクッと作成できます。
ただ、API GatewayのREST APIは、Lambda統合のタイムアウトが最大29秒なので、データのダウンロードとか時間のかかる処理を行うとタイムアウトしてしまうことがあります。そこで、Step Functionsを使ってタイムアウトの問題を解決してみます。
※つまり、以下で説明されているようなことを、Amplify CLIでやろうという感じです。とてもわかりやすくまとまっているので、まず読んでもらってから続きを見てもらえると良いかと思います。
【API Gatewayタイムアウト対策】Step Functionsを組み合わせて非同期処理にしてみる
概要
- Amplify CLIでAPI GatewayのREST APIとLambdaを作る。
- Custom CloudFormation stacksでStep Functionsを作成して、REST APIとLambdaの間に挟む。
- タスクの完了確認は、API Gatewayに確認用のリソースを作ってポーリングして確認する。
内容
最終的な結果は以下に置いてあります。
https://github.com/two-pack/example-amplify-api-with-stepfunctions
amplify init
amplify init のダイアログは全部デフォルトでいきます。
$ mkdir example-amplify-api-with-stepfunctions
$ cd example-amplify-api-with-stepfunctions
$ amplify init
REST APIとLambdaの追加
Amplify CLIからAPIを追加します。このなかでLambdaも作ってしまいました。
以下で設定しています。
設定 | 内容 |
---|---|
API名 | HelloApi |
APIパス | /hello |
APIの制限 | なし |
Lambda名 | HelloFunc |
Lambdaランタイム | NodeJS |
$ amplify api add
? Please select from one of the below mentioned services: REST
? Provide a friendly name for your resource to be used as a label for this category in the project: HelloApi
? Provide a path (e.g., /book/{isbn}): /hello
? Choose a Lambda source Create a new Lambda function
? Provide an AWS Lambda function name: HelloFunc
? Choose the runtime that you want to use: NodeJS
? Choose the function template that you want to use: Hello World
<snip>
? Do you want to configure advanced settings? No
? Do you want to edit the local lambda function now? No
Successfully added resource HelloFunc locally.
<snip>
$ amplify status
Current Environment: dev
| Category | Resource name | Operation | Provider plugin |
| -------- | ------------- | --------- | ----------------- |
| Function | HelloFunc | Create | awscloudformation |
| Api | HelloApi | Create | awscloudformation |
一度ここでpushしておきます。
$ amplify push
Step Functionsの追加
次にStep FunctionsをCustom CloudFormation stackで追加します。
追加するStep Functionsは、 HelloStep としてHelloFuncラムダを実行するタスクとします。
カテゴリとリソースの追加
/amplify/backend/backend-config.json を編集して、Amplify CLIで扱うカテゴリとしてStep Functionsを追加します。
HelloFuncラムダを呼び出すのでdependsOnに書いています。
"stepFunction": {
"HelloStep": {
"service": "Step Function",
"providerPlugin": "awscloudformation",
"dependsOn": [
{
"category": "function",
"resourceName": "HelloFunc",
"attributes": ["Name", "Arn"]
}
]
}
},
次に、フォルダを作成してCloudFormationのテンプレートとして、 /amplify/backend/stepFunction/HelloStep/HelloStep-cloudformation-template.json を作成します。参考ページのテンプレートをベースにしつつ、Amplify CLIが生成したLambdaやREST APIのテンプレートの記述を参考にして作成します。
ロールの設定は、ここで書かれているもの相当の設定が必要です。このため、AssumeRolePolicyDocumentのServiceにapigateway.amazonaws.com 、Policiesに**states:***アクションの許可を追加します。
また、HelloFunc Lambdaのテンプレートを参考に RoleName属性 も追加してください。
"StatesExecutionRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"RoleName": {
"Fn::If": [
"ShouldNotCreateEnvResources",
"exampleamplifyapiwitStatesExecutionRole",
{
"Fn::Join": [
"",
[
"exampleamplifyapiwitStatesExecutionRole",
"-",
{
"Ref": "env"
}
]
]
}
]
},
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
{
"Fn::Sub": "states.${AWS::Region}.amazonaws.com"
}
]
},
"Action": "sts:AssumeRole"
},
{
"Effect": "Allow",
"Principal": {
"Service": "apigateway.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"Path": "/",
"Policies": [
{
"PolicyName": "StatesExecutionPolicy",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["lambda:InvokeFunction"],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "states:*",
"Resource": "*"
}
]
}
}
]
}
},
次に、ステートマシンの定義です。CloudFormationの仕様に従ったものになりますが、以下のあたりがポイントかと思います。
- StateMachineNameは、Amplifyの環境名を付与してステートマシンの名前を定義しています。
-
DefinitionStringは、ステートマシンの定義です。
- Step Functionsのコンソールから定義をJSONで確認できるので、コンソールで作成したもの持ってくると簡単かと思います。
- lambdaArn はプレースホルダーの参照です。テンプレート内の Parameters でHelloFunc LambdaのARNを参照できるように記載しています。
"HelloStepStateMachine": {
"Type": "AWS::StepFunctions::StateMachine",
"Properties": {
"StateMachineName": {
"Fn::If": [
"ShouldNotCreateEnvResources",
"HelloStep",
{
"Fn::Join": [
"",
[
"HelloStep",
"-",
{
"Ref": "env"
}
]
]
}
]
},
"DefinitionString": {
"Fn::Sub": [
"{\n \"Comment\": \"A Hello World example using an AWS Lambda function\",\n \"StartAt\": \"HelloWorld\",\n \"States\": {\n \"HelloWorld\": {\n \"Type\": \"Task\",\n \"Resource\": \"${lambdaArn}\",\n \"End\": true\n }\n }\n}",
{
"lambdaArn": {
"Ref": "functionHelloFuncArn"
}
}
]
},
"RoleArn": {
"Fn::GetAtt": ["StatesExecutionRole", "Arn"]
}
}
}
},
ここまでできたら、以下のコマンドでAmplify CLIにカテゴリとリソースとしてHelloStepを認識させます。
amplify env checkout を行うと認識させることができます。
$ amplify env checkout dev
$ amplify status
Current Environment: dev
| Category | Resource name | Operation | Provider plugin |
| ------------ | ------------- | --------- | ----------------- |
| Stepfunction | HelloStep | Create | awscloudformation |
| Function | HelloFunc | No Change | awscloudformation |
| Api | HelloApi | No Change | awscloudformation |
REST API endpoint: https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev
というわけで amplify push します。
$ amplify push
AWS ConsoleでStep Functionsを確認すると、 HelloStep-dev というステートマシンができていると思います(-dev は、Amplifyの環境が付与されているものです)。
実行すると、HelloFunc-dev Lambdaが実行されることを確認できます。
REST APIからStep Functionsを呼び出す
ステートマシンができたので、次はREST APIからStep Functionsのステートマシンを呼び出すようにします。
今回は、API Gatewayのコンソールで統合リクエストをStep Functionsの呼び出しに変更して、その結果をエクスポートしたものをもとに、テンプレートを書き換えていきます。1
依存関係を変更する
/amplify/backend/backend-config.json の api の dependsOn をHelloFuncからHelloStepへ変えます。
attributes は、参照して使うArnとRoleNameを定義しておきます。
"api": {
"HelloApi": {
"service": "API Gateway",
"providerPlugin": "awscloudformation",
"dependsOn": [
{
"category": "stepFunction",
"resourceName": "HelloStep",
"attributes": ["Arn", "RoleName"]
}
]
}
}
同様に /amplify/backend/api/HelloApi/api-params.json の dependsOn もHelloFuncからHelloStepへ変えます。
統合先をStep Functionsに変更する
API Gatewayのコンソールで、Amplifyで作成した HelloApi の統合先を変更します。今回は、CloudFormationのテンプレートを変更する前に、コンソールで設定を試してみてから、結果をエクスポートしてテンプレートに適用していきます。
/hello と /hello/{proxy+} のANYリソース の統合リクエストを開き、統合タイプをAWSサービスにしてStep Functionsが呼び出されるように設定します。設定時のARNなど、Amplifyで作成したステートマシンをコンソールで見ながら入れていきます。
アクションは**StartExecution**を指定することで、ステートマシンを実行することができます。
このあたりは、参考ページで新規に作っていく流れが詳しく書かれているので参照してもらえればと思います。
加えて、ここに記載のある、マッピングテンプレートの設定も行います。
設定をしたら、リソースのテストで確認します。
メソッドは POST 、リクエスト本文は {} でとりあえずOKです。
設定がうまく行っていれば、HelloStepが呼び出されます。Step Functionsのコンソールから確認しましょう。
APIのデプロイ結果をエクスポート
変更をデプロイして、API Gatewayのコンソールのステージから設定した内容をエクスポートします。
Swagger +API Gateway 拡張の形式でエクスポートでJSONを選ぶと、JSONファイルがダウンロードされます。
REST APIのCloudFormationに適用
エクスポートしたJSONとREST APIのCloudFormationのファイル(/amplify/backend/api/HelloApi/HelloApi-cloudformation-template.json)とで、 paths 以下の見比べながら適用していきます。
エクスポートしたJSONではARNなどが実際の直値担っていると思いますが、Amplify CLIで生成された他のテンプレートと同様にユーティリティ関数(Fn::Josinとか)を使って生成するように修正しながら適用します(credentials、uri、requestTemplatesなどが該当)。
また、HelloStepのテンプレートで定義したARNやロールを参照するために、 Parameters に書き加えます。
"stepFunctionHelloStepArn": {
"Type": "String"
},
"stepFunctionHelloStepRoleName": {
"Type": "String"
}
また、APIからはHelloFuncを参照しなくなるため、関連箇所は削除しておきます。
できたら amplify push します。
REST APIからの呼び出し
API Gatewayのエンドポイントを確認してcurlで呼び出します。
Step Functionsのコンソールで実行されたことが確認できます。やった!2
$ curl -X POST -H "Content-Type: application/json" -d '{}' <エンドポイントURL>/hello
{"executionArn":"arn:aws:states:ap-northeast-1:xxxxxxxxxxxx:execution:HelloStep-dev:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","startDate":1.607175965069E9}
ステートマシンの状態を確認する
ステートマシンの実行ができたので、次は状態確認を作ります。
これは参考ページの DescribeExecution についての記載に従ってAPI Gatewayのコンソールでリソースを作成してもらい、それをCloudFormationのテンプレートに適用します。
これはStartExecutionと同じ感じなので割愛しますが、 /status のリソースを追加したとして進めます。
確認用にLambdaを変更
状態を確認するために、HelloFuncを書き換えて20秒待たせます。
exports.handler = async (event) => {
await new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 20000);
});
const response = {
statusCode: 200,
body: JSON.stringify("Hello from Step Functions with API Gateway!"),
};
return response;
};
忘れずに amplify push しましょう。
REST APIからの呼び出し
状態確認のAPIであるDescribeExecutionには、StartExecutionのレスポンスに含まれるexecutionArnを渡してあげます。
ちょっと長くて見にくいですが、statusがRUNNINGの状態が続いた後にSUCCEEDEDになっています。statusにはここで定義されているものが返るので、これで状態を確認できます。やった!2
$ curl -X POST -H "Content-Type: application/json" -d '{}' <エンドポイントURL>/hello
{"executionArn":"arn:aws:states:ap-northeast-1:xxxxxxxxxxxx:execution:HelloStep-dev:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","startDate":1.607175965069E9}
$ curl -X POST -H "Content-Type: application/json" -d '{"executionArn":"arn:aws:states:ap-northeast-1:xxxxxxxxxxxx:execution:HelloStep-dev:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}' <エンドポイントURL>/status
{"executionArn":"arn:aws:states:ap-northeast-1:xxxxxxxxxxxx:execution:HelloStep-dev:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","input":"{}","inputDetails":{"__type":"com.amazonaws.swf.base.model#CloudWatchEventsExecutionDataDetails","included":true},"name":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","startDate":1.607215992503E9,"stateMachineArn":"arn:aws:states:ap-northeast-1:xxxxxxxxxxxx:stateMachine:HelloStep-dev","status":"RUNNING","traceHeader":"Root=1-xxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx;Sampled=1"}
$ curl -X POST -H "Content-Type: application/json" -d '{"executionArn":"arn:aws:states:ap-northeast-1:xxxxxxxxxxxx:execution:HelloStep-dev:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}' <エンドポイントURL>/status
{"executionArn":"arn:aws:states:ap-northeast-1:xxxxxxxxxxxx:execution:HelloStep-dev:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","input":"{}","inputDetails":{"__type":"com.amazonaws.swf.base.model#CloudWatchEventsExecutionDataDetails","included":true},"name":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","startDate":1.607215992503E9,"stateMachineArn":"arn:aws:states:ap-northeast-1:xxxxxxxxxxxx:stateMachine:HelloStep-dev","status":"RUNNING","traceHeader":"Root=1-xxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx;Sampled=1"}
$ curl -X POST -H "Content-Type: application/json" -d '{"executionArn":"arn:aws:states:ap-northeast-1:xxxxxxxxxxxx:execution:HelloStep-dev:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}' <エンドポイントURL>/status
{"executionArn":"arn:aws:states:ap-northeast-1:xxxxxxxxxxxx:execution:HelloStep-dev:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","input":"{}","inputDetails":{"__type":"com.amazonaws.swf.base.model#CloudWatchEventsExecutionDataDetails","included":true},"name":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","startDate":1.607215992503E9,"stateMachineArn":"arn:aws:states:ap-northeast-1:xxxxxxxxxxxx:stateMachine:HelloStep-dev","status":"RUNNING","traceHeader":"Root=1-xxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx;Sampled=1"}
$ curl -X POST -H "Content-Type: application/json" -d '{"executionArn":"arn:aws:states:ap-northeast-1:xxxxxxxxxxxx:execution:HelloStep-dev:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}' <エンドポイントURL>/status
{"executionArn":"arn:aws:states:ap-northeast-1:xxxxxxxxxxxx:execution:HelloStep-dev:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","input":"{}","inputDetails":{"__type":"com.amazonaws.swf.base.model#CloudWatchEventsExecutionDataDetails","included":true},"name":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","output":"{\"statusCode\":200,\"body\":\"\\\"Hello from Step Functions with API Gateway!\\\"\"}","outputDetails":{"__type":"com.amazonaws.swf.base.model#CloudWatchEventsExecutionDataDetails","included":true},"startDate":1.607215992503E9,"stateMachineArn":"arn:aws:states:ap-northeast-1:xxxxxxxxxxxx:stateMachine:HelloStep-dev","status":"SUCCEEDED","stopDate":1.607216012948E9,"traceHeader":"Root=1-xxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx;Sampled=1"}
さいごに
Amplify CLIを使って、API Gateway経由でのStep Functions実行を行うことができました。
AWSのコンソールで設定したものもJSONなどで出力が比較的できるので、初心者でも試行錯誤しやすかった気もします3。
Custom CloudFormation stackを使えば、色々幅が広がりそうなので、いろいろやってみたいと思います。
参考ページ
- 【API Gatewayタイムアウト対策】Step Functionsを組み合わせて非同期処理にしてみる
- API Gateway を使用して Step Functions API を作成する
- AWS CloudFormation を使用してStep Functions 用 Lambda ステートマシンを作成する
- Custom CloudFormation stacks
- EXTEND AWS AMPLIFY WITH CUSTOM RESOURCES