GCP Cloud Functions with Claude Code: HTTP, Secret Manager, Cloud Logging
Build Cloud Run functions with Claude Code: HTTP handlers, Secret Manager, Cloud Logging, deploy checks, and pitfalls.
Google Cloud documentation now often presents Cloud Functions through the Cloud Run functions path. The practical model is simple: you deploy source code, Google Cloud builds it into a container, and the result runs as a Cloud Run service that is invoked by HTTP requests or events.
This guide upgrades the Claude Code workflow around GCP Cloud Functions: one Node.js HTTP function, one Cloud Storage event function through Eventarc, Secret Manager, Cloud Logging, and deployment verification. Eventarc is Google Cloud’s event delivery layer. Functions Framework is the small adapter that lets the same function shape run locally and in the cloud.
My rule at ClaudeCodeLab is to use Cloud Run functions when the unit of work is naturally small: a webhook receiver, a lightweight notification bridge, a CSV import trigger, or a scheduled job entry point. I choose Cloud Run instead when I need many routes, a full API, Next.js or Express, WebSockets, a custom Dockerfile, long-running work, or detailed container control. The related GCP Cloud Run guide covers that side.
Claude Code is useful for generating the repeatable parts: package.json, Functions Framework registration, curl commands, gcloud run deploy, IAM notes, and a review prompt. Human review still matters for authentication, Secret Manager, retries, idempotency, and logs. Idempotency means receiving the same request or event twice does not corrupt the result.
Good Use Cases
| Use case | Entry point | What to review |
|---|---|---|
| Stripe, GitHub, or internal webhooks | HTTP function | Signature checks, Bearer token, replay handling, Secret Manager |
| CSV import after Cloud Storage upload | Eventarc + CloudEvent function | Duplicate events, bucket location, file naming rules |
| Contact form notification | HTTP function or Pub/Sub handoff | Fast 200 response, queue handoff, rate limits |
| Nightly sync or report trigger | Cloud Scheduler + HTTP function | OIDC authentication, time zone, timeout |
These cases work well with Claude Code because the input, validation, logging, and failure behavior are easy to turn into a checklist. The design becomes fragile when one function owns many unrelated jobs, runs long loops, or acts as an entire user-facing API.
For team processes, pair this article with Claude Code code review workflows and secret management with Claude Code.
Minimal Project
This example uses CommonJS Node.js so it can run without a build step. The official docs use Functions Framework to register HTTP and CloudEvent functions, and the same framework works locally.
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
Function Code
Save this as index.js. The HTTP function checks Authorization: Bearer ... and uses Idempotency-Key as the duplicate-prevention key. The Storage event function stores the CloudEvent ID in Firestore so a retried event does not create the same job twice.
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 });
});
Structured logs make Cloud Logging usable during incidents. Cloud Run automatically sends stdout and stderr to Cloud Logging, so JSON lines with severity, message, eventId, and userId are much easier to filter than plain strings.
Local Checks
Start the HTTP function:
export API_TOKEN="local-token"
npm run start:http
Send a request from another terminal:
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"}'
Start the CloudEvent function:
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"}'
If the local code connects to Firestore, run gcloud auth application-default login or use a dedicated test project. Do not point smoke tests at production data.
Secrets and IAM
Do not put tokens in source code or .env files that get copied into reviews. Store the token in Secret Manager and grant the runtime service account access.
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"
Keep two identities separate. The deployer needs permission to create Cloud Run functions and act as the service account. The runtime service account needs only the permissions the function uses, such as Secret Manager access and Firestore writes.
Deploy With gcloud run
Current docs show Cloud Run functions deployed with gcloud run deploy. For a private HTTP endpoint, start authenticated by default:
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
For the Storage event function, deploy the service first and then create an 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 triggers can take a few minutes to become active. Also check region alignment: the Cloud Storage bucket, Eventarc trigger, and Cloud Run service should be planned together. Region choices affect latency, data residency, and pricing tiers.
Logs and Verification
After deploy, test the endpoint and inspect logs before considering the function ready.
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
Log enough to diagnose incidents: event ID, file name, duplicate flag, external API status, and safe business IDs. Do not log tokens, full message bodies, or personal data.
Pitfalls
First, retries without idempotency create duplicate side effects. Billing, emails, inventory updates, and imports need a stored event ID or business key.
Second, Secret Manager access must be granted to the runtime service account. A deploy can succeed while production requests fail with Permission denied.
Third, public HTTP functions need more than --allow-unauthenticated. For public webhooks, add signature checks, rate limits, and an edge design such as API Gateway or Cloud Armor when appropriate. For internal jobs, prefer authenticated calls.
Fourth, do not turn Cloud Run functions into a full application host. Multiple routes, long processing, custom packages, GPU needs, and detailed container control belong in Cloud Run, Cloud Run jobs, or Workflows.
Fifth, include cost and cleanup in the prompt. Cloud Run may scale to zero, but Artifact Registry, Cloud Build, Cloud Storage, Eventarc, and Cloud Logging can still create costs.
Review Prompt
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 turns these operational checklists into products and templates and team training. They are useful when you want serverless reviews to be repeatable instead of depending on one senior engineer’s memory.
Official Docs
- 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
Result
The tested shape is small but production-like: Node.js 24 Cloud Run functions, local Functions Framework commands, curl-based HTTP and CloudEvent checks, Secret Manager injection, Firestore-backed idempotency, and Cloud Logging queries. The important habit is to review identity, logs, retries, region, and cost before the function receives real traffic.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Obsidian to CLAUDE.md Workflow: Stop Re-explaining Context
Turn Obsidian working notes into concise CLAUDE.md operating notes that make Claude Code sessions easier to resume.
Claude Code Revenue CTA Routing: Send Articles to PDF, Gumroad, and Consultation
A Claude Code workflow for routing article readers to the free PDF, Gumroad products, or consultation by intent.
Claude Code Team Handoff Rules: Review Evidence, Permissions, Rollback, and Revenue Paths
A practical Claude Code handoff format for team review, proof, permission rules, rollback, free PDF, Gumroad, and consultation paths.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.