サーバーレスで作ったWeb APIのシナリオテストや定常監視もサーバーレスで。

# Summary

[Serverless(2)アドベントカレンダー](http://qiita.com/advent-calendar/2016/serverless2)の18日目です。

- 複雑なWeb APIのシナリオテストを簡単に構築する
- これを応用し、Web APIの定常監視システムを簡単に構築する

といったをことをサーバーレスで実現できないかを考察します。

この記事は、主にAWSを使ったサーバーレスアーキテクチャについて取り扱います。
GCPやAzureに関する内容は含まれません。(が、同じようなことはそれぞれ実現可能かと思います。)

# 概要
AWSのAPI GatewayとLambdaを使ってREST Web APIの開発は非常に容易になりました。(言わずもがなだけど。)

Web APIを簡単に開発できたとしても、テストや運用監視などの部分の開発も効率よく行いたいものです。
テストや監視の方法は色々あると思いますが、単機能のAPIテストを自由に組み合わせてシナリオテスト的なものを作り、定期的に実行できれば便利かなと考え、実現可能性を検討します。

---

# 検討と実装

## テスト対象のWeb API (Sample)
例えば以下のようなRest Web APIを開発したとします。ありがちな写真サービスを想定しています。

| Resource PATH      | Method | Description                              |
|:-------------------|:------:|:-----------------------------------------|
| /auth/token        | POST   | サインイン。Resources Access Tokenを取得する   |
| /auth/token        | DELETE | サインアウト。Resources Access Tokenを破棄する |
| /photos            | POST   | 写真を投稿する                           |
| /photos            | GET    | 投稿済みの写真一覧を取得する             |
| /photos/{photo_id} | GET    | 写真のメタデータを取得する               |
| /photos/{photo_id} | DELETE | 写真を削除する                           |
| /albums            | POST   | アルバムを作成する                       |
| /albums            | GET    | アルバム一覧を取得する                   |
| /albums/{album_id} | GET    | アルバム情報を取得する                   |
| /albums/{album_id} | DELETE | アルバムを削除する                       |

※ APIの内容はテキトーなのでツッコミは無しで...

## Web APIの単機能のテスト

Web APIの単機能としては、例えば `POST /auth/token` であれば、「E-Mail AddressとPasswordを渡せばTokenを取得できる」 が期待動作になります。これを試しにLambda Functionで正常系のテストを書いてみると、こんな感じでしょうか。

Runtimeはnodejs4.3、Http Clientとしてsuperagentを使っています。

```Javascript
'use strict'

const request = require('superagent');
const apiBaseUrl = 'https://${api-gateway-host-name}/${stage_name}';

exports.handler = (event, context, callback) => {
    const url =  apiBaseUrl + '/auth/token';

    const reqBody = {
        email: 'foo@bar.com',
        password: 'hoge1234'
    };

    request.post(url)
        .send(reqBody)
        .set('Content-Type', 'application/json')
        .end((err, res) => {
            if (err || !res.ok) {
                let error = new Error(JSON.stringify({
                    description: 'POST /auth/token Failed',
                    status: res.status,
                    body: res.body
                }));
                callback(error);
            } else {
                callback(null, res.body);
            }
        });
};
```

APIのリクエストに成功したらcallbackでResponse Bodyを渡し、失敗したらエラーオブジェクトを作成しています。こういった感じで単一のAPIの機能テストは簡単に書けます。

## テストシナリオを作ってみる

前節のような単体のAPI機能テストをつなげて、シナリオテストを実現できないかを考えます。
例えば、 `POST /auth/token` でAccess Tokenを取得し、 `/POST /albums` でアルバムリソースを作成し、作成したアルバムに `POST /photos` で写真を投稿、最後に `DELETE /auth/token` でサインアウト、といった具合です。

せっかくなので、先日のre:Invent 2016で発表された[**Step Functions**](https://aws.amazon.com/jp/step-functions/)を使ってみます。
(Step Functionsの詳しい解説はこの記事では省略します。)

事前にそれぞれのAPIについて、Lambda Functionで単一APIのテスト実装は完了しているものとします。

<img width="664" alt="TestFunctions.png" src="https://qiita-image-store.s3.amazonaws.com/0/39743/3bb1fc67-a4ff-3a62-c4d5-037637bc7e24.png">


前述の例 (`POST /auth/token` -> `POST /albums` -> `/POST /photos` -> `DELETE /auth/token`) に対し、[Amazon States Language](https://states-language.net/spec.html) を使ってステートマシンを組むと、次のようなJSONになります。直列のタスク実行なのでシンプルですね。

```json
{
  "Comment": "Scenario Test for normal case.",
  "StartAt": "testPostAuthToken",
  "States": {
    "testPostAuthToken": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:testPostAuthToken",
      "Next": "testPostAlbums"
    },
    "testPostAlbums": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:testPostAlbums",
      "Next": "testPostPhotos"
    },
    "testPostPhotos": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:testPostPhotos",
      "Next": "testDeleteAuthToken"
    },
    "testDeleteAuthToken": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:testDeleteAuthToken",
      "End": true
    }
  }
}
```

AWS Consoleから確認すると、以下のようなステートマシンになります。

<img width="247" alt="StateMachine.png" src="https://qiita-image-store.s3.amazonaws.com/0/39743/5fbc0a5c-9181-12c1-320b-88a0fbe7c83d.png">

## テスト実行

Consoleから実行してみるとテストが順次実行され、テストが成功するとGreenになります。

![testExec.png](https://qiita-image-store.s3.amazonaws.com/0/39743/b1ce6950-0b03-98fe-dcca-a788d41db852.png)

もし途中でAPI呼び出しに失敗した場合、下図のように失敗が表現されるのでどこで失敗したかを確認することができます。

![testFail.png](https://qiita-image-store.s3.amazonaws.com/0/39743/32111961-6d6f-f8a0-5e3a-bfa7b9d129fb.png)

## ステートマシンの改善の余地

これまでにご紹介した例は問題を抱えていて改善の余地があります。
上記の例では単純にAPIを連続して実行しただけになっています。実際はタスク間でデータの受け渡しをすることで、タスク間に依存関係を持たせる必要があります。例えば最初のTask実行時に取得したTokenを次のTaskに渡してTokenを使いたい、など。

ステートマシンのJSON仕様を見ていると、タスク間のデータ渡しもできそうではあるので、別の機会にやってみようと思います。

## テストケースの追加

Step Functionsのステートマシンは、分岐や失敗時の挙動もちゃんと書けるので、多種多様なWeb APIの呼び出しシナリオがStep Functionsで書けます (時間の都合上、この記事では扱いません)。
こんな感じでシナリオテストの構築が実現できます。

---

# システム監視に使う

作ったテストシナリオを定期的に実行することで、システム監視用のダミークライアントとしても使えそうです。
定期実行のためにCloudWatch Eventsを使おうかと考えましたが、CloudWatch EventsからStep Functionsの呼び出しは2016/12/18時点ではサポートされていないようです。

--- 

2017/03/24追記
CloudWatch EnventsからのStepFunctionsの呼び出しがサポートされました！
https://aws.amazon.com/jp/about-aws/whats-new/2017/03/cloudwatch-events-now-supports-aws-step-functions-as-a-target/

---

代替手段として、CloudWatch EventsからLambda Functionを呼び出し、Lambda Function内でAWS SDKを使ってStep Functionsを呼び出すことにします。

対象のLambda FunctionのRoleに、Step Functionsの実行許可を割り当てる必要があることに注意してください。

コードは以下のように、ただ単にState MachineのARNを指定して`startExecution`するだけでOKです。
この例では、aws-sdk-js 2.17.15 を使いました。

```Javascript
'use strict'

const AWS = require('aws-sdk');
const stepfunctions = new AWS.StepFunctions();

exports.handler = (event, context, callback) => {
    const params = {
        stateMachineArn: 'arn:aws:states:{region}:{account_id}:stateMachine:{state_machine_name}'
    };

    stepfunctions.startExecution(params, (err, data) => {
        if (err) callback(err, err.stack); // an error occurred
        else callback(null, data); // successful
    });
};
```

あとはCloudWatch Eventsで、例えば1時間毎に上記のLambda Functionを実行するように設定しておけば、1時間毎にAPIの動作を確認することができます。
定期実行の間隔に関しては、稼働しているシステムのサービスレベルに応じて決めれば良いですね。

監視として使うのであれば、Step Functionsのステートマシンの処理の最後に、処理結果をSlackに飛ばしたり、もしエラーになった場合はPagerDutyやOpsGenieなどを使って警報を飛ばす、といった実装をする必要があります。

また、監視として使う場合は、万が一のAWSの障害に備え、Produnction環境が稼働しているRegionとは別のRegionで動作させておいた方が良いでしょう。

# 所感

"Serverlessで構築したシステムってどのように監視すればいいの？" という課題を持っている人は多いと思いますが、こういったアプローチもアリかなぁと思いました。

そしてAWS Step Functionsはとても便利ですね。
AWS Console上でステートマシンの編集ができない (と言っても、普通はステートマシンはコードとして管理し、AWS SDKかCLI経由で使うことがほとんどだと思うので問題ない) とか、他のAWSサービスとの連携がまだ手薄だったりしますが、Benefitの方が大きいですね。

ところでAdvent Calendarって、中途半端な記事でも半強制的に投稿しないといけないので辛いですね...。

# References

- [Step Functions API Reference](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/StepFunctions.html)
- [Amazon States Language](https://states-language.net/spec.html)