2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code × GCP Cloud Functions|サーバーレス関数を素早く実装する実践ガイド

2
Posted at

はじめに

「Cloud Runは大げさ、でもサーバーレスで HTTP エンドポイントが欲しい」——そんなときに最適なのが GCP Cloud Functions Gen2 です。

この記事では Claude Code を活用して、4 種類のトリガー(HTTP・Pub/Sub・Firestore・Cloud Scheduler)を持つ Cloud Functions を爆速で実装する方法を解説します。コードはすべてコピペで動くレベルで掲載します。


なぜ Cloud Functions に Claude Code が向いているか

Cloud Functions は定型パターンが多く、Claude Code との相性が抜群です。

  • トリガー定義コードが毎回ほぼ同じ → パターン補完が効く
  • gcloud デプロイコマンドのオプションが多い → 抜け漏れを防げる
  • Functions Framework のローカル設定が初見で詰まりやすい → 一括生成できる

1. HTTP トリガー関数

最もよく使う構成です。Bearer トークン検証 + Firestore ログ書き込みのパターン。

import { http, HttpFunction } from "@google-cloud/functions-framework";
import { Request, Response } from "express";
import { Firestore } from "@google-cloud/firestore";

const db = new Firestore();
const VALID_TOKEN = process.env.API_SECRET_TOKEN;

interface ActionRequest {
  userId: string;
  action: string;
}

const handleAction: HttpFunction = async (req: Request, res: Response) => {
  // CORS プリフライト対応
  res.set("Access-Control-Allow-Origin", "*");
  if (req.method === "OPTIONS") {
    res.set("Access-Control-Allow-Methods", "POST");
    res.set("Access-Control-Allow-Headers", "Authorization, Content-Type");
    res.status(204).send("");
    return;
  }

  if (req.method !== "POST") {
    res.status(405).json({ success: false, message: "Method Not Allowed" });
    return;
  }

  // Bearer トークン検証
  const authHeader = req.headers.authorization ?? "";
  if (!authHeader.startsWith("Bearer ") || authHeader.slice(7) !== VALID_TOKEN) {
    res.status(401).json({ success: false, message: "Unauthorized" });
    return;
  }

  const body = req.body as Partial<ActionRequest>;
  if (!body.userId || !body.action) {
    res.status(400).json({ success: false, message: "userId and action are required" });
    return;
  }

  try {
    const logRef = await db.collection("action_logs").add({
      userId: body.userId,
      action: body.action,
      timestamp: new Date(),
      ip: req.ip,
    });
    res.status(200).json({ success: true, logId: logRef.id });
  } catch (err) {
    console.error("Firestore write error:", err);
    res.status(500).json({ success: false, message: "Internal Server Error" });
  }
};

http("handleAction", handleAction);

2. Pub/Sub トリガー関数

非同期イベント処理の定番パターン。例外をスロー = Pub/Sub が再試行正常リターン = メッセージ確認済みというセマンティクスが重要です。

import { cloudEvent, CloudEvent } from "@google-cloud/functions-framework";
import { MessagePublishedData } from "@google/events/cloud/pubsub/v1/MessagePublishedData";
import { Storage } from "@google-cloud/storage";

const storage = new Storage();

interface ImageUploadedMessage {
  bucketName: string;
  filePath: string;
}

cloudEvent<MessagePublishedData>("handleImageUploaded", async (event: CloudEvent<MessagePublishedData>) => {
  const base64Data = event.data?.message?.data;
  if (!base64Data) return; // データなし → 再試行不要

  // base64 デコード
  const rawJson = Buffer.from(base64Data, "base64").toString("utf-8");
  let payload: ImageUploadedMessage;
  try {
    payload = JSON.parse(rawJson) as ImageUploadedMessage;
  } catch {
    return; // JSON パースエラーは再試行しても無駄
  }

  try {
    const [metadata] = await storage
      .bucket(payload.bucketName)
      .file(payload.filePath)
      .getMetadata();
    console.log("Processed:", metadata.name, metadata.size);
  } catch (err) {
    throw err; // ← これで Pub/Sub が再試行する
  }
});

3. Firestore トリガー関数

before.existsafter.exists の組み合わせで作成・更新・削除を判別します。

import { onDocumentWritten, Change, FirestoreEvent } from "firebase-functions/v2/firestore";
import { QueryDocumentSnapshot } from "firebase-admin/firestore";
import * as admin from "firebase-admin";

admin.initializeApp();
const db = admin.firestore();

export const onUserWrite = onDocumentWritten(
  "users/{userId}",
  async (event: FirestoreEvent<Change<QueryDocumentSnapshot> | undefined, { userId: string }>) => {
    const { userId } = event.params;
    const before = event.data?.before;
    const after = event.data?.after;

    // 作成
    if (!before?.exists && after?.exists) {
      await db.collection("email_queue").add({
        to: after.data()?.email,
        template: "welcome",
        createdAt: admin.firestore.FieldValue.serverTimestamp(),
        status: "pending",
      });
    }

    // 削除 → アーカイブ
    if (before?.exists && !after?.exists) {
      await db.collection("deleted_users").doc(userId).set({
        ...before.data(),
        deletedAt: admin.firestore.FieldValue.serverTimestamp(),
      });
    }
  }
);

4. ローカルテスト(Functions Framework)

# セットアップ
npm install --save-dev @google-cloud/functions-framework
npm run build

# 起動
npx @google-cloud/functions-framework --target=handleAction --port=8080

# HTTP テスト
curl -X POST http://localhost:8080 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your-token" \
  -d '{"userId": "user-123", "action": "login"}'

# Pub/Sub エミュレート
MESSAGE_DATA=$(echo -n '{"bucketName":"my-bucket","filePath":"test.png"}' | base64)
curl -X POST http://localhost:8080 \
  -H "Content-Type: application/json" \
  -d "{\"specversion\":\"1.0\",\"type\":\"google.cloud.pubsub.topic.v1.messagePublished\",\"source\":\"//pubsub.googleapis.com\",\"id\":\"test-id\",\"data\":{\"message\":{\"data\":\"$MESSAGE_DATA\",\"attributes\":{}}}}"

5. デプロイコマンド

gcloud functions deploy handleAction \
  --gen2 \
  --runtime=nodejs22 \
  --region=asia-northeast1 \
  --source=. \
  --entry-point=handleAction \
  --trigger-http \
  --allow-unauthenticated \
  --memory=512Mi \
  --timeout=60s \
  --min-instances=0 \
  --max-instances=100 \
  --set-secrets="API_SECRET_TOKEN=api-secret-token:latest" \
  --set-env-vars="NODE_ENV=production"

落とし穴 4 選

落とし穴 原因 対策
コールドスタート遅延 インスタンス起動に 2〜5 秒 --min-instances=1 を設定
タイムアウト 9 分制限 Gen2 の上限は 540 秒 Pub/Sub でチャンク分割処理
Secret Manager 権限エラー 実行 SA に権限がない secretmanager.secretAccessor ロールを付与
メモリ不足 OOM デフォルト 256MB は少ない --memory=2Gi --cpu=2 に変更

Secret Manager の権限付与コマンド:

PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)")
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
  --role="roles/secretmanager.secretAccessor"

まとめ

トリガー 用途 Claude Code で生成できるもの
HTTP Webhook・API 認証・バリデーション・エラーハンドリング一式
Pub/Sub 非同期処理 base64 デコード・再試行ロジック
Firestore データ変更反応 作成/更新/削除の判別ロジック
Cloud Scheduler 定時バッチ OIDC 検証・タイムゾーン計算

Claude Code に「GCP Cloud Functions の HTTP トリガーを TypeScript で書いて、Bearer トークン検証付き」と依頼するだけで、本記事のようなコードが数秒で出てきます。あとはプロジェクトの要件に合わせて微調整するだけです。


より詳しく読みたい方へ

実務レベルの詳細(Cloud Scheduler + OIDC 連携・GitHub Actions CI/CD・コールドスタート計測結果)は以下のブログ記事で解説しています。

Claude Code × GCP Cloud Functions 完全ガイド | claudecode-lab.com

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?