
Google Chat Bot を Cloud Functions + Python + uv で最小構成で作ってみた
はじめに
Google Workspace を使っている組織で、Google Chat に簡単なボットを作りたいと思い、Cloud Functions(第2世代)で最小構成のエコーボットを構築しました。

本記事では、gcloud CLI のセットアップからデプロイ、そして実際にハマったポイントとその解決方法までを紹介します。
AWSユーザーの視点から、GCPの認証周りの違いについても触れます。
構成
| 項目 | 選択 |
|---|---|
| ランタイム | Cloud Functions 第2世代 |
| 言語 | Python 3.14 |
| パッケージマネージャー | uv |
| トリガー | HTTPS エンドポイント |
| リージョン | asia-northeast1(東京) |
前提
- Google Workspace に所属しているアカウントがあること
- GCP プロジェクトが作成済みで、課金が有効になっていること
- macOS 環境(Homebrew が使える前提)
gcloud CLI のセットアップ
インストール
brew install --cask google-cloud-sdk
認証
GCP の認証は AWS と比べてシンプルです。AWS のように aws-vault のようなツールは不要で、gcloud CLI 自体が認証情報を管理します。
# CLI 認証(ブラウザが開きます)
gcloud auth login
# Application Default Credentials(ADC)の設定
gcloud auth application-default login
ここで2種類の認証がある点に注意してください。
| コマンド | 用途 |
|---|---|
gcloud auth login |
gcloud コマンド自体の認証 |
gcloud auth application-default login |
ローカルで動くコードが GCP API を呼ぶ際の認証 |
gcloud auth login は CLI ツール用、gcloud auth application-default login はコード用です。後者を実行すると ~/.config/gcloud/application_default_credentials.json にファイルが生成され、Google のクライアントライブラリが自動的にこのファイルを参照します。
Quota Project の設定
ADC を設定した際に以下のような警告が出る場合があります。
WARNING:
Cannot find a quota project to add to ADC. You might receive a "quota exceeded" or "API not enabled" error.
これは API 使用量のクォータがプロジェクトに紐づいていないためです。以下で解消します。
gcloud auth application-default set-quota-project YOUR_PROJECT_ID
プロジェクトと API の設定
# デフォルトプロジェクトの設定
gcloud config set project YOUR_PROJECT_ID
# 必要な API の有効化
gcloud services enable cloudfunctions.googleapis.com \
cloudbuild.googleapis.com \
chat.googleapis.com \
run.googleapis.com
Cloud Functions 第2世代は内部的に Cloud Run 上で動作するため、run.googleapis.com も必要です。また、cloudbuild.googleapis.com はデプロイ時にコンテナイメージをビルドするために使われます。
複数プロジェクトの切り替え
AWS の --profile に相当する仕組みとして、gcloud には configurations があります。
# 新しい configuration を作成
gcloud config configurations create my-other-project
gcloud config set project other-project-id
gcloud auth login
# configuration の一覧
gcloud config configurations list
# 切り替え
gcloud config configurations activate my-other-project
ADC も同様に、プロジェクトごとに切り替えが可能です。
# ADC の quota project を切り替え
gcloud auth application-default set-quota-project other-project-id
ただし ADC のクレデンシャル自体(~/.config/gcloud/application_default_credentials.json)はアカウント単位で1つなので、異なる Google アカウントで ADC を使いたい場合は gcloud auth application-default login を再実行する必要があります。
Cloud Functions が uv と Python 3.14 をネイティブサポートしている
調べてみたところ、Cloud Functions の Python 3.14 ランタイムでは、uv がデフォルトのパッケージマネージャーとして採用されていることがわかりました。
これにより:
pyproject.tomlを直接読み込んでくれる —requirements.txtの生成は不要uvでロックされた依存関係がそのままデプロイされる
ただし、pyproject.toml と requirements.txt の両方が存在する場合、requirements.txt が優先されるため注意が必要です。
プロジェクト構成
google-chat-bot/
├── main.py # Cloud Function エントリーポイント
├── pyproject.toml # uv 管理の依存関係
├── uv.lock # uv ロックファイル
├── .python-version # Python バージョン
└── .gitignore
プロジェクトの初期化
# uv でプロジェクト初期化
uv init --no-readme
rm hello.py # 自動生成されるファイルを削除
# 依存関係の追加
uv add functions-framework
uv add --dev pytest
ボットのコード
main.py:
import json
import sys
import functions_framework
from flask import jsonify
def create_message(text):
"""Google Workspace Add-ons のレスポンス形式でテキストをラップする"""
return {
"hostAppDataAction": {
"chatDataAction": {
"createMessageAction": {
"message": {
"text": text,
}
}
}
}
}
@functions_framework.http
def handle_chat(request):
"""Google Chat ボットのエントリーポイント"""
body = request.get_json(silent=True)
if not body:
return jsonify(create_message("Empty request"))
chat = body.get("chat", {})
message_payload = chat.get("messagePayload", {})
message = message_payload.get("message", {})
user_text = message.get("text", "")
sender = message.get("sender", {}).get("displayName", "someone")
return jsonify(create_message(f"Hello {sender}! You said: {user_text}"))
ここで重要なのは リクエストとレスポンスの形式 です。これは後述するトラブルシューティングで詳しく説明します。
ローカルテスト
functions-framework を使えば、ローカルで Cloud Function を起動してテストできます。
uv run functions-framework --target=handle_chat --port=8080
別のターミナルから:
curl -X POST http://localhost:8080 \
-H "Content-Type: application/json" \
-d '{"chat": {"messagePayload": {"message": {"text": "hello", "sender": {"displayName": "Test User"}}}}}'
デプロイ
gcloud functions deploy google-chat-bot \
--gen2 \
--runtime=python314 \
--region=asia-northeast1 \
--source=. \
--entry-point=handle_chat \
--trigger-http \
--allow-unauthenticated
| フラグ | 意味 |
|---|---|
--gen2 |
第2世代の Cloud Functions を使用 |
--runtime=python314 |
Python 3.14 ランタイム |
--entry-point=handle_chat |
main.py 内の呼び出される関数名 |
--trigger-http |
HTTPS トリガー |
--allow-unauthenticated |
認証なしでアクセス可能(後で変更します) |
デプロイ後、以下のような URL が発行されます:
https://asia-northeast1-YOUR_PROJECT_ID.cloudfunctions.net/google-chat-bot
Google Chat API の設定
GCP Console での手動設定が必要です。この設定は gcloud CLI では行えません。
Google Chat API Configuration を開き、Configuration タブを選択します。
以下を設定します:
| 項目 | 値 |
|---|---|
| App name | 任意のボット名 |
| Avatar url | アプリのアバター画像 |
| Description | ボットの説明 |
トリガーの設定
Connection settings(トリガー)では、2つの選択肢があります:
- すべてのトリガーに共通の HTTP エンドポイント URL を使用する — 1つの URL ですべてのイベントを処理
- 各トリガーの HTTP エンドポイント URL を指定する — イベントタイプごとに異なる URL を設定
今回はエコーボットなので、すべてのイベントを1つの Cloud Function で処理すれば十分です。「すべてのトリガーに共通の HTTP エンドポイント URL を使用する」 を選択し、デプロイ時に発行された Cloud Function の URL を貼り付けます:
https://asia-northeast1-YOUR_PROJECT_ID.cloudfunctions.net/google-chat-bot

公開範囲の設定
| 項目 | 値 |
|---|---|
| Visibility | 「Make this app available to specific people and groups」を選択し、自分のメールアドレスを追加 |

Save をクリックします。反映に数分かかる場合があります。
動作確認
Google Chat API の設定が保存できたら、実際にボットにメッセージを送って動作確認します。
- Google Chat を開く
- 左サイドバーの「新しいチャット」をクリック
- ボット名(設定した App name)を検索し、選択する

- ボットのインストールを求められた場合は、インストール をクリック

- チャット画面が開いたら、メッセージを送信する(例:
hello) - ボットから
Hello YourName! You said: helloのような返信が返ってくれば成功

ボットが検索に表示されない場合は、設定の反映に数分かかることがあるので、しばらく待ってから再度検索してみてください。
ハマったポイント: ボットが「応答がありません」
ここからが本記事の一番伝えたい部分です。デプロイ後、Google Chat でボットにメッセージを送ると、「○○ から応答がありません」 と表示されました。
curl で直接 Cloud Function を叩くと正常にレスポンスが返ってくるのに、Google Chat 経由だと応答なし。ここから原因を特定するまでのプロセスを紹介します。
原因1: 認証方式
最初に --allow-unauthenticated でデプロイし、誰でもアクセス可能にしていましたが、Google Chat のドキュメントを確認すると、推奨される方式は 認証付き、つまり Google Chat のサービスアカウントにのみ invoker 権限を付与する方式でした。
# 認証付きでリデプロイ
gcloud functions deploy google-chat-bot \
--gen2 \
--runtime=python314 \
--region=asia-northeast1 \
--source=. \
--entry-point=handle_chat \
--trigger-http \
--no-allow-unauthenticated
# Google Chat のサービスアカウントに invoker 権限を付与
gcloud run services add-iam-policy-binding google-chat-bot \
--region=asia-northeast1 \
--member="serviceAccount:chat@system.gserviceaccount.com" \
--role="roles/run.invoker"
しかし、これだけでは解決しませんでした。IAM ポリシーを設定しても 403 エラーが発生。最終的には --allow-unauthenticated に戻しました(この点は今後調査が必要です)。
原因2(本当の原因): リクエスト/レスポンス形式の違い
Cloud Function のログを確認したところ、Google Chat が送信するリクエストの形式が、多くのチュートリアルで紹介されている形式と異なっていました。
よく紹介されている(古い)形式
{
"type": "MESSAGE",
"message": {
"text": "hello",
"sender": {"displayName": "User"}
}
}
実際に送られてきた形式
{
"commonEventObject": { ... },
"authorizationEventObject": { ... },
"chat": {
"messagePayload": {
"message": {
"text": "こんにちは",
"sender": {"displayName": "林昱辰"}
}
}
}
}
メッセージは body["message"] ではなく、body["chat"]["messagePayload"]["message"] にネストされていました。
さらに、レスポンスの形式も異なっていました。
よく紹介されている(古い)レスポンス
{"text": "Hello!"}
実際に期待されるレスポンス
{
"hostAppDataAction": {
"chatDataAction": {
"createMessageAction": {
"message": {
"text": "Hello!"
}
}
}
}
}
これは Google Chat API が Google Workspace Add-ons の形式に移行したためです。現在の HTTP エンドポイント方式では、リクエスト・レスポンスともに Workspace Add-ons の形式を使う必要があります。
デバッグのコツ
Cloud Functions のログを確認する際、Python の logging モジュールの出力が表示されない場合があります。print(..., file=sys.stderr, flush=True) を使うとログに表示されました。
print(f"REQUEST BODY: {json.dumps(body, ensure_ascii=False)}", file=sys.stderr, flush=True)
Cloud Run のログを確認するコマンド:
gcloud logging read \
'resource.type="cloud_run_revision" AND resource.labels.service_name="google-chat-bot"' \
--limit=10 \
--format="table(timestamp,severity,textPayload)" \
--project=YOUR_PROJECT_ID
まとめ
- Cloud Functions 第2世代 + Python 3.14 + uv で Google Chat ボットを構築しました
- Cloud Functions の Python 3.14 ランタイムは
uvをネイティブサポートしており、pyproject.tomlから直接デプロイできます - 現在の Google Chat の HTTP エンドポイントは Google Workspace Add-ons 形式 を使います。古いチュートリアルの
{"text": "..."}形式では動作しません - リクエストは
body["chat"]["messagePayload"]["message"]からメッセージを取得 - レスポンスは
hostAppDataAction.chatDataAction.createMessageAction.messageの形式で返す必要があります - デバッグ時は
print(..., file=sys.stderr, flush=True)で Cloud Run ログに出力するのが確実です







