2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

どうもこんにちは。ちょっと久々の技術記事なのですが...

今回は、Amazon Bedrock AgentCoreを使って、AIにブラウザ操作をさせてみる検証をしました。(というかまだやってなかったんかい...)

この記事は誰向け?

この記事は以下のような方向けに執筆しています。

  • Amazon Bedrock AgentCore を使ってみたい方
  • Amazon Bedrock AgentCore を使って、AIにブラウザを操作させたい方
  • AWS CDK(Python)でAmazon Bedrock AgentCore Runtimeへエージェントをデプロイしたい方

デモコードの場所

実際に動かしたい方は、以下を参照ください。

実装の流れ

0. 作業プロジェクトの用意

mkdir bedrock-agentcore-browser-demo && cd $_

1. CDKプロジェクトを初期化する

1-1 初期化コマンド

空の作業ディレクトリへ移動し、Python CDKプロジェクトを生成します。

mkdir bedrock-agentcore-browser-demo
cd bedrock-agentcore-browser-demo

cdk init app --language python

主に次のファイルが自動生成されます。

.
├── app.py
├── cdk.json
├── requirements.txt
├── requirements-dev.txt
├── source.bat
├── .gitignore
├── bedrock_agentcore_browser_demo/
│   ├── __init__.py
│   └── bedrock_agentcore_browser_demo_stack.py
└── tests/
    ├── __init__.py
    └── unit/
        ├── __init__.py
        └── test_bedrock_agentcore_browser_demo_stack.py

このプロジェクトでは、CDKの標準生成ディレクトリをinfra/へ整理します。

mkdir -p infra agent/browser_agent tests
touch infra/__init__.py
touch agent/browser_agent/__init__.py

rm -rf bedrock_agentcore_browser_demo
rm -rf tests/unit

最終的な構成は次のとおりです。

.
├── app.py
├── cdk.json
├── requirements.txt
├── requirements-dev.txt
├── infra/
│   ├── __init__.py
│   └── agentcore_browser_stack.py
├── agent/
│   ├── .dockerignore
│   ├── Dockerfile
│   ├── requirements.txt
│   └── browser_agent/
│       ├── __init__.py
│       ├── agent.py
│       └── runtime_app.py
└── tests/
    ├── test_agent.py
    └── test_agentcore_browser_stack.py

1-2. CDK依存パッケージ

生成されたrequirements.txtの内容を次の内容へ置き換えます。

requirements.txt
aws-cdk-lib==2.255.0
constructs>=10.0.0,<11.0.0
cdk-ecr-deployment>=4.0.0,<5.0.0

cdk-ecr-deploymentは、CDKがビルドしたイメージを専用ECR Repositoryへ
コピーするために使用します。

requirements-dev.txt
-r requirements.txt
pytest>=8.0.0,<9.0.0

1-3. CDKエントリーポイント

生成されたapp.pyの内容を次のコードへ置き換えます。

app.py
#!/usr/bin/env python3
import os

import aws_cdk as cdk

from infra.agentcore_browser_stack import AgentCoreBrowserDemoStack


app = cdk.App()

AgentCoreBrowserDemoStack(
    app,
    "AgentCoreBrowserDemoStack",
    env=cdk.Environment(
        account=os.environ.get("CDK_DEFAULT_ACCOUNT"),
        region=os.environ.get("CDK_DEFAULT_REGION", "ap-northeast-1"),
    ),
)

app.synth()

1-4. CDK実行設定

cdk.jsonappを次のように変更します。他のCDK contextは生成された内容を維持して構いません。

cdk.json
{
  "app": ".venv/bin/python app.py"
}

.venv/bin/pythonを明示することで、仮想環境をactivateしていなくてもCDKが依存パッケージの入ったPythonを使用します。

1-5. 仮想環境

python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements-dev.txt

AWS認証とリージョンを確認します。

aws sts get-caller-identity
export AWS_REGION=ap-northeast-1

対象アカウントとリージョンで初めてCDKを使う場合はbootstrapします。

cdk bootstrap

2. カスタムAgentCore Browserを作成する

以降のインフラコードは、特記がない限り同じCDKスタックへ記述します。

2-1. importとスタッククラスを定義

infra/agentcore_browser_stack.py
from pathlib import Path

import cdk_ecr_deployment as ecr_deployment
from aws_cdk import Aws, CfnOutput, Duration, RemovalPolicy, Stack
from aws_cdk import aws_bedrockagentcore as bedrock_agentcore
from aws_cdk import aws_ecr as ecr
from aws_cdk import aws_ecr_assets as ecr_assets
from aws_cdk import aws_iam as iam
from aws_cdk import aws_s3 as s3
from constructs import Construct


DEFAULT_MODEL_ID = "jp.anthropic.claude-sonnet-4-6"


class AgentCoreBrowserDemoStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        model_id = self.node.try_get_context("modelId") or DEFAULT_MODEL_ID

リソースの定義は、この__init__内へ追加します。

2-2. 録画用S3バケットを用意

今回は、エージェントがブラウザ操作している様子を録画してS3に保存しておこうと思います。
infra/agentcore_browser_stack.py__init__関数に以下を追加します。

infra/agentcore_browser_stack.py
recording_bucket = s3.Bucket(
    self,
    "BrowserRecordingBucket",
    block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
    encryption=s3.BucketEncryption.S3_MANAGED,
    enforce_ssl=True,
    lifecycle_rules=[
        s3.LifecycleRule(
            expiration=Duration.days(30),
            noncurrent_version_expiration=Duration.days(7),
        )
    ],
    auto_delete_objects=True,
    removal_policy=RemovalPolicy.DESTROY,
)
  • 録画は30日後に削除します
  • パブリックアクセスは全てブロックしておきましょう

2-3. Browser実行ロール

次に、録画されたファイルをS3バケットに保存するための権限(ロール)を定義します。
infra/agentcore_browser_stack.py__init__関数に以下を追加します。

infra/agentcore_browser_stack.py
browser_execution_role = iam.Role(
    self,
    "BrowserExecutionRole",
    assumed_by=iam.ServicePrincipal("bedrock-agentcore.amazonaws.com"),
    description="Execution role used by the custom AgentCore Browser.",
)
recording_bucket.grant_read_write(browser_execution_role)

2.4 カスタムBrowser

エージェントが使用するためのブラウザを定義します。これはメソッドとして定義します。(メソッド化するかは好みの問題。)

infra/agentcore_browser_stack.py
def bedrock_agent_core_browser(
    scope: Construct,
    *,
    recording_bucket: s3.Bucket,
    execution_role: iam.Role,
) -> bedrock_agentcore.CfnBrowserCustom:
    browser = bedrock_agentcore.CfnBrowserCustom(
        scope,
        "CustomBrowser",
        name="AgentCoreBrowserDemo",
        description="Custom browser used by the Strands AgentCore Browser demo.",
        execution_role_arn=execution_role.role_arn,
        network_configuration=(
            bedrock_agentcore.CfnBrowserCustom.BrowserNetworkConfigurationProperty(
                network_mode="PUBLIC",
            )
        ),
        recording_config=bedrock_agentcore.CfnBrowserCustom.RecordingConfigProperty(
            enabled=True,
            s3_location=bedrock_agentcore.CfnBrowserCustom.S3LocationProperty(
                bucket=recording_bucket.bucket_name,
                prefix="browser-recordings/",
            ),
        ),
        tags={
            "Application": "agentcore-browser-demo",
            "Purpose": "browser-validation",
        },
    )
    browser.node.add_dependency(execution_role)
    return browser

AgentCoreBrowserDemoStackクラス内に定義する必要はありません。
network_modePUBLICを指定することで、Browserから外部Webサイトへアクセスできます。
録画を有効化し、実行中はAWSコンソールのLive Viewで操作を確認できます。

3. Strands Agentを実装する

agent/browser_agent/agent.py
import os

import boto3
from strands import Agent
from strands.models import BedrockModel
from strands_tools.browser import AgentCoreBrowser


DEFAULT_REGION = "ap-northeast-1"
DEFAULT_MODEL_ID = "jp.anthropic.claude-sonnet-4-6"
SYSTEM_PROMPT = """You are a web-browsing assistant.

Use the browser tool whenever the user asks you to visit, inspect, search, or
interact with a website. Report what you actually observed. Do not claim that
you opened a page or performed an action unless the browser tool succeeded.
Avoid submitting purchases, publishing content, or changing account settings
unless the user explicitly requests the action.
"""


def get_region() -> str:
    return (
        os.environ.get("AWS_REGION")
        or os.environ.get("AWS_DEFAULT_REGION")
        or DEFAULT_REGION
    )


def get_model_id() -> str:
    return os.environ.get("BEDROCK_MODEL_ID", DEFAULT_MODEL_ID)


def get_browser_id() -> str:
    browser_id = os.environ.get("AGENTCORE_BROWSER_ID", "").strip()
    if not browser_id:
        raise RuntimeError("AGENTCORE_BROWSER_ID is required")
    return browser_id


def create_agent() -> Agent:
    region = get_region()
    model = BedrockModel(
        model_id=get_model_id(),
        boto_session=boto3.Session(region_name=region),
    )
    browser_tool = AgentCoreBrowser(
        region=region,
        identifier=get_browser_id(),
        session_timeout=900,
    )
    return Agent(
        model=model,
        tools=[browser_tool.browser],
        system_prompt=SYSTEM_PROMPT,
        callback_handler=None,
    )


def extract_text(response: object) -> str:
    message = getattr(response, "message", None)
    if not isinstance(message, dict):
        return str(response)

    content = message.get("content") or []
    texts = [
        item["text"].strip()
        for item in content
        if isinstance(item, dict) and isinstance(item.get("text"), str)
    ]
    return "\n".join(text for text in texts if text) or str(response)

重要なのは次の部分です。

browser_tool = AgentCoreBrowser(
    region=region,
    identifier=get_browser_id(),
    session_timeout=900,
)

Agent(tools=[browser_tool.browser])

CDKで作成したBrowser IDを使ってAgentCoreBrowserを生成し、browserをStrands Agentのツールとして登録します。

4. AgentCore Runtime APIを実装する

エージェントを呼び出すためのエンドポイント(@app.entrypoint)を定義します。

agent/browser_agent/runtime_app.py
import logging

from bedrock_agentcore.runtime import BedrockAgentCoreApp

from browser_agent.agent import create_agent, extract_text


logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = BedrockAgentCoreApp()


@app.entrypoint
def invoke(payload: dict) -> dict:
    prompt = payload.get("prompt")
    if not isinstance(prompt, str) or not prompt.strip():
        raise ValueError("payload.prompt must be a non-empty string")

    logger.info("Invoking browser agent")
    response = create_agent()(prompt.strip())
    return {"response": extract_text(response)}


if __name__ == "__main__":
    app.run()

Runtimeの入力は次の形式とします。

{
  "prompt": "指定したWebサイトを開いて内容を要約してください"
}

BedrockAgentCoreAppがポート8080でHTTPサーバーを起動し、
AgentCore Runtimeに必要な/invocations/pingを提供します。

5. エージェントをDocker化する

実装したStrands Agentsをデプロイするために、Docker化します。

5-1. コンテナ側のPython依存

agent/requirements.txt
bedrock-agentcore>=1.3.0,<2.0.0
strands-agents>=1.0.0,<2.0.0
strands-agents-tools>=0.2.0,<1.0.0
boto3>=1.42.0
playwright>=1.52.0,<2.0.0
nest-asyncio>=1.6.0,<2.0.0

5-2. Dockerfile

DockerイメージをビルドするためのDockerfileを用意します。

agent/Dockerfile
FROM public.ecr.aws/docker/library/python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PORT=8080

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY browser_agent ./browser_agent

EXPOSE 8080

CMD ["python", "-m", "browser_agent.runtime_app"]

依存ファイルを先にコピーすることで、エージェントコードだけを変更した場合に
pipインストール層のDockerキャッシュが再利用されます。

5-3. Docker除外設定

イメージビルドする必要のないファイルは除外しておきます。

agent/.dockerignore
__pycache__/
*.py[cod]
.pytest_cache/

5-4. ローカルビルド

プロジェクトルートで実行します。

docker build \
  --platform linux/arm64 \
  -t agentcore-browser-demo:test \
  agent

モジュールを読み込めることを確認します。

docker run --rm \
  --platform linux/arm64 \
  agentcore-browser-demo:test \
  python -c \
  "from browser_agent.runtime_app import app; from strands_tools.browser import AgentCoreBrowser; print(type(app).__name__, AgentCoreBrowser.__name__)"

6. ECRリポジトリとイメージ配布を実装する

ECRリポジトリを用意して、デプロイする処理を定義します。__init__関数内へ追記してください。

6-1. 専用ECRリポジトリ

ECRリポジトリを用意します。

infra/agentcore_browser_stack.py
image_repository = ecr.Repository(
    self,
    "AgentImageRepository",
    repository_name="agentcore-browser-demo",
    image_scan_on_push=True,
    image_tag_mutability=ecr.TagMutability.IMMUTABLE,
    lifecycle_rules=[
        ecr.LifecycleRule(
            description="Keep the ten most recent agent images",
            max_image_count=10,
        )
    ],
    removal_policy=RemovalPolicy.DESTROY,
    empty_on_delete=True,
)

6-2. ARM64イメージのビルド

infra/agentcore_browser_stack.py
image_asset = ecr_assets.DockerImageAsset(
    self,
    "AgentImageAsset",
    directory=str(Path(__file__).resolve().parents[1] / "agent"),
    platform=ecr_assets.Platform.LINUX_ARM64,
)

6.3 専用ECRへのコピー

infra/agentcore_browser_stack.py
image_tag = image_asset.asset_hash
runtime_image_uri = image_repository.repository_uri_for_tag(image_tag)

image_deployment = ecr_deployment.ECRDeployment(
    self,
    "DeployAgentImage",
    src=ecr_deployment.DockerImageName(image_asset.image_uri),
    dest=ecr_deployment.DockerImageName(runtime_image_uri),
)

DockerImageAssetはイメージをCDKアセット用ECRへPushします。
ECRDeploymentがそこから専用ECRへコピーします。

タグにはソース内容のハッシュを使用するため、コードが変更されると新しいタグになります。

7. Runtime用IAMロールを実装する

AgentCoreBrowserDemoStackクラスに次のメソッドを追加します。

infra/agentcore_browser_stack.py
def _create_runtime_role(
    self,
    *,
    image_repository: ecr.Repository,
    browser: bedrock_agentcore.CfnBrowserCustom,
) -> iam.Role:
    runtime_role = iam.Role(
        self,
        "AgentRuntimeRole",
        assumed_by=iam.ServicePrincipal(
            "bedrock-agentcore.amazonaws.com",
            conditions={
                "StringEquals": {"aws:SourceAccount": Aws.ACCOUNT_ID},
                "ArnLike": {
                    "aws:SourceArn": (
                        f"arn:{Aws.PARTITION}:bedrock-agentcore:"
                        f"{Aws.REGION}:{Aws.ACCOUNT_ID}:*"
                    )
                },
            },
        ),
    )

    image_repository.grant_pull(runtime_role)
    runtime_role.add_to_policy(
        iam.PolicyStatement(
            actions=["ecr:GetAuthorizationToken"],
            resources=["*"],
        )
    )
    runtime_role.add_to_policy(
        iam.PolicyStatement(
            actions=[
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:DescribeLogGroups",
                "logs:DescribeLogStreams",
                "logs:PutLogEvents",
            ],
            resources=["*"],
        )
    )
    runtime_role.add_to_policy(
        iam.PolicyStatement(
            actions=[
                "bedrock:InvokeModel",
                "bedrock:InvokeModelWithResponseStream",
            ],
            resources=[
                f"arn:{Aws.PARTITION}:bedrock:*::foundation-model/*",
                (
                    f"arn:{Aws.PARTITION}:bedrock:{Aws.REGION}:"
                    f"{Aws.ACCOUNT_ID}:inference-profile/*"
                ),
            ],
        )
    )
    runtime_role.add_to_policy(
        iam.PolicyStatement(
            actions=[
                "bedrock-agentcore:StartBrowserSession",
                "bedrock-agentcore:GetBrowserSession",
                "bedrock-agentcore:ListBrowserSessions",
                "bedrock-agentcore:StopBrowserSession",
                "bedrock-agentcore:UpdateBrowserStream",
                "bedrock-agentcore:ConnectBrowserAutomationStream",
                "bedrock-agentcore:ConnectBrowserLiveViewStream",
            ],
            resources=[browser.attr_browser_arn],
        )
    )
    return runtime_role

__init__内でメソッドを呼び出します。

infra/agentcore_browser_stack.py
runtime_role = self._create_runtime_role(
    image_repository=image_repository,
    browser=browser,
)

このロールには以下を許可しています。

  • ECRからのイメージ取得
  • CloudWatch Logsへの出力
  • Bedrockモデル呼び出し
  • Browserセッション操作
  • Automation StreamおよびLive View Streamへの接続

8. AgentCore Runtimeを作成する

AgentCoreBrowserDemoStack.__init__内に以下を記述します。

infra/agentcore_browser_stack.py
runtime = bedrock_agentcore.CfnRuntime(
    self,
    "AgentRuntime",
    agent_runtime_name="AgentCoreBrowserDemoRuntime",
    description="Strands agent that operates a custom AgentCore Browser.",
    agent_runtime_artifact=(
        bedrock_agentcore.CfnRuntime.AgentRuntimeArtifactProperty(
            container_configuration=(
                bedrock_agentcore.CfnRuntime.ContainerConfigurationProperty(
                    container_uri=runtime_image_uri,
                )
            )
        )
    ),
    network_configuration=(
        bedrock_agentcore.CfnRuntime.NetworkConfigurationProperty(
            network_mode="PUBLIC",
        )
    ),
    protocol_configuration="HTTP",
    role_arn=runtime_role.role_arn,
    environment_variables={
        "AWS_REGION": self.region,
        "BEDROCK_MODEL_ID": model_id,
        "AGENTCORE_BROWSER_ID": browser.attr_browser_id,
    },
    lifecycle_configuration=(
        bedrock_agentcore.CfnRuntime.LifecycleConfigurationProperty(
            idle_runtime_session_timeout=900,
            max_lifetime=3600,
        )
    ),
)

runtime.node.add_dependency(image_deployment)
runtime.node.add_dependency(runtime_role)
runtime.node.add_dependency(browser)

環境変数の用途は次のとおり。

環境変数 用途
AWS_REGION BedrockとBrowserを呼び出すリージョン
BEDROCK_MODEL_ID Strands Agentが使用するモデル
AGENTCORE_BROWSER_ID 操作対象のカスタムBrowser

依存関係により、イメージコピーとBrowser作成の完了後にRuntimeを作成します。

CloudFormation Outputs

上記の記述の直後に以下を記述します。

infra/agentcore_browser_stack.py
CfnOutput(self, "AgentRuntimeArn", value=runtime.attr_agent_runtime_arn)
CfnOutput(self, "AgentRuntimeId", value=runtime.attr_agent_runtime_id)
CfnOutput(self, "CustomBrowserArn", value=browser.attr_browser_arn)
CfnOutput(self, "CustomBrowserId", value=browser.attr_browser_id)
CfnOutput(
    self,
    "AgentImageRepositoryUri",
    value=image_repository.repository_uri,
)
CfnOutput(self, "AgentImageUri", value=runtime_image_uri)
CfnOutput(
    self,
    "BrowserRecordingBucketName",
    value=recording_bucket.bucket_name,
)

9. テストする

9-1. CDKテスト

CDKコードの単体テストを記述します。

tests/test_agentcore_browser_stack.py
import aws_cdk as cdk
from aws_cdk.assertions import Match, Template

from infra.agentcore_browser_stack import AgentCoreBrowserDemoStack


def synth_template() -> Template:
    app = cdk.App(context={"modelId": "test.model-id"})
    stack = AgentCoreBrowserDemoStack(
        app,
        "TestStack",
        env=cdk.Environment(
            account="123456789012",
            region="ap-northeast-1",
        ),
    )
    return Template.from_stack(stack)


def test_creates_custom_browser_with_recording() -> None:
    template = synth_template()
    template.has_resource_properties(
        "AWS::BedrockAgentCore::BrowserCustom",
        {
            "Name": "AgentCoreBrowserDemo",
            "NetworkConfiguration": {"NetworkMode": "PUBLIC"},
            "RecordingConfig": {
                "Enabled": True,
                "S3Location": {
                    "Prefix": "browser-recordings/",
                    "Bucket": Match.any_value(),
                },
            },
        },
    )

実際のプロジェクトでは、このほかにECR、Runtime環境変数、IAM権限も検証します。

9-2. エージェント応答変換テスト

pythontests/test_agent.py
from types import SimpleNamespace

from agent.browser_agent.agent import extract_text


def test_extract_text_combines_text_blocks() -> None:
    response = SimpleNamespace(
        message={
            "content": [
                {"text": "first"},
                {"toolUse": {"name": "browser"}},
                {"text": "second"},
            ]
        }
    )

    assert extract_text(response) == "first\nsecond"

9-3. テストコマンド

source .venv/bin/activate
pytest
cdk synth

10. デプロイして動作確認する

10-1. デプロイ

cdk deploy

別のモデルを使う場合はCDK contextで上書きします。

cdk deploy -c modelId=your-model-or-inference-profile-id

また、Dockerイメージのビルドもこのタイミングでやってくれます。

10-2. AWSコンソールでの確認

今回は、ブラウザでYahooの検索画面を読んでもらおうと思います。以下の操作をして、 ランタイムプレイグラウンド の画面へ遷移してください。

  1. Bedrock AgentCore コンソールへ遷移
  2. 左メニューの テスト セクション以下の ランタイムプレイグラウンド をクリック
  3. ランタイムエージェントで、今回作成したランタイム名を選択し、エンドポイントが DEFAULT となっていることを確認

では、入力欄に以下を入力します。

{"prompt": "ブラウザでYahooの検索画面を開いでください。"}

実行 ボタンをクリックしたら、以下のようにレスポンスが返ってくるはずです。(ちょっと見づらいけど、整形したら以下のようになります!)

image.png

検索ボックスの場所とかがちゃんと解釈できているので、ブラウザ操作はできていると判断できそうですね!

11. リソースを削除する

検証終了後は、無駄な料金支払いを抑えるためにも以下のコマンドを実行しておきましょう。

cdk destroy

ただし、以下の場合で、削除が失敗する可能性があります。

Browser削除がAlreadyExistsで失敗する場合

Browserを直接削除してから、CloudFormationの削除を再実行します。

aws bedrock-agentcore-control list-browsers \
  --region ap-northeast-1

aws bedrock-agentcore-control delete-browser \
  --browser-id <browser-id> \
  --region ap-northeast-1

aws cloudformation delete-stack \
  --stack-name AgentCoreBrowserDemoStack \
  --region ap-northeast-1

上記はAWS CLIを使用して削除するコマンドです。AWS マネジメントコンソール上で削除することも可能です。

録画バケットが空でない場合

Browser削除後に最終録画が遅延書き込みされると、S3バケットの削除が失敗する場合があります。
検証用データであることを確認してから空にします。

aws s3 rm s3://<recording-bucket-name> \
  --recursive \
  --region ap-northeast-1

aws cloudformation delete-stack \
  --stack-name AgentCoreBrowserDemoStack \
  --region ap-northeast-1

aws cloudformation wait stack-delete-complete \
  --stack-name AgentCoreBrowserDemoStack \
  --region ap-northeast-1

まとめ

今回は、Strands Agents × Amazon Bedrock AgentCore × AWS CDK でAIにブラウザ操作させてみる検証を行いました。

ソースコードについては、以下のリポジトリを参照いただけますと幸いです。

以上

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?