Agent platform のための手始めとしてGKEでA2Aを動かしてみる
はじめに
ここ最近、AI エージェントプラットフォーム というキーワードをよく見かけるようになっています。具体的な動きを挙げると、以下のような流れがあります。
- Google が Gemini Enterprise Agent Platform を発表。Vertex AI のエージェント関連機能を発展・統合する形で、エージェントの構築・デプロイ・データ連携・ガバナンス・最適化までを 1 つの基盤に集約する方向を示しました(Google Cloud Blog)。
- Gartner の予測: 2027 年までに企業の 70% がマルチ AI エージェントを採用するとされ、市場規模も 2028 年には 150 億ドル規模に到達する見通し。国内でも『マルチ AI エージェント/マルチエージェント・プラットフォーム白書 2026 年版』が刊行されるなど、潮流が広がっています。
「単一の LLM をプロンプトで呼び出す」段階から、「複数のエージェントを継続的に動かす運用基盤を作る」段階に、業界の関心が一気に移ってきている実感があります。
私自身もこの分野に強い関心があり、ゆくゆくは Kubernetes 上で AI エージェントのプラットフォームを自分でも作りたい と考えています。複数のエージェントが Pod として独立に動き、サービス越しに会話して、必要に応じてスケールアウトする。さらにその先には、共有メモリ基盤やオーケストレーションのレイヤーが乗ってくる。そういう絵を描いています。
ただ、いきなり「プラットフォーム」を組み立てる前に、その土台になる 2 つの要素 ── エージェント間通信の標準プロトコル(A2A)と、Kubernetes 上での分散実行(GKE) が、最小構成でどう噛み合うのかを整理しておきたい。本記事ではその最小構成を解説します。
題材は、Google 公式の Agent Development Kit (ADK) で 3 つのエージェントを作り、A2A プロトコルで会話させ、最終的に GKE 上に乗せる構成です。
題材の構成
題材とするのは、以下の構成を GKE 上で動かす最小システムです。
各 Pod から Vertex AI への点線の矢印は、Workload Identity(以下 WI)という GKE の仕組みで認証しています。サービスアカウントの鍵ファイルを一切使わずに、Pod が GCP の API を叩けるようになる仕組みで、詳しくは後の章で説明します。
「20 面ダイスを振って、出た数が素数か判定してください」と話しかけると、root-agent が dice-agent でダイスを振り、その結果を prime-agent に渡して素数判定し、最終回答を統合して返してきます。
裏では root-agent Pod から dice-agent Pod へ HTTP の A2A RPC が飛び、続けて prime-agent Pod へもう一度 A2A RPC が飛んでいます。それぞれの Pod は Vertex AI に Workload Identity 経由でアクセスし、サービスアカウントキーは一切使っていません。
本記事のゴール
読み終わった時点で、以下を実装レベルで理解できる状態を目指します。
- A2A プロトコルの最低限の用語(Agent Card / Message / Task / Part)が、実装とどう対応するか
- ADK の
to_a2a()
とRemoteA2aAgent
が何をしているのか - ローカルから GKE へ移すときに、何が変わるか
- Workload Identity で Vertex AI をキーレスで呼ぶ最低限の設定
本記事で扱わないこと
学習を最小構成に絞るため、以下は対象外にしています。
- root-agent の外部公開(LoadBalancer / Ingress / Gateway API)
- Session / 長期メモリ / RAG
- HPA / KEDA などの autoscaling
- NetworkPolicy / Istio などのサービスメッシュ
- 本格的なログ・トレーシング基盤
本記事の構成は クラスタ内通信のみ で動くもので、本番運用にはセキュリティの追加検討が必要です。
使用バージョン
-
google-adk
: 1.20 系(to_a2a
/RemoteA2aAgent
は本記事執筆時点でEXPERIMENTAL) - A2A プロトコル: ADK 同梱バージョン(実際の値は
/.well-known/agent-card.json
のprotocolVersion
で確認可能) - GKE: Standard クラスタ(
--release-channel=regular
) - Python: 3.11
- LLM: Vertex AI 上の
gemini-2.5-flash
to_a2a()と
RemoteA2aAgentは ADK の experimental 機能なので、起動時に警告が出ます。本記事のコードは執筆時点で動作確認していますが、API は今後変わる可能性があります。各 API の最新仕様は ADK / A2A の公式ドキュメントを確認してください。
全体構成
本記事で作ったエージェントは 3 つ。
| エージェント | 役割 | 公開ポート(GKE) |
|---|---|---|
root_agent |
ユーザー入力を受け、dice / prime に処理を委譲 | 8000 |
dice_agent |
roll_die(sides)でダイスを振る |
8001 |
prime_agent |
is_prime(n)で素数判定する |
8002 |
使ったスタック。
| 層 | 採用 |
|---|---|
| LLM | Vertex AI (Gemini 2.5 Flash) |
| エージェントフレームワーク | Google ADK |
| エージェント間通信 | A2A プロトコル (HTTP + JSON-RPC) |
| ローカル実行 | uvicorn |
| コンテナビルド | Docker (buildx) |
| GKE 実行 | GKE Standard (Workload Identity) |
| イメージレジストリ | Artifact Registry |
ここから先は、この構成を ローカル → GKE の順で組み立てていきます。
A2A の用語整理
A2A は HTTP + JSON-RPC ベースの、エージェント同士が相互に発見し、能力を理解し、標準形式でタスクを投げ合うためのプロトコル です。「LLM 同士をなんとなくチャットさせる仕組み」ではなく、リモートのエージェントに対して構造化されたタスクを送るための規約です。
実装に入る前に、最低限おさえる用語だけ表にします。
| 用語 | 役割 | 本記事での具体例 |
|---|---|---|
| Agent Card | エージェントの能力・URL・認証要件を記述するメタデータ。/.well-known/agent-card.jsonで公開 |
dice_agentの name / description / roll_dieスキル |
| Message | 1 回の発話・入力・応答 | 「20面ダイスを振って」 |
| Task | 状態を持つ作業単位 | dice エージェントへの「ダイスを振る」依頼 |
| Part | Message / Artifact を構成する最小単位(text / file / data) | text part |
| Artifact | Task の最終成果物 | ダイスの出目を含む応答 |
「クライアントが Agent Card を取得 → タスクを送る → エージェントが Task を処理 → Artifact を返す」が一通りの流れです。
MCP との違いを 1 行で言うと、MCP はエージェントとツールの間、A2A はエージェントとエージェントの間 のプロトコルです。役割が違うので、共存することも多いです。
to_a2a() で A2A Server 化
最初に作るのは、まずは呼び出される側のエージェント、ダイスを振るだけのものです。これを ADK で作り、
to_a2a()を 1 行呼ぶだけで HTTP の A2A Server に変えます。
元の ADK Agent
agents/dice_agent/agent.py:
import random
from google.adk.agents import Agent
def roll_die(sides: int) -> int:
"""指定された面数のダイスを 1 回振り、出目を返す。"""
if sides < 1:
raise ValueError("sides must be >= 1")
return random.randint(1, sides)
root_agent = Agent(
name="dice_agent",
model="gemini-2.5-flash",
description="任意の面数のダイスを振るエージェント。",
instruction=(
"あなたはダイスを振るエージェントです。"
"ユーザーから面数を指定されたら、必ず roll_die ツールを呼び出して結果を取得してください。"
"ツールの結果を踏まえ、自然な日本語で短く返答してください。"
),
tools=[roll_die],
)
ここまでは普通の ADK Agent で、
adk run agents/dice_agentでローカル対話できます。
to_a2a() で A2A Server 化
A2A Server にするのは
to_a2a()の 1 行だけです。
agents/dice_agent/main.py:
import os
from google.adk.a2a.utils.agent_to_a2a import to_a2a
from .agent import root_agent
HOST = os.getenv("A2A_HOST", "localhost")
PORT = int(os.getenv("A2A_PORT", "8001"))
app = to_a2a(root_agent, host=HOST, port=PORT, protocol="http")
これを uvicorn で起動します。
uv run uvicorn agents.dice_agent.main:app --host 127.0.0.1 --port 8001
起動ログにこんな行が出れば成功です。
INFO: Started server process [12345] INFO: Uvicorn running on http://127.0.0.1:8001 (Press CTRL+C to quit)
to_a2a()が裏でやっているのは、おおまかに 3 つです。
- ADK Agent を A2A プロトコルのエンドポイントに包む(
POST /
で JSON-RPC を受ける) - Agent Card を 自動生成して
/.well-known/agent-card.json
で公開する - Starlette アプリ(
app
変数)として返す
A2A_HOST環境変数を切り出しているのは、Agent Card に書かれる URL を切り替えられるようにするため です。
ここまでが A2A Server を立ち上げる手順です。次に、これを呼ぶ側のエージェントを見ていきます。
RemoteA2aAgent からの呼び出し
A2A は Server を立てただけでは何も起きません。Client 側が Agent Card を取りに行って、タスクを送って、初めて通信が成立 します。ADK にはそのクライアント役の
RemoteA2aAgentがあります。
root_agent の実装
agents/root_agent/agent.py(抜粋):
import os
from google.adk.agents import Agent
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
DICE_AGENT_URL = os.getenv(
"DICE_AGENT_URL",
"http://localhost:8001/.well-known/agent-card.json",
)
dice_remote = RemoteA2aAgent(
name="dice_agent",
agent_card=DICE_AGENT_URL,
description="任意の面数のダイスを振るリモートエージェント。",
)
root_agent = Agent(
name="root_agent",
model="gemini-2.5-flash",
description="ユーザーの依頼を受け、dice エージェントへ処理を委譲するルートエージェント。",
instruction=(
"あなたはユーザーのリクエストを処理するルートエージェントです。\n"
"ダイスを振るリクエスト (例: 「20面ダイスを振って」) は dice_agent サブエージェントに委譲してください。\n"
"委譲先からの結果を踏まえ、ユーザーへ短く分かりやすく返答してください。"
),
sub_agents=[dice_remote],
)
-
RemoteA2aAgent
はURL を持つだけのプロキシで、ローカルの ADK Agent と同じインターフェースでsub_agents
に並べられます - URL は
Agent Card の URL
(/.well-known/agent-card.json
)を指します
DICE_AGENT_URLを環境変数化しているのは、ローカルと GKE で参照先を切り替えるため です。ローカルでは
http://localhost:8001/...、GKE では Service DNS 名になります。
動作確認
別ターミナルで root_agent を起動します。
uv run adk run agents/root_agent
ここで「20 面ダイスを振って」と話しかけると、root_agent が dice_agent に処理を委譲して結果を返してきます。
このとき、A2A Server 側(dice_agent を起動しているターミナル)を見ると、
POST /のアクセスログが出ています。
INFO: 127.0.0.1:54321 - "POST / HTTP/1.1" 200 OK
これが A2A RPC の正体です。 /.well-known/agent-card.json で能力を発見し、POST / で JSON-RPC を投げる、それだけのプロトコルです。
なお
RemoteA2aAgentおよび
to_a2aを初めて使うと、起動時に EXPERIMENTAL のプレフィックス付きの警告が出ます。ADK の A2A 機能は執筆時点で experimental 扱いなので、API が今後変わる可能性はあります。動作には影響しません。
curl での Agent Card 確認
A2A の中心は Agent Card です。動いている実物を 1 度見ておくと、後の章の挙動が理解しやすくなります。
A2A Server (dice_agent) を起動した状態で、別ターミナルから叩きます。
curl http://localhost:8001/.well-known/agent-card.json | jq .
返ってくる JSON(抜粋・主要フィールドのみ):
{
"name": "dice_agent",
"description": "任意の面数のダイスを振るエージェント。",
"url": "http://localhost:8001/",
"protocolVersion": "0.3.0",
"capabilities": {
"streaming": true
},
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["text/plain"],
"skills": [
/* ADK が agent.py の情報から自動生成する skill エントリ */
]
}
skillsフィールドの中身は ADK のバージョンや
to_a2a()の生成ロジックによって変わるので、自分の手元で
curlを叩いて確認するのが確実です。本記事で重要なのは
urlと
protocolVersionの 2 つです。
注目してほしいのは
"url": "http://localhost:8001/"です。これは クライアント( RemoteA2aAgent)が JSON-RPC を投げる宛先 で、Server 側の
A2A_HOSTで決まります。
ローカルで自分自身から呼ぶ分にはこれで十分ですが、別ホスト(別 Pod)から呼ぶときには、ここが解決可能なホスト名でないと通信が成立しません。
たとえば
A2A_HOSTを指定せずに 0.0.0.0 でリッスンさせた状態で Agent Card を取ると、URL が
http://0.0.0.0:8001/になってしまい、クライアントが「0.0.0.0 には接続できない」と返してきます。
ここまでがローカル A2A の最小構成です。
- ADK Agent を
to_a2a()
で A2A Server 化できる - Agent Card がどう公開されるかを実物で確認できる
-
RemoteA2aAgent
で別エージェントから A2A 越しに呼べる
次はこの構成をそのまま GKE に持っていきます。
GKE への Pod デプロイ
ローカルの構成を、ほぼそのまま GKE に持っていきます。ここでやることは大きく 3 つだけです。
- GKE Standard クラスタを作る(Workload Identity を有効にして)
- Artifact Registry にイメージを push する(linux/amd64 で)
- Deployment / Service を apply する
最終的な GKE 構成は上の図のようになります。Namespace
adk-a2a-lab配下に root / dice / prime の Deployment と ClusterIP Service が並び、root-agent から dice / prime の Service DNS 名で内部通信する構造です。
この章ではまず root-agent と dice-agent の 2 つだけを GKE に展開 します。prime-agent はこのあとの「1 ターン 1 remote」の章で追加します。
クラスタ作成
export PROJECT_ID=a2a-gke export REGION=us-central1 export ZONE=us-central1-a export CLUSTER=a2a-gke-lab gcloud container clusters create $CLUSTER \ --project=$PROJECT_ID \ --zone=$ZONE \ --num-nodes=2 \ --machine-type=e2-small \ --workload-pool=$PROJECT_ID.svc.id.goog \ --release-channel=regular
--workload-pool=$PROJECT_ID.svc.id.googを指定することで Workload Identity が有効化 されます。後で「KSA から GSA の権限を借りる」設定をするので、ここで指定しておかないとそもそも借りられません。
クラスタ作成は 3〜7 分程度。学習用クラスタなので
e2-small × 2です。Standard クラスタを選んだ理由は、Workload Identity の設定を手で書きたかったから(Autopilot だと多くが自動化されてしまう)です。
Artifact Registry への push
docker buildx build --platform linux/amd64 \ -t us-central1-docker.pkg.dev/a2a-gke/a2a-gke-lab/dice-agent:dev \ -f docker/dice-agent.Dockerfile \ --push .
GKE のノードは linux/amd64 なので、ホストのアーキテクチャが違う場合に備えて 常に --platform linux/amd64 を明示 しておくのが安全です。
Deployment / Service の apply
k8s/dice-agent-deployment.yaml(抜粋):
apiVersion: apps/v1
kind: Deployment
metadata:
name: dice-agent
namespace: adk-a2a-lab
spec:
replicas: 1
selector:
matchLabels:
app: dice-agent
template:
metadata:
labels:
app: dice-agent
spec:
serviceAccountName: adk-a2a-runtime # ← KSA
containers:
- name: dice-agent
image: us-central1-docker.pkg.dev/a2a-gke/a2a-gke-lab/dice-agent:dev
ports:
- containerPort: 8001
env:
- name: GOOGLE_GENAI_USE_VERTEXAI
value: "TRUE"
- name: GOOGLE_CLOUD_PROJECT
value: a2a-gke
- name: A2A_HOST
value: dice-agent-svc.adk-a2a-lab.svc.cluster.local
- name: A2A_PORT
value: "8001"
readinessProbe:
httpGet:
path: /.well-known/agent-card.json
port: 8001
readinessProbeで Agent Card のパスを叩いているのは、A2A Server として実用に耐える状態になったかどうかを確認するため です。プロセスが起動しているだけでなく、Agent Card が返せる = A2A クライアントを受け入れられる状態、というところまでを「Ready」とみなします。
Service は ClusterIP のみ:
apiVersion: v1
kind: Service
metadata:
name: dice-agent-svc
namespace: adk-a2a-lab
spec:
type: ClusterIP
selector:
app: dice-agent
ports:
- port: 8001
targetPort: 8001
remote-agent はクラスタ外に公開する理由がないので ClusterIP のみ。root-agent も同様に ClusterIP にしておき、確認は
kubectl port-forwardで済ませます。
kubectl apply -f k8s/namespace.yaml kubectl apply -f k8s/serviceaccount.yaml kubectl apply -f k8s/dice-agent-deployment.yaml kubectl apply -f k8s/dice-agent-service.yaml kubectl apply -f k8s/root-agent-deployment.yaml kubectl apply -f k8s/root-agent-service.yaml kubectl get pods -n adk-a2a-lab -w
ただし
kubectl applyを行っても、Pod 自体は起動しても、実際にリクエストすると Vertex AI 認証で失敗します。この認証を成立させるのが、次に扱う Workload Identity です。
Workload Identity でキーレス認証
Pod が Vertex AI を呼ぶには、何らかの形で GCP の認証情報が必要です。Service Account キーを Secret として配って
GOOGLE_APPLICATION_CREDENTIALSで参照させる、というのが昔ながらのやり方ですが、キーファイルの管理 / ローテーション / 漏洩リスクなど、運用上の苦労が一気に増えます。
Workload Identity (WI) は、これを 「Pod に紐づく Kubernetes ServiceAccount (KSA) が、対応する GCP ServiceAccount (GSA) の権限を借りる」 という仕組みで解決します。鍵を全く配らずに Vertex AI を呼べるようになります。
Pod が KSA を参照し、KSA の annotation で GSA を指し、GSA 側の IAM policy が KSA からの権限借用を許可する、という 3 つのリンクで Vertex AI 呼び出しの権限が成立します。
三角関係の中身
-
GSA(GCP 側):
adk-a2a-runtime@a2a-gke.iam.gserviceaccount.com
、roles/aiplatform.user
を持つ -
KSA(K8s 側): namespace
adk-a2a-lab
のadk-a2a-runtime
、Pod のserviceAccountName
に指定する -
WI binding: KSA に対し
roles/iam.workloadIdentityUser
をGSA の側に与え、「この KSA が GSA を借りていい」と宣言する
要するに、IAM 側で 1 つの binding、K8s 側で 1 つの annotation を入れるだけです。
設定 3 ステップ
1. GSA 作成 + Vertex AI 権限付与
gcloud iam service-accounts create adk-a2a-runtime \ --display-name="ADK A2A runtime" \ --project=$PROJECT_ID gcloud projects add-iam-policy-binding $PROJECT_ID \ --member="serviceAccount:adk-a2a-runtime@$PROJECT_ID.iam.gserviceaccount.com" \ --role="roles/aiplatform.user"
2. KSA 側に annotation を付ける
k8s/serviceaccount.yaml:
apiVersion: v1
kind: ServiceAccount
metadata:
name: adk-a2a-runtime
namespace: adk-a2a-lab
annotations:
iam.gke.io/gcp-service-account: adk-a2a-runtime@a2a-gke.iam.gserviceaccount.com
annotation の名前は
iam.gke.io/gcp-service-accountで、ここに 借りたい GSA のメールアドレス を入れます。
3. WI binding
gcloud iam service-accounts add-iam-policy-binding \ adk-a2a-runtime@$PROJECT_ID.iam.gserviceaccount.com \ --role roles/iam.workloadIdentityUser \ --member "serviceAccount:$PROJECT_ID.svc.id.goog[adk-a2a-lab/adk-a2a-runtime]"
これが、KSA
adk-a2a-lab/adk-a2a-runtimeに対して「GSA を借りてよい」と許可するコマンドです。
<project>.svc.id.goog[<namespace>/<ksa>]という独特な形式が WI の特徴です。
WI が通れば、Pod 内で
gcloud auth application-default print-access-tokenを実行すると、GSA に紐づくアクセストークンが取得できます。サービスアカウントキーを 1 枚も作っていないにもかかわらず Pod が GCP の API を呼び出せる、これが WI が解決する課題です。
Service DNS と Agent Card URL
GKE に乗せて WI が通った後、次にぶつかるのは A2A 特有の問題です。Agent Card 内の URL が、クラスタ内で解決可能なホスト名になっていないと、root-agent から dice-agent に繋がらない のです。
Pod から見える dice-agent の名前
dice-agent を ClusterIP で公開すると、クラスタ内ではフル DNS 名で名前解決できます。
dice-agent-svc.adk-a2a-lab.svc.cluster.local
この名前を Agent Card 内の URL に書き込まないと、root-agent が叩く宛先が
0.0.0.0のままで通信が成立しません。設定は Deployment の env で渡します(GKE デプロイの章で出した YAML の再掲)。
env:
- name: A2A_HOST
value: dice-agent-svc.adk-a2a-lab.svc.cluster.local
- name: A2A_PORT
value: "8001"
そして root-agent 側の
DICE_AGENT_URLも同じ DNS を指します。
env:
- name: DICE_AGENT_URL
value: "http://dice-agent-svc.adk-a2a-lab.svc.cluster.local:8001/.well-known/agent-card.json"
動作確認
port-forward で Agent Card を見て、URL が DNS 名になっていることを確認します。
kubectl port-forward -n adk-a2a-lab svc/dice-agent-svc 8001:8001 & curl http://localhost:8001/.well-known/agent-card.json | jq .url # → "http://dice-agent-svc.adk-a2a-lab.svc.cluster.local:8001/"
http://0.0.0.0:8001/が返ってきたら
A2A_HOSTの設定漏れです。Deployment の env を見直してください。
設計判断としては、「Agent Card URL は固定値ではなく環境変数で差し替える」という方針です。同じイメージで
A2A_HOSTを変えるだけで、ローカルと GKE のどちらの環境でも動かせます。設定分離の典型例です。
「1 ターン 1 remote」制約
ここまでで root → dice の単純な A2A 通信が成立します。次に remote agent を 2 つに増やして、もう少し複雑なシナリオを扱います。これで本記事の最終ゴール「ダイスを振って素数判定する」が完成します。
そして、ここで
RemoteA2aAgentの 設計上の制約 に行き当たります。
prime_agent の追加
prime_agentは dice_agent とほぼ同じ構造で、
is_primeツールを持つだけです。
def is_prime(n: int) -> dict:
if n < 2:
return {"n": n, "is_prime": False}
if n == 2:
return {"n": n, "is_prime": True}
if n % 2 == 0:
return {"n": n, "is_prime": False}
i = 3
while i * i <= n:
if n % i == 0:
return {"n": n, "is_prime": False}
i += 2
return {"n": n, "is_prime": True}
to_a2a()で A2A Server 化し、port 8002 で公開、
prime-agent-deployment.yamlを apply する、という流れは dice_agent と全く同じです。
root_agent には 2 つ目の
RemoteA2aAgentを sub_agents に追加します。
prime_remote = RemoteA2aAgent(
name="prime_agent",
agent_card=PRIME_AGENT_URL,
description="整数が素数かどうかを判定するリモートエージェント。",
)
root_agent = Agent(
name="root_agent",
model="gemini-2.5-flash",
instruction=(
"ダイスを振るリクエストは dice_agent に委譲してください。\n"
"整数の素数判定は prime_agent に委譲してください。\n"
"「ダイスを振って、その出目が素数か判定して」のように両方が必要な依頼では、"
"まず dice_agent でダイスを振り、その結果の数値を prime_agent に渡して判定し、"
"最後に統合した結果を返してください。"
),
sub_agents=[dice_remote, prime_remote],
)
動作確認
実際に「20 面ダイスを振って、その出目が素数か判定してください」と話しかけると、それらしく統合された応答が返ってきます。ところが、
kubectl logsで内部の流れを追うと、思ったより手数が多いことに気付きます。
ユーザーからの 1 回の依頼に対して、root-agent が「dice を呼ぶ → 結果を踏まえて prime を呼ぶ → 統合する」という 3 段階のターンを内部で踏みます。
制約の正体
少なくとも今回の ADK /
RemoteA2aAgentの構成では、1 回の判断につき 1 つの remote agent にルーティングされる挙動 になっていました。そのため、「ダイスを振って、素数か判定して」という複合要求は、内部で次の 3 ステップに分解されて処理されます。
- root が LLM 推論で「まず dice_agent を呼ぶ」と判断 → dice_agent へ A2A RPC → 結果(例: 17)
- root が再度 LLM 推論で「次に prime_agent を呼ぶ」と判断 → 17 を引数に prime_agent へ A2A RPC → 結果(
{n:17, is_prime: true}) - root が LLM 推論で結果を統合 → ユーザーに返答
つまり、「1 つの会話ターン」のなかで LLM が何度も計画を立て直し、A2A 呼び出しを 1 個ずつ積み重ねていく 構造です。SequentialAgent や独自のワークフローを書けば呼び出し順を明示できますが、学習段階では LLM のルーティングに任せても十分動きます。
制約の捉え方
A2A プロトコルそのものは並列・逐次・複合呼び出しを禁じていません。「1 ターン 1 remote」は ADK の RemoteA2aAgent という実装の選択 であって、必要なら以下の手段に切り替えられます。
- SequentialAgent / ParallelAgent: 呼び出し順を ADK 側で明示する
-
自前ワークフロー + A2A SDK:
RemoteA2aAgent
を使わず、A2A SDK で直接 RPC を投げる
学習目的では LLM の判断に任せる構造が一番シンプルで、root_agent の instruction を整えればこれで足ります。オーケストレーション責務がどこに集約されているか(本構成では root の instruction)を意識しておけば、後で SequentialAgent などに移しても破綻しません。
まとめ
ADK で作ったエージェントを A2A プロトコルで会話させ、GKE に乗せるまでの最小構成について、要点を以下に整理します。
- A2A は HTTP + JSON-RPC の薄い規約で、Agent Card さえ取れれば誰でもクライアントになれる
-
ADK ので、A2A Server / Client 化はそれぞれ実質 1 行ずつ
to_a2a()
/RemoteA2aAgent
-
ことが、ローカルと GKE 共通の鍵になる
A2A_HOST
環境変数で Agent Card 内の URL を切り替える - Workload Identity を使えばサービスアカウントキー無しで Vertex AI を呼べる
-
「1 ターン 1 remote」制約は ADK の
RemoteA2aAgent
の設計選択で、複合要求は LLM が多ターンに分解して処理する
A2A は思ったよりも素朴で、Kubernetes の Service / DNS / ServiceAccount といった既存の標準的な分散実行と非常に相性が良いプロトコルでした。エージェントを Pod として独立にスケールさせる、エージェントごとに権限を分ける、リリースを別ライフサイクルで回す、というのが普通の Kubernetes ワークロードと同じ感覚でできます。
コスト感
- GKE Standard のノード代(
e2-small × 2
): 月 35 USD 程度 -
GKE クラスタ管理費(Standard zonal クラスタの場合、
$0.10/hour
相当)が別途かかります。Free tier(月 1 クラスタの管理費が無料)の対象になっていない場合、合計で月 100 USD 近くになることがあるので、最新の料金は公式の料金ページで必ず確認してください - Artifact Registry のイメージ保管: ほぼ無視できる額
- Vertex AI (Gemini 2.5 Flash): 試行錯誤しても数 USD オーダー
学習用に立ち上げたクラスタは、最後に必ず削除してください。
gcloud container clusters delete $CLUSTER --zone=$ZONE --project=$PROJECT_ID
次にやりたいこと
これで「エージェント間通信 + 分散実行」の最低限の足場が見えてきます。ここから本来のゴール(Kubernetes 上の AI エージェントプラットフォーム)に向けて積み増していきたいところです。
-
Session / Memory: ADK の
InMemoryMemoryService
からVertexAIMemoryBankService
への切り替え - 共有メモリ基盤: TiDB などの外部ストアにエージェント横断のメモリを置く
- 権限分離: NetworkPolicy で remote-agent の inbound を root-agent に限定、KSA / GSA を agent 単位で分ける