SQL - TECH PLAY - TECH PLAY

TECH PLAY

SQL

イベント

該当するコンテンツが見つかりませんでした

マガジン

技術ブログ

G-gen の今村です。オンプレミスの PostgreSQL から Cloud SQL for PostgreSQL への移行において、 postgresql.conf の設定をどのように扱うべきか、マネージドサービスの仕様に基づくパラメータの分類と代替手法を解説します。 概要 データベースフラグの概要 データベースフラグとは フラグ設定時の注意点 設定値の確認と更新 パラメータの確認 パラメータの更新 Google が管理するパラメータ 前提と注意点 ネットワークと接続管理 ログ管理 ハードウェア依存の設定 ユーザーが管理するパラメータ 前提と注意点 パフォーマンスチューニングフラグ データベース内での代替設定 概要 Cloud SQL for PostgreSQL をデータベースとして採用する場合や、オンプレミスの PostgreSQL から Cloud SQL for PostgreSQL への移行を検討する際、データベース管理者が直面するのが postgresql.conf で定義するパラメータの扱いです。 Cloud SQL はフルマネージドサービスであるため、OS やインフラストラクチャの運用から解放される半面、すべてのパラメータを自由に設定できるわけではありません。また、設定できる項目とそうでない項目は、Cloud SQL の仕様によってあらかじめ決まっています。 当記事では、オンプレミス版(オープンソース版)の PostgreSQL でよく使用されるパラメータを例に挙げて、それらが Cloud SQL 版では Google が管理するパラメータ (サービスが管理するためユーザー側で設定が不可のパラメータ)と ユーザーが管理するパラメータ のどちらに分類されるかを解説します。あわせて、設定がサポートされていないパラメータの代替手法についても紹介します。 Cloud SQL の基本的な知識については、以下の記事を参照してください。 blog.g-gen.co.jp データベースフラグの概要 データベースフラグとは オンプレミス環境では、PostgreSQL のシステム全体の設定は主に postgresql.conf ファイルで管理します。しかし、Cloud SQL ではマネージドサービスの性質上、このファイルを直接編集できません。 代わりに、Cloud SQL では データベースフラグ を使用してパラメータを設定します。データベースフラグは MySQL や SQL Server でも同様にサポートされていますが、当記事では PostgreSQL を例に解説します。 参考 : データベース フラグを構成する フラグ設定時の注意点 データベースフラグを構成する際、以下の2点に注意する必要があります。 1つ目は、サポートされる値や範囲の違いです。各フラグについて、Cloud SQL でサポートされる値や範囲が、対応する PostgreSQL のパラメータやオプションと異なる場合があります。 2つ目は、再起動の発生です。すでに起動しているデータベースインスタンスに対してフラグを設定、変更、または削除すると、インスタンスの再起動が必要になる場合があります。稼働中のシステムに変更を加える際は、ダウンタイムに留意してください。 参考 : データベース フラグを構成する - サポートされているフラグ 設定値の確認と更新 パラメータの確認 Google Cloud コンソールから、現在インスタンスに設定されているデータベースフラグの一覧を確認できます。該当インスタンスの概要ページを開き、データベースフラグのセクションを確認します。 参考 : データベース フラグを構成する - インスタンスに設定されているデータベース フラグを確認する 設定されているデータベースフラグの例 また現在の設定値は、 psql クライアントなどでインスタンスにログインし、以下の SQL 文を実行することでも確認可能です。 SELECT name, setting FROM pg_settings; 参考 : データベース フラグを構成する - データベース フラグの現在の値を表示する パラメータの更新 Google Cloud コンソールや gcloud コマンドを使用して変更を行います。 システム全体に影響を与えるパラメータの多くは、このデータベースフラグを通じて設定が可能です。 Cloud SQL インスタンスを編集 フラグとパラメータの編集 gcloud コマンドでは、以下のようにフラグ名と値を対応させて実行します。 gcloud sql instances patch INSTANCE_NAME \ --database-flags = FLAG1 =VALUE1, FLAG2 =VALUE2 参考 : データベース フラグを構成する - データベース フラグを設定する Google が管理するパラメータ 前提と注意点 当セクションで紹介する「Google が管理するパラメータ」は、Cloud SQL では Google が完全に管理しており、ユーザー側で設定できないものです。これらはデータベースフラグとしてサポートされていません。 なお、当セクションで紹介するパラメータは、よく用いられる設定のごく一部です。実際には、システム要件と公式ドキュメントを照らし合わせ、事前に十分なパラメータ設計を行ってください。 ネットワークと接続管理 オンプレミスでは必須となる listen_addresses や port の設定は、Google Cloud では不要です。 Cloud SQL では、PostgreSQL の標準ポート( 5432 )が固定で使用されます。アクセス制御は pg_hba.conf を編集するのではなく、VPC ネットワークピアリングや承認済みネットワークなど、Google Cloud のネットワーク機能を使用して管理します。 参考 : Cloud SQL への接続方法を選択する 参考 : 接続の問題をデバッグする - 開いているローカルポート インスタンスの接続情報 VPC についての詳細は、以下の記事を参照してください。 blog.g-gen.co.jp blog.g-gen.co.jp ログ管理 log_destination 、 logging_collector 、 log_file_mode などのログファイルの出力先やローテーションに関する設定もマネージドサービスで代替可能です。 Cloud SQL のログは自動的に Cloud Logging に統合されます。ログの検索、監視などはデータベース側で行うのではなく、Google Cloud のオブザーバビリティ機能を使用して行います。 参考 : インスタンスのログを表示する Cloud Logging についての詳細は、以下の記事を参照してください。 blog.g-gen.co.jp ハードウェア依存の設定 dynamic_shared_memory_type などの OS やハードウェア基盤に強く依存するパラメータは設定できません。これらは Cloud SQL の基盤側で自動的に最適化されるため、ユーザーが意識する必要はありません。 参考 : マシンシリーズを選択する ユーザーが管理するパラメータ 前提と注意点 当セクションで紹介する「ユーザーが管理するパラメータ」は、Cloud SQL に移行した後でも、引き続きユーザー側でチューニングや設定を行う必要があるパラメータです。 当セクションで紹介するパラメータは例示であり、ごく一部です。実際には、システム要件を考慮し、どのパラメータに対してフラグや代替手段を用いた設定が必要になるのかを、公式ドキュメントと照らし合わせて十分に精査してください。 パフォーマンスチューニングフラグ max_connections 、 shared_buffers 、 maintenance_work_mem など、データベースのパフォーマンスに直結する重要なパラメータの多くが、データベースフラグとしてサポートされています。 なお、一部のフラグ( max_connections や max_worker_processes など)は、インスタンスのメモリサイズに応じて上限値やデフォルト値が自動的にスケーリングする仕様になっています。オンプレミスの設定値をそのまま移行するのではなく、自動設定されるデフォルト値を確認し、マネージドサービスへ設定を委譲できるかを評価してください。 参考 : データベース フラグを構成する - サポートされているフラグ データベース内での代替設定 データベースフラグのリストに存在しない場合でも、 ALTER DATABASE などの SQL コマンドを用いてデータベース内で設定できるパラメータがあります。 例えば、タイムゾーン( timezone )、日付の表示形式( datestyle )、ロケール書式( lc_monetary や lc_numeric など)は、インスタンス全体のフラグとして設定できなくても、特定のデータベースやユーザーに対して個別に適用できます。 マルチテナント環境などで、データベースごとに異なる言語設定や検索設定( default_text_search_config )を適用したい場合に有効な手法です。 参考 : データベース フラグを構成する - トラブルシューティング ALTER DATABASE の実行例 今村 壱生 (記事一覧) クラウドソリューション部 ソリューションアーキテクト課 2026年3月にG-genへ入社。約7年間 Web 広告運用やウェブ解析に携わり、その後は社内 SE として開発業務に従事。広告運用の現場感と技術的な視点、その双方を併せ持つ経験をベースに、現在は Google Cloud のスキルアップに注力。データ活用とクラウド技術を融合させ、お客様のビジネス成長を支えるエンジニアを目指している。 Follow
G-gen の奥田です。当記事では、Claude Agent SDK で実装したエージェントを Cloud Run にデプロイし、Gemini Enterprise app に A2A エージェントとして直接登録して呼び出す手順を解説します。 当記事について Gemini Enterprise app へのエージェント登録 当記事で行うこと 前提条件 手順 1. サービスアカウントと IAM の設定 2. エージェントの実装(Claude Agent SDK) 3. A2A サーバーの実装(a2a-sdk) 4. Cloud Run へのデプロイ 5. Gemini Enterprise app への登録 動作確認 サービスへの疎通確認 Agent Card と A2A エンドポイントの確認 Gemini Enterprise app からの実行 注意点 Agent Card の url はサービスの URL と一致させる a2a-sdk は 0.3 系に固定する MIME タイプは text/plain にする 当記事について Gemini Enterprise app へのエージェント登録 Gemini Enterprise app(通称 Gemini Enterprise)には、自社で開発したエージェントを登録して、ユーザーに使用させることができます。登録方法の 1 つが、Agent2Agent(以下、A2A)プロトコルによる登録です。エージェント側が A2A の Agent Card を公開していれば、Google Cloud コンソールまたは REST API から Gemini Enterprise app に登録できます。 Gemini Enterprise app の概要は以下の記事を参照してください。 blog.g-gen.co.jp A2A による登録では、エージェントの実行基盤は自分で用意します。Gemini Enterprise app が持つのは Agent Card の登録情報だけで、実行時には Agent Card の url を呼び出します。当記事では、その実行基盤として Cloud Run を使用します。 A2A エージェントをいったん Agent Registry に登録し、そこから Gemini Enterprise app に取り込むこともできます。しかし当記事では Agent Registry を経由せず、Gemini Enterprise app に直接登録します。 参考 : A2A エージェントを登録して管理する 当記事で行うこと Google が提供する AI エージェント開発用フレームワークである Agent Development Kit(ADK)で開発したエージェントを、Gemini Enterprise app に登録できることはよく知られています。 しかし Gemini Enterprise app には、ADK 以外で実装したエージェントも登録可能です。 当記事では、Anthropic が提供する Claude Agent SDK で開発したエージェントを、Gemini Enterprise app に登録する方法を検証します。Claude Agent SDK は、Claude Code のエージェントループとツール実行の仕組みをライブラリとして使用できるようにしたものです。LLM の呼び出し先は Agent Platform(旧称 Vertex AI)上の Claude Haiku 4.5 です。 構成は以下のとおりです。 [エンドユーザー] ↓ [Gemini Enterprise app] ↓ A2A(直接登録) [Cloud Run サービス] Python 3.12 / FastAPI / claude-agent-sdk / a2a-sdk ├─ Claude Haiku 4.5(Agent Platform) ├─ BigQuery リモート MCP サーバー → BigQuery のテーブル └─ 外部 API(気象データ) エージェントは、BigQuery のテーブルを BigQuery リモート MCP サーバー経由で参照し、外部 API から取得したデータと突き合わせて回答します。外部 API の例として、Open-Meteo の過去気象データ API を使用します。非商用の使用では API キーが不要と案内されています。 参考 : Claude Code Python Agent SDK 参考 : BigQuery リモート MCP サーバーを使用する 参考 : Historical Weather API 前提条件 当記事の手順を実行するには、以下が必要です。 Gemini Enterprise app が作成済みであること Cloud Run とエージェントを動かす Google Cloud プロジェクト(当記事では my-project )で、Claude Haiku 4.5 が Model Garden で有効化されていること gcloud CLI と python3 が使用できること。 Claude モデルは、Google Cloud プロジェクトごとに Model Garden で有効化する必要があります。有効化されていないプロジェクトから呼び出すと 404 エラーが返ります。 参考 : Claude モデルで予測をリクエストする 参考 : Claude Code on Google Cloud's Agent Platform - Troubleshooting 当記事で付与する IAM ロールは以下のとおりです。 付与先 ロール 用途 エージェントのサービスアカウント Agent Platform ユーザー( roles/aiplatform.user ) Claude モデルの呼び出し エージェントのサービスアカウント MCP ツールユーザー( roles/mcp.toolUser )、BigQuery ジョブユーザー( roles/bigquery.jobUser ) BigQuery リモート MCP サーバーの呼び出し エージェントのサービスアカウント BigQuery データ閲覧者( roles/bigquery.dataViewer )。テーブル単位で付与 対象テーブルの参照 エージェントのサービスアカウント Cloud Run 起動元( roles/run.invoker )。サービス単位で付与 動作確認での ID トークンによる呼び出し Discovery Engine サービスエージェント Cloud Run 起動元( roles/run.invoker ) Gemini Enterprise app からの呼び出し 作業者のユーザーアカウント サービス アカウント トークン作成者( roles/iam.serviceAccountTokenCreator ) 動作確認用の ID トークンの発行 手順 1. サービスアカウントと IAM の設定 以降のコマンドで使う値を環境変数に代入しておきます。BigQuery のテーブルは Cloud Run と別のプロジェクトに置くこともできるため、 BQ_PROJECT_ID を分けています。 export PROJECT_ID =my-project export BQ_PROJECT_ID =my-project export REGION =us-central1 export SERVICE_NAME =analysis-agent export SERVICE_ACCOUNT =analysis-agent-sa@ ${PROJECT_ID} .iam.gserviceaccount.com export BQ_DATASET =sales_data export BQ_TABLE =sales API を有効化し、エージェント用のサービスアカウントを作成してロールを付与します。BigQuery データ閲覧者はデータセットではなくテーブルに対して付与し、エージェントが参照できる範囲を対象テーブルだけに絞ります。 gcloud services enable aiplatform.googleapis.com bigquery.googleapis.com \ discoveryengine.googleapis.com run.googleapis.com \ artifactregistry.googleapis.com cloudbuild.googleapis.com \ --project =" ${PROJECT_ID} " gcloud iam service-accounts create analysis-agent-sa --project =" ${PROJECT_ID} " gcloud projects add-iam-policy-binding " ${PROJECT_ID} " \ --member =" serviceAccount: ${SERVICE_ACCOUNT} " \ --role =" roles/aiplatform.user " for role in roles/bigquery.jobUser roles/mcp.toolUser; do gcloud projects add-iam-policy-binding " ${BQ_PROJECT_ID} " \ --member =" serviceAccount: ${SERVICE_ACCOUNT} " \ --role =" ${role} " done bq add-iam-policy-binding \ --project_id =" ${BQ_PROJECT_ID} " \ --member =" serviceAccount: ${SERVICE_ACCOUNT} " \ --role =" roles/bigquery.dataViewer " \ " ${BQ_PROJECT_ID} : ${BQ_DATASET} . ${BQ_TABLE} " 2. エージェントの実装(Claude Agent SDK) まず依存パッケージです。a2a-sdk は 0.3 系に固定します。理由は「注意点」で説明します。 claude-agent-sdk>=0.2.140 fastapi>=0.115.0 uvicorn[standard]>=0.32.0 google-auth>=2.35.0 httpx>=0.28.0 a2a-sdk[http-server]>=0.3.26,<1.0 システムプロンプトでは対象テーブルを固定し、集計は SQL 側で行うように指示します。 import os class Config : BQ_PROJECT_ID = os.environ[ "BQ_PROJECT_ID" ] BQ_DATASET = os.environ.get( "BQ_DATASET" , "sales_data" ) BQ_TABLE = os.environ.get( "BQ_TABLE" , "sales" ) BQ_MCP_URL = "https://bigquery.googleapis.com/mcp" MAX_TURNS = int (os.environ.get( "MAX_TURNS" , "20" )) AGENT_CWD = "/tmp/agent" AGENT_CONFIG_DIR = "/tmp/agent-config" AGENT_BASE_URL = os.environ.get( "AGENT_BASE_URL" , "http://localhost:8080" ).rstrip( "/" ) EXTERNAL_API_URL = "https://archive-api.open-meteo.com/v1/archive" @ classmethod def table_fqn (cls) -> str : return f "{cls.BQ_PROJECT_ID}.{cls.BQ_DATASET}.{cls.BQ_TABLE}" @ classmethod def system_prompt (cls) -> str : return f """あなたは BigQuery 上のデータを分析するアナリストです。 ## 対象テーブル `{cls.table_fqn()}` 1 行が 1 件の購入です。主な列は prefecture(都道府県)、sales_date(購入日)、sales_amount(金額)です。 ## ルール - 使えるツールは BigQuery MCP(mcp__bigquery__*)と外部データ(mcp__external__*)だけです - スキーマが不明なときは get_table_info で確認してから SQL を書いてください - 集計は SQL 側で行い、全行を取得しないでください - 回答には根拠にした SQL を添えてください - 外部データと社内データを突き合わせるときは、どちらがどの数値かを明示してください """ 外部 API を呼ぶツールは、Claude Agent SDK の tool デコレータと create_sdk_mcp_server で定義します。このサーバーはアプリケーションのプロセス内で動くため、別プロセスの起動は不要です。ツールの戻り値は content の配列で、失敗時は is_error を True にすると Claude が失敗として扱います。 import json import httpx from claude_agent_sdk import ToolAnnotations, create_sdk_mcp_server, tool from config import Config @ tool ( "get_daily_weather" , "指定した緯度経度・期間の日別平均気温(摂氏)を外部 API から取得する。日付は YYYY-MM-DD 形式。" , { "latitude" : float , "longitude" : float , "start_date" : str , "end_date" : str }, annotations=ToolAnnotations(readOnlyHint= True ), ) async def get_daily_weather (args: dict ) -> dict : params = { "latitude" : args[ "latitude" ], "longitude" : args[ "longitude" ], "start_date" : args[ "start_date" ], "end_date" : args[ "end_date" ], "daily" : "temperature_2m_mean" , "timezone" : "Asia/Tokyo" , } async with httpx.AsyncClient(timeout= 20 ) as client: response = await client.get(Config.EXTERNAL_API_URL, params=params) if response.status_code != 200 : return { "content" : [{ "type" : "text" , "text" : f "外部 API エラー: HTTP {response.status_code}" }], "is_error" : True , } daily = response.json().get( "daily" , {}) return { "content" : [{ "type" : "text" , "text" : json.dumps(daily, ensure_ascii= False )}]} external_server = create_sdk_mcp_server( name= "external" , version= "1.0.0" , tools=[get_daily_weather] ) 参考 : Give Claude custom tools 次に、エージェント本体のソースコードを解説します。 ClaudeAgentOptions の mcp_servers に、BigQuery リモート MCP サーバー(HTTP 型)と上記のプロセス内サーバーの 2 つを渡します。BigQuery リモート MCP サーバーは OAuth 2.0 のアクセストークンで認証するため、Application Default Credentials から取得したトークンを Authorization ヘッダーに載せます。トークンは失効するので、リクエストのたびに取得し直します。 allowed_tools には mcp__<サーバー名>__* の形で 2 つのサーバーのツールを指定します。 setting_sources=[] は、コンテナ内の設定ファイルを読み込まないための指定です。 import os from collections.abc import AsyncIterator import google.auth import google.auth.transport.requests from claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, ResultMessage, query import external_tools from config import Config def access_token () -> str : credentials, _ = google.auth.default( scopes=[ "https://www.googleapis.com/auth/cloud-platform" ] ) credentials.refresh(google.auth.transport.requests.Request()) return credentials.token def build_options () -> ClaudeAgentOptions: os.makedirs(Config.AGENT_CWD, exist_ok= True ) os.makedirs(Config.AGENT_CONFIG_DIR, exist_ok= True ) return ClaudeAgentOptions( mcp_servers={ "bigquery" : { "type" : "http" , "url" : Config.BQ_MCP_URL, "headers" : { "Authorization" : f "Bearer {access_token()}" }, }, "external" : external_tools.external_server, }, allowed_tools=[ "mcp__bigquery__*" , "mcp__external__*" ], setting_sources=[], env={ "CLAUDE_CONFIG_DIR" : Config.AGENT_CONFIG_DIR}, cwd=Config.AGENT_CWD, max_turns=Config.MAX_TURNS, system_prompt=Config.system_prompt(), ) async def stream (prompt: str ) -> AsyncIterator[ dict ]: async for message in query(prompt=prompt, options=build_options()): if isinstance (message, AssistantMessage): for block in message.content: name = getattr (block, "name" , None ) if name: yield { "kind" : "tool" , "name" : name} elif isinstance (message, ResultMessage): yield { "kind" : "result" , "result" : message.result, "is_error" : message.is_error, } Claude の呼び出し先を Agent Platform にする設定は、コードではなく環境変数で行います。 CLAUDE_CODE_USE_VERTEX=1 、 CLOUD_ML_REGION 、 ANTHROPIC_VERTEX_PROJECT_ID 、 ANTHROPIC_MODEL の 4 つで、手順 4 で Cloud Run の環境変数として設定します。 参考 : Connect to external tools with MCP 参考 : Claude Code on Google Cloud's Agent Platform 3. A2A サーバーの実装(a2a-sdk) A2A のレイヤは a2a-sdk で実装します。Agent Card は、Gemini Enterprise app の公式ドキュメントのサンプルと同じフィールド構成にします。 url にはこのサービス自身の URL を入れます。 AgentExecutor を継承したクラスで、A2A のリクエストを手順 2 の stream() に橋渡しします。ツール呼び出しのたびに working 状態の更新を送り、回答を artifact として追加して completed にします。 Task の状態は completed か failed のどちらかに 1 回だけ遷移できます。結果を受け取ったらループを抜け、 except では未遷移のときだけ failed を呼びます。Claude 側がエラーを返したときは is_error で判定し、 failed として返します。 import logging from a2a.server.agent_execution import AgentExecutor, RequestContext from a2a.server.apps import A2AFastAPIApplication from a2a.server.events import EventQueue from a2a.server.request_handlers import DefaultRequestHandler from a2a.server.tasks import InMemoryTaskStore, TaskUpdater from a2a.types import AgentCard, Part, TaskState, TextPart from a2a.utils import AGENT_CARD_WELL_KNOWN_PATH from fastapi import FastAPI import agent from config import Config log = logging.getLogger(__name__) def _text (value: str ) -> list [Part]: return [Part(root=TextPart(text=value))] def build_card () -> AgentCard: return AgentCard.model_validate({ "protocolVersion" : "0.3" , "name" : "業務データ分析エージェント" , "description" : "BigQuery 上の業務データと外部データを自然言語で分析するエージェント" , "url" : Config.AGENT_BASE_URL, "version" : "0.1.0" , "capabilities" : { "streaming" : True }, "defaultInputModes" : [ "text/plain" ], "defaultOutputModes" : [ "text/plain" ], "skills" : [ { "id" : "bigquery-analysis" , "name" : "BigQuery 分析" , "description" : "対象テーブルに対する集計・傾向分析を自然言語で受け付ける" , "tags" : [ "bigquery" , "analytics" ], }, { "id" : "external-join" , "name" : "外部データとの突き合わせ" , "description" : "社内データの集計結果を外部 API のデータと掛け合わせる" , "tags" : [ "external" , "join" ], }, ], }) class AnalysisAgentExecutor (AgentExecutor): async def execute (self, context: RequestContext, event_queue: EventQueue) -> None : updater = TaskUpdater(event_queue, context.task_id, context.context_id) if context.current_task is None : await updater.submit() await updater.start_work() done = False try : async for event in agent.stream(context.get_user_input()): if event[ "kind" ] == "tool" : await updater.update_status( TaskState.working, updater.new_agent_message(_text(f "実行中: {event['name']}" )), ) elif event[ "kind" ] == "result" : done = True if event[ "is_error" ]: message = event[ "result" ] or "エージェントがエラーを返しました" await updater.failed(updater.new_agent_message(_text(message))) else : await updater.add_artifact( _text(event[ "result" ] or "" ), name= "answer" ) await updater.complete() break except Exception as e: log.exception( "エージェントの実行に失敗" ) if not done: await updater.failed( updater.new_agent_message(_text(f "実行に失敗しました: {e}" )) ) async def cancel (self, context: RequestContext, event_queue: EventQueue) -> None : updater = TaskUpdater(event_queue, context.task_id, context.context_id) await updater.cancel(updater.new_agent_message(_text( "キャンセルされました" ))) def mount (app: FastAPI) -> AgentCard: card = build_card() handler = DefaultRequestHandler( agent_executor=AnalysisAgentExecutor(), task_store=InMemoryTaskStore(), ) A2AFastAPIApplication(agent_card=card, http_handler=handler).add_routes_to_app( app, agent_card_url=AGENT_CARD_WELL_KNOWN_PATH, rpc_url= "/" ) return card FastAPI のエントリーポイントでは、疎通確認用の /health を定義したうえで、A2A のルートを追加します。Agent Card は A2A の仕様どおり /.well-known/agent-card.json で配信され、JSON-RPC のエンドポイントは / です。 import logging from fastapi import FastAPI import a2a_app logging.basicConfig(level=logging.INFO, format = "%(message)s" ) app = FastAPI(title= "analysis-agent" ) @ app.get ( "/health" ) async def health () -> dict : return { "status" : "ok" } AGENT_CARD = a2a_app.mount(app) 参考 : A2A Protocol Specification (v0.3.0) 参考 : a2a-sdk - PyPI 4. Cloud Run へのデプロイ Dockerfile です。Claude Agent SDK は Claude Code の CLI をサブプロセスとして起動し、CLI がセッション情報を HOME 配下に書き込みます。Cloud Run のコンテナでは書き込み可能な /tmp を HOME にします。 FROM python:3.12-slim ENV HOME=/tmp \ PYTHONUNBUFFERED=1 WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD exec uvicorn main:app --host 0 . 0 . 0 . 0 --port ${PORT :- 8080 } 環境変数は YAML ファイルで渡します。 ANTHROPIC_MODEL の値に @ が含まれるなど、区切り文字と衝突しやすい値があるため、 --set-env-vars ではなく --env-vars-file を使用します。 AGENT_BASE_URL は、この時点ではまだサービスの URL がわからないので仮の値にしておきます。 CLAUDE_CODE_USE_VERTEX : '1' CLOUD_ML_REGION : 'global' ANTHROPIC_VERTEX_PROJECT_ID : 'my-project' ANTHROPIC_MODEL : 'claude-haiku-4-5@20251001' BQ_PROJECT_ID : 'my-project' BQ_DATASET : 'sales_data' BQ_TABLE : 'sales' MAX_TURNS : '20' AGENT_BASE_URL : 'http://localhost:8080' 当記事では、デプロイを 2 回実行します。1 回目でサービスの URL を確定させ、その URL を AGENT_BASE_URL に書き戻してから 2 回目をデプロイします。Agent Card の url が実際のサービスの URL と一致していないと、Gemini Enterprise app からの呼び出しが認証で失敗するためです。詳細は「注意点」を参照してください。 まずは 1 回目のデプロイです。Cloud Run へソースコードを直接デプロイします。Gemini Enterprise app 以外の任意の主体から呼びだされることがないよう、認証を必須( --no-allow-unauthenticated )にします。デプロイ後、確定したサービスの URL を SERVICE_URL に取得します。 gcloud run deploy " ${SERVICE_NAME} " \ --source = . \ --project =" ${PROJECT_ID} " \ --region =" ${REGION} " \ --service-account =" ${SERVICE_ACCOUNT} " \ --no-allow-unauthenticated \ --memory = 2Gi \ --cpu = 2 \ --concurrency = 1 \ --timeout = 3600s \ --env-vars-file = env.yaml SERVICE_URL = $( gcloud run services describe " ${SERVICE_NAME} " \ --project =" ${PROJECT_ID} " --region =" ${REGION} " --format =' value(status.url) ' ) echo " ${SERVICE_URL} " 次に、2 回目のデプロイです。取得した SERVICE_URL を env.yaml の AGENT_BASE_URL に書き戻し、1 回目とまったく同じコマンドでもう一度デプロイします。これで Agent Card の url が、実際のサービスの URL と一致します。 sed -i " s|^AGENT_BASE_URL:.*|AGENT_BASE_URL: ' ${SERVICE_URL} '| " env.yaml gcloud run deploy " ${SERVICE_NAME} " \ --source = . \ --project =" ${PROJECT_ID} " \ --region =" ${REGION} " \ --service-account =" ${SERVICE_ACCOUNT} " \ --no-allow-unauthenticated \ --memory = 2Gi \ --cpu = 2 \ --concurrency = 1 \ --timeout = 3600s \ --env-vars-file = env.yaml 参考 : gcloud run deploy 参考 : gcloud topic gcloudignore 5. Gemini Enterprise app への登録 Gemini Enterprise app は、Discovery Engine のサービスエージェント( service-プロジェクト番号@gcp-sa-discoveryengine.iam.gserviceaccount.com )としてエージェントを呼び出します。認証必須の Cloud Run サービスを呼べるように、このサービスエージェントに Cloud Run 起動元を付与します。 PROJECT_NUMBER = $( gcloud projects describe " ${PROJECT_ID} " --format =' value(projectNumber) ' ) gcloud run services add-iam-policy-binding " ${SERVICE_NAME} " \ --project =" ${PROJECT_ID} " --region =" ${REGION} " \ --member =" serviceAccount:service- ${PROJECT_NUMBER} @gcp-sa-discoveryengine.iam.gserviceaccount.com " \ --role =" roles/run.invoker " gcloud run services add-iam-policy-binding " ${SERVICE_NAME} " \ --project =" ${PROJECT_ID} " --region =" ${REGION} " \ --member =" serviceAccount: ${SERVICE_ACCOUNT} " \ --role =" roles/run.invoker " 2 つ目は動作確認のためのものです。このあと、エージェントのサービスアカウントの権限を借用して ID トークンを発行し、そのトークンでサービスを呼び出します。サービスを実行するサービスアカウントであることと、そのサービスを呼び出せることは別なので、このアカウントにも Cloud Run 起動元を付与します。付与されていないと、呼び出しは 403 と insufficient_scope を返します。 登録には、デプロイ済みのサービスから取得した Agent Card の JSON をそのまま使用します。Cloud Run の認証を通すために、サービスアカウントの権限を借用して、オーディエンスをサービスの URL にした ID トークンを発行します。そのために、作業者のアカウントにサービス アカウント トークン作成者を付与しておきます。 gcloud iam service-accounts add-iam-policy-binding " ${SERVICE_ACCOUNT} " \ --project =" ${PROJECT_ID} " \ --member =" user: $( gcloud config get-value account ) " \ --role =" roles/iam.serviceAccountTokenCreator " TOKEN = $( gcloud auth print-identity-token \ --impersonate-service-account =" ${SERVICE_ACCOUNT} " --audiences =" ${SERVICE_URL} " ) CARD = $( curl -sS -H " Authorization: Bearer ${TOKEN} " " ${SERVICE_URL} /.well-known/agent-card.json " ) Gemini Enterprise app の登録 API に POST します。 a2aAgentDefinition.jsonAgentCard には、Agent Card の JSON を文字列として埋め込みます。当記事の Gemini Enterprise app はロケーションが global なので、エンドポイントのホスト名にロケーションの接頭辞は付きません。 export APP_ID =my-gemini-enterprise-app ENDPOINT = " https://discoveryengine.googleapis.com/v1alpha/projects/ ${PROJECT_ID} /locations/global/collections/default_collection/engines/ ${APP_ID} /assistants/default_assistant/agents " BODY = $( printf ' %s ' " ${CARD} " | python3 -c ' import json, sys print(json.dumps({ "displayName": "業務データ分析エージェント", "description": "BigQuery の業務データと外部データを分析するエージェント", "a2aAgentDefinition": {"jsonAgentCard": sys.stdin.read()}, }, ensure_ascii=False)) ' ) curl -sS -X POST \ -H " Authorization: Bearer $( gcloud auth print-access-token ) " \ -H " X-Goog-User-Project: ${PROJECT_ID} " \ -H " Content-Type: application/json " \ " ${ENDPOINT} " -d " ${BODY} " 登録されたエージェントは、同じエンドポイントへの GET で一覧できます。Google Cloud コンソールの Gemini Enterprise app の管理画面でも確認できます。 参考 : A2A エージェントを登録して管理する 参考 : サービス間認証 動作確認 サービスへの疎通確認 まず、Cloud Run のサービスにリクエストが届いているかを確認します。認証必須のサービスなので、手順 5 で発行した TOKEN を付けて /health を呼びます。 curl -sS -H " Authorization: Bearer ${TOKEN} " " ${SERVICE_URL} /health " {"status":"ok"} が返れば、認証を通ってコンテナまでリクエストが届き、FastAPI が応答しています。ここから先で失敗した場合は、原因をエージェント本体か A2A の実装に絞り込めます。 403 が返る場合は IAM の設定を、応答がない場合はコンテナの起動を疑います。 参考 : サービスにコンテナのヘルスチェックを構成する Agent Card と A2A エンドポイントの確認 Gemini Enterprise app を通さずに、A2A のエンドポイントを直接呼んで確認します。手順 5 で発行した ID トークンをそのまま使い、JSON-RPC の message/send を送ります。 curl -sS -X POST " ${SERVICE_URL} / " \ -H " Authorization: Bearer ${TOKEN} " \ -H " Content-Type: application/json " \ -d ' { "jsonrpc": "2.0", "id": "1", "method": "message/send", "params": { "message": { "role": "user", "parts": [{"kind": "text", "text": "このテーブルは全部で何行ありますか"}], "messageId": "msg-1" } } } ' レスポンスの status.state が completed になり、 artifacts に回答のテキストが入っていれば、A2A サーバーとエージェント本体は正常です。 Gemini Enterprise app からの実行 Gemini Enterprise app の画面で、登録したエージェントを選んで日本語で質問します。当記事では以下の 2 つを試しました。 質問 呼ばれたツール( ToolSearch を除く) このテーブルは全部で何行ありますか BigQuery MCP のツールのみ 2023年の東京都の月別売上合計を出して、同じ期間の東京の月平均気温と突き合わせて get_table_info → execute_sql_readonly → get_daily_weather → execute_sql_readonly 2 つ目の質問では、1 回の応答の中で BigQuery のツールと外部 API のツールが両方呼ばれ、月別の売上と気温を対応づけた表と分析コメントが返りました。 ツール呼び出しの先頭には ToolSearch が入ります。Claude Agent SDK のツール検索は既定で有効と記載されており、Agent Platform 上の Claude Haiku 4.5 以降のモデルが対象です。ツールの数が少ない構成では先にすべて読み込むほうが速いと記載されているため、環境変数 ENABLE_TOOL_SEARCH に false を指定すると、この 1 往復を省けます。 参考 : Scale to many tools with tool search 注意点 Agent Card の url はサービスの URL と一致させる Cloud Run の ID トークンは、オーディエンスを受信側サービスの URL にする必要があります。Gemini Enterprise app は Agent Card の url を宛先として呼び出すため、 url が実際のサービスの URL と違うと認証に失敗します。 Cloud Run は、すべてのサービスにハッシュを含む非決定論的 URL を割り当てます。サービス名の長さが許せば、決定論的 URL も追加で割り当てられます。1 つのサービスに 2 つの URL があり、 gcloud run services describe の表示では決定論的 URL が優先されると記載されています。一方 --format='value(status.url)' は status.url の値をそのまま返すため、両者は一致しないことがあります。URL は手で組み立てず、コマンドでの出力をそのまま使います。 gcloud run services describe SERVICE --format ' value(status.url) ' 手順 4 でデプロイを 2 回実行しているのは、1 回目で確定したサービスの URL を env.yaml の AGENT_BASE_URL に書き戻し、Agent Card の url に反映させるためです。 参考 : サービス間認証 参考 : HTTPS リクエストで呼び出す 参考 : A2A エージェントを登録して管理する a2a-sdk は 0.3 系に固定する 2026年9月現在、Gemini Enterprise app は「A2A v0.3 のストリーミング機構をサポートしている」と案内されています。また a2a-sdk は 1.0 で A2AFastAPIApplication などのラッパークラスが削除され、サーバーの組み立て方が変わりました。 当記事のコードは 0.3 系を前提にしているため、バージョンの上限も含めて固定します。 参考 : a2a-python v1.0 Migration Guide MIME タイプは text/plain にする Agent Card の defaultInputModes と defaultOutputModes は、公式ドキュメントのサンプルと同じ text/plain にします。検証中に text と書いた Agent Card は、Gemini Enterprise app への登録で拒否されました。 奥田 梨紗 (記事一覧) クラウドソリューション部データインテリジェンス課 Google Cloudの可能性に惹かれ、2024年4月G-genにジョイン。 Google Cloud Partner Top Engineer 2025&2026 Follow @risa_hochiminh
前提:楽楽明細とは別のアプリとして作った 起きたこと:12機能を1本のプルリクにまとめた 原因:4層の名前までしか決めていなかった 対策:機械に判定させる範囲を広げ、人が見る範囲を絞る 実例:レビューで通せなかった箇所 次にやるなら:プルリクの切り方から変える チェックリスト:着手前に決めておく項目 編集後記:インタビューを終えて ラクス技術広報です。 開発本部では、各部署のAI活用の取り組みを技術広報がインタビューし、記事にしています。今回は電子請求書発行システム「楽楽明細」の新しいオプション機能を、AIにほぼ実装させて約2か月でリリースした事例です。 本記事は、AIを使って順調に機能開発が進んだ話だけでなく、うまくいかなかったこと・その対策をまとめています。 AIに任せる範囲を広げる前に、正しさを機械が判定できる状態を作る。 今回はこの状態を作らないまま進めました。その結果、機能ごとに実装の仕方がばらばらになり、1機能のレビューで入った指摘を他の機能に横展開できず、同じ指摘を機能の数だけ受けることになりました。 納期の都合で12機能を1本のプルリクエストにまとめたことも重なり、変更ファイルは700を超え、サーバーサイドのレビュー担当が全量を確認し終えるまでに5営業日かかっています。 新規プロダクトでAI駆動開発を始める方 AIが書いたコードのレビューが追いつかないと感じている方 に向けて、着手前に決めておく項目を記事の最後にチェックリストとして置きました。是非ご参考ください。 [図1:任せる範囲と、機械が判定できる範囲] 前提:楽楽明細とは別のアプリとして作った   楽楽明細は、請求書や支払明細などの帳票を電子発行するクラウドサービスです。帳票のもとになる売上データは、お客様が販売管理システムやExcelで管理しているものを取り込みます。 販売管理システムを使っていないお客様は、売上データをExcelで管理しています。 ・営業や拠点ごとにファイルが分かれ、どれが最新かわからなくなる ・転記のときにミスも起きる ここを解決するために作ったのが、今回の 売上登録オプション です。専用画面で売上データを入力すると、そのまま請求データとしてCSV出力できます。 仕様はプロダクトマネージャーが決めました。AI(Claude)でプロトタイプを作り、動くものをお客様に見せながら40社に話を聞いています。話を聞くうちに最小限の機能で導入いただけるとわかり、最初のプロトタイプから機能を約30%削って、CSV出力と税額計算に必要な設定に絞りました。 楽楽明細本体には手を入れず、別のアプリとして切り出しています。長く運用してきた本体にAIを入れると既存機能を壊す可能性があるため、影響範囲を限定してAIに任せる範囲を広げる判断です。 項目 内容 期間 開発決定4月7日 → リリース5月末(実質2か月未満) 体制 設計・実装は2026年1月入社の川島卓大さんがほぼ一人。 サーバーサイドとフロントエンドで各1名がレビュー。 技術 Java、Spring Boot、React、TypeScript、PostgreSQL AIの使い方 仕様の書き起こしはClaude Code、kiro(cc-sdd)でspec化してエージェントに読ませる。 実装の主軸はCodex。 git worktreeで機能ごとに作業を隔離し、複数セッションを同時に走らせる。 起きたこと:12機能を1本のプルリクにまとめた   実装に使える期間は実質1か月弱で、機能は12ありました。機能ごとにプルリクエスト(以下プルリク)を切ってレビューを待つ余裕がないため、まず全機能をつなげて動かし、そこから改善する方針を選びました。 項目 実績 変更ファイル数 700超 サーバーサイドのレビュー 機能単位で半日〜1日。 最初のプルリクを全量見切るまでに5営業日(期間で2週間弱)。 フロントエンドのレビュー 大型プルリクと、機能単位に分割された後続15件のフロント部分を合わせて2人日。 GitHub Copilotによるレビューも入れていましたが、人手で見きれる量を超えています。機能ごとに切り分けてレビューできたのは、パッケージ構成をfeature-firstで先に決めていたためです。 原因:4層の名前までしか決めていなかった   プロダクトマネージャーからの要求仕様書は、お客様の求めるものが整理された状態で渡されています。ただ、そのままAIに渡せる粒度ではなかったと川島さんは語ります。 「そのままでは渡せませんでした。特に受け入れ条件と例外系が足りませんでした。」 そこで要件を「WHEN 条件 THEN 結果」のEARS形式で1アクション単位に分解し、「IF 制約 THEN 拒否」の例外パスを正常系と並べて書いてからspecに落としました。 実装側で先に決めていたのは、feature-firstのパッケージ構成と、DDD(オニオンアーキテクチャ)の4層の名前までです。層の中で誰が何を担うのか、検証はどこでやるのか、副作用はどこに置くのかは決めていませんでした。 その状態で機能ごとに並列でエージェントを走らせると、1機能のレビューで入った指摘を他の機能に横展開できず、同じ指摘を機能の数だけ受けることになりました。 「仕様自体は要件通り作成できているものが多かったが、責務が分離できていないものが多く、レイヤー間の妥当性を見る時間が多かった。」 要求がテストできる形になっていても、実装方針が決まっていなければ、AIはそれぞれの解釈で書きます。 対策:機械に判定させる範囲を広げ、人が見る範囲を絞る   [図2:判定を3段に分ける] CIとpre-commitは最初から入れていました。ルールの書き出しとマージゲートの追加は、機能ごとに実装がばらばらになった反省から足したものです。 「一番効いているのは『できました』と言わせず証拠を出させることです。」 川島さんは、サブエージェントの「テストが通った」という報告も鵜呑みにせず、親のエージェントがdiffとテスト出力を自分で確認します。 レビューは一次をGitHub CopilotとClaude Codeのスキルで出し、人が二次で見ます。観点が複数あるときは同じエージェントに全部任せず、観点ごとにサブエージェントを分けて並列で走らせます。指摘にはmust、should、ask、imo、nitのラベルを付け、対応するかどうかを人が判断しやすくしています。 実例:レビューで通せなかった箇所   本機能にて、サーバーサイドとフロントエンドのレビューを担当した2人に「レビューで通せなかった箇所」についてお伺いしました。 領域 レビューで通せなかった箇所 サーバーサイド メール送信のように非同期で構わない処理が、トランザクションの中に入っていた。 初期のプルリクでは、メールは送信されたのにDBがロールバックされ、無効なパスワード初期化用URLがユーザーに届く実装になっていた(リリース前に修正)。 サーバーサイド 業務ロジックがユースケースやインフラの層に流れ出す責務違反。 サーバーサイド N+1が起きやすい構造。 明細行20行の売上データを取得するのに、最初はSQLが23本走っていた。 フロントエンド 方針はバックエンドで検証して送信時に表示することだったが、全画面にリアルタイムバリデーションが入っていた(全画面から削除)。 フロントエンド すでにある共通コンポーネントを使わず画面ごとに直書きし、ドラッグ・アンド・ドロップも複雑に自前実装していた(ライブラリを使う形に置き換え)。 フロントエンド テーブル内のテキストフィールドに1文字打つごとに、テーブル全体が再レンダリングされていた。 レビューを担当した2人が、共通して口にしたことがあります。 「『動く』のと『そのまま出せる』の間には距離がある」 また、これから同じ進め方を始めるチームへ、2人からのコメントです。 サーバーサイド レビュー担当者 「設計、実装の前にどうやってAIをハンドリングをしていくかという工程をちゃんと設けるべきではあった。期日までが短く急ぎ早で始めたものの手戻りも多く、事前準備をちゃんと設けていても間に合わせられていたのではと思う。 」 フロントエンド レビュー担当者 「レビューする側もAIを使っていいということ。作る速度が上がる分、見る側もAIで理解スピードを早めないとレビューが追いつかなくボトルネックになる。」 次にやるなら:プルリクの切り方から変える   [図3:プルリクの切り方] 切れ目を先に決めておけば、レビューは内側から外側へ積み上げる順に流れます。 一人でフルスタックに進めた体制では、フロントとバックエンドの間で調整が発生しないため、動くものを作る速度は出ました。一方で、一人にかかる負担は大きくなりました。API規約を先に確定させれば、フロントとバックエンドを分担できます。着手前にAIのハンドリングを詰める工程を置くことも、次の案件に向けた課題として残りました。 チェックリスト:着手前に決めておく項目   冒頭でもお伝えした通り、本取り組みから得た知見を基に着手前に決めておく項目をまとめました。 ご自身のプロジェクトに当ててみてください。 仕様と設計で決めておくこと (→ 原因の章 ) □ 受け入れ条件と例外系を、1アクション単位で書き出したか(EARS形式など) □ 各層で誰が何を担うか、検証はどこでやるか、副作用はどこに置くかを決めたか 機械に判定させる仕組み (→ 対策の章 ) □ CIで必須にする項目を並べたか(lint、format、型チェック、単体テスト、secret scanning、依存パッケージの実在チェック) □ テストを実データに近い環境で流せるか(Testcontainersなど) □ 機械では拾えない観点を、プロジェクト固有のレビュー観点として文字にしたか □ エージェントに、テスト出力とdiffを証拠として出させる運用にしたか 分割の単位 (→ 次にやるならの章 ) □ API規約(インターフェース)を、実装より先に確定させたか □ プルリクの切れ目を、レビューできる大きさで先に決めたか 最後に、川島さんからのメッセージです。 「AIに任せる範囲を広げる前に、正しさを機械が判定できる環境を先に作ることが大事だと思いました。最初の壁はコードが書けないことではなく、動いているように見えて設計方針から外れたコードが、レビューの追いつかない速度で積み上がることです。AI駆動開発で変わるのは、コードを書く仕事の比重です。良し悪しを定義し検証する仕事へ重心が移ります。ツールは半年で入れ替わりますが、この土台はどのツールに乗り換えても効き続けます。」 編集後記:インタビューを終えて   取材していて印象に残ったのは、川島さんもレビュー担当の2人も、うまくいった話より「次はこうする」を具体的に話してくれたことでした。700ファイルのプルリクは、社外に出すには気の重い話だと思います。それでも数字と経緯をそのまま出してくれたので、この記事が書けました。 開発本部では、うまくいったところとやり直したいところの両方を、これからも技術広報が聞いてご紹介していきます。

動画

書籍