ABEJA Tech Blog

中の人の興味のある情報を発信していきます

serverlessで作る外形監視

エンジニアの鎗水です。

ABEJA Platformの機能は様々なAPIによって支えられています。 今回はそれらのAPIに対し行っている外形監視について紹介します。

今回紹介する外形監視は、ユーザーの利用シナリオに沿って行われます。

ABEJA Platform上の特定のresourceの作成、更新、削除といった一連の作業をユーザーが正しく行えるかという視点でテストを行います。

1. 構成

f:id:i03yari2:20180312000058p:plain

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-resourceget-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として実行します。

f:id:i03yari2:20180312001421p:plainf:id:i03yari2:20180312001426p:plain
成功例:失敗例

テストが成功した場合、すべてのステートが緑になっていることが分かります。 また、テストに失敗した場合は、対象のステートが赤く表示されています。この場合、 PUT /resources/<id>の実行に失敗しているためこのAPIに何かしらの問題があることが分かります。エラーメッセージを見れば何のテストに失敗しているか分かりますが、コンソールから視覚的に確認できるのは地味に嬉しいです。

f:id:i03yari2:20180312003322p:plain:w300

ちなみに、Step FunctionsではParallelを使うことで一連のLambdaを並列に実行することができます。ABEJA PlatformではAPIに対し異なるパラメータで実行したい場合、並列化しテストを実行しています。

2.4 Sentryへの通知

テストコード内で例外が投げられた場合は、以下のようにSentryに通知されます。Sentryでエラーの詳細について確認します。

f:id:i03yari2:20180312003941p:plain

2.5 Slackへの通知

SentryとSlackを連携し、アラート用のチャンネルへ通知を行います。 アラート用チャンネルの通知を確認し開発メンバーが対応するような運用になっています。

f:id:i03yari2:20180312005333p:plain

まとめ

 serverlessにすることで、テストの実行環境を構築する必要がなく楽にテストができるようになっています。また、Step Functionsと組み合わせてユーザーの利用シナリオに沿ったシナリオでテストを実行しるようにしています。

 ABEJA Platformでは本番環境へリリースの前にステージング環境で今回紹介したテストが通ることを確認するという運用を行っています。 このテストを通すことで、ユーザーが問題なく利用シナリオを実行できることを保証することができます。また、サービスに問題が起きた場合も早期に発見し対応を行うことができるようになっています。

 この仕組みを入れたことで、チーム内からも「安心してリリースできるようになった」「問題を早く発見できるようになった」という良い評価をもらっていおりおすすめです。  この記事が安心・安全な開発のヒントになれば幸いです。

宣伝

ABEJAでは一緒にPlatformを開発するメンバーを募集しています!

ご興味あるかたはぜひこちらをご確認ください!!

www.wantedly.com