0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Google ADK 2.0 へのアップグレードで Postgres セッションストアにハマった3つの話

0
Last updated at Posted at 2026-06-04

Cloud Run + Cloud SQL(PostgreSQL)で動かしているエージェントの google-adk を 1.x から >=2.0.0 に上げた。セッションストアには ADK の DatabaseSessionService を使っている。依存を一行上げるだけ、のつもりが全然そうじゃなかった。

ハマったのは3つ。地味さの昇順で並べるとこうなる。

  1. ADK 2.0 は Postgres と asyncpg で喋るので接続 URL が変わる。しかもその URL は同期コードと共有している。
  2. events テーブルに 2カラム追加が必要。これを入れずにデプロイすると、コンテナは生きているのにチャットだけ無言で 500 になる。
  3. レガシーな v0(Pickle)スキーマはまだ動く。v1(JSON)への移行は任意で、しかも in-place ではできない。

順に書く。

1. async ドライバへの切り替え — そして同期コードと共有する URL

ADK 2.0 のセッションサービスは async で、async な Postgres ドライバを期待する。具体的には DATABASE_URL のスキームが変わる。

postgresql://appuser:...@host/db          # 1.x
postgresql+asyncpg://appuser:...@host/db   # 2.0

ここまでは簡単。Secret を書き換えて再デプロイすればいい。問題は、この同じ URL を async じゃないコードも読んでいること。うちは独自のストレージ(トークン保存、pending state 保存)を素の同期 SQLAlchemy で組んでいて、create_engine()+asyncpg を理解しない。2.0 の URL を渡すと、同期エンジンに async ドライバを import しようとして起動時にコケる。

直し方は小さな正規化レイヤを挟むだけ。URL は async 形式で保存し(主要な利用者が ADK なので)、同期エンジンを作る箇所でドライバ指定を剥がす。

from sqlalchemy import create_engine
from sqlalchemy.engine import Engine


def _sync_db_url(db_url: str) -> str:
    """非同期ドライバ指定の URL を同期 SQLAlchemy 用に正規化する."""
    return db_url.replace("postgresql+asyncpg://", "postgresql://", 1)


def create_db_engine(db_url: str) -> Engine:
    return create_engine(
        _sync_db_url(db_url),
        pool_size=2,
        max_overflow=1,
        pool_pre_ping=True,
        pool_recycle=300,
    )

設計の意図として強調したいのは、Secret を2本に分けず、URL は1本にしてエッジで正規化するという判断。ADK には欲しい +asyncpg 形式を渡しつつ、同期側の利用者は全員 create_db_engine() を通してドライバ指定を剥がしたものを受け取る。replace(..., 1) でスキーム部分しか触らないので、パスワードにたまたま同じ文字列が混ざっていても壊れない。ADK 2.0 と一緒に同期 DB アクセスが残っているなら、この種のシムは必須。入れないと async URL が create_engine() まで漏れて、アップグレードと無関係に見える起動時 import エラーを食らう。

2. 足りない event カラム — 本番で無言の 500

これが、気づく前に dev 環境を実際に落とした犯人。

ADK 2.0 は events テーブルに2カラム追加した。

input_transcription  jsonb
output_transcription  jsonb

ADK 2.0 はセッション GET と /run_sse ストリーミングエンドポイントでこのカラムを無条件に読む。1.x で作った DB にはカラムが無いので、Postgres が UndefinedColumnError を投げる。厄介なのは症状で、明確な起動クラッシュにはならない。コンテナは普通に起動して /health は 200 を返す。なのにチャットのターンが毎回 500になり、セッション読み込みも失敗する。dev でまさにこれを再現した。コンテナは健康、チャットは死亡。

直し方は前方互換な ALTER TABLEデプロイより前に流すこと。

ALTER TABLE events ADD COLUMN IF NOT EXISTS input_transcription jsonb;
ALTER TABLE events ADD COLUMN IF NOT EXISTS output_transcription jsonb;

IF NOT EXISTS で冪等になるし、nullable カラムの追加は Postgres ではテーブル書き換えを伴わない非ブロッキング操作なので、稼働中の DB に流しても安全。順序が大事で、DB を先にパッチ → そのあとデプロイ。逆にやると、新イメージが旧スキーマに対して動く時間帯ができてチャットが落ちる。

Cloud SQL Auth Proxy 経由だとパッチ全体はこうなる。

cloud_sql_proxy -instances=PROJECT:asia-northeast1:INSTANCE=tcp:127.0.0.1:15433 &

PGPASSWORD="$DB_PASSWORD" psql -h 127.0.0.1 -p 15433 -U appuser -d appdb <<'SQL'
ALTER TABLE events ADD COLUMN IF NOT EXISTS input_transcription jsonb;
ALTER TABLE events ADD COLUMN IF NOT EXISTS output_transcription jsonb;
SELECT column_name FROM information_schema.columns
WHERE table_name = 'events'
  AND column_name IN ('input_transcription', 'output_transcription');
-- 2行返れば OK
SQL

ロールバックの観点では朗報がある。このカラムは ADK 1.x からは無視されるので、追加しても旧バージョンは壊れない。アップグレードを決め切る前に、先にパッチだけ当てておける。

3. v0 → v1 スキーマ移行は任意(たぶん後回しでいい)

1.x で作った DB だと、ADK 2.0 は起動時にこれを吐く。

The database is using the legacy v0 schema, which uses Pickle to serialize
event actions. The v0 schema will not be supported going forward and will be
deprecated in a few rollouts. Please migrate to the v1 schema which uses JSON
serialization for event data.

重要な気づきは、ADK 2.0 は v0 を読み書きできるということ。これは deprecation 警告であって、ハードな必須要件ではない。うちは 2.0 を v0 スキーマのまま動かして移行を後回しにした。アップグレードと移行は独立した意思決定で、切り離すぶんだけリスクの高いデプロイが小さくなる。

実際に移行するときの肝は、in-place ではできないこと。スキーマが構造的に違う。

events カラム v0 v1
actions bytea(Pickle) なし
event_data なし jsonb(全イベントデータ)
メタデータテーブル なし adk_internal_metadata

v0 はイベントの actions を個別カラム + pickle 化した blob で持つが、v1 は全部を event_data JSONB 1カラムに畳む。カラム構成が変わるので、ADK は「片方の DB から読んで、新しく作った DB に書く」移行コマンドを用意している。

# CREATE DATABASE はトランザクション内で実行できない → 単独で流す
psql ... -d postgres -c "CREATE DATABASE appdb_v1;"

SOURCE_URL="postgresql://appuser:${PW}@127.0.0.1:15433/appdb"
DEST_URL="postgresql://appuser:${PW}@127.0.0.1:15433/appdb_v1"

uv run adk migrate session \
  --source_db_url="${SOURCE_URL}" \
  --dest_db_url="${DEST_URL}"

adk migrate session が面倒を見るのは ADK 自身の4テーブル、app_states / user_states / sessions / events。自分で足したテーブル(OAuth トークンやアプリ固有の state)は対象外で別途コピーが必要だが、それは ADK のスコープ外なのでこの記事では触れない。

移行後は宛先 DB を検証する。

# 1 が返れば v1
psql ... -d appdb_v1 -c \
  "SELECT value FROM adk_internal_metadata WHERE key='schema_version';"

# event_data があり actions が無いこと
psql ... -d appdb_v1 -c \
  "SELECT column_name FROM information_schema.columns WHERE table_name='events';"

カットオーバーは接続 Secret を新 DB に向けて再デプロイするだけ。新しい DB に移行しているので元の DB は無傷で、ロールバックは Secret を戻すだけ。確信が持てるまで、データ損失も破壊的な操作も発生しない。

実際に回るデプロイ順序

まとめるとこの順番になる。

  1. DB をパッチALTER TABLE events ...)— 500 の窓を作らないため、何よりも先に。
  2. URL を切り替えpostgresql+asyncpg:// に(同期側がちゃんと戻して正規化していることを確認)。
  3. 2.0 イメージをデプロイ
  4. スモークテスト: /health → 200、既存セッションの GET → 500 にならない、新規 /run_sse チャット → 応答がストリームされる。
  5. (任意・後日) v0 → v1 を新 DB に移行してカットオーバー。

刺さりがちなポイント

  • pg_dump のバージョン差。ローカルクライアントが Cloud SQL サーバより古い(例: client 16 vs server 17)と pg_dump でのデータコピーは単に拒否される。バージョンを揃えるか、スクリプトでコピーする。
  • CREATE DATABASE はトランザクション外。トランザクション内で実行できないので、BEGIN ... COMMIT の塊に GRANT と一緒に詰め込まず、単独の文として流す。
  • バージョン間のセッション互換性。2.0 が書いたセッションは 1.x(特に古い 1.x)では読めない可能性がある。カットオーバー後に作られたセッションについては、ダウングレードはロスありと考え、旧イメージは短期の緊急脱出口としてのみ残す。
  • /health は嘘をつく。ヘルスチェックの 200 はスキーマが合っているかを何も語らない。実際のセッション読み込みと本物のチャットターンでスモークテストする。

まとめ

google-adk 2.0 への更新は紙の上では小さく、現場では鋭い。async ドライバへの切り替えは URL を共有する同期 DB コードに波及するし、新しい events カラムはパッチ前にデプロイすると「健康そうなコンテナ × チャット障害」を生む。v0 の deprecation 警告は大声だが必須ではなく、v0 のまま運用して自分のスケジュールで新 DB に移行できる。先にパッチ、URL はエッジで正規化、本物の経路でスモークテスト、スキーマ移行は別プロジェクトとして扱う。これでいい。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?