24
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

みなさんはE2Eテストをご存じでしょうか?EndtoEndテストの略称であり、ブラウザ上でのシステム全体の動作を確認するためのテスト手法です。E2Eテストを自動化するためのツールとして、PlayWrightが注目されています。PlayWrightは、Webブラウザの操作を自動化するためのライブラリです。
E2Eテストに限った話ではありませんが、テストの保守という観点では下記が課題になりがちです。

  • E2Eテストの文化を馴染めせるために、PlayWrightの使い方をチームで習得する必要がある。
  • デプロイによって画面の挙動が変更された際にも、テストを持続的に更新していく必要がある。(テストコードを腐らせない)

「自然言語でブラウザを自動操作して証跡を残すワークフロー」が実現できれば、上記課題の解決の一助になるかと考えます。
ということで、本日は生成AIを活用して、自然言語でブラウザ操作してみます。

実現したいこと

  • 自然言語でPlayWrightを操作する
  • CICDパイプラインを構成する前提で、AWS Lambda等のサーバレス環境で実行できる環境を作成する。
    ◆あいうえお ◆あいうえい (1).png

対象読者

  • 小規模のプロダクトを扱っていてE2Eテストを未導入だが、今後導入を検討している方
  • 最近見聞きするMCP(ModelContextProtocol)を使ってみたい方
  • 生成AIとMCP運用をAWS環境にまとめることで、請求一本化したい方

構成図

Lambda上にすべてのリソースを立てます。Lambda内に立てたMCPサーバーからPlayWrightの使い方を習得したMCPクライアントはBedrockのモデルを用いてタスクを完遂します。今回はPlayWrightを動かすので、Lambda内にインストールしたブラウザを用いてタスクを実行します。
また今回は事後処理として、MCPクライアントによる処理が完了したあとに指定のフォルダ配下の画像ファイルをS3にアップロードする処理を加えています。
playWrigthMCP2.drawio.png

MCPについて

Model Context Protocolの略称であり、生成AIが各種APIやツールを理解できるようにするための規格です。
AIエージェントがツールを理解することで、AIエージェント自身で作業を実行できる点がMCPの魅力だと思ってます。
誰かにメールを送る作業を今までは人間がメールアプリを開いて操作していましたが、MCPサーバーとAIエージェントがあれば、自然言語の指示を入れるだけでAIエージェントが完遂してくれるようなイメージです。MCPサーバーがどんどん普及して、あらゆる定型作業が減ると嬉しいですね。

より丁寧に解説している記事が大変勉強になりますので、紹介させていただきます。
MCPの基礎とUbieにおける活用事例 /ubie-mc
やさしいMCP入門

Amazon StrandsAgentsについて

BedrockではもともとAIエージェント開発機能が具備されていましたが、さらに簡潔に作成できるようにしたSDKです。2025年4月に登場しており、まだまだ文献が少ないですが今後利用が拡大していくと予想しています。
MCPサーバーとの接続にも対応しており、Streamable HTTP トランスポート方式のMCPサーバーとの接続も可能です。今回はLambda内にローカルのMCPサーバーをホストしていますが、外部にMCPサーバーを立ててHTTPエンドポイント経由で接続したいケースのときには役に立ってくれると思います。
Strands Agents SDK

対応可能なMCPサーバー一覧

著名なサービスにおけるMCPサーバーの作り方をまとめたGithubリポジトリがあります。今回はmicrosoftが提供するplaywright/MCPを選択しましたが、ぜひ読者の皆様のご要望に合うMCPサーバーがないか探してみてください。
modelcontextprotocol/servers

コールバック処理について

"モデルの出力を整理中なのか","Toolの選定中なのか","ライフサイクルのイベントが変更になったのか"等のAIエージェントの現在の状況に応じて、実行したい関数を任意に設定できます。
例えば「モデルの出力は一部のみをロギングしたい」等の要望がある際に、カスタムコールバックハンドラを設定することで対応が可能になります。
Callbacks Handler

    ####################################################
    # おまけ カスタムコールバックハンドラーを定義する
    ####################################################
    # タスク終了時に実行したいコールバックハンドラーを定義
    class OriginalHandler:
        def __call__(self, **kwargs):
            try:
                # 1チャンクごとに data が来る
                if "data" in kwargs:
                    logger.info(kwargs["data"].rstrip("\n"))

                # 動作終了時に(最終チャンク)hogehogeを実行したい
                if kwargs.get("complete"):
                    logger.info("<<< COMPLETE >>>")
                    ###########  ここで任意の処理を実行する ###########
                    
            except Exception as e:
                # コールバック例外で処理が止まらないようにする
                logger.exception("Callback error: %s", e)
            
    # 標準で用意されているPrintingハンドラとカスタムハンドラ を合成
    # これをAgent呼び出しする際の引数として設定する。Agent(callback_handler=handler)
    handler = CompositeCallbackHandler(
        PrintingCallbackHandler(),
        OriginalHandler()
    )

今回作成するLambdaコンテナ

Lambdaで実行するため、書き込み可能なパスは/tmp配下のエフェメラルストレージのみになります。Lambda特有のファイルのReadWriteに関する参照先は環境変数で管理しています。

dockerfile


# Playwright MCPのインストール
FROM node:slim AS node

WORKDIR /playwright/app
RUN npm install @playwright/mcp@latest && \
    npx -y playwright install --with-deps chromium



RUN npm ci

# Python実行環境の構築
FROM python:3.13 AS base
WORKDIR /playwright

RUN apt-get update && apt-get install -y curl gnupg wget fonts-noto && \
    curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
    apt-get install -y nodejs && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# playwrightと依存ライブラリのコピー
COPY --from=node /root/.cache/ms-playwright/ /opt/.cache/ms-playwright/
COPY --from=node /usr/lib /usr/lib
COPY --from=node /lib /lib
COPY --from=node /playwright/app/node_modules /var/task/node_modules
COPY --from=node /playwright/app/package.json /var/task/package.json

FROM base AS prd
WORKDIR /var/task


RUN pip install --no-cache-dir strands-agents strands-agents-tools awslambdaric

COPY mcp.config.json  sample_strands_agent.py ./

# playwrightがnpxで実行される際に発生する書き込み処理のパスを指定する。
ENV npm_config_cache=/tmp/.npm
# .chache/ms-playwrightの参照先を指名
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/.cache/ms-playwright
ENV HOME=/opt



ENTRYPOINT [ "python", "-m", "awslambdaric" ]
CMD [ "sample_strands_agent.lambda_handler" ]

sample_strands_agent.py

from datetime import datetime, timedelta
from mcp import stdio_client, StdioServerParameters
from pathlib import Path
from strands import Agent
from strands.handlers.callback_handler import CompositeCallbackHandler, PrintingCallbackHandler
from strands.models import BedrockModel
from strands.tools.mcp import MCPClient

import boto3
import logging
import os
import shutil
import threading


# Enables Strands debug log level
logger = logging.getLogger("strands")
logger.setLevel(logging.INFO)

finished = threading.Event() 

os.environ["DEBUG"]="pw:browser*"
os.environ["HOME"]="/tmp"


def get_chromium_executable_path() -> str:
    """ 
    PlaywrightのChromium実行ファイルのパスを取得する関数
    chromium-{バージョン番号}に実行ファイルが保存されるため、今回のコンテナで駆動している番号を動的に取得する必要がある。
    :return: Chromium実行ファイルのパス
    """
    base_path_str = "/opt/.cache/ms-playwright/"
    chromium_dir = next(iter(Path(base_path_str).glob("chromium-*")))

    return chromium_dir.joinpath("chrome-linux", "chrome").as_posix()


def upload_screenshot_to_s3(screenshots_dir: str, output_s3_bucket_uri: str):
    """ S3にスクリーンショットをアップロードする関数
    :param screenshots_dir: スクリーンショットを保存するディレクトリ
    :param output_s3_bucket_uri: S3のバケットURI
    :return: None
    """
    # スクリーンショットが存在する場合、S3にアップロード
    logger.info("Checking for screenshots to upload. screenshots-dir: %s Output S3 bucket URI: %s", screenshots_dir, output_s3_bucket_uri)
    s3_client = boto3.client('s3')
    

    # スクリーンショットディレクトリが存在しない場合は何もしない
    if not os.path.exists(screenshots_dir):
        logger.info("No screenshots directory found at %s", screenshots_dir)
        return
    logger.info("scrrenshots_dir files: %s", os.listdir(screenshots_dir))
    # スクリーンショットディレクトリが存在する場合は、ファイルをS3にアップロード
    for file_name in os.listdir(screenshots_dir):
        if file_name.endswith(".png") or file_name.endswith(".jpg") or file_name.endswith(".jpeg"):
            file_path = os.path.join(screenshots_dir, file_name)
            s3_key = f"{file_name}"
            bucket_name, *folder_path = output_s3_bucket_uri.split("s3://")[1].split("/", 1)
            s3_client.upload_file(file_path, bucket_name, "".join(folder_path + [s3_key]))
            logger.info(f"Uploaded {file_path} to s3://{output_s3_bucket_uri.split('s3://')[1]}/{s3_key}")    
    return



def lambda_handler(event, context):
    
    ####################################################
    # 事前準備
    ####################################################

    # playwrightパスを取得する
    playwright_browser_path = get_chromium_executable_path()

    # 今回のサンプルで設定するテストイベントのキーの有無をチェック
    if event.get("access_url") is None or event.get("output_s3_bucket_uri") is None or event.get("text_task") is None:
        raise ValueError("Missing required parameters: access_url, output_s3_bucket_uri, text_task")
    access_url = event.get("access_url")
    output_s3_bucket_uri = event.get("output_s3_bucket_uri")
    text_task = event.get("text_task")
    bedrock_model_id = event.get("bedrock_model_id", "anthropic.claude-3-5-sonnet-20241022-v2:0")

    # 現在の日時を取得(JST: UTC+9)
    current_time = (datetime.utcnow() + timedelta(hours=9)).strftime("%Y-%m-%dT%H:%M:%S")



    # スクリーンショット用の現在のディレクトリ配下に作成する。
    screenshots_dir = "/tmp/screenshots"
    if not os.path.exists(screenshots_dir):
        os.makedirs(screenshots_dir)
        logger.info("Created screenshots directory: %s", screenshots_dir)
    # スクリーンショットディレクトリが存在する場合
    else:
        # 既存のスクリーンショットディレクトリをリセット
        
        shutil.rmtree(screenshots_dir)
        os.makedirs(screenshots_dir)
        logger.info("Reset and created new screenshots directory: %s", screenshots_dir)



    ####################################################
    # MCPクライアントの設定とエージェントの実行
    ####################################################

    # 利用する市中のMCPサーバーのクライアントを作成
    playWright_client = MCPClient(
        lambda: stdio_client(StdioServerParameters(command="npx", args=["-y", "@playwright/mcp@0.0.29", "--config", "/var/task/mcp.config.json" ,"--executable-path" ,f"{playwright_browser_path}", "--output-dir", f"{screenshots_dir}"], environ=os.environ))
        
    )

    # Bedrockモデルの定義
    bedrock_model = BedrockModel(
        model_id=bedrock_model_id,
        temperature=0.3,
        streaming=True,
    )

    


    text_prompt = f"""
    以下のアプリに対して、taskの内容を実行してください。 
    ## target
    {access_url}

    ## rule
    - If you are using PlayWright, ensure that headless: true is always set.
    -作業をする際には 下記順番で必ず実行してください。
        - browser_navigate → browser_capture_snapshot → browser_resize → browser_take_screenshot
        - スクリーンショットを撮影する場合のキー名のprefixとして{current_time}を使用してください。
        - スクリーンショットを撮影する場合はbrowser_take_screenshotを使用し、その引数filenameに上記キー名を指定すること。


    ## task
    {text_task}

    ## Output 
    エラーが発生した際にはそのエラーログの原文をそのまま出力してください。読み上げて下さい。
    - 各ケースの結果をOK/NGで報告してください
    - テスト結果がNGの場合、失敗した場合は簡潔な理由を添えてください
    - すべてのテスト結果をテーブル表示で、サマリを表示してください
    
    """
    


    

    ######################################################
    # Playwright MCPクライアントを使用してエージェントを実行
    ######################################################
    with playWright_client:
        # Agentを作成し、ツール(作業タスク)を登録
        agent = Agent(tools=playWright_client.list_tools_sync(), model=bedrock_model)
        response = agent(text_prompt, stream=True)
        finished.wait(timeout=20)
        logger.info("Agent response: %s", response)

        upload_screenshot_to_s3(
            screenshots_dir=screenshots_dir,
            output_s3_bucket_uri=output_s3_bucket_uri,
        )
        logger.info("self printing. Screenshots uploaded to S3 bucket: %s", output_s3_bucket_uri)
        
    logger.info("Lambda handler completed successfully.")
    return 

mcp.config.json

playwright/mcpを動かす際のオプション設定情報のファイルです。特に今回はコンテナ駆動ということもあり、ブラウザはヘッドレスモードでの実行が必須です。そういった設定をconfigファイル内に記載します。

{
    "browser": {
        "browserName": "chromium",
        "launchOptions": {
            "headless": true,
            "args": [
                "--disable-features=UseDBus",
                "--no-sandbox",
                "--disable-gpu",
                "--disable-dev-shm-usage",
                "--no-zygote"
            ]
        }
    }
}

Lambdaのテストイベント

{
  "access_url": "https://www.ntt-east.co.jp/",
  "output_s3_bucket_uri": "s3://{YOUR BUCKET NAME}/{YOUR FOLDER PATH}/",
  "text_task": "* iPhone14promaxでサイトのにアクセスして、レスポンシブに表示されるか確認し、スクリーンショットを保存してください。",
  "bedrock_model_id": "anthropic.claude-3-5-sonnet-20240620-v1:0"
}

そのほかLambdaを作る際に気を付けること

ブラウザ操作をするため、特にメモリが重要です。今回は下記設定で実行しましたが、タスク内容に応じてより多くのメモリをアタッチすることも検討してください。

  • メモリ: 2048MB
  • エフェメラルストレージ: 1024MB
  • タイムアウト: 2分00秒

動かした結果

入力プロンプトにいれた# ruleの内容を意識しながら、イベントから読み込んだtext_taskの内容を実行してくれていることがわかります。またiPhone14Proサイズで表示したスクリーンショットがS3の指定のバケットに保存されていることも確認できました。

テスト項目	結果	備考
iPhone 14 Pro Maxでの表示	OK	430 × 932 px で正常表示
レスポンシブデザインの適用	OK	モバイル向けレイアウトに適切に変更
スクリーンショットの保存	OK	指定名で保存完了

NTT東日本のウェブサイトは iPhone 14 Pro Max サイズでも問題なく表示され、
レスポンシブデザインが適切に機能していることを確認しました。
コンテンツ構成もモバイル向けに最適化され、ユーザーエクスペリエンスを損なう要因は見られませんでした。
スクリーンショットも正常に保存されています。

特に難しかったところ、発生エラー

ブラウザのパスが見つからない

npx playwright installを実行するとブラウザをインストールしてくれますが、デフォルトのインストールパスは/root配下($HOME配下)になっています。Lambdaでは/rootにアクセスできないので、ブラウザを見つけられずにエラーで終了してしまう点に注意が必要です。別のディレクトリにコピーもしくは移動して,その絶対パスを環境変数HOMEに設定しておきましょう。

エラー内容

Failed to ensure browser: browserType.launch: Executable doesn't exist at /root/.cache/ms-playwright/chromium_headless_shell-1169/chrome-linux/headless_shell

(余談ですが、コンテナでブラウザを動かす点にかなり苦労しました。ローカルで立てたコンテナでは正常に動くけど、Lambdaにホストするとブラウザが動かないエラーの改善に時間がかかりました。。。)

Lambda関数が最後まで完了せずに終了してしまう。

今回のLambdaではAIエージェントが作業したあとに、事後処理としてS3ファイルアップロードを行っています。その事後処理を行う前に、Lambdaが終了してしまうケースに遭遇しました。
MCPサーバーとのセッションが終了してしまうとLambdaが終了してしまうので、threadにスリープ処理を加えることで無理やり延命して、事後処理を完遂させています。

with playWright_client:
        # Agentを作成し、ツール(作業タスク)を登録
        agent = Agent(tools=playWright_client.list_tools_sync(), model=bedrock_model)
        response = agent(text_prompt, stream=True)
        finished.wait(timeout=20)
        logger.info("Agent response: %s", response)

        upload_screenshot_to_s3(
            screenshots_dir=screenshots_dir,
            output_s3_bucket_uri=output_s3_bucket_uri,
        )

さいごに

今回のデモでは「ブラウザ上でスクリーンショットを撮影する」ケースを紹介しているため、「E2Eテストではなくない?」と思った方もいらっしゃると思います。実務でE2Eテストを利用するとなると"認証認可の突破"も避けて通れないと思いますので、自然言語を使ったE2Eテストにおいてのノウハウも今後学習してみようと思います。
せっかくサーバレスコンテナ実装にしたので、PullRequest作成時にE2Eテストが自動実行されるようなCICD構成につなげてみたいですね。
Claude DesktopがなくてもMCPサーバー活用できる糸口が見えたので、個人的にはどんどん活用したいですね。

その他参考資料

自然言語でブラウザE2Eテストを回す

24
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?