Google Chat Bot を Cloud Functions + Python + uv で最小構成で作ってみた

Google Chat Bot を Cloud Functions + Python + uv で最小構成で作ってみた

Cloud Functions 第2世代と Python 3.14 + uv で Google Chat のエコーボットを最小構成で構築しました。Google Chat API が Workspace Add-ons 形式に移行しており、古いチュートリアルのリクエスト/レスポンス形式では動作しない点と、その解決方法を紹介します。
2026.05.27

はじめに

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

SCR-20260527-mhih-redacted_dot_app

本記事では、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.tomlrequirements.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 \
  --no-allow-unauthenticated
フラグ 意味
--gen2 第2世代の Cloud Functions を使用
--runtime=python314 Python 3.14 ランタイム
--entry-point=handle_chat main.py 内の呼び出される関数名
--trigger-http HTTPS トリガー
--no-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

SCR-20260527-mqnh-redacted_dot_app

公開範囲の設定

項目
Visibility 「Make this app available to specific people and groups」を選択し、自分のメールアドレスを追加

SCR-20260527-mrgs-redacted_dot_app

Save をクリックします。反映に数分かかる場合があります。

動作確認

Google Chat API の設定が保存できたら、実際にボットにメッセージを送って動作確認します。

  1. Google Chat を開く
  2. 左サイドバーの「新しいチャット」をクリック
  3. ボット名(設定した App name)を検索し、選択する

choose-your-bot

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

install-app-for-bot

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

SCR-20260527-mhih-redacted_dot_app

ボットが検索に表示されない場合は、設定の反映に数分かかることがあるので、しばらく待ってから再度検索してみてください。

ハマったポイント: ボットが「応答がありません」

ここからが本記事の一番伝えたい部分です。デプロイ後、Google Chat でボットにメッセージを送ると、「○○ から応答がありません」 と表示されました。

curl で直接 Cloud Function を叩くと正常にレスポンスが返ってくるのに、Google Chat 経由だと応答なし。ここから原因を特定するまでのプロセスを紹介します。

原因1: 間違ったサービスアカウントで IAM 設定

最初に --allow-unauthenticated でデプロイし、誰でもアクセス可能にしていましたが、Google Chat のドキュメントを確認すると、推奨される方式は 認証付き、つまり Google Chat のサービスアカウントにのみ invoker 権限を付与する方式でした。

ネットの情報を参考に chat@system.gserviceaccount.com に invoker 権限を付与しましたが、403 エラーが発生し続けました。

原因は サービスアカウントが違った ことです。現在の Google Chat は HTTP エンドポイント方式で Google Workspace Add-ons の仕組みを使っており、リクエストを送信するサービスアカウントは chat@system.gserviceaccount.com ではなく:

service-PROJECT_NUMBER@gcp-sa-gsuiteaddons.iam.gserviceaccount.com

正しいサービスアカウントの確認方法は2つあります:

  1. Google Chat API Configuration ページConfiguration を開くと、Service account 欄にサービスアカウントのメールアドレスが表示されています
  2. JWT トークンのデコード — Cloud Run のログからリクエストの Authorization ヘッダーを取得し、JWT をデコードすると email フィールドで実際の呼び出し元を確認できます
# 認証付きでリデプロイ
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="service-PROJECT_NUMBER@gcp-sa-gsuiteaddons.iam.gserviceaccount.com" \
  --role="roles/run.invoker"

正しいサービスアカウントに invoker 権限を付与したところ、認証付きデプロイが問題なく動作しました。

原因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 ログに出力するのが確実です

参考

この記事をシェアする

関連記事