用 Claude Code 构建 GCP Cloud Functions:HTTP、Secret Manager、Cloud Logging
用 Claude Code 构建 Cloud Run functions:HTTP、密钥、日志、部署验证与常见坑。
Google Cloud 的当前文档经常把 Cloud Functions 放在 Cloud Run functions 这条路径下说明。实际理解可以很简单:你部署源代码,Google Cloud 把它构建成容器,然后它作为一个 Cloud Run 服务运行,由 HTTP 请求或事件触发。
这篇文章整理如何用 Claude Code 构建 GCP Cloud Functions:一个 Node.js HTTP 函数,一个通过 Eventarc 接收 Cloud Storage 事件的函数,Secret Manager、Cloud Logging,以及部署后的确认步骤。Eventarc 是 Google Cloud 的事件投递层。Functions Framework 是让同一种函数形态可以在本地和云端运行的适配层。
在 ClaudeCodeLab,我会把 Cloud Run functions 用在小而清晰的工作单元上:接收 webhook、发送轻量通知、触发 CSV 导入、作为定时任务入口。如果需要多路由、完整 API、Next.js 或 Express、WebSocket、自定义 Dockerfile、长时间任务或更细的容器控制,我会选择 Cloud Run。相关判断可以看内部文章 GCP Cloud Run 指南。
Claude Code 很适合生成重复性内容:package.json、Functions Framework 注册、curl 命令、gcloud run deploy、IAM 备注和 review prompt。但认证、Secret Manager、重试、幂等性和日志仍然需要人工审查。幂等性指的是同一个请求或事件来两次,也不会破坏结果。
适合的用例
| 用例 | 入口 | 需要审查的点 |
|---|---|---|
| Stripe、GitHub 或内部工具 webhook | HTTP 函数 | 签名校验、Bearer token、防重放、Secret Manager |
| Cloud Storage 上传后导入 CSV | Eventarc + CloudEvent 函数 | 重复事件、bucket 区域、文件命名规则 |
| 联系表单通知 | HTTP 函数或 Pub/Sub 转交 | 快速返回 200、队列转交、速率限制 |
| 夜间同步或报表触发 | Cloud Scheduler + HTTP 函数 | OIDC 认证、时区、timeout |
这些场景适合 Claude Code,因为输入、校验、日志和失败处理都容易变成 checklist。如果一个函数承担太多职责、执行很长的循环,或者被当作完整公开 API,设计会很快变得难维护。
团队流程中,可以搭配 Claude Code 代码审查 和 Claude Code 密钥管理。
最小项目
示例使用 CommonJS Node.js,这样不需要 build 步骤。官方文档通过 Functions Framework 注册函数,同一个 framework 也能在本地运行。
functions-demo/
index.js
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
函数代码
把下面内容保存为 index.js。HTTP 函数检查 Authorization: Bearer ...,并使用 Idempotency-Key 作为防重复键。Storage 函数把 CloudEvent ID 保存到 Firestore,避免重试事件创建两次同一个 job。
const functions = require("@google-cloud/functions-framework");
const { Firestore } = require("@google-cloud/firestore");
const crypto = require("node:crypto");
const db = new Firestore();
function jsonLog(severity, message, extra = {}) {
console.log(JSON.stringify({ severity, message, ...extra }));
}
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");
}
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" });
}
});
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 });
});
结构化日志能让 Cloud Logging 在故障排查时真正可用。Cloud Run 会自动把 stdout 和 stderr 发送到 Cloud Logging。带有 severity、message、eventId 和 userId 的 JSON 日志,比普通字符串更容易过滤。
本地测试
启动 HTTP 函数:
export API_TOKEN="local-token"
npm run start:http
在另一个终端发送请求:
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 函数:
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 或单独的测试项目。不要把 smoke test 指向生产数据。
Secret Manager 和 IAM
不要把 token 放进源代码,也不要放进会进入 review 的 .env 文件。把它存到 Secret Manager,并只给运行时 service account 访问权限。
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"
要区分两个身份:部署者需要创建 Cloud Run functions 并使用 service account 的权限;运行时 service account 只需要代码实际用到的最小权限。
用 gcloud run 部署
当前流程使用 gcloud run deploy。如果 HTTP endpoint 不是公开 webhook,先部署为需要认证的服务。
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
Storage 事件函数需要先部署服务,再创建 Eventarc trigger:
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 trigger 可能需要几分钟才会生效。bucket 区域、trigger 位置和 Cloud Run 区域要一起设计,因为它们会影响延迟、数据驻留和价格。
日志与验证
部署后不要只看 URL。先测试 endpoint,再读取日志。
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
日志要足够支持排查:event ID、文件名、是否重复、外部 API 状态、安全的业务 ID。不要记录 token、完整消息体或个人数据。
常见坑
第一,只有重试没有幂等性会造成重复副作用。计费、邮件、库存和导入任务都需要持久化 event ID 或业务 key。
第二,Secret Manager 权限必须给运行时 service account。部署可以成功,但生产请求仍可能因为 Permission denied 失败。
第三,公开 HTTP 函数不能只依赖 --allow-unauthenticated。公开 webhook 需要签名校验、速率限制,必要时加 API Gateway 或 Cloud Armor。内部 job 应优先使用认证调用。
第四,不要把 Cloud Run functions 当成完整应用托管。多路由、长任务、系统包、GPU、细粒度容器控制更适合 Cloud Run、Cloud Run jobs 或 Workflows。
第五,把成本和清理写进 prompt。即使 Cloud Run 可以 scale to zero,Artifact Registry、Cloud Build、Storage、Eventarc 和 Cloud Logging 仍可能产生费用。
审查提示词
Review this Cloud Run functions implementation.
Check:
- Functions Framework registration, gcloud run deploy --function, and package.json target match
- HTTP authentication, input validation, and error responses are safe
- Eventarc retries cannot create duplicate side effects
- Secret Manager values are not logged or returned in exceptions
- The runtime service account has only the minimum IAM roles
- Cloud Logging entries are structured enough for incident review
- Any workload that should be Cloud Run is not forced into a function
If there are issues, return severity, reason, corrected code, and verification commands.
ClaudeCodeLab 会把这类运维 checklist 做成 产品与模板 和 团队培训。当你希望 serverless review 可重复,而不是依赖某一位资深工程师的记忆时,这很有用。
官方文档
- Cloud Run functions documentation
- Deploy a Cloud Run function
- Deploy a Cloud Run function using the gcloud CLI
- Write Cloud Run functions
- Local functions development
- Trigger functions from Cloud Storage using Eventarc
- Configure secrets for Cloud Run services
- Logging and viewing logs in Cloud Run
- Compare Cloud Run functions
- Cloud Run locations
结果
这个验证形态很小,但接近生产:Node.js 24 Cloud Run functions、本地 Functions Framework 命令、用 curl 检查 HTTP 和 CloudEvent、Secret Manager 注入、Firestore 幂等性、Cloud Logging 查询。真正接入流量前,最重要的是检查身份、日志、重试、区域和成本。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。