← 기사 목록
日本語https://zenn.dev/topics/ai/feed

Agent platform のための手始めとしてGKEでA2Aを動かしてみる

추출된 키워드

40
GKE·5A2A·5AI エージェントプラットフォーム·5Agent platform·5ADK·4RemoteA2aAgent·4to_a2a·4Agent Card·4Workload Identity·4Agent Development Kit·4エージェント間通信の標準プロトコル·4Kubernetes·4Vertex AI·4Gemini Enterprise Agent Platform·4Google·4Part·3GSA·3KSA·3Deployment·3ClusterIP Service·3Artifact·3GKE Standard·3Artifact Registry·3HTTP·3JSON-RPC·3gemini-2.5-flash·3google-adk·3Task·3Message·3prime-agent·3dice-agent·3root-agent·3マルチ AI エージェント·3Gartner·3MCP·2uvicorn·2Docker·2IAM policy·2Service DNS 名·2readinessProbe·2

원문

25,042
Agent platform のための手始めとしてGKEでA2Aを動かしてみる

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

使ったスタック。

採用
LLMVertex 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 単位で分ける