Cloud Functions の使い方:gcloud functions deploy と Cloud Run の違いを実例で
GCP Cloud Functions の使い方を実例で。HTTP/イベントトリガー、2nd gen と Cloud Run の違い、gcloud functions deploy、Secret Manager を、動くコード付きで解説。
「Cloud Functions のチュートリアル通りにやってるのに、コンソールに Cloud Run functions って出てくる。これ別物?」
去年、後輩からこの質問をもらって、僕は一瞬うまく答えられませんでした。GCP のドキュメントが gcloud functions deploy と gcloud run deploy の両方を載せていて、どっちが正解なのか自分でも曖昧だったんです。
答えを先に言うと、2026年の Cloud Functions(2nd gen)は Cloud Run の上に乗っている。だから名前が混ざる。ここを腹落ちさせると、デプロイで迷わなくなります。今日は Claude Code に関数を書かせながら、HTTPトリガー、イベントトリガー、デプロイ、Secret Manager、そして「Cloud Run と何が違って、いつどっちを選ぶのか」までを、手を動かして整理します。
この記事の要点
- Cloud Functions の 2nd gen は Cloud Run 基盤。だから
gcloud functions deploy --gen2でもgcloud run deploy --source . --function ...でも、ほぼ同じものができる。 - 使い分けの一行: 入口が1つで処理も1つなら Functions。複数ルートのAPIや独自 Dockerfile が要るなら Cloud Run。
- トリガーは2種類: HTTP(Webhook・Scheduler)と、イベント(Cloud Storage や Pub/Sub を Eventarc 経由で受ける)。
- コードの足場は Functions Framework 一本。ローカルでも本番でも同じ関数の形で動くので、Claude Code の出力をそのまま
curlで試せる。 - 任せていい所と、人間が見る所を分ける。関数の中身は Claude Code でいい。IAM・Secret Manager・冪等性・公開範囲は自分でレビューする。
そもそも Cloud Functions は今どうなっているのか
昔の Cloud Functions は「小さな関数を1個ポンと置くだけ」のサービスでした。今もその気軽さは残っていますが、中身が入れ替わっています。
2nd gen の関数をデプロイすると、裏でソースコードがコンテナにビルドされ、Cloud Run サービスとして動きます。コンソールに Cloud Run functions と表示されるのはこのためです。別物に乗り換わったのではなく、土台が Cloud Run になった、という理解で合っています。
ありがたいのは、Google が旧来の API・gcloud コマンド・Terraform を引き続きサポートすると明言している点です。つまり gcloud functions deploy --gen2 は今も現役。新しく gcloud run deploy --function も使える。どちらで作っても、Cloud Run Admin API と Cloud Functions v2 API の両方から管理できます。
専門用語を2つだけ先にほどいておきます。Functions Framework は、ローカルでもクラウドでも関数を同じ形で呼び出せるようにする小さな足場(ライブラリ)です。Eventarc は、Cloud Storage への書き込みのような GCP 内のイベントを、あなたの関数まで届ける配達係です。この2つが分かれば、後は読み進められます。
Cloud Functions と Cloud Run、結局どっちを使う
ここが一番よく聞かれます。両方 Cloud Run 基盤なら、何が違うのか。実務で効く差だけ表にしました。
| 観点 | Cloud Functions(関数) | Cloud Run(サービス) |
|---|---|---|
| コードの渡し方 | 入口1つ(Functions Framework) | コンテナ/独自 Dockerfile |
| 向く形 | Webhook・通知・取り込みの起点 | 複数ルートのAPI、本格バックエンド |
| ルーティング | 基本1関数=1入口 | Express等で自由に組める |
| コンテナ制御 | ほぼ不要(自動ビルド) | 細かく制御できる |
| 起動の手数 | 少ない | やや多い(Dockerfile設計) |
両方とも 0 までスケールし、リクエストがなければ料金は基本かかりません。タイムアウトやメモリの上限も近い水準です(HTTP関数は最大60分、イベント駆動は最大9分という制約があるので、長い処理はそこだけ注意)。
僕の使い分けは単純です。「入口が1つで、処理も1つに閉じるか?」 だけを見ます。Stripe や GitHub の Webhook、フォーム通知、CSV取り込みの起点、夜間バッチの呼び出し口。ここは Functions が一番ラク。逆に、複数エンドポイントを持つAPI、Next.js や Express の本体、WebSocket、独自OSパッケージが要るものは Cloud Run にします。判断に迷ったら、まず Cloud Run 入門の記事 でコンテナ側の手触りを確認してから決めると、後戻りが減ります。サーバーレス関数(FaaS)そのものの考え方や他クラウドとの比較は サーバーレス関数とは何かの記事 にまとめてあります。
向いているユースケース
「とりあえず Functions に寄せる」と事故ります。相性のいい形は、入力・検証・ログ・失敗時の扱いをチェックリスト化しやすいものです。
| ユースケース | 入口 | 実務で見る点 |
|---|---|---|
| Stripe・GitHub・社内ツールの Webhook 受信 | HTTP関数 | 署名検証、Bearerトークン、再送対策 |
| Cloud Storage に置かれた CSV の取り込み | Eventarc + イベント関数 | イベント重複、バケットとリージョン |
| 問い合わせフォームの通知 | HTTP関数 / Pub/Sub連携 | すぐ200を返す、後段キュー、レート制限 |
| 夜間集計・同期処理の起動口 | Cloud Scheduler + HTTP関数 | OIDC認証、タイムゾーン、タイムアウト |
逆に避けたいのは、「1つの関数に複数の責務を詰める」「関数内で長いループを回す」「ユーザー向けAPI全体を1関数で受ける」形。レビューも運用も一気に重くなります。
最小プロジェクト構成
ビルド手順なしで動く CommonJS の Node.js 例にします。Functions Framework を使えば、ローカル実行も本番も同じ仕組みなので、Claude Code に生成させたコードをそのまま curl で叩けます。
functions-demo/
index.js
package.json
package.json はこれだけ。
{
"name": "claude-code-gcp-functions-demo",
"version": "1.0.0",
"private": true,
"main": "index.js",
"scripts": {
"start:http": "functions-framework --target=handleAction --port=8080",
"start:event": "functions-framework --target=handleStorageObject --signature-type=cloudevent --port=8081"
},
"dependencies": {
"@google-cloud/firestore": "^7.11.0",
"@google-cloud/functions-framework": "^3.4.6"
}
}
npm install
HTTPトリガーとイベントトリガーを1ファイルで
次の index.js は、そのまま貼って構文チェックできます。HTTP側は Authorization: Bearer ... を検証し、Idempotency-Key があればそれを重複防止キーにします。イベント側は CloudEvent の ID を Firestore に保存して、同じイベントが再配達されてもジョブを二重に作らないようにしています。冪等性(同じ入力が2回来ても結果が壊れない性質)は、サーバーレスで一番効く保険です。
const functions = require("@google-cloud/functions-framework");
const { Firestore } = require("@google-cloud/firestore");
const crypto = require("node:crypto");
const db = new Firestore();
// Cloud Logging で絞り込みやすいよう、ログは構造化(JSON)で出す
function jsonLog(severity, message, extra = {}) {
console.log(JSON.stringify({ severity, message, ...extra }));
}
// 環境変数のトークンと突き合わせる門番。揃って一致したときだけ true
function requireBearerToken(req) {
const expected = process.env.API_TOKEN;
const header = req.get("Authorization") || "";
return Boolean(expected && header === `Bearer ${expected}`);
}
function stableHash(value) {
return crypto.createHash("sha256").update(value).digest("hex");
}
// --- HTTPトリガー ---
functions.http("handleAction", async (req, res) => {
if (req.method !== "POST") {
res.status(405).json({ ok: false, error: "POST only" });
return;
}
if (!requireBearerToken(req)) {
res.status(401).json({ ok: false, error: "invalid token" });
return;
}
const body = req.body || {};
if (typeof body.userId !== "string" || typeof body.action !== "string") {
res.status(400).json({ ok: false, error: "userId and action are required" });
return;
}
// ヘッダにキーが無ければ、内容から安定した重複防止キーを作る
const idempotencyKey =
req.get("Idempotency-Key") ||
stableHash(`${body.userId}:${body.action}:${body.requestedAt || ""}`);
const requestRef = db.collection("function_requests").doc(idempotencyKey);
const logRef = db.collection("action_logs").doc(idempotencyKey);
try {
let duplicate = false;
await db.runTransaction(async (tx) => {
const existing = await tx.get(requestRef);
if (existing.exists) {
duplicate = true; // 既に処理済み。二重実行しない
return;
}
tx.create(requestRef, {
userId: body.userId,
action: body.action,
createdAt: new Date(),
source: "handleAction",
});
tx.set(logRef, { userId: body.userId, action: body.action, createdAt: new Date() });
});
jsonLog("INFO", "action accepted", { userId: body.userId, duplicate });
res.status(200).json({ ok: true, duplicate, idempotencyKey });
} catch (error) {
jsonLog("ERROR", "action failed", { error: String(error) });
res.status(500).json({ ok: false, error: "internal error" });
}
});
// --- イベントトリガー(Cloud Storage を Eventarc 経由で受ける) ---
functions.cloudEvent("handleStorageObject", async (cloudEvent) => {
const data = cloudEvent.data || {};
const bucket = data.bucket;
const name = data.name;
if (!bucket || !name) {
jsonLog("WARNING", "storage event missing bucket or name", { eventId: cloudEvent.id });
return;
}
const eventRef = db.collection("processed_storage_events").doc(cloudEvent.id);
const jobRef = db.collection("storage_import_jobs").doc(stableHash(`${bucket}/${name}`));
await db.runTransaction(async (tx) => {
const existing = await tx.get(eventRef);
if (existing.exists) {
jsonLog("INFO", "duplicate storage event ignored", { eventId: cloudEvent.id });
return; // 同じイベントの再配達は捨てる
}
tx.create(eventRef, { bucket, name, eventType: cloudEvent.type, createdAt: new Date() });
tx.set(jobRef, { bucket, name, status: "queued", updatedAt: new Date() }, { merge: true });
});
jsonLog("INFO", "storage import job queued", { bucket, name, eventId: cloudEvent.id });
});
ログを console.log(JSON.stringify(...)) にしているのには理由があります。Cloud Run(つまり 2nd gen の関数)は標準出力をそのまま Cloud Logging に送ります。文字列だけのログより、severity や eventId、userId を入れた構造化ログのほうが、障害調査で一発で絞り込めるんです。深夜の調査でこの差は体に効きます。
ローカルで動作確認する
クラウドに上げる前に、手元で必ず動かします。HTTP関数を起動。
export API_TOKEN="local-token"
npm run start:http
Windows PowerShell なら、永続化される setx ではなく、そのシェルだけに効く $env:API_TOKEN="local-token" を使うと安全です。別ターミナルから送ります。
curl -X POST http://localhost:8080 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer local-token" \
-H "Idempotency-Key: local-001" \
-d '{"userId":"user-123","action":"login","requestedAt":"2026-06-03T00:00:00Z"}'
イベント関数もローカルで再現できます。CloudEvent はただの HTTP POST なので、ヘッダを ce- 付きで送るだけです。
npm run start:event
curl -X POST http://localhost:8081 \
-H "Content-Type: application/json" \
-H "ce-id: local-event-001" \
-H "ce-specversion: 1.0" \
-H "ce-type: google.cloud.storage.object.v1.finalized" \
-H "ce-source: //storage.googleapis.com/projects/_/buckets/demo-bucket" \
-d '{"bucket":"demo-bucket","name":"inbox/sample.csv","metageneration":"1"}'
ローカルから Firestore に触る場合は gcloud auth application-default login が要ります。本番データに向けたくないので、僕は検証用プロジェクトか Firestore Emulator を使います。Claude Code に頼むときも、最初の一言に「本番プロジェクトを使わないローカル確認手順も出して」と足しておくと、事故の芽を先に摘めます。
Secret Manager と IAM を用意する
トークンを .env やソースに直書きすると、レビューやログから漏れます。Secret Manager に入れて、実行サービスアカウントだけが読めるようにします。
PROJECT_ID="$(gcloud config get-value project)"
REGION="asia-northeast1"
RUNTIME_SA="functions-runtime@${PROJECT_ID}.iam.gserviceaccount.com"
gcloud iam service-accounts create functions-runtime \
--display-name="Functions runtime service account"
printf "replace-with-real-token" | gcloud secrets create api-token \
--replication-policy="automatic" \
--data-file=-
gcloud secrets add-iam-policy-binding api-token \
--member="serviceAccount:${RUNTIME_SA}" \
--role="roles/secretmanager.secretAccessor"
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
--member="serviceAccount:${RUNTIME_SA}" \
--role="roles/datastore.user"
ここで一番ハマるのが、デプロイする人の権限と、関数が実行時に使う権限を混ぜることです。シークレットを実際に読むのはデプロイ担当者ではなく、関数の実行サービスアカウント。デプロイは成功したのに本番リクエストで Permission denied、というときは、まず実行サービスアカウントの権限を疑ってください。シークレットの扱いを設計から見直したい人は シークレット管理を見直す記事 もどうぞ。
デプロイする:2つの書き方
ここが本題の「名前が混ざる問題」の答えです。同じ関数を、2通りで上げられます。
書き方A:従来どおり gcloud functions deploy --gen2。Functions の手癖をそのまま使いたい人向け。トリガーをこのコマンドの中で指定します。
gcloud functions deploy handle-action \
--gen2 \
--runtime=nodejs24 \
--region="${REGION}" \
--source=. \
--entry-point=handleAction \
--trigger-http \
--no-allow-unauthenticated \
--service-account="${RUNTIME_SA}" \
--set-secrets=API_TOKEN=api-token:latest \
--memory=512Mi \
--timeout=60s \
--max-instances=20
書き方B:Cloud Run 流の gcloud run deploy --function。Cloud Run 側の細かい設定も触りたいとき向け。
gcloud run deploy handle-action \
--source=. \
--function=handleAction \
--base-image=nodejs24 \
--region="${REGION}" \
--service-account="${RUNTIME_SA}" \
--no-allow-unauthenticated \
--memory=512Mi \
--timeout=60s \
--max-instances=20
gcloud run services update handle-action \
--region="${REGION}" \
--update-secrets=API_TOKEN=api-token:latest
どちらでも、できあがるのは Cloud Run 基盤の関数です。チームに Functions の資産が多いなら A、Cloud Run の流儀で揃えたいなら B。僕は新規は B に寄せていますが、既存の Terraform が gcloud functions 前提なら無理に変えません。両方とも --no-allow-unauthenticated を最初に付けて、認証必須から始めるのは共通です。
イベント関数は、関数を上げてから Eventarc トリガーを足します(書き方A・Bどちらの後でも同じ)。
BUCKET="your-import-bucket"
EVENTARC_SA="eventarc-invoker@${PROJECT_ID}.iam.gserviceaccount.com"
gcloud iam service-accounts create eventarc-invoker \
--display-name="Eventarc trigger invoker"
gcloud run deploy storage-import \
--source=. \
--function=handleStorageObject \
--base-image=nodejs24 \
--region="${REGION}" \
--service-account="${RUNTIME_SA}" \
--no-allow-unauthenticated \
--memory=512Mi \
--timeout=120s \
--max-instances=10
gcloud run services add-iam-policy-binding storage-import \
--region="${REGION}" \
--member="serviceAccount:${EVENTARC_SA}" \
--role="roles/run.invoker"
gcloud eventarc triggers create storage-finalized-to-function \
--location="${REGION}" \
--destination-run-service=storage-import \
--destination-run-region="${REGION}" \
--event-filters="type=google.cloud.storage.object.v1.finalized" \
--event-filters="bucket=${BUCKET}" \
--service-account="${EVENTARC_SA}"
Eventarc トリガーは作成直後、配達が始まるまで数分かかることがあります。そしてよくある詰まりが、バケットのロケーションとトリガーのロケーションがズレてイベントが届かないこと。日本向けなら asia-northeast1 が候補ですが、料金階層・データ所在地・Firestore や Storage の場所もセットで揃えます。
デプロイ後の確認とログ
URL が返っただけで安心しないこと。認証・Secret Manager・Firestore 書き込み・ログまで、本番で1回通します。
SERVICE_URL="$(gcloud run services describe handle-action \
--region "${REGION}" \
--format='value(status.url)')"
curl -X POST "${SERVICE_URL}" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer replace-with-real-token" \
-H "Idempotency-Key: prod-smoke-001" \
-d '{"userId":"smoke-user","action":"deploy-check","requestedAt":"2026-06-03T00:00:00Z"}'
gcloud run services logs read handle-action --region "${REGION}" --limit 20
gcloud logging read \
'resource.type="cloud_run_revision" AND resource.labels.service_name="handle-action" AND jsonPayload.message="action accepted"' \
--limit 20 --format json
構造化ログにしておいたおかげで、jsonPayload.message で狙い撃ちできます。トークン・メール本文・個人情報はログに出さない。これだけは徹底してください。
僕がやらかした落とし穴3つ
正直に書きます。最初の頃、同じ罠に何度もはまりました。
ひとつ目は、再試行を有効にしたのに冪等性を入れなかったこと。Eventarc も Pub/Sub も「少なくとも1回届く」前提で、平気で再配達します。請求・メール送信・在庫更新では、上のコードのようにイベントIDや業務IDを Firestore に保存して、二重実行を止める。これを怠ってメールを2通送った日は、本当に冷や汗が出ました。
ふたつ目は、公開HTTP関数を雑に --allow-unauthenticated にしたこと。Webhook で公開が要る場合でも、署名検証・IP制限・レート制限を必ず重ねます。社内バッチや Cloud Scheduler から呼ぶだけなら、OIDC 認証つきの非公開呼び出しが基本です。
みっつ目は、関数に責務を詰め込みすぎたこと。複数ルート、長時間処理、独自OSパッケージが欲しくなった時点で、それは Functions の仕事じゃありません。Cloud Run や Cloud Run jobs に分ける。Functions は「入口の薄い処理」に絞るほど、不思議と安定します。
Claude Code にレビューさせるプロンプト
実装後はそのまま放流せず、Claude Code に観点を渡してレビューさせると、抜けが面白いほど見つかります。
このCloud Run functions実装をレビューしてください。
観点:
- Functions Frameworkの登録名、デプロイの--function(または--entry-point)、package.jsonのtargetが一致しているか
- HTTP関数の認証、入力検証、エラーレスポンスが安全か
- Eventarcから同じイベントが再配達されても二重処理しないか
- Secret Managerの値をログや例外に出していないか
- 実行サービスアカウントに必要最小限のIAMだけを付けているか
- Cloud Loggingで障害調査できる構造化ログになっているか
- Cloud Runに分けたほうがよい責務を関数へ押し込んでいないか
問題があれば、重大度、理由、修正コード、確認コマンドを出してください。
このチェックリストはチームで標準化すると効きます。レビュー観点を文書化したい人は 教材・テンプレート集 にも実務用の型を置いています。
よくある質問
Q. gcloud functions deploy はもう使えないの?
いいえ、2026年も現役です。--gen2 を付ければ Cloud Run 基盤の関数が作れます。Google が旧来のコマンド・API・Terraform の継続サポートを明言しているので、既存資産はそのまま使えます。
Q. gcloud functions deploy --gen2 と gcloud run deploy --function は何が違う?
できあがる実体はほぼ同じ(どちらも Cloud Run サービスとしての関数)。違いは操作の入口です。前者は Functions の流儀でトリガーをコマンド内に書き、後者は Cloud Run の設定オプションを細かく触れます。チームの既存資産に合わせて選べばOKです。
Q. Cloud Functions と Cloud Run、初心者はどっちから? 入口1つ・処理1つの小さな自動化(Webhook、通知、取り込み)なら Cloud Functions が圧倒的にラクです。複数ルートのAPIや独自 Dockerfile が要るなら Cloud Run。迷ったら小さい Functions から始めて、足りなくなったら Cloud Run に引っ越すのが安全です。
Q. HTTP関数とイベント関数、コードの違いは?
登録の仕方だけです。HTTPは functions.http(...) で req/res を扱い、イベントは functions.cloudEvent(...) で cloudEvent を受け取ります。中身のビジネスロジックや Functions Framework の足場は共通なので、片方書ければもう片方もすぐ書けます。
Q. デプロイは成功するのに本番で Permission denied になる。
実行サービスアカウントの権限不足がほぼ原因です。シークレットを読むのはデプロイ担当者ではなく関数の実行サービスアカウント。roles/secretmanager.secretAccessor や roles/datastore.user が実行SAに付いているか確認してください。
実際に試した結果
検証プロジェクトで一通り通してみました。Node.js 24 の Functions Framework によるローカルHTTP実行、CloudEvent の curl 再現、Secret Manager からの環境変数注入、Firestore を使った冪等性チェック、Cloud Logging の構造化ログ検索まで、小さく確認できました。
一番の収穫は、冒頭の「名前が混ざる問題」がただの土台の話だと腹落ちしたことです。gcloud functions deploy --gen2 でも gcloud run deploy --function でも、行き着く先は Cloud Run。だからこそ、コマンドの綴りで悩むより、公開範囲・実行サービスアカウント・ログに出す値・リージョンとコストを人間が見る。関数の中身は Claude Code に任せていい。この線引きが、サーバーレス運用で一番効く安全策でした。
公式の一次情報はここから当たれます。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
制作会社がClaude Codeに触らせる前に決める権限チェックリスト
クライアントサイトを壊さずにAI編集を使うための、制作会社向け権限と確認の型です。
SaaSサポートのバグ報告をClaude Codeで再現手順に変える実務フロー
問い合わせ文をそのまま開発へ投げず、再現手順、証拠、次の一手に整えるサポート向け手順です。
Obsidianの古いメモをClaude Codeの指示書に変える10分ルーチン
Obsidianに溜めたメモが毎回ゴミになる人へ。事実・決定・未確認に仕分けして、Claude Codeがそのまま動ける指示書に変える朝の10分の型を紹介します。