LoginSignup
6
6

More than 3 years have passed since last update.

AmplifyでAPI Gateway + Step Functions

Posted at

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に書いています。

/amplify/backend/backend-config.json
  "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のテンプレートの記述を参考にして作成します。

ロールの設定は、ここで書かれているもの相当の設定が必要です。このため、AssumeRolePolicyDocumentServiceapigateway.amazonaws.comPoliciesstates:*アクションの許可を追加します。
また、HelloFunc Lambdaのテンプレートを参考に RoleName属性 も追加してください。

amplify/backend/stepFunction/HelloStep/HelloStep-cloudformation-template.json
    "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を参照できるように記載しています。
amplify/backend/stepFunction/HelloStep/HelloStep-cloudformation-template.json
    "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.jsonapidependsOn をHelloFuncからHelloStepへ変えます。
attributes は、参照して使うArnとRoleNameを定義しておきます。

/amplify/backend/backend-config.json
  "api": {
    "HelloApi": {
      "service": "API Gateway",
      "providerPlugin": "awscloudformation",
      "dependsOn": [
        {
          "category": "stepFunction",
          "resourceName": "HelloStep",
          "attributes": ["Arn", "RoleName"]
        }
      ]
    }
  }

同様に /amplify/backend/api/HelloApi/api-params.jsondependsOn も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 に書き加えます。

/amplify/backend/api/HelloApi/HelloApi-cloudformation-template.json
    "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秒待たせます。

amplify/backend/function/HelloFunc/src/index.js
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を渡してあげます。
ちょっと長くて見にくいですが、statusRUNNINGの状態が続いた後に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を使えば、色々幅が広がりそうなので、いろいろやってみたいと思います。

参考ページ


  1. CloudFormationを熟知していれば即書いていけばよいのだと思いますが、AWS歴4ヶ月のワタシにはハードルが高かった・・・ 

  2. amplify pushしてからエンドポイントにPOSTすると、{"message": "Internal server error"}{"message":"Missing Authentication Token"} が返ってくる場合がありました。その際は、API Gatewayのコンソールからデプロイすると解消しました。 

  3. しやすかったは言いすぎか?ちゃんとCloudFormationの勉強をしろって話な気がする・・・ 

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6