API GatewayとStep Functionsを組み合わせた非同期APIが最強だった話

最強というかカンタン？

サーバーレスの構成として一般的な
API Gateway＆Lambdaですが

**API Gatewayは29秒制限**、
**Lambdaは15分制限**があるわけですね


Lambdaの性能を目一杯使いつつ、
そのギャップを埋めるため
**なるべくシンプルな構成で非同期APIにしたい** ってこと
あると思います。

そのときに必要なのは

- LambdaをキックするためのAPI
- キックしたLambdaが完了したか判断し、レスポンスを受け取るためのAPI

だと思いますが、下記Step Functionsにはその**両方が備わっています**

# Step Functions

Step Functionsでは、
マイクロサービスの連携などワークフローを視覚的でイケメンな感じに
構成することができます。

**公式例：**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285581/8d8acfed-7f47-5a2d-589d-feb66a03fe8b.png)

https://aws.amazon.com/jp/blogs/news/new-aws-step-functions-build-distributed-applications-using-visual-workflows/

やばいですね
なんでもできちゃいそうです

TaskとしてLambdaを定義することで
このワークフローにじゃかじゃか組み込んでいけちゃうんですね

ちなみに今回作るのはこれです（かわいい）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285581/3a108dac-492c-77d2-7a09-f436b36a84b7.png)


# API Gatewayをつくる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285581/1259ed9d-2b03-d3ee-e229-5b37573c305c.png)

こんなかんじでつくります

start-executionが上述の`LambdaをキックするためのAPI`
describe-executionが`Lambdaからレスポンスを受け取るためのAPI`
（名前はなんでもいいです）

**両方POSTです**

# Serverless Framework

https://github.com/ChaseSan/async-api-sample


んじゃSLSで実際に作っていきます
CFnで作るより格段に楽なんですが、
プラグインとして `serverless-step-functions` と `serverless-pseudo-parameters` を入れる必要があります

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

関数はこんな感じでつくります
**ラーメンタイマーです**

```serverless.yml
provider:
  name: aws
  runtime: python3.8
  stage: ${opt:stage, 'dev'}
  region: ap-northeast-1
  memorySize: 128
  timeout: 900

custom:
  basePath: ramen

functions:
  async-api:
    handler: app.lambda_handler
    name: async-api-${self:provider.stage}
    environment:
      TIMER: 30
```

```app.py
import os
from time import sleep
import logging

logger = logging.getLogger()
logger.setLevel(logging.DEBUG)


def lambda_handler(event, context):
    logger.info(f"event: {event}")

    seconds = int(os.environ.get("TIMER"))
    sleep(seconds)

    return {"message": "らーめんが ゆであがりました はやく たべないと のびて しまいます"}
```

---

Step Functionsの定義はこんな感じに
`serverless-step-functions`プラグインを使う場合
API Gatewayの定義も`stateMachines`ブロック内に書きます

```serverless.yml
stepFunctions:
  stateMachines:
    state-machine:
      name: state-machine-${self:provider.stage}
      events:
        - http:
            path: ${self:custom.basePath}/start-execution
            method: post
            action: StartExecution
            iamRole:
              Fn::GetAtt: [AsyncApiRole, Arn]
            request:
              template:
                application/json: |-
                  {
                    "input": "$util.escapeJavaScript($input.json('$'))",
                    "stateMachineArn":"arn:aws:states:#{AWS::Region}:#{AWS::AccountId}:stateMachine:state-machine-${self:provider.stage}"
                  }
        - http:
            path: ${self:custom.basePath}/describe-execution
            method: post
            action: DescribeExecution
            iamRole:
              Fn::GetAtt: [AsyncApiRole, Arn]
            response:
              template:
                application/json: |-
                  {
                    "input": $util.parseJson($input.json('$.input')),
                    #if($input.path('$.output') != "")
                      "output": $util.parseJson($input.json('$.output')),
                    #end
                    "status": $input.json('$.status')
                  }
      definition:
        StartAt: async-api-task
        States:
          async-api-task:
            Type: Task
            Resource:
              Fn::GetAtt: [async-api, Arn]
            End: true
```

**ポインツ**

- `start-execution`には`events.http.action`に[StartExecution](https://docs.amazonaws.cn/en_us/step-functions/latest/apireference/API_StartExecution.html)を記載
  - `events.http.request.template`に実行するStateMachineのArnをJson形式で記載
- `describe-execution`には`events.http.action`に[DescribeExecution](https://docs.amazonaws.cn/en_us/step-functions/latest/apireference/API_DescribeExecution.html)を記載
  - `events.http.request.template`にレスポンスをJson形式で記載

```json
{
    "response": $input.json('$'),
}
```

とかにすれば[DescribeExecution](https://docs.amazonaws.cn/en_us/step-functions/latest/apireference/API_DescribeExecution.html)の中身が全部返るんだけど
必要なものだけ返してあげたほうが、インターフェース的にヤサシイでしょう（たぶん）

# 動かす

じゃあデプロイしてうごかしましょう
`sls deploy`っと
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285581/015870bd-3ee0-7835-f332-4780dd2ae64f.png)
コンソールに出たエンドポイントでお試しします

**start-execution**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285581/8755a4eb-d91b-7540-e401-324abd94358a.png)
これで取得した`executionArn`をつかって次のAPIを叩きます

**describe-execution**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285581/ea664a0d-88dc-2b29-b658-5dda63519b8e.png)

なんか返ってきましたね。
`status`が`RUNNING`です。
まだラーメンできていないようです。

少し間をおいて、もう一度叩いてみましょう
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285581/626d704b-e249-0a26-0cfb-ee9b808af315.png)

お、`status`が`SUCCEEDED`になり、ラーメンができあがったことをおしえてくれましたね

# 以上

いやーめちゃめちゃ楽ですね
これがなかったらDynamoとかSQSとかをフル活用して自前実装しないといけないとこでした（地獄）