
Python
Pythonは明確で読みやすい構文を持っているため、プログラミング初心者にもおすすめの言語です。また多くのコミュニティがあり、それぞれがライブラリ開発やフレームワーク開発に貢献しています。
イベント
マガジン
技術ブログ
本記事は 2026 年 7 月 6 日 に公開された「 Deploying VCF 9.1 on Amazon EVS with End-to-End Automation 」を翻訳したものです。 はじめに Amazon Elastic VMware Service (Amazon EVS) を使用すると、Amazon VPC 内の AWS ベアメタル EC2 インスタンス上で VMware Cloud Foundation (VCF) を直接実行できます。EVS では、使い慣れた VMware スタックの運用一貫性を維持しながら、AWS クラウドサービスの弾力性、セキュリティ、豊富な機能を活用できます。 VCF 9 は VMware Cloud Foundation の最新リリースであり、以前の VCF 5.2 リリースとは根本的に異なる方法で Amazon EVS と連携します。VCF 5.2 では、EVS サービスがデプロイ全体をエンドツーエンドで処理していました。VLAN サブネットのデプロイ、ホストを含む EVS 環境、および完全な VCF スタックはすべて、サービスが自動的にプロビジョニングおよび設定していました。VCF 9 では、Amazon EVS はセルフデプロイモードと呼ばれる方式でのみ動作します。 セルフデプロイモードとは? セルフデプロイモードでは、Amazon EVS が基盤となる AWS インフラストラクチャをプロビジョニングします。具体的には、VCF に必要な VLAN サブネットと、選択した ESXi バージョン(9.0 または 9.1)を実行するベアメタル EC2 ホストです。ただし、サービスが担うのはここまでです。EVS は VCF 自体のインストールや設定は行いません。EVS が代わりに VCF 9 をデプロイするオプションは存在しません。 つまり、EVS がホストとネットワークを提供した後は、VCF のインストールはお客様の責任となります。VMware のドキュメントに従って手動でインストールするか、自動化を使ってプログラム的に処理できます。本ブログ記事ではまさにその自動化を取り上げます。AWS が公開した エンドツーエンドの自動化ツールキット は、前提条件となる AWS インフラストラクチャと EVS 環境のプロビジョニングから、VCF のインストールおよび設定まで、ワークフロー全体を処理します。このツールキットは VCF 9.0 と 9.1 の両方に対応しています。 VCF デプロイを自動化する理由 セルフデプロイモードでは VCF 9 のインストールはお客様の責任であるため、自動化が特に効力を発揮します。VCF の手動インストールには、数十もの設定ステップ、複雑な JSON 仕様の作成、パスワード生成、そして数時間の待機が必要です。本ツールキットはそれを 3 つの CLI コマンドに集約します。 再現性:リージョンやアカウントをまたいで同一環境をデプロイ可能 スピード:数時間かかる手動プロセスを 3 コマンドに短縮 監査性:すべての設定判断がバージョン管理されたコードに記録 エラー削減:DNS レコード、VLAN CIDR、bringup 仕様における手動入力ミスを排除 セキュリティ:パスワードを自動生成して AWS Secrets Manager に保存 構築するもの 自動化ツールキットは VCF 9.0 と 9.1 の両方に対応していますが、本ブログでは VCF 9.1 のデプロイに絞って説明します。このウォークスルーを完了すると、以下が構築されます。 Route 53 DNS、BGP 対応 Route Server、必要なネットワーキングを備えた VPC ESXi 9.1 を実行する 3 台のベアメタルホストを持つ Amazon EVS 環境 完全に動作する VCF 9.1 スタック:vCenter、SDDC Manager、NSX Manager、VCF Operations、VCF Cloud Proxy、2 台の NSX Edge NSX オーバーレイセグメントと VPC ルートテーブル間で動的なルート伝播を行う Tier-0 および Tier-1 ゲートウェイ EVS サービス内の Ops Manager Connector コストの考慮事項 本自動化でデプロイされるリソースには、EC2 ベアメタルコンピューティング、Amazon EVS コントロールプレーン、VPC Route Server、Transit Gateway(有効化した場合)、NAT Gateway、EBS ボリューム、Route 53 ホストゾーンなど、相当な月額費用が発生します。デプロイ前に Amazon EVS 料金ページ を確認し、 AWS Pricing Calculator を使って環境全体のコストを把握することをお勧めします。 アーキテクチャの概要 本自動化は、完全な Amazon EVS 環境と VCF 9 を 3 つのステージにわたってデプロイします(1 ステージにつき 1 つのレイヤー)。最初に設定の詳細を入力するだけで、各ステージで生成されたリソース ARN などのメタデータとともに設定値が次のステージへ自動的に引き継がれます。同じ情報を 2 度入力する必要はなく、設定ミスを大幅に削減できます。各ステージの内容は次のとおりです。 AWS 前提条件とアンダーレイ: VPC、サービスアクセスサブネット、Route 53(フォワードおよびリバースゾーン)、2 つの BGP エンドポイントを持つ Route Server、セキュリティグループ、オプションの Transit Gateway、オプションの Windows ジャンプボックス(踏み台サーバー)、オプションのパブリックまたはプライベート HCX 接続などを構築します。 EVS レイヤー: まず、管理、vMotion、vSAN、vTEP、エッジアップリンクなどに使用する 10 個の VLAN サブネットを作成し、EVS 環境を構築します。次に、ESXi 9.1 で起動する 3 台のベアメタルホスト(ユーザーが指定する i4i.metal または i7i.metal-24xl)をプロビジョニングします。最後に、VCF Installer をステージングするデータストアとして EBS ボリュームを作成し、いずれかのホストにマウントします。 VCF のインストールと NSX ルーティング: VCF Installer のデポを設定し、ターゲット VCF バージョンに基づいてバイナリのダウンロードを自動的にトリガーします。次に vCenter、SDDC Manager、NSX Manager、VCF Operations、VCF Cloud Proxy、アクティブ/スタンバイ構成の 2 台の NSX Edge をデプロイします。エッジは BGP で AWS Route Server とピアリングし、NSX セグメントルートを VPC にアドバタイズします。 3 フェーズの自動化アプローチ 本自動化は 3 つの順次フェーズで構成されており、それぞれ独自のツールと目的を持ちます。 フェーズ スタック 内容 1 Terraform VPC、Route 53、Route Server、セキュリティグループ、オプションの Transit Gateway、オプションのジャンプボックス、オプションのパブリック接続など 2 Python (boto3) EVS 環境と VLAN サブネットの作成、ESXi 9.1 を搭載したベアメタルホストのプロビジョニング、インストーラーステージング用 EBS ボリュームの作成とアタッチ 3 Python (VCF SDK + boto3) デポの設定、バイナリのダウンロード、VCF bringup、インストーラーのクリーンアップ、NSX Edge Cluster とルーティングのデプロイ、Ops Manager Connector の作成 フェーズ 1 と 2 は Mac、Linux、または Windows ワークステーションからローカルで実行します。フェーズ 3 は VPC 内のジャンプボックスから実行します(VCF Installer および NSX Manager への直接接続に必要)。唯一の手動ステップは、フェーズ 2 と 3 の間に行う 2 つの簡単な ESXi 設定と VCF Installer OVA のデプロイです。デプロイ全体の所要時間はおよそ 3.5〜6 時間で、大部分はフェーズ 3 の VCF bringup プロセスが占めます。 前提条件 AWS アカウントと権限 以下を満たす AWS アカウント AWS Business Support 以上 EVS 環境あたりのホスト数サービスクォータが 3 以上に設定済み オンデマンドスタンダード(A、C、D、H、I、M、R、T、Z)インスタンスのサービスクォータが 256 以上に設定済み コードリポジトリのフェーズ 1 配下にある iam_policy.json ファイルで定義された権限ポリシーを持つ IAM ユーザーまたはロール ローカルワークステーションのツール Terraform 1.7 以降 Python 3.12 以降 AWS CLI v2 (AWS_PROFILE または環境変数でクレデンシャルを設定済み) pip (Python 依存関係のインストール用) git (コードリポジトリのクローン用) Broadcom デポトークン フェーズ 3 では Broadcom デポから VCF コンポーネントのバイナリをダウンロードします。Broadcom サポートポータルの「My Dashboard」→「Quick Links」→「Generate Download Token」からダウンロードトークンを生成してください。このトークンはフェーズ 3 の実行前に VCF_DEPOT_TOKEN 環境変数として設定します。 VCF Installer OVA Broadcom ダウンロードポータルから VCF 9.1 Installer OVA をダウンロードしてください。このアプライアンスはフェーズ 2 と 3 の間に、いずれかの ESXi ホスト上へ手動でデプロイします。 リポジトリのクローン 自動化リポジトリをクローン してローカルワークステーションに配置してください(このコマンドは Linux と Windows の両方で使用できます)。 git clone https://github.com/aws/solutions-for-amazon-evs.git cd solutions-for-amazon-evs/Deploy/VCF9-Phased-Deployment フェーズ 0:デプロイ前の入念な計画 自動化を実行する前に、設定ファイルを慎重に計画して入力する必要があります。このステップは非常に重要です。インフラストラクチャをデプロイした後は、環境全体を解体して再構築しなければ変更できない値が多数あるため、時間をかけて正確に設定してください。 自動化では、最初に 1 度だけ入力する 2 つの設定ファイルを使用します。その後のフェーズはこれらのファイルから値を読み取り、次のフェーズへ自動的に引き継ぎます。 terraform.tfvars(フェーズ 1 の入力) このファイルは AWS インフラストラクチャの基盤を定義します。 terraform.tfvars.example を terraform.tfvars にコピーして以下を設定してください(Linux)。 cd Phase_1_Base_Infrastructure cp terraform.tfvars.example terraform.tfvars Windows の場合: cd Phase_1_Base_Infrastructure copy terraform.tfvars.example terraform.tfvars 以下の値を実際の環境に合わせて更新してください。 region :デプロイ先の AWS リージョン(例:us-east-2)。Amazon EVS が利用可能なリージョンを指定してください。 availability_zone :リージョン内の特定の AZ(例:us-east-2a)。すべてのリソースは単一の AZ にデプロイされます。EC2 キャパシティ予約がある場合は、予約済みの AZ と一致させてください。 fqdn :環境の完全修飾ドメイン名(例:mylab.evs.aws)。Route 53 フォワードホストゾーンとなり、すべての DNS レコードに使用されます。慎重に選択してください。 cidr_prefix :VPC CIDR の最初の 2 オクテット(例:”10.0.”)。自動化はこのプレフィックスからすべての VLAN サブネット CIDR を導出します。Transit Gateway または Direct Connect で接続する予定のネットワークと重複しないようにしてください。 DNS ホスト名 :各 VCF アプライアンスと ESXi ホストの短い DNS 名(esxi01_name から vcf_fleet_name まで)。 create_tgw :他の VPC やオンプレミスネットワークへの Transit Gateway 接続が必要な場合は true に設定してください。 create_jumpbox :Windows ジャンプホストを作成する場合は true に設定してください。VPC 内から実行する必要があるフェーズ 3 に必須です。このフラグを有効にすることをお勧めします。 enable_public_hcx :HCX インターネット接続用のパブリック IP スペースを割り当てる場合は true に設定してください。false の場合、HCX はプライベート VLAN を使用します(Direct Connect または Transit Gateway 経由の HCX に適しています)。 VPC CIDR の分割方法 cidr_prefix で最初の 2 オクテットのみ指定します(例:”10.0.”)。自動化は /16 の VPC を作成し、ハードコードされた第 3 オクテットを使って /24 の VLAN サブネットに分割します。 <prefix>0.0/24 — サービスアクセスサブネット <prefix>10.0/24 — ホスト vmkernel 管理(vmkManagement) <prefix>20.0/24 — vMotion <prefix>30.0/24 — vSAN <prefix>40.0/24 — Host TEP (vTep) <prefix>50.0/24 — Edge TEP (edgeVTep) <prefix>60.0/24 — VM 管理(vmManagement) <prefix>70.0/24 — HCX <prefix>80.0/24 — NSX アップリンク(nsxUplink) <prefix>90.0/24 — 拡張 VLAN 1 <prefix>100.0/24 — 拡張 VLAN 2 config.json でこれらの VLAN サブネット CIDR を変更する場合は、NSX Edge IP、Route 53 インバウンドリゾルバーエンドポイント(<prefix>0.100 および <prefix>0.101)、BGP アップリンクアドレスなど、サブネット内に設定されるハードコードされた IP アドレスと競合しないよう注意してください。 config.json(フェーズ 2 の入力) このファイルは EVS 環境と VCF デプロイパラメーターを定義します。 config.example.json を config.json にコピーして以下を設定してください(Linux)。 cd Phase_2_evs_env/python cp config.example.json config.json Windows の場合: cd Phase_2_evs_env/python copy config.example.json config.json 以下の値を実際の環境に合わせて更新してください。 environmentName :EVS 環境のわかりやすい名前。AWS コンソールに表示され、リソースのタグ付けに使用されます。 vcfInstallerProductVersion :VCF Installer の製品バージョン番号のみ(ビルド文字列は含めない)。例:”9.1.0.0″。正確な値は OVA ファイル名またはインストーラー UI の設定ページで確認してください。 simpleDeployment :単一ラック・シンプルデプロイ(最小 3 ホスト)の場合は true、単一ラック・HA デプロイ(最小 4 ホスト)の場合は false を設定してください。この設定は、デプロイされる NSX Manager と VCF Operations ノードの数に影響します。詳細は Single-Rack vSphere クラスターモデルのドキュメント を参照してください。 vcfSizing :vCenter(vmSize、storageSize)、NSX(nsxSize)、VCF Operations(operationsApplianceSize、operationsCollectorApplianceSize)のアプライアンスサイジング。ほとんどのデプロイでは “medium” のデフォルト値が適切です。 ヒント: FQDN、CIDR プレフィックス、ホスト名の選択は事前に十分確認してください。フェーズ 1 のデプロイ後にも検証することをお勧めします。フェーズ 1 の削除と再デプロイは数分で完了するため、この時点であれば変更は容易です。フェーズ 2 で FQDN や CIDR などの値を組み込んだ EVS 環境がデプロイされると、変更には最初から再構築が必要になります。 フェーズ 1:基盤 AWS インフラストラクチャ(Terraform) フェーズ 1 で作成されるもの フェーズ 1 は、EVS が必要とする AWS ネットワークおよびサポートリソースをすべてプロビジョニングします。 terraform apply が完了すると(通常 3〜5 分)、以下が構成されます。 VCF アプライアンスと ESXi ホスト用の Route 53 プライベートホストゾーン フォワードホストゾーン(A レコード) リバースホストゾーン(PTR レコード) 2 つの IP を持つインバウンドリゾルバーエンドポイント Amazon VPC 専用サービスアクセスルートテーブルに関連付けられたプライベートサービスアクセスサブネット 専用パブリックアクセスルートテーブルに関連付けられたパブリックアクセスサブネット Elastic IP を持つ NAT Gateway Internet Gateway FQDN、Route 53 インバウンドリゾルバー IP、NTP(169.254.169.123:AWS NTP サーバー)を設定した DHCP オプションセット VPC CIDR からの受信を許可する専用 EVS サービスアクセスセキュリティグループ VPC Route Server VPC に関連付け済み それぞれピア IP、ピア ASN(65000)、BGP キープアライブを設定した 2 つの Route Server エンドポイント サービスアクセスルートテーブルとパブリックアクセスルートテーブルへの 2 つの Route Server 伝播 EVS ホスト用の EC2 キーペア (オプション)アンダーレイ VPC への VPC アタッチメントを持つ Transit Gateway (オプション)VPC 内の Windows ジャンプホスト ジャンプホスト用の VPC パブリックサブネット Internet Gateway へのルートを持つジャンプホスト用専用ルートテーブル 専用ルートテーブルへの Route Server 伝播 VPC CIDR と EVS サービスアクセスセキュリティグループからのすべてのトラフィックを許可するジャンプホスト用専用セキュリティグループ 50 GB 暗号化 gp3 ルートボリュームとパブリック IPv4 アドレスを持つ、Windows Server 2025 を実行する t3.2xlarge EC2 インスタンス ジャンプホスト用専用 EC2 キーペア 注:このキーペアは “Jumpbox-KeyPair” という名前です。同名のキーペアが既に存在する場合、このコードは失敗します。その場合はコード内でキーペア名を変更してください。 (オプション)パブリック HCX 接続 IPv4 IPAM プール 連続したパブリック IP スペースの /28 VPC CIDR へのパブリック /28 の追加 デプロイの実行(Linux): export AWS_PROFILE=your-profile-name terraform init terraform plan terraform apply Windows の場合: $env:AWS_PROFILE = "your-profile-name" terraform init terraform plan terraform apply Terraform は VPC ID、サブネット ID、Route Server エンドポイント IP、セキュリティグループ ID などの値を出力します。フェーズ 2 はこれらを terraform.tfstate ファイルから直接読み取ります。フェーズ 1 のデプロイはおよそ 5〜10 分で完了します。 インフラストラクチャの検証 フェーズ 1 の完了後、AWS コンソールで以下を確認してください。 期待される CIDR で VPC が作成されていること Route 53 ホストゾーンに、すべての VCF アプライアンスと ESXi ホストの A レコードおよび PTR レコードが含まれていること Route Server の 2 つのエンドポイントが “Available” 状態であること ジャンプボックスを使用する場合:インスタンスが実行中でパブリック IP を持つこと ジャンプボックスをデプロイした場合は、以下の手順で RDP アクセスを確立してください。 Jumpbox-SG セキュリティグループに、自分の IP アドレスからの RDP(TCP 3389)を許可するインバウンドルールを追加する EC2 キーペアを取得して Windows Administrator パスワードを復号する。PEM ファイルをダウンロードする(Linux): KEY_PAIR_ID=$(terraform output -raw jumpbox_key_pair_id) aws ssm get-parameter --name /ec2/keypair/$KEY_PAIR_ID --with-decryption \ --region us-east-2 --query 'Parameter.Value' --output text > jumpbox.pem chmod 400 jumpbox.pem Windows の場合: $KEY_PAIR_ID = terraform output -raw jumpbox_key_pair_id aws ssm get-parameter --name "/ec2/keypair/$KEY_PAIR_ID" --with-decryption ` --region us-east-2 --query 'Parameter.Value' --output text | Out-File -Encoding ascii jumpbox.pem icacls jumpbox.pem /inheritance:r /grant:r "$($env:USERNAME):(R)" この PEM ファイルを EC2 コンソール(Connect > Get Windows Password)で使用して Administrator パスワードを復号してください。 最後に、VCF Installer OVA をジャンプホストにダウンロードまたはコピーしてください。フェーズ 2 と 3 の間の手動作業ステップで必要になります。 フェーズ 2:EVS 環境のデプロイ(Python) フェーズ 2 の処理内容 フェーズ 2 は Python CLI で、AWS API 経由で EVS デプロイを調整し、SDDC bringup 仕様と NSX デプロイ仕様を構築します。単一の CLI コマンドで、以下の 6 つのサブステップを順番に実行します。 pre-evs-sync-config :フェーズ 1 の Terraform ステートを読み取り、VCF bringup 仕様とエッジクラスター仕様の初期版を生成 create-environment-and-hosts :AWS API を呼び出して EVS 環境を作成し、完了後にベアメタルホストをプロビジョニング(15〜20 分) すべてのホストが CREATED ステータスになるまで待機(20〜40 分) post-evs-sync-config :環境とホスト ID から派生した名前で SDDC 仕様を確定し、対応する VCF パスワードを Secrets Manager にプロビジョニング associate-vlan-subnets :10 個の EVS VLAN サブネットをすべてサービスアクセスルートテーブルに関連付け create-and-attach-ebs :256 GB の gp3 EBS ボリュームを作成し、VCF Installer VMFS 用として 1 台のホストにアタッチ deploy-environment の実行 Python 環境をセットアップし、単一コマンドでフェーズ 2 のパイプライン全体を実行します(Linux)。 python -m venv .venv source .venv/bin/activate pip install -r requirements.txt python -m src.main deploy-environment \ --profile your-aws-profile \ --config config.json \ --tfstate ../../Phase_1_Base_Infrastructure/terraform.tfstate \ --instance-type <INSTANCE_TYPE> # 'i4i.metal' or 'i7i.metal-24xl' Windows の場合: py -m venv .venv .venv\Scripts\Activate.ps1 pip install -r requirements.txt python -m src.main deploy-environment ` --profile your-aws-profile ` --config config.json ` --tfstate ..\..\Phase_1_Base_Infrastructure\terraform.tfstate ` --instance-type <INSTANCE_TYPE> # 'i4i.metal' or 'i7i.metal-24xl' このコマンドは 6 つのサブステップを自動的に連結して実行します。合計所要時間はおよそ 30〜45 分で、大部分はベアメタルホストがプロビジョニングされて CREATED ステータスになるまでの待機時間が占めます。デバッグ用に各サブステップを個別に実行することも可能ですが、単一コマンドによるアプローチを 強く推奨 します。 Secrets Manager によるパスワード管理 フェーズ 2 の構築中、post-evs-sync-config サブステップがすべての VCF アプライアンス用の複雑なパスワードを自動生成し、AWS Secrets Manager に保存します。命名パターンは evs-<environment-id>_<role> です。 VCF のアカウントには vcenterRoot 、 vcenterSso 、 nsxRoot 、 nsxAdmin 、 nsxAudit 、 sddcManagerRoot 、 sddcManagerSsh 、 sddcManagerLocal 、 operationsAdmin 、 operationsMaster 、 operationsCollector 、 edgeAppliance が含まれます。 bringup 仕様はプレースホルダートークンを通じてこれらのパスワードを参照します。フェーズ 3 は実行時に Secrets Manager からパスワードを解決するため、平文パスワードがディスクに書き込まれることはありません。 手動による事前作業:ESXi 設定とインストーラー OVA フェーズ 2 と 3 の間に、いくつかの簡単な手順を手動で実施する必要があります。デプロイ全体を通じて唯一の非自動化部分であり、所要時間はおよそ 30 分です。以下の手順はすべて、フェーズ 2 で EBS ボリュームを受け取った 1 台の ESXi ホスト上で実施します。 EBS ボリュームへの VMFS データストアの作成 フェーズ 2 でいずれかの ESXi ホストに 256 GB の EBS ボリュームをアタッチしました。フェーズ 2 のコンソール出力で、どのホストがボリュームを受け取ったか確認できます。VCF Installer アプライアンスをホストするために、VMFS データストアとしてフォーマットする必要があります。 ESXi ホスト UI( https://<esxi-host-ip> )に root としてログインします。root パスワードは AWS Secrets Manager から取得してください(シークレット名は evs-<environment-id>_<role> のパターンです)。 Storage に移動し、New Datastore を選択する NVMe デバイス(256 GB EBS ボリューム)を選択して VMFS データストアを作成する VM Network ポートグループへの VLAN タグ付け EBS ボリュームを持つ ESXi ホストで、デフォルトの “VM Network” ポートグループに VLAN ID 20 をタグ付けし、VCF Installer が VM 管理ネットワークで通信できるようにします。 ESXi UI で Networking に移動し、Port groups を選択する “VM Network” ポートグループを編集する VLAN ID を 20 に設定する VCF Installer OVA のデプロイ VMFS データストアを持つホスト上に VCF Installer アプライアンスをデプロイします。 ESXi UI で Virtual Machines > Create/Register VM > Deploy OVF template に移動する VCF 9.1 Installer OVA ファイルをアップロードする 上記で作成した 256 GB VMFS データストアに配置する “VM Network” ポートグループにアタッチする 管理 IP を DNS 設定の SDDC Manager IP アドレスに設定する ネットマスクを 255.255.255.0 に設定する admin@local パスワードを設定する。手動で選択する唯一のパスワードです。独自のパスワードを作成するか、Secrets Manager に生成済みのパスワードを使用することもできます。フェーズ 3 で必要になるためパスワードを控えておいてください。 アプライアンスの電源を入れ、起動を待つ( https://<sddcm_name>.<your-fqdn> でアクセスを確認) フェーズ 3:VCF Bringup と NSX Edge Cluster(Python) フェーズ 3 の処理内容 フェーズ 3 は EVS 環境をホストする VPC 内のジャンプホストから実行する必要があります。公式 VMware VCF Python SDK(vcf-installer、vcf-nsx)と vCenter 操作用 pyvmomi、および対応する AWS 操作用 boto3 ライブラリを使用します。単一の CLI コマンドで、以下の 7 つのサブステップを順番に実行します。 Secrets Manager 事前確認 :開始前に必要なすべての Secrets Manager シークレットが存在することを確認。不足している場合、不足している VCF アカウントを明示したエラーでパイプラインを即座に終了。 prepare-depot :Broadcom ダウンロードトークンをインストーラーに設定し、デポカタログを同期して必要なすべてのコンポーネントバイナリをダウンロード(約 30〜60 分) start-bringup --wait :bringup 仕様を VCF Installer に送信し、ワークフローが完了するまでポーリング。vCenter、NSX Manager、SDDC Manager、VCF Operations をデプロイ(約 2〜4 時間) remove-installer-datastore :手動で作成した 256 GB VMFS データストア上の VM を vSAN にストレージ vMotion し、データストアをアンマウント(約 5〜10 分) destroy-ebs-volume :AWS 側からアンマウント済み VMFS データストアをホストしていた EBS ボリュームをデタッチして削除(約 30 秒) deploy-edge-cluster :2 台の NSX Edge アプライアンスをデプロイし、エッジクラスターを作成し、Tier-0/Tier-1 ルーティングを設定し、AWS Route Server との BGP ピアリングを確立(約 30〜50 分) create-connector :VCF Operations Manager コネクターを CreateEnvironmentConnector 経由で EVS に登録。 ACTIVE になるまでポーリング(約 2〜5 分) 以下のサブセクションでは、フェーズ 3 の内部処理をより詳細に段階的に説明します。 deploy-vcf-and-edge の実行 と題したセクションに、フェーズ 3 のワークフロー全体を実行する単一コマンドが記載されています。 デポとバンドルの管理 prepare-depot サブステップは、VCF Installer UI にログインして Broadcom デポを設定する手動プロセスを自動化します。具体的には以下を実行します。 VCF SDK 経由でデポトークンをインストーラーに保存 メタデータ同期をトリガーしてバンドルカタログを更新 ターゲットバージョンに一致するすべての INSTALL タイプバンドルを特定 必要なすべてのコンポーネントバイナリ(vCenter、NSX、SDDC Manager、VCF Operations、ESXi)をダウンロード すべてのダウンロードが完了するまでポーリングし、その後 bringup に進む ヒント :デポの問題をデバッグする必要がある場合、list-bundles、get-depot-settings、sync-depot などの個別アクションをスタンドアロン CLI コマンドとして実行できます。 VCF bringup ワークフロー start-bringup アクションは型付きの SddcSpec を VCF Installer に送信し、ワークフローを監視します。bringup 中にインストーラーは以下を実行します。 ESXi ホストを VCF 管理ドメインにコミッション vCenter Server をデプロイして設定 NSX Manager をデプロイして仮想 IP アドレスでクラスター化 SDDC Manager をデプロイ VCF Operations をデプロイ すべてのホストで vSAN ESA を設定 必要なすべてのポートグループを持つ分散仮想スイッチを作成 ヒント: CLI は 10 分ごとにインストーラーをポーリングして進捗を報告します。bringup が失敗した場合は、 https://<sddcm_name>.<your-fqdn> のインストーラー UI でタスクごとの詳細なエラー情報を確認してください。 インストーラーのクリーンアップ bringup が正常に完了した後、自動化は 2 つのクリーンアップステップを実行します。 remove-installer-datastore :ローカル VMFS データストアに配置された VM を vSAN にストレージ vMotion し、データストアをホストからアンマウントします。これにより EBS ボリュームが削除可能になります。 destroy-ebs-volume :EC2 インスタンスから EBS ボリュームをデタッチして削除します。ボリュームは ManagedBy タグと EnvironmentId タグで識別され、他のボリュームを誤って削除しないよう保護されています。 NSX Edge Cluster のデプロイ 次のサブステップは NSX Manager と vCenter API を直接使用して NSX Edge Cluster をデプロイします。7 つのステージで実行されます。 prep-edge-cluster :DVS TRUNK ポートグループ、IP プール、アップリンクプロファイル、VLAN トランスポートゾーンを作成 deploy-edge-nodes :2 台のラージフォームファクター エッジトランスポートノードを作成(OVA デプロイをトリガー) create-edge-cluster :両方のトランスポートノードをグループ化するエッジクラスターを作成 create-tier0 :ロケールサービスと BGP を有効にした Tier-0 ゲートウェイを作成 create-tier1 :Tier-0 とエッジクラスターにアタッチした Tier-1 ゲートウェイを作成 configure-routing :アップリンクセグメント、インターフェース、BGP ネイバー、プレフィックスリスト、スタティックルート、再配布を設定 create-anti-affinity :エッジ VM 用の vCenter DRS アンチアフィニティルールを作成 各エッジアプライアンスは ASN 65000(NSX 側)と 65022(AWS 側)を使って 1 つの AWS Route Server エンドポイントと BGP ピアリングします。ピアリングが確立されると、NSX オーバーレイセグメントが VPC ルートテーブルにルートとして自動的にアドバタイズされます。 Ops Manager Connector 最後のサブステップは VCF Operations Manager コネクターを Amazon EVS に登録します。これは仮想マシンの Windows Server ライセンスエンタイトルメントの作成など、後続のアクションに使用されます。 環境変数の設定 フェーズ 3 では Secrets Manager に保存されていない 2 つのシークレットが必要です。手動手順で設定した VCF_INSTALLER_PASSWORD と、Broadcom ポータルから取得した VCF_DEPOT_TOKEN です。以下のコマンドで環境変数として設定します(Linux)。 # The admin@local password you set when deploying the installer OVA: read -rs VCF_INSTALLER_PASSWORD ; export VCF_INSTALLER_PASSWORD # Your Broadcom depot download token: read -rs VCF_DEPOT_TOKEN ; export VCF_DEPOT_TOKEN Windows の場合: # The admin@local password you set when deploying the installer OVA: $env:VCF_INSTALLER_PASSWORD = Read-Host -MaskInput "VCF Installer Password" # Your Broadcom depot download token: $env:VCF_DEPOT_TOKEN = Read-Host -MaskInput "VCF Depot Token" 他のパスワード(vCenter、NSX、SDDC Manager など)はすべて Secrets Manager から自動的に解決されます。 deploy-vcf-and-edge の実行 Python 環境を作成し、単一の CLI コマンドでフェーズ 3 のパイプライン全体をエンドツーエンドで実行します(Linux)。 cd Phase_3_VCF9/python python -m venv .venv source .venv/bin/activate pip install -r requirements.txt python -m src.main deploy-vcf-and-edge \ --installer-host sddcm.<your-fqdn> \ --target-version 9.1.0 \ --nsx-manager-host nsx.<your-fqdn> \ --vcenter-host vc.<your-fqdn> \ --aws-profile your-aws-profile Windows の場合: cd Phase_3_VCF9\python python -m venv .venv .venv\Scripts\Activate.ps1 pip install -r requirements.txt python -m src.main deploy-vcf-and-edge ` --installer-host sddcm.<your-fqdn> ` --target-version 9.1.0 ` --nsx-manager-host nsx.<your-fqdn> ` --vcenter-host vc.<your-fqdn> ` --aws-profile your-aws-profile デバッグ用に各サブステップを個別に実行することも可能ですが、単一コマンドによるアプローチを 強く推奨 します。 検証と次のステップ deploy-vcf-and-edge が正常に返ったら、以下でエンドツーエンドの接続性を確認してください。 Route Server BGP ピアの確認 AWS コンソールで VPC > Route Servers > Peers に移動してください。両方の BGP ピアの BGP ステータスが “Up” になっているはずです。”学習済みルート” タブには NSX から発信された CIDR が表示されます(後で作成したセグメントも自動的にここに表示されます)。 テスト用 NSX セグメントの作成 NSX Manager( https://nsx.<your-fqdn> )にログインし、Tier-1 ゲートウェイにアタッチした新しいオーバーレイセグメントを作成します。 Networking > Segments > Add Segment に移動する セグメント名を test-segment とする 作成した Tier-1 ゲートウェイにセグメントをアタッチする サブネットを設定する(例:ゲートウェイ 172.16.1.1 の 172.16.1.0/24) このセグメントにテスト VM をデプロイし、IP アドレスを受け取ることを確認する オーバーレイからアンダーレイへの接続確認 NSX セグメント上のテスト VM から以下を実行します。 パブリックサブネット内のジャンプボックスに Ping する(デプロイ済みの場合) サービスアクセスサブネット内の EC2 インスタンスに Ping する セグメント CIDR(172.16.1.0/24)が VPC ルートテーブルに学習済みルートとして表示されることを確認する 3 つの確認がすべて成功すれば、NSX オーバーレイと AWS アンダーレイ間の動的なルート伝播が確立された、完全に動作する EVS 環境の構築完了です。 次のステップ: VMware HCX やその他の移行ツールを使用してオンプレミスからワークロードを移行する データセンターへのハイブリッド接続のために AWS Transit Gateway を設定する(フェーズ 1 でオプションとして作成しなかった場合) 補足ストレージとして Amazon FSx for NetApp ONTAP をセットアップする 運用監視のために AWS Systems Manager を統合する SDDC Manager 経由で追加のワークロードドメインをデプロイする トラブルシューティング よくある障害パターンと解決策を以下にまとめます。 フェーズ 1 および 2:AWS リソースエラー 対象リソースの AWS コンソールを確認してください。Terraform または boto3 のエラーテキストは通常、正しくプロビジョニングされなかった特定のリソース(AZ の誤り、サブネットの欠落、API コールのスロットリング)を指しています。 サービス制限を確認してください:EVS サービスクォータ、Dedicated Host の割り当て、VPC Route Server クォータ、EBS ボリューム制限。 ホストの実現がタイムアウトした場合(90 分超)、コンソールで EVS 環境のステータスを確認してください。FAILED 状態はキャパシティまたは設定の問題を示します。 フェーズ 3:デポの同期問題 Broadcom デポトークンが有効で有効期限が切れていないことを確認してください インストーラーアプライアンスが depot.broadcom.com へのアウトバウンドインターネットアクセス(ポート 443)を持つことを確認してください ダウンロードが止まった場合、自動化は指数バックオフで再試行し、5 回失敗後にエラーを報告します。その場合は download-all-product-binaries --wait で手動再試行できます フェーズ 3:Bringup の失敗 VCF Installer UI( https://<sddcm_name>.<your-fqdn> )はタスクごとの進捗と詳細なエラーメッセージを提供しており、API エラー本文よりも参考になります。 スタックトレースについてはインストーラーアプライアンスの /var/log/vmware/vcf/domainmanager/domainmanager.log を確認してください。 bringup が “VMwareProductVersion can not be null or empty” で失敗した場合、デポに必要なバイナリがキャッシュされていません。 prepare-depot を再実行して再試行してください。 フェーズ 3:Edge Cluster のデプロイ vSAN データストアに十分な空き容量がない場合、エッジノードのデプロイが失敗することがあります(各エッジ VM には約 200 GB が必要) BGP ピアリングの問題:エッジクラスター仕様の Route Server エンドポイント IP がフェーズ 1 の Terraform 出力と一致することを確認してください configure-routing が失敗した場合、クラスターが作成される前に両方のエッジノードが Success 状態に達していることを確認してください ヒント :自動化の各アクションはべき等です。ステップが失敗した場合は、根本的な問題を修正して同じコマンドを安全に再実行できます。副作用は発生しません。 クリーンアップ 構築した環境を解体する場合は、以下の順序で実施してください。 EVS 内の仮想マシンを保持したい場合は、まず別の場所に移行してください。 AWS コンソールから EVS ホストを 1 台ずつ削除してください。 AWS コンソールから EVS 環境を削除してください。 フェーズ 1 を実行した元のワークステーションから Phase_1_Base_Infrastructure ディレクトリに移動し、 terraform destroy を実行してください。Terraform は既存の状態ファイルを使用してすべてのフェーズ 1 リソースを特定して削除します。 まとめ 本ウォークスルーでは、3 フェーズの自動化ツールキットを使って VMware Cloud Foundation 9.1 を Amazon EVS にデプロイしました。空の AWS アカウントから出発し、Terraform で前提条件となる AWS インフラストラクチャをすべてプロビジョニングし、AWS API 経由の Python CLI で EVS 環境とベアメタルホストをデプロイし、公式 VCF Python SDK を使って NSX Edge Cluster ルーティングを含む完全な VCF スタックを立ち上げました。 terraform apply から BGP ピアリングの確認まで、デプロイ全体でおよそ 3.5〜6 時間かかります。自動化により、作業は 3 つの CLI コマンドと ESXi 設定・OVA デプロイに必要な約 30 分の手作業のみに削減されます。 このアプローチの主な利点: Infrastructure as Code:すべてのリソースが Terraform または Python 自動化で定義されています デフォルトセキュア:すべての VCF パスワードが厳格な複雑性要件に従って生成され、AWS Secrets Manager に保存されます 安全な再実行:フェーズ 1 と 3 は完全にべき等であり、デバッグや復旧のためにどのステップも副作用なく安全に再実行できます SDK 駆動:フェーズ 3 は公式 VMware VCF Python SDK を使用し、手動による REST ペイロードの構築を排除します モジュラー設計:1 つのコマンドでパイプライン全体を実行するか、デバッグや部分的な再実行のために個別ステップを実行できます 開発環境のセットアップ、本番環境のデプロイ、顧客デモ用の再現可能なラボなど、あらゆるユースケースに対応できる堅牢な基盤として、バージョン管理・カスタマイズ・拡張が可能です。 著者について David Piet Amazon Web Services (AWS) のプリンシパルソリューションアーキテクトとして、VMware ベースのワークロードを持つエンタープライズのお客様が AWS へ移行し、モダナイズするための支援を担っています。2018 年にパートナーソリューションアーキテクトとして AWS に入社し、VMware Cloud on AWS を専門としてきました。以来、AWS 最大規模のエンタープライズ顧客を数多くサポートし、VMware ベースのワークロードのクラウド移行と最適化戦略を牽引してきました。長年にわたり、AWS での VMware ビジネスの成長とともに担当範囲を広げ、現在もお客様のモダナイゼーションジャーニーの支援に深く関わっています。ノースウェスタン大学で博士号を取得しており、応用数学とエンジニアリングを専門としています。AWS 入社前はシリコンバレーのストレージスタートアップでソフトウェアデベロッパーとして勤務していました。 翻訳はパートナーソリューションアーキテクト 豊田が担当しました。原文は こちら です。
はじめに こんにちは、NTT西日本の中川です。 本記事では、IndexedDB(保存)と Transformers.js(計算)を組み合わせ、ブラウザ内でメモの意味検索までのLocal-First AI入門を、ミニサンプルを用いて紹介します。 本記事は2026年6月時点の情報に基づきます。 生成AIが身近になった一方で、「APIにすべて任せればよいのでは?」と感じる方も多いと思います。 私も最初はそう思っていました。ただ、議事メモをクラウドに貼るたびに「この本文、送ってよいのかな」と迷う場面があり、そのたびに判断するのは負担に感じたので、ブラウザ内で完結する検索を試してみたところ、それなりに使い所があるように感じたため、サンプルを交えてご紹介します。 対象読者 LocalStorageやIndexedDBの基本的な使い方は理解している方 AIをアプリケーションに組み込みたいが、サーバー費用やプライバシー面に課題を感じている方 Local-First AI の有用性が抽象的で、自社プロダクトのどこで効くかイメージしづらい方 「AIにコードを書かせる」段階から、「AIをブラウザ環境へ最適化する」段階へステップアップしたい方 目次 はじめに 対象読者 目次 1. 背景・目的:なぜ今「Local-First AI」なのか? 1.1 Local-First AI とは何か 1.2 イメージしやすい Before / After(社内メモ検索の例) Before(クラウドAPI中心) After(Local-First 寄り) 1.3 本記事の知識が活きるユースケース 1.4 現実的な構成:ローカルとクラウドの役割分担 1.5 ベクトル検索を用いたアプリケーションが直面しやすい3つの課題 1.6 Local-First AIが向かないケース 2. 技術的アプローチ:IndexedDB(長期記憶)× Transformers.js(推論)× Worker(滑らかさ) 2.1 ブラウザを「AIの脳」にする:Transformers.jsと推論バックエンド(WASM/WebGPU) 2.2 IndexedDBを「AIの長期記憶」として使う 3. 設計上の前提 3.1 この記事で「守ること/守れないこと」 3.2 埋め込み化が遅い・動かないときの対処 4. 【実装】ブラウザだけで動く「ローカルAIメモ検索」サンプル 4.1 何を作るか(完成イメージ) 4.2 ファイル構成 4.3 試し方 4.3.1 デモデータと検索クエリ例 4.3.2 検索結果のスコアとコサイン類似度 判定の流れ(検索1回あたり) コサイン類似度(意味の近さ)とは スコアの見方 4.4 index.html(UI・Worker起動) 4.5 embedding.worker.js(CDN + IndexedDB + 検索) 4.6 動かない時のチェックポイント 4.7 つまずきポイント 5. まとめ:AIにコードを任せ、設計を握ることが大事 執筆者 参考資料・出典 商標 1. 背景・目的:なぜ今「Local-First AI」なのか? 1.1 Local-First AI とは何か Local-First AI は、「すべてのAIをブラウザだけで完結させる」ことと同義ではありません。クラウドのサーバーなどに頼るのではなく、自身のパソコンやスマートフォンなど手元の端末(ローカル環境)上でAIの処理や学習を完結させる考え方の事を指します。本記事で扱うのは、こんな設計です。 データ(メモ・ログ・下書き)はブラウザ内(IndexedDB)に置く 取得(埋め込み・類似検索・分類など)は端末側で回す 文章生成(長文の要約・高度な回答)は必要なときだけクラウドAPIへ寄せる つまりハイブリッドな構成です。「通常のLLMの圧倒的な賢さ」よりも、送りたくないデータを送らない/待ちを減らす/API代を抑える、といった要件にマッチする考え方になります。 1.2 イメージしやすい Before / After(社内メモ検索の例) 社内Webツールを想定した、体験の違いです。 Before(クラウドAPI中心) 議事メモ100件の中から検索したい 入力のたびに自前のバックエンドAPI経由で、Embeddingモデル(テキストや画像などの非構造化データを数値の配列(ベクトル)に変換する技術のこと)へ本文を送信し、埋め込み化 懸念: 情報持ち出しポリシー、ログ保存、API課金、回線が弱いと待ちが発生してしまう After(Local-First 寄り) メモ本文は IndexedDB に保存(ブラウザ内) 「先週の障害対応」などのクエリは端末内で埋め込み化 → 類似メモを上位表示 本文を外部へ送らない設計にしやすい(初回のモデル取得通信は別途発生) モデル取得後の検索は、埋め込み計算を端末側で実行し、サーバーへ本文を送らずに応答できる この記事のサンプルは After のうち「意味が近いメモを探す」部分だけです。要約やチャット生成までは扱わないので留意してください。 1.3 本記事の知識が活きるユースケース 次のようなユースケースでは本記事の考え方が活躍できるかなと思います。 ご自身のプロダクトと照らし合わせて検討してみてください。 シーン 困りごと Local-First でやりやすいこと 社内・個人のメモ・議事録の検索 クラウドLLMに貼ると情報管理が不安 本文を送らず意味検索(本記事のデモ) 回線が不安定な現場・店舗・イベント 回線が不安定でAPI待ちが致命的 モデル取得後はオフライン寄りで検索・分類 入力中の補助UI サーバー往復で入力の反応が鈍くなる Worker + ローカル推論で体感速度を改善 企画・検証フェーズ サーバー構築前に体験を試したい ローカルファイルで仮説検証 1.4 現実的な構成:ローカルとクラウドの役割分担 全部ローカルに寄せる必要はありません。役割を分けると設計しやすいです。 ブラウザ: 保存・検索・分類など「自分のデータにだけ効く処理」 クラウド: 品質最優先の生成、全社ナレッジ横断など「データを送ってもよい処理」 1.5 ベクトル検索を用いたアプリケーションが直面しやすい3つの課題 こうしたユースケースが効く背景として、次の3つがよく出てきます。 APIコスト: すべてのリクエストをサーバーへ送ると、ユーザー増・利用回数に伴いコストが膨らみやすい。 プライバシー: 議事メモ・顧客対応メモなど、外部サーバーへ送りたくないデータがある。 応答性(レイテンシー): ネットワーク経由の推論は、通常、待機時間が発生する。 IndexedDB(保存)と Transformers.js などを使った端末内推論(計算)を組み合わせると、これらを同時に緩和しやすい場面があります(端末性能・ブラウザ対応などの制約はあります)。 1.6 Local-First AIが向かないケース 万能ではないので、向かないケースも書いておきます。 高品質な長文生成が主目的 全端末で同じ品質を厳密に保証したい 社内文書を数百万件単位で横断検索したい(専用ベクトルDBが必要) モデル配布・更新を厳格に統制したい環境 こうした要件が主なら、サーバー側のRAG(検索拡張生成)やクラウドAPIを主役にし、ローカルは補助とする構成が現実的です。 今回のデモがやっているのは、これだけです。 自分のメモだけを、キーワード一致ではなく「意味が近い順」で探せる。メモ本文を自前APIへ送らずに済む。 RAG(検索拡張生成)でいうと、本デモは R(Retrieval=検索)だけです。LLM への渡し方や回答生成(G)は扱いません。 2. 技術的アプローチ:IndexedDB(長期記憶)× Transformers.js(推論)× Worker(滑らかさ) 先ほどの図で示した「ブラウザ内」を、具体的な技術要素に分解します。 2.1 ブラウザを「AIの脳」にする:Transformers.jsと推論バックエンド(WASM/WebGPU) 現代のブラウザはWebAssemblyやWebGPU APIを通じて、高速な演算を活用できる環境が増えてきました。ここにJavaScript向け推論ライブラリ(例:Transformers.js)を組み合わせると、Python環境や自前サーバーに依存せず、ブラウザ上で推論を実行できる構成を取りやすくなります。 サンプルは、Transformers.js を CDN(jsDelivr)から読み込むだけで動かします。ビルドツールは使いません。 Transformers.js は環境に応じて WebAssembly(WASM)や WebGPU などへフォールバックします。本記事のサンプルコードは、デフォルトの WASM バックエンドで推論します。 2.2 IndexedDBを「AIの長期記憶」として使う RAG(検索拡張生成)は「必要な知識を検索してから生成に渡す」考え方です。ここでは、ブラウザ内に保存したメモ群を埋め込み(ベクトル)化してIndexedDBに保存し、クエリに近いものを取り出す、というできるだけ軽めな構成にしています。 また、ブラウザでAI処理を動かす際のボトルネックになりやすいのが、メインスレッドの占有です。UI描画・入力・スクロールが詰まると、ユーザー体験に悪影響が出ます。メインスレッドと Worker で役割を分けます。 メインスレッド: UIの描画、ユーザー入力、結果表示 Web Worker: モデルロード、埋め込み計算、類似度計算、IndexedDBへの書き込み 3. 設計上の前提 3.1 この記事で「守ること/守れないこと」 「ローカルに保存する=安全」ではありません。ここは誤解が起きやすいので、前提を設けます。 守りやすいこと(設計でコントロールしやすい) ユーザーが入力したメモ本文を、自前APIへ送らない設計にできる APIコストを抑えやすい(端末内推論に寄せるほど) 守れないこと(別対策が必要) 端末のマルウェア感染、悪性ブラウザ拡張、端末盗難などはローカル保存だけでは防げない XSS(クロスサイトスクリプティング)で同一オリジン上のデータを読まれるリスクは致命的(CSP=Content Security Policy・サニタイズなどが前提) 初回だけ外部通信が発生する Transformers.js はモデル重みを CDN / Hugging Face から取得します(メモ本文そのものを送るわけではないが、通信自体は発生します) 社内プロキシやファイアウォールで外部 CDN が制限されている環境では、初回のモデル取得が失敗することがあります(別途ネットワーク設定の確認が必要) 3.2 埋め込み化が遅い・動かないときの対処 端末やブラウザによって、埋め込みの速さはかなり変わります。本記事のサンプルは、Transformers.js の既定どおり WASM(CPU上で動く方式)で動く前提です。WebGPU が使える環境では速くなることもありますが、必須ではありません。 困ったときは、この順で試すとよいです。 まず動かす — WASM で登録・検索まで通す(今回のデモはここまで) 余力があれば速くする — 端末が対応していれば WebGPU を使う(Transformers.js が自動で選ぶ) それでも厳しい — 意味検索をやめるのではなく、負荷を下げる(キーワード検索に切り替える、登録件数や topK を減らす など) 4. 【実装】ブラウザだけで動く「ローカルAIメモ検索」サンプル 4.1 何を作るか(完成イメージ) 1章の After を、HTML とJavaScriptの2ファイルで動かす段階です。メモ登録 → 埋め込み保存 → クエリで意味検索、という流れだけに絞っています。チャット回答や社外API連携は範囲外です。 4.2 ファイル構成 次の2ファイルを同じフォルダに置きます。 index.html … UI(メインスレッド) embedding.worker.js … 埋め込み化・IndexedDB・検索(Worker) 4.3 試し方 この章の index.html と embedding.worker.js のコードを、それぞれ同名ファイルとして同じフォルダに保存する Visual Studio Code の Live Server などで、そのフォルダを http://localhost:... 経由で開く( file:// で直接開くと Worker が動かないことが多いです) Google Chrome または Microsoft Edge で index.html を表示する 「デモデータを一括登録」でサンプルを入れる(初回はモデル取得で数十秒かかることがあります)。「登録」から1件ずつ足して試してもかまいません 下の検証表と結果を比べたいときだけ、件数が6件でなければ「すべて削除」→「デモデータを一括登録」で揃える 検証表のクエリで検索してみる(メモが6件未満でも検索は動きます。表示件数は、登録件数と5件の少ない方まで) 動作確認の目安: 検証表を使う場合、各クエリで「期待される上位デモデータ」が1位、環境によっては2位以内なら意図どおりです。利用モデルは Xenova/all-MiniLM-L6-v2 で、英語向けが主です。日本語のデモでも動きますが、順位は端末やクエリで前後することがあります。結果の数値(例: 0.165 )の読み方は、続く「検索結果のスコアとコサイン類似度」を見てください。Transformers.js は @xenova/transformers@2.17.2 (CDN)で固定しています。 4.3.1 デモデータと検索クエリ例 「デモデータを一括登録」すると6件入ります。6件しか扱えないわけではなく、下の検証表がこの6件を想定しているだけです。 一括登録されるデモデータ [障害対応] 2026-03-15 認証基盤タイムアウトの件。原因はDBのコネクションプール枯渇。再発防止策としてリトライ上限を3回に変更し、アラート閾値を調整した。 [手順] 社外からのVPN接続について。社外ネットワークからはポータル経由でのみアクセス可能。パスワードはITサポートにチケットを発行して取得すること。 [顧客A社] 契約上の特記事項:ログに個人情報(氏名・メアド)を一切残さないこと。問い合わせ対応は専任のサポート窓口を経由すること。 [議事録] 4/1 開発定例。次期リリースの目玉はRAG機能の強化。インフラ費用を抑えるため、一部の検索処理をフロントエンド(ブラウザ側)にオフロードする方針で合意。 [技術メモ] 埋め込み推論は Transformers.js(WASM)が既定。端末が対応していれば WebGPU バックエンドで GPU 推論に切り替え可能。 [オンボーディング] 新入社員向けPCセットアップ手順。初期パスワードは入社初日に配布される資料を参照。まずはセキュリティ研修動画の視聴を完了させること。 検索クエリと期待される上位デモデータ 検索クエリ 期待される上位デモデータ 先週の認証エラーの再発防止 メモ1 社外から社内システムに入る メモ2 個人情報の取り扱いルール メモ3 コスト削減の設計 メモ4 GPU 推論 メモ5 新人の初期設定 メモ6 登壇 該当メモなし(語句一致なし・相対順位のみ) 4.3.2 検索結果のスコアとコサイン類似度 検索結果は、だいたい次の形で出ます。 0.165 (意味 0.165) [語句一致 +0.12] - [id=5] [技術メモ] 埋め込み推論は Transformers.js(WASM)が既定。… 0.411 (意味 0.411) [語句一致なし] - [id=14] [手順] 社外からのVPN接続について。… 先頭の 0.165 は最終スコアで、括弧内の 意味 0.165 はコサイン類似度(意味の近さ)だけを抜き出した値です。 [語句一致 +0.12] はクエリ文字列が本文に含まれるときの加点、 [語句一致なし] は加点が付いていないことを示します。サンプルでは searchScore でスコアを作り、降順に並べています。 判定の流れ(検索1回あたり) 「検索」ボタンを押してから結果が並ぶまでの流れです(Web Worker 内で完結し、メモ本文は外部 API へ送りません)。 topK(トップケイ)は、スコア順の上位K件だけ返す、という意味です。K は返す件数で、変数名として topK と書くことが多いです。本サンプルでは worker.postMessage({ type: 'search', payload: { query, topK: 5 } }) のとおり 5件です。 メモ1件あたりのスコア計算( searchScore )は、意味の近さ(ベクトル)と語句一致(文字列)を組み合わせています。 登録時は逆方向です。メモ本文を埋め込み化して text と vector を IndexedDB に保存します(検索のたびに再計算しません)。 コサイン類似度(意味の近さ)とは Transformers.js のような埋め込みモデルは、メモ本文も検索語もベクトル(数値の並び)に変換します。似た意味の文は、似たベクトルになります。意味で探すときは、ベクトル同士の近さで並べるのが一般的です。 その近さを測る指標がコサイン類似度です。画面の (意味 0.xxx) がこれで、おおむね 0〜1 の範囲です(1に近いほど意味が近い)。検索の方法自体は他にもあり、語が本文に含まれるかだけを見るキーワード検索(Google検索に近い)もよく使われます。本デモは意味検索を主にし、語句一致は [語句一致] の小幅な加点として足しています。そのため「障害」「タイムアウト」といった語が本文に無くても、内容が近ければ上位に来ます。 最終スコアは、コサイン類似度に、検索語が本文にそのまま含まれるときの小幅な加点( [語句一致] )を足したものです。 スコアの見方 スコアを読むときは、次の3つを見れば十分です。 順位:0.05 でも 0.4 でも、数値より並びが自然かを見る [語句一致] :付いていれば、検索語が本文に含まれている。無ければコサイン類似度だけで並んでいる 件数:最大5件まで出るが、登録が3件なら3件まで。近い順に並んでいるだけのことがある 検証表どおり6件入れた状態で 登壇 を検索すると、 0.411 などの数値が出て「見つかったのかな?」と迷いました。どのメモにもその語はなく、すべて [語句一致なし] でした。数字が付いていても、登録されているメモのうち相対的に近かっただけ、という意味です。画面下の注意文と (意味 0.xxx) もあわせて見てください。 4.4 index.html (UI・Worker起動) メインスレッドは UI と Worker へのメッセージ送受信だけを担当します。 <!doctype html> < html lang = "ja" > < head > < meta charset = "UTF-8" /> < meta name = "viewport" content = "width=device-width, initial-scale=1.0" /> < title > Local-First AI Memo Search </ title > < style > body { font-family : system-ui , sans-serif ; max-width : 720px ; margin : 2rem auto ; padding : 0 1rem ; line-height : 1.6 ; } textarea , input { width : 100% ; box-sizing : border-box ; } button { margin-top : 0.5rem ; } #status { min-height : 1.5rem ; color : #333 ; margin-top : 1rem ; } ol , ul { padding-left : 1.25rem ; } #memos li { margin-bottom : 0.5rem ; font-size : 0.95rem ; } #memos .meta { color : #666 ; font-size : 0.85rem ; } .note { font-size : 0.9rem ; color : #555 ; background : #f6f6f6 ; padding : 0.75rem 1rem ; border-radius : 6px ; } </ style > </ head > < body > < h1 > Local-First AI Memo Search </ h1 > < section > < h2 > メモ登録 </ h2 > < textarea id = "memo" rows = "4" placeholder = "メモを入力" ></ textarea > < button id = "add" type = "button" > 登録 </ button > < button id = "loadDemo" type = "button" style = "margin-left: 1rem;" > デモデータを一括登録 </ button > < button id = "clearAll" type = "button" style = "margin-left: 1rem;" > すべて削除 </ button > </ section > < section > < h2 id = "memosHeading" > 登録済みメモ(読み込み中…) </ h2 > < ul id = "memos" ></ ul > </ section > < section > < h2 > 検索 </ h2 > < input id = "q" type = "search" placeholder = "例: WebGPU 推論 / 社外から社内システム / 登壇(無関係クエリの例)" /> < button id = "go" type = "button" > 検索 </ button > < p class = "note" style = "margin-top: 0.5rem;" > 結果は < code > 最終スコア (意味スコア) [語句一致] </ code > の順です。語句一致が無いときは、 登録メモの中でベクトルが相対的に近い順に並んでいるだけで、必ずしも関連メモがあるわけではありません。 </ p > < ol id = "results" ></ ol > </ section > < div id = "status" aria-live = "polite" ></ div > < script type = "module" > const memoEl = document . getElementById ( 'memo' ) ; const addBtn = document . getElementById ( 'add' ) ; const loadDemoBtn = document . getElementById ( 'loadDemo' ) ; const clearAllBtn = document . getElementById ( 'clearAll' ) ; const memosHeading = document . getElementById ( 'memosHeading' ) ; const memosEl = document . getElementById ( 'memos' ) ; const qEl = document . getElementById ( 'q' ) ; const goBtn = document . getElementById ( 'go' ) ; const resultsEl = document . getElementById ( 'results' ) ; const statusEl = document . getElementById ( 'status' ) ; const worker = new Worker ( './embedding.worker.js' , { type : 'module' }) ; function refreshMemos () { worker . postMessage ({ type : 'listMemos' }) ; } function renderMemos ( memos ) { memosHeading . textContent = `登録済みメモ( ${ memos . length } 件)` ; memosEl . innerHTML = '' ; if ( memos . length === 0 ) { const li = document . createElement ( 'li' ) ; li . textContent = 'まだメモは登録されていません。「デモデータを一括登録」で試せます。' ; memosEl . appendChild ( li ) ; return; } for ( const m of memos ) { const li = document . createElement ( 'li' ) ; const meta = document . createElement ( 'div' ) ; meta . className = 'meta' ; meta . textContent = `id= ${ m . id } ` ; li . appendChild ( meta ) ; li . appendChild ( document . createTextNode ( m . text )) ; memosEl . appendChild ( li ) ; } } worker . onmessage = ( ev ) => { const { type , payload } = ev . data ; if ( type === 'listMemosResult' ) { renderMemos ( payload . memos ) ; return; } if ( type === 'addMemoResult' ) { statusEl . textContent = `登録しました(id= ${ payload . id } )` ; refreshMemos () ; return; } if ( type === 'loadDemoResult' ) { statusEl . textContent = `デモデータ( ${ payload . count } 件)を一括登録しました(既存データは置き換え済み)。` ; refreshMemos () ; return; } if ( type === 'clearAllResult' ) { statusEl . textContent = 'すべてのメモを削除しました。' ; refreshMemos () ; return; } if ( type === 'searchResult' ) { resultsEl . innerHTML = '' ; if ( payload . items . length === 0 ) { const li = document . createElement ( 'li' ) ; li . textContent = '該当するメモがありません' ; resultsEl . appendChild ( li ) ; statusEl . textContent = '' ; } else { const anyKeyword = payload . items . some (( it ) => it . keywordMatch ) ; for ( const it of payload . items ) { const li = document . createElement ( 'li' ) ; const kwTag = it . keywordMatch ? ` [語句一致 + ${( it . keywordBoost ?? 0 . 12 ) . toFixed ( 2 )} ]` : ' [語句一致なし]' ; li . textContent = ` ${ it . score . toFixed ( 3 )} (意味 ${ it . semantic . toFixed ( 3 )} ) ${ kwTag } - [id= ${ it . id } ] ${ it . text } ` ; resultsEl . appendChild ( li ) ; } if ( ! anyKeyword ) { statusEl . textContent = 'いずれも語句一致なしです。表示は topK=5 件の「相対順位」であり、クエリに近いメモが無い場合でもスコア付きで並びます。本番ではスコア閾値やメタデータ絞り込みを検討してください。' ; } else { statusEl . textContent = '' ; } } return; } if ( type === 'error' ) { statusEl . textContent = `エラー: ${ payload . message } ` ; } } ; addBtn . addEventListener ( 'click' , () => { const text = memoEl . value . trim () ; if ( ! text ) return; statusEl . textContent = '登録中...(初回はモデル取得のため数十秒かかる場合があります)' ; worker . postMessage ({ type : 'addMemo' , payload : { text } }) ; memoEl . value = '' ; }) ; loadDemoBtn . addEventListener ( 'click' , () => { statusEl . textContent = 'デモデータを登録中...(初回はモデル取得のため数十秒かかる場合があります)' ; worker . postMessage ({ type : 'loadDemoData' }) ; }) ; clearAllBtn . addEventListener ( 'click' , () => { if ( ! confirm ( '登録済みメモをすべて削除します。よろしいですか?' )) return; worker . postMessage ({ type : 'clearAllMemos' }) ; }) ; goBtn . addEventListener ( 'click' , () => { const query = qEl . value . trim () ; if ( ! query ) return; resultsEl . innerHTML = '' ; statusEl . textContent = '検索中...' ; worker . postMessage ({ type : 'search' , payload : { query , topK : 5 } }) ; }) ; refreshMemos () ; </ script > </ body > </ html > 4.5 embedding.worker.js (CDN + IndexedDB + 検索) Worker 側で Transformers.js(CDN)を読み込み、IndexedDB(生API)へ保存・検索します。 import { pipeline } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2' ; const DB_NAME = 'AI_Memory' ; const DB_VERSION = 1 ; const STORE_NAME = 'memos' ; const MODEL_ID = 'Xenova/all-MiniLM-L6-v2' ; let embedder ; // --- IndexedDB(生API) --- function openDb () { return new Promise (( resolve , reject ) => { const req = indexedDB . open ( DB_NAME , DB_VERSION ) ; req . onupgradeneeded = ( event ) => { const db = event . target . result ; if ( ! db . objectStoreNames . contains ( STORE_NAME )) { const store = db . createObjectStore ( STORE_NAME , { keyPath : 'id' , autoIncrement : true , }) ; store . createIndex ( 'timestamp' , 'timestamp' ) ; } } ; req . onsuccess = () => resolve ( req . result ) ; req . onerror = () => reject ( req . error ) ; }) ; } function dbAdd ( record ) { return openDb () . then ( ( db ) => new Promise (( resolve , reject ) => { const tx = db . transaction ( STORE_NAME , 'readwrite' ) ; const req = tx . objectStore ( STORE_NAME ) . add ( record ) ; req . onsuccess = () => resolve ( req . result ) ; req . onerror = () => reject ( req . error ) ; tx . oncomplete = () => db . close () ; }) , ) ; } function dbGetAll () { return openDb () . then ( ( db ) => new Promise (( resolve , reject ) => { const tx = db . transaction ( STORE_NAME , 'readonly' ) ; const req = tx . objectStore ( STORE_NAME ) . getAll () ; req . onsuccess = () => resolve ( req . result ) ; req . onerror = () => reject ( req . error ) ; tx . oncomplete = () => db . close () ; }) , ) ; } function dbClearAll () { return openDb () . then ( ( db ) => new Promise (( resolve , reject ) => { const tx = db . transaction ( STORE_NAME , 'readwrite' ) ; const req = tx . objectStore ( STORE_NAME ) . clear () ; req . onsuccess = () => resolve () ; req . onerror = () => reject ( req . error ) ; tx . oncomplete = () => db . close () ; }) , ) ; } async function listMemosForUi () { const all = await dbGetAll () ; return all . map (( m ) => ({ id : m . id , text : m . text , timestamp : m . timestamp })) . sort (( a , b ) => b . timestamp - a . timestamp ) ; } // --- ベクトルユーティリティ --- function float32ToBuffer ( f32 ) { return f32 . buffer . slice ( f32 . byteOffset , f32 . byteOffset + f32 . byteLength ) ; } function bufferToFloat32 ( buf ) { return new Float32Array ( buf ) ; } function cosineSimilarity ( a , b ) { if ( a . length ! == b . length ) throw new Error ( 'ベクトル長が一致しません' ) ; let dot = 0 , na = 0 , nb = 0 ; for ( let i = 0 ; i < a . length ; i ++ ) { dot += a [ i ] * b [ i ] ; na += a [ i ] * a [ i ] ; nb += b [ i ] * b [ i ] ; } const denom = Math . sqrt ( na ) * Math . sqrt ( nb ) ; return denom === 0 ? 0 : dot / denom ; } /** 短いクエリ向け: 本文にクエリ語が含まれるときだけ小幅ブースト(ハイブリッド検索の簡易版) */ function searchScore ( query , qvec , memo ) { const semantic = cosineSimilarity ( qvec , bufferToFloat32 ( memo . vector )) ; const q = query . trim () . toLowerCase () ; if ( q . length < 2 ) { return { score : semantic , semantic , keywordMatch : false , keywordBoost : 0 } ; } const text = memo . text . toLowerCase () ; const keywordMatch = text . includes ( q ) ; const keywordBoost = keywordMatch ? 0 . 12 : 0 ; return { score : semantic + keywordBoost , semantic , keywordMatch , keywordBoost , } ; } // --- 埋め込み(Transformers.js / デフォルトは WASM バックエンド) --- async function getEmbedder () { if ( embedder ) return embedder ; embedder = await pipeline ( 'feature-extraction' , MODEL_ID ) ; return embedder ; } async function embedText ( text ) { const e = await getEmbedder () ; const out = await e ( text , { pooling : 'mean' , normalize : true }) ; const data = out ?. data ?? out ; if ( data instanceof Float32Array ) return data ; if ( Array . isArray ( data )) return new Float32Array ( data . flat ()) ; throw new Error ( '埋め込みベクトルの形式が想定外です' ) ; } // --- メッセージ処理 --- self .onmessage = async ( ev ) => { try { const { type , payload } = ev . data ; if ( type === 'listMemos' ) { const memos = await listMemosForUi () ; self . postMessage ({ type : 'listMemosResult' , payload : { memos } }) ; return; } if ( type === 'clearAllMemos' ) { await dbClearAll () ; self . postMessage ({ type : 'clearAllResult' , payload : {} }) ; return; } if ( type === 'loadDemoData' ) { await dbClearAll () ; const demoMemos = [ "[障害対応] 2026-03-15 認証基盤タイムアウトの件。原因はDBのコネクションプール枯渇。再発防止策としてリトライ上限を3回に変更し、アラート閾値を調整した。" , "[手順] 社外からのVPN接続について。社外ネットワークからはポータル経由でのみアクセス可能。パスワードはITサポートにチケットを発行して取得すること。" , "[顧客A社] 契約上の特記事項:ログに個人情報(氏名・メアド)を一切残さないこと。問い合わせ対応は専任のサポート窓口を経由すること。" , "[議事録] 4/1 開発定例。次期リリースの目玉はRAG機能の強化。インフラ費用を抑えるため、一部の検索処理をフロントエンド(ブラウザ側)にオフロードする方針で合意。" , "[技術メモ] 埋め込み推論は Transformers.js(WASM)が既定。端末が対応していれば WebGPU バックエンドで GPU 推論に切り替え可能。" , "[オンボーディング] 新入社員向けPCセットアップ手順。初期パスワードは入社初日に配布される資料を参照。まずはセキュリティ研修動画の視聴を完了させること。" ] ; for ( const text of demoMemos ) { const vec = await embedText ( text ) ; await dbAdd ({ text , vector : float32ToBuffer ( vec ) , timestamp : Date . now () , model : MODEL_ID , }) ; } self . postMessage ({ type : 'loadDemoResult' , payload : { count : demoMemos . length } }) ; return; } if ( type === 'addMemo' ) { const { text } = payload ; const vec = await embedText ( text ) ; const id = await dbAdd ({ text , vector : float32ToBuffer ( vec ) , timestamp : Date . now () , model : MODEL_ID , }) ; self . postMessage ({ type : 'addMemoResult' , payload : { id } }) ; return; } if ( type === 'search' ) { const { query , topK = 5 } = payload ; const qvec = await embedText ( query ) ; const all = await dbGetAll () ; const items = all . map (( m ) => { const { score , semantic , keywordMatch , keywordBoost } = searchScore ( query , qvec , m , ) ; return { id : m . id , text : m . text , score , semantic , keywordMatch , keywordBoost , } ; }) . sort (( a , b ) => b . score - a . score ) . slice ( 0 , topK ) ; self . postMessage ({ type : 'searchResult' , payload : { items } }) ; return; } self . postMessage ({ type : 'error' , payload : { message : `不明な操作です: ${ type } ` } }) ; } catch ( e ) { self . postMessage ({ type : 'error' , payload : { message : String ( e ?. message ?? e ) } }) ; } } ; 4.6 動かない時のチェックポイント 「検索結果がおかしい」「いつも同じ」ように見えるときは、よくあるのは次のどれかです。 Workerが動いていない( type: 'module' を忘れている、 file:// で開いている) out.data の形が想定と違う( embedText の分岐で吸収) ベクトルの正規化が効いていない( normalize: true を確認) 保存した vector の復元が壊れている( ArrayBuffer の扱いミス) 4.7 つまずきポイント IndexedDBの永続性: ブラウザ設定やストレージ圧迫状況によっては、データが削除される可能性があります。 初回のモデルロード: 初回はモデル取得で時間がかかります。ローディング表示を出しておくと安心です。 端末差: 推論速度(WASM/WebGPUの処理能力)は端末依存です。フォールバックや件数上限(例: 最大100件)を検討してください。 件数が増えると重くなる: 本サンプルは IndexedDB から全件取得し、1件ずつスコア計算します(6件のデモでは問題になりにくい)。数百件以上では索引付きベクトルDBが必要になります。 5. まとめ:AIにコードを任せ、設計を握ることが大事 「AIにIndexedDBのコードを書いて」と言えば、一瞬で動くものが手に入る時代です。ただ、今回のデモでも、語句一致なしでも特定の件数表示されるといった挙動や、初回だけモデル取得が走る点は、コードを書くだけでは見落としやすいと感じました。設計側(制約、フォールバック、UX)を握る価値は、むしろ大きくなっていると思います。 LocalStorage は一時的な設定、IndexedDB はメモとベクトルの置き場、Transformers.js と Web Worker は推論の実行場所、と分けて考えるとよいです。 HTML 2ファイルとブラウザだけで、保存・埋め込み・類似検索の流れは試せます。動かしたあと、「うちのプロダクトで、外部に送りたくないデータは何か?」をチームで1つ決めてみてください。ローカルに寄せる範囲の判断が、だいぶはっきりしてきます。 ぜひいろいろ試してみてくださいね。 執筆者 中川 拓哉(NTT西日本 デジタル革新本部 デジタル改革推進部所属) NTT西日本のWebアプリケーションの開発・運営に従事。 好きな技術スタック:TypeScript, Vue.js, GraphQL, Laravel 参考資料・出典 本記事を執筆するにあたり、以下のサイトを参考にしました。 MDN Web Docs: IndexedDB( https://developer.mozilla.org/docs/Web/API/IndexedDB_API ) MDN Web Docs: WebGPU( https://developer.mozilla.org/docs/Web/API/WebGPU_API ) MDN Web Docs: Web Workers( https://developer.mozilla.org/docs/Web/API/Web_Workers_API ) Transformers.js( https://github.com/huggingface/transformers.js ) jsDelivr CDN( https://www.jsdelivr.com/ ) 商標 「Google Chrome」は、Google LLCの商標または登録商標です。 「Microsoft Edge」は、Microsoft Corporationの商標または登録商標です。 「Visual Studio Code」は、Microsoft Corporationの商標または登録商標です。 「Firefox」は、Mozilla Foundationの商標または登録商標です。 「Node.js」は、OpenJS Foundationの商標または登録商標です。 「Hugging Face」は、Hugging Face, Inc.の商標または登録商標です。 Transformers.js は、Hugging Face, Inc.が提供するライブラリです。 記載の会社名・製品名は、それぞれ各社の商標または登録商標です。
こんにちは! Principal Generative AI Engineerの森田です。私の所属するAIファーストGでは、社内の生成AI活用にとどまらず、販売店やトヨタグループにおけるAI活用支援を行っております。 社内でAIエージェントの活用が進む中、ある部署から「AIエージェント開発に取り組みたいが、どこから始めていいかわからない」と相談をもらいました。座学だけでは手触り感が得られないので、実際に手を動かすワークショップ形式で開催することにしました。 弊社はAWSを主なクラウド基盤としているため、AWSが提供するAIエージェントの構築・運用基盤である Amazon Bedrock AgentCore と、エージェントのツール連携プロトコルである MCP(Model Context Protocol) を中心テーマに据えました。 教材として使用した書籍 ゼロから資料を作るよりも、体系的にまとまった書籍のハンズオンをベースにした方が、参加者が後から復習しやすいと考えました。今回は以下の2冊を使用しています。 『 Amazon Bedrock AgentCore 実践入門 ── Strands Agentsで構築するAIエージェント 』 (SBクリエイティブ) ※以下、AgentCore本 『 AWSではじめるMCP実践ガイド ── 基礎からAIエージェント構築まで徹底解説 』 (技術評論社) ※以下、MCP本 AgentCore本でエージェントフレームワークであるStrands AgentsやAgentCoreの各機能を学び、MCP本でMCPサーバーの自作とAgentCore Gatewayによるツール統合を学ぶ、という組み合わせです。 実は、どちらも私が共著者として執筆に携わった書籍です。手前味噌で恐縮なのですが、中身を隅々まで把握できている分、参加者がハンズオンで詰まったときにも補足しやすく、教材として選びました。 開発環境 今回は環境を揃えるため、 GitHub Codespaces 上に開発環境を用意しました。書籍の手順をベースに、以下の工夫で環境構築にかかる時間を短縮しています。 ツール管理は mise に統一し、 mise use aws-cli node uv claude でAWS CLI・Node.js・uv・Claude Codeを一括導入 AWSへは aws configure sso でSSOログイン コーディングのお供として Claude Code をセットアップ サンプルコードは書籍著者が公開しているGitHubリポジトリを git clone して参照できるようにし、すべてのコードを手打ちしなくていいようにしました。 ワークショップの構成 1日のワークショップとして、午前・午後に分けて以下の流れで進めました。 午前の部(1.5時間)・・・Strands Agentsに触れる # タイトル 書籍・該当箇所 概要 1 Strands Agents入門 AgentCore本 3.4節 ツール定義、エージェント作成、会話ループの実装など基本を体験 2 リサーチエージェントの構築 AgentCore本 4章 Webから情報収集・要約するエージェントを構築し、ツール連携の開発フローを掴む 任意 AgentCoreハーネス AgentCore本 5.2節 時間に余裕がある人向けに、AgentCoreハーネスを体験 午後の部(3時間)・・・AgentCoreとMCPを実践する # タイトル 書籍・該当箇所 概要 3 AgentCoreランタイム入門 AgentCore本 5.3節 agentcore create → agentcore deploy でエージェントをクラウドにデプロイ 4 アンビエントエージェントの構築 AgentCore本 15章 S3への領収書アップロードをトリガーに、解析→Confluence記録→メール通知するイベント駆動型エージェントをCDKで構築 5 MCPサーバーの構築 MCP本 4.2節 自前のMCPサーバーとホストを実装 6 AgentCore Gateway MCP本 6.2節 MCPサーバーをAgentCore Gatewayに登録し、認証・認可を統合 社内実施にあたって変更が必要だったポイント 書籍のハンズオンをそのまま社内で実施するにあたり、いくつか環境差異への対応が必要でした。 :::message メールアドレスのエイリアス 15章のアンビエントエージェントでは、サンプルデータ内の複数ユーザーのメールアドレスにそれぞれ通知を送る設計になっています。しかし社内のメール環境ではエイリアスが使えなかったため、 data/users.json のメールアドレスをすべて自分のアドレスに統一しました。その結果、同じアドレスでSNSサブスクリプションが重複してしまうため、 chapter15/cdk/lib/expense-agent-stack.ts で重複を排除するよう修正しました。 + const uniqueEmails = [...new Set(emailAddresses)]; - emailAddresses.forEach((email, i) => { + uniqueEmails.forEach((email, i) => { // 各アドレスごとに SNS サブスクリプションを作成 }); ::: :::message Googleカレンダー連携が使えない環境 13章のフルスタックエージェント構築ハンズオンはGoogleカレンダー連携(Google認証)を前提としているため、社内環境では実施が難しく割愛しました。興味がある方には自宅での実施を案内しています。 ::: :::message AWSアカウントの共有 複数人で1つのAWS環境を使ったため、リソース名が他の人と重複・混同しないよう、各自の名前を付与するルールにしました。特にS3バケット名はアカウント内で一意である必要があり、そのままだと衝突してしまいます。CDKスタックにハードコードされたバケット名( expense-agent-${account} )に自分の名前を付けて回避しました。AgentCoreランタイムも、 agentcore create で付ける名前を handsonmorita のように各自で区別できるものにしています。 ::: :::message alert リージョンの差異 AgentCore本とMCP本で、使用リージョンが異なっているため、単一リージョンで実施するには読み替えが必要でした。GitHubで公開されているサンプルソースを利用して進めていたのですが、どこを修正すればよいかが見落としやすいポイントでした。 ::: 参加者からもらった質問 ワークショップ中、参加者からいくつか印象的な質問がありました。 Q. ツールの定義はPythonでもできるのか? Strands Agentsのツール定義はPythonのデコレータ @tool で行えます。関数のdocstringがそのままツールの説明文(description)としてLLMに渡されるため、Pythonで完結します。MCPサーバーとして公開する場合もPython SDKが利用可能です。 Q. Swarmエージェントはどうやってタスクを振り分けているのか? マルチエージェント構成(Swarm)では、オーケストレーターとなるエージェントがLLMの判断に基づいて、各サブエージェントにタスクをルーティングします。各エージェントのnameやdescriptionに基づき、他のエージェントの役割を把握したうえで、作業を委譲するエージェントを選択します。 Q. マルチエージェントにするか、シングルエージェントにするか、どのように決めるのが良いか? 個人的な感覚としては、最初から厳密に切り分けようとしない方がうまくいきます。シングルエージェントとして作り始めて、複雑なタスクをうまく処理できなくなったり、コンテキストを分けた方が動きが安定すると感じた時点で、サブエージェントへの分割を考える、というのが実際の進め方に近いです。 Q. エージェントで使用するモデルはどのような基準で選んでいるのか? コストを最初から意識しすぎないことが大事だと思っています。Sonnetをベースにまず作ってみて、判断の精度が足りない部分はOpusに切り替え、単純作業の部分だけHaikuでコストを下げる、という順番です。先にHaikuでコストを抑えようとすると、実現できる機能のレベルが下がってしまうので、まずは「エージェントとして実現できるか」を確認してから、コスト最適化に移る方がスムーズに進みます。 参加者の声 ワークショップの締めに参加者で「振り返り会」を行い、そこで出た声の中から印象的だったものをいくつか紹介します。 ある参加者は、「エージェントの動きが、これまでふんわりとマジックのように感じていたのに、こういう仕組みで動いていたのかと腹落ちした」と話してくれました。難しさの本質はプロンプトの作り込みにある、という気づきを得られたようです。 別の参加者は、ハーネスの手軽さに驚いたそうです。テンプレートに「あなたはプロ野球に詳しい人です」のような一行を書くだけでエージェントが動いてしまうのを見て、拍子抜けするほど単純だったと話していました。 MCPサーバーの自作に取り組んだ参加者は、午後の中でも特に難しかったと振り返っていました。それでも自分の手で実装したことで、普段使っているMCPの裏側の動きまで想像できるようになった、という言葉が印象的でした。 これまで固定パイプライン的な実装(情報を取得し、LLMに渡し、整形して出力する)に慣れていたという参加者は、エージェントが入力に応じて処理のルートを動的に決めていく発想自体が新鮮だったと語っていました。同じ入力でも出力のパスが事前に決まっていない、というところに面白さを感じたそうです。 ワークショップを終えて 1日にかなりの内容を詰め込んだこともあり、進み具合には個人差が出ました。午前の任意項目だったAgentCoreハーネス(5.2節)まで到達できた方もいれば、そこまで手が回らなかった方もいます。午後はそれぞれの興味に合わせて、アンビエントエージェントに取り組むグループとMCPサーバー構築に取り組むグループに分かれて進めてもらいました。 今回のワークショップを通して、書籍のハンズオンをベースにしたことで準備の負担を抑えつつ、実践的な内容を届けられたのは良かったです。本記事が、これからAIエージェント開発に取り組む方や、同様のワークショップを企画する方の参考になれば幸いです。最後までお読みいただき、ありがとうございました。



















