エンジニアの鎗水です。
ABEJA Platformの機能は様々なAPIによって支えられています。 今回はそれらのAPIに対し行っている外形監視について紹介します。
今回紹介する外形監視は、ユーザーの利用シナリオに沿って行われます。
ABEJA Platform上の特定のresourceの作成、更新、削除といった一連の作業をユーザーが正しく行えるかという視点でテストを行います。
1. 構成
AWS Step Functions
AWS Step Functionsは、複数のLambdaを組み合わせてワークフローを組むことができるサービスです。 ワークフローはAmazon States LanguageというDSLを使って記述し、実行するLambdaの定義やLambdaのリトライ、Lambda間の遷移条件や待ち時間などを設定することができます。
Serverless
Serverless Frameworkを使ってLambdaの管理・プロイを行っています。 StepFunctionsを扱うためserverless-step-functionsというプラグインを使っています。 各Step Functionsが定期的にされるようスケジュール設定しデプロイを行います。
Sentry
Sentryはエラートラッキングツールです。エラーが起きた際にイベントログを送ってくれます。 StepFunctionsで行うテスト内で異常が発見された場合、エラーとしてSentryに送られます。 また、SentryをSlackと連携することでエラー発生と同時に開発メンバーにも通知されるようになっています。
2. 実際のテストケース
今回は https://blogs.abeja.io/resources
という仮のエンドポイントを例に、テストの作成とエラー発生時の通知までの流れを紹介します。
resoureの作成から削除までの一連のシナリオをテストとして実行していきます。
2.1 テストコードの実装
Lambdaで実行するテストコードをpythonで実装します。
- Resourceの作成(
POST /resources
) - Resourceの取得(
GET /resources/<id>
) - Resourceの削除(
DELETE /resources/<id>
) etc...
といったテスト対象となるAPI毎にLambdaのハンドラーを実装していきます。
# post_test.py @handle_test_context @handle_test_error_report('post-resource') def handler(test_context, *args): url = 'https://blogs.abeja.io/resources'; payload = { 'name': 'example' } resp = requests.post(url, json=payload) # レスポンスが200系以外だった場合、例外を投げます resp.raise_for_status() resource = resp.json() # レスポンスにidが含まれない場合、例外を投げます nose.tools.assert_in('id', resource) # ここで定義したdictを後続のLambdaに共有します test_context_output = { 'resource_id': resource['id'] } return resource, test_context_output
# get_test.py @handle_test_context @handle_test_error_report('get-resource') def handler(test_context, *args): # post-resourceで作成されたresourceのidを取り出します resource_id = test_context.get('resource_id') url = f'https://blogs.abeja.io/resources/{resource_id}'; resp = requests.get(url) # レスポンスが200系以外だった場合、例外を投げます resp.raise_for_status() resource = resp.json() # レスポンスにidが含まれない場合、例外を投げます nose.tools.assert_in('id', resource) return resource
テストに失敗した場合は、handlerから例外を投げます。
この例ではAPIのレスポンスが200系以外だった場合、レスポンスにid
が含まれていない場合に例外が投げられます。
例外が投げられた場合、handle_test_error_report
デコレータによってSentryに通知が飛ぶような実装になっており、テスト名(この場合post-resource
やget-resource
)のタグ付きでイベントが登録されるようになっています。
また、StepFunctionsでは1つ後のLambdaにしか出力が渡らないため、 同じワークフロー内の複数のLambdaで出力が共有できるよう handle_test_context
デコレータで工夫しています。
テストコードから返されたタプル(出力と引き継ぎたい出力)をhandle_test_context
デコレータで処理し、引き継ぐ出力については後続のテストコードの引数test_context
に含めるようにしています。
今回の例では、post_test.py
で作成されたresourceのidをget_test.py
と共有する実装になっています。
2.2 serverless.yamlの作成
- Lambdaの定義
- StepFunctionsの定義
を serverless.yml
に記述していきます。
# serverless.yaml service: blog-system-test plugins: - serverless-python-requirements - serverless-step-functions - serverless-pseudo-parameters provider: name: aws runtime: python3.6 environment: SENTRY_DSN: ${env:SENTRY_DSN} functions: test_post_resource: name: system-test-post-resource handler: functions/resources/post_test.handler test_get_resources: name: system-test-get-resources handler: functions/resources/get_list_test.handler test_get_resource: name: system-test-get-resource handler: functions/resources/get_test.handler test_put_resource: name: system-test-put-resource handler: functions/resources/put_test.handler test_delete_resource: name: system-test-delete-resource handler: functions/resources/delete_test.handler stepFunctions: stateMachines: platformResourceTest: name: PlatformResourceTest events: - schedule: rate: cron(0 * * * ? *) name: scheduled-test definition: Comment: Platform Resource Test StartAt: PostResource States: PostResource: Type: Task Resource: arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:system-test-post-resource Next: GetResources GetResources: Type: Task Resource: arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:system-test-get-resources Next: GetResource GetResource: Type: Task Resource: arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:system-test-get-resource Next: PutResource PutResource: Type: Task Resource: arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:system-test-put-resource Next: DeleteResource DeleteResource: Type: Task Resource: arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:system-test-delete-resource End: true
serverless
でデプロイするため、上記のようなYAMLを記述します。
functions
下でLambdaのデプロイ定義を行っています。この例では5つのLambdaが作成されます。
次に、Step Functionsのワークフローを定義していきます。実行したいシナリオになるように順に上部で定義したLambdaからワークフローを組んでいきます。また、定期実行するためスケジュールを設定しCloudWatch EventsからStep Functionsが起動されるようにしています。
(Step Functioinsのワークフローを作成する際にLambdaをARNで指定しなければいけないところに少し辛みがあります。serverless-pseudo-parametersプラグインを使うことで多少辛みが和らぎます。)
(Stepfunctionsではparallelの中にparallelを記述した場合などドキュメントには書いてありませんが、うまく動作しないこともあります。そういう場合は素直に複数のStepFunctionsに分割してあげた方が良さそうです)
ちなみに、ABEJA Platformが提供するAPIの中には、リクエスト時に非同期にジョブの実行を開始するものもあります。例えば、リクエストされたユーザーの学習コードを実行し、完了後に学習結果の確認を行う場合などです。このような場合、StepFunctionsのWaitを使い、一定の待ち時間を挟んでいます。また、実行にかかる時間も常に均一とは限らないため一定数Retryするように設定しています。このように非同期な処理がある場合はStep Functions側に任せることで、テストコード側で待ち時間やリトライ回数を調整する必要がなく、Lambdaの実行時間も短くすることができます。
2.3. StepFunctionsの実行
作成したステートマシンをStepFunctionとして実行します。
テストが成功した場合、すべてのステートが緑になっていることが分かります。
また、テストに失敗した場合は、対象のステートが赤く表示されています。この場合、 PUT /resources/<id>
の実行に失敗しているためこのAPIに何かしらの問題があることが分かります。エラーメッセージを見れば何のテストに失敗しているか分かりますが、コンソールから視覚的に確認できるのは地味に嬉しいです。
ちなみに、Step FunctionsではParallelを使うことで一連のLambdaを並列に実行することができます。ABEJA PlatformではAPIに対し異なるパラメータで実行したい場合、並列化しテストを実行しています。
2.4 Sentryへの通知
テストコード内で例外が投げられた場合は、以下のようにSentryに通知されます。Sentryでエラーの詳細について確認します。
2.5 Slackへの通知
SentryとSlackを連携し、アラート用のチャンネルへ通知を行います。 アラート用チャンネルの通知を確認し開発メンバーが対応するような運用になっています。
まとめ
serverlessにすることで、テストの実行環境を構築する必要がなく楽にテストができるようになっています。また、Step Functionsと組み合わせてユーザーの利用シナリオに沿ったシナリオでテストを実行しるようにしています。
ABEJA Platformでは本番環境へリリースの前にステージング環境で今回紹介したテストが通ることを確認するという運用を行っています。 このテストを通すことで、ユーザーが問題なく利用シナリオを実行できることを保証することができます。また、サービスに問題が起きた場合も早期に発見し対応を行うことができるようになっています。
この仕組みを入れたことで、チーム内からも「安心してリリースできるようになった」「問題を早く発見できるようになった」という良い評価をもらっていおりおすすめです。 この記事が安心・安全な開発のヒントになれば幸いです。
宣伝
ABEJAでは一緒にPlatformを開発するメンバーを募集しています!
ご興味あるかたはぜひこちらをご確認ください!!