🎯

CloudWatch Synthetics Canaryの検証を通してユーザビリティを学んだ

2022/08/11に公開

Amazon CloudWatchに統合されているSyntethic Canary、E2Eテストに相当するアプリケーション・モニタリングですが職場のシステム環境でも使い始めたら思った以上に有用だったので記事にまとめようと思い立ちました。

ユーザビリティを理解してからE2Eテストを開発するのも必要ですが、逆の順序で始めるのも悪くないという話です。

*システムに関わる情報は適宜ボカしてます
*今回のCanaryスクリプトはPythonランタイムです

導入編:ランタイム環境

この時点で簡単さがないと有用さも感じられないので重要ですが、AWS Lambdaを使ったことがある人であれば違和感なく使い始めることができそうです。ざっと必要なものは次の通り。

  • Canaryスクリプトのコード
  • コード内容の実行に必要なIAMロール権限
  • コードで使用する環境変数
  • Canary実行間隔
  • Canary実行結果の保存先(S3バケット)

コーディング編:Canaryスクリプト

使い始める前は個人的にも「WebDriverを含んでいるだけだし、AWS提供のsyntheticライブラリを使うこともないか」という粗い認識でしたが、実際に動かしてみて良さを知りました。

自力でLambda関数をデプロイしてE2Eテスト環境を構築することに比べて、Synthetic Canaryの長所は以下の部分に感じました。

  • 1つのCanaryに複数のステップ(テストシナリオ)を含めることも容易でexecuteStep()を使って任意の関数を呼び出せばOK
  • 既存のWebDriverスクリプトを流用しつつ、Synthetic Canaryを実行できるので既存コードのナレッジも活かせる
    • PythonランタイムであればSelenium前提になります
  • ロガーもSyntheticから提供されており、例外処理でERRORレベルを使うと任意のステップでFailedメトリクスを記録できる
    • 逆に例外処理でWarningやINFOを使えば対象ステップのエラー終了を回避できます
  • Syntheticで既存コードをラップするだけでも「メトリクスデータのCloudWatch保存」「ステップ別画面キャプチャのS3保存」が自動化されるため、ユーザはテストシナリオのコーディングに集中できる

参考Blog: Create canaries in Python and Selenium using Amazon CloudWatch Synthetics

サンプルコード(ロジック省略)

import os

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

from aws_synthetics.selenium import synthetics_webdriver as webdriver
from aws_synthetics.common import synthetics_logger as logger # To access the logging capabability
from aws_synthetics.common import synthetics_configuration # Configuration parameters

Env = os.getenv('Env')
FQDN = os.getenv('FQDN')
LoginID = os.getenv('LoginID')
LoginPASS = os.getenv('LoginPASS')
wait_key = '.....'
if Env == 'prod':
    url = f"https://{FQDN}"
else:
    url = f"https://staging.{FQDN}"

async def main():
    browser = webdriver.Chrome()
    wait = WebDriverWait(browser, 5)
    
    synthetics_configuration.set_config(
        {
            "screenshot_on_step_start": False,
            "screenshot_on_step_success": True,
            "screenshot_on_step_failure": True
        }
    )

    def get_TopPage():
        browser.get(url)
        wait.until(EC.presence_of_element_located((By.ID, wait_key)))
	# 中略
    await webdriver.execute_step(f"TopPage", get_TopPage)

    def member_login():
        browser.get(f"{url}/login")
        wait.until(EC.presence_of_element_located((By.ID, wait_key)))
	# 中略
    await webdriver.execute_step('MemberLogin', member_login)

    def transit_Store():
        browser.get(f"{url}/store")
        wait.until(EC.presence_of_element_located((By.ID, wait_key)))
	# 中略
    await webdriver.execute_step('NetStore', transit_Store)

    logger.info("Selenium Python canary Ending.")


async def handler(event, context):
    logger.info("Selenium Python canary Starting.")
    return await main()

検証編:テストシナリオ

ある程度複雑なユースケースであればその分、AWS仕様に倣うことで開発者は楽できそうな実感はあります。

  • (前述のexecuteStep()によって)ステップごとにHTTP応答コード・応答時間・画面キャプチャを記録するため、実際の挙動を確認しつつ調整できた
    • 実際のところ、トラブルシュートでは画面キャプチャ頼りになることも多かったです
  • 2022/08時点、CloudFormationの動的参照にSynthetic Canaryは非対応なためIaC管理にはやや難点あり...
    • SSMパラメータをCanary環境変数にセットできないため、グローバルスコープなどのステップ外で初期化してGetParameterする方法で対処しました
    • (ステップ内でパラメータ取得するとその分、Durationに含まれてしまうため)
  • 20分間隔では正常終了するステップが起動間隔を短くすると必ず失敗する...
    • CanaryのWebDriverでもLambda関数のキャッシュデータを参照するらしく、それが原因で前回実行時のセッション情報を使ってしまう事象でした
    • 試しにWebDriverを手動closeするコードに変更してもキャッシュデータを使うため、キャッシュ前提のコード実装に変更して対応しました
    • AWS Lambda 実行環境 - シャットダウンフェーズ

    /tmp ディレクトリに 512 MB と 10,240 MB を 1 MB 刻みで提供します。ディレクトリのコンテンツは、実行環境が停止された際に維持され、複数の呼び出しに使用できる一時的なキャッシュを提供します。

  • Canaryメトリクスの応答時間DurationはPCブラウザ基準から考えるとかなり遅い
    • ランタイム環境はAWS Lambdaであり計算資源はPCの方が何倍もあることが理由のはず
    • なので、メトリクス監視ではAnomary Detection前提にするか、Canaryメトリクスの時系列データをしばらく取得してから閾値設定(相対的に評価)する方針が良い

Canary Tips

  • CanaryメトリクスのFailedが記録される条件はAWSドキュメントに明記されていませんが、自分の検証とAWSサポート問合せの結果も合わせるとおおよそ次の2パターン
    • Canaryスクリプト内から例外が送出される
    • ロガーがERRORレベルを出力する
  • Canaryメトリクスの4xx/5xxが記録される条件はWebDriver仕様に則る

学習編:ユーザ体験について

例えばECサイトであれば、Webサイトにおける(期待されている)ユーザ動線は想像しやすいです。単純なシナリオとして、次のあたりはすぐ思いつくあたりです。

  • Topページをみる
  • ログインする
  • 商品カテゴリから商品を検索する
  • 商品詳細ページをみる
  • カートに商品を追加する

仕事でSynthetic Canaryを使って得られた知見はどれも内部仕様に関わるため公開しづらいですが、、抽象的には次のような部分に発見がありました。

  • UI表示からデータの状態をどのくらい判断できるか?
    • ある属性の新旧や多少を知りたい場合、どこのページからアクセス可能か
    • 組み合わせによっては「そんなページは無い」という結論になることもあり得る
  • 一連の画面遷移において、UI表示と画面が実際に取得するデータ(DBの状態)に一貫性はあるか?
    • 画面から実行されるAPIや保持データの種類・状態は多岐にわたるため、UIから直感的に判別できないこともある...
  • 開発者視点でもユーザ提供しているUIベースでデータモデルされている状態は理想的に思える
    • その場合、とある画面ではなく「一連の画面遷移」に基づいてモデリングすることが良さそう
    • 適当な言葉を探してみたらOOUIという考え方に近い気がする(正直まだ把握していないです)

参考:
オブジェクトモデリング入門編!SmartHRでOOUIワークショップを開催しました
OOUI – オブジェクトベースのUIモデリング
OOUIは強力だが、合理的なDB構造を導くわけではない

システムを外部(画面)から操作する場合、永続層(データベース)を意識することはないですが誤解されない(orミスマッチしない)ようにUIとデータの状態を維持する必要があるなーと、改めてオブジェクト指向のありがたみを感じました。

Discussion