Use Cases (업데이트: 2026. 6. 3.)

Claude Code로 GCP Cloud Functions 만들기: HTTP, Secret Manager, Cloud Logging

Claude Code로 Cloud Run functions를 구축합니다. HTTP, secrets, logging, 배포 확인과 흔한 함정을 다룹니다.

Claude Code로 GCP Cloud Functions 만들기: HTTP, Secret Manager, Cloud Logging

Google Cloud 문서는 이제 Cloud Functions를 Cloud Run functions 흐름으로 설명하는 경우가 많습니다. 실무적으로는 소스 코드를 배포하면 Google Cloud가 컨테이너로 빌드하고, 그 결과가 HTTP 요청이나 이벤트로 호출되는 Cloud Run 서비스로 실행된다고 이해하면 됩니다.

이 글은 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 메모, 리뷰 프롬프트가 여기에 해당합니다. 그래도 인증, Secret Manager, 재시도, 멱등성, 로그는 사람이 반드시 검토해야 합니다. 멱등성이란 같은 요청이나 이벤트가 두 번 와도 결과가 깨지지 않는 성질입니다.


적합한 사용 사례

사용 사례진입점리뷰할 내용
Stripe, GitHub, 사내 도구 WebhookHTTP 함수서명 검증, Bearer 토큰, 재전송 대응, Secret Manager
Cloud Storage 업로드 후 CSV 가져오기Eventarc + CloudEvent 함수중복 이벤트, 버킷 리전, 파일명 규칙
문의 폼 알림HTTP 함수 또는 Pub/Sub 전달빠른 200 응답, 큐 전달, rate limit
야간 동기화나 리포트 시작Cloud Scheduler + HTTP 함수OIDC 인증, 시간대, timeout

이런 사례는 입력, 검증, 로깅, 실패 처리를 체크리스트로 만들기 쉬워 Claude Code와 잘 맞습니다. 하나의 함수에 여러 책임을 넣거나, 긴 루프를 돌리거나, 전체 공개 API로 만들면 운영이 쉽게 불안정해집니다.

팀 운영에서는 Claude Code 코드 리뷰Claude Code secret 관리도 함께 연결하면 좋습니다.


최소 프로젝트

빌드 단계 없이 실행되도록 CommonJS Node.js를 사용합니다. 공식 문서는 Functions 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에 저장하여 재전달된 이벤트가 같은 작업을 두 번 만들지 않게 합니다.

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

토큰을 소스 코드나 리뷰에 노출되는 .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"

두 신원을 분리해서 봐야 합니다. 배포자는 Cloud Run functions를 만들고 서비스 계정을 사용할 권한이 필요합니다. 런타임 서비스 계정에는 코드가 실제로 쓰는 최소 권한만 부여합니다.


gcloud run으로 배포

현재 흐름은 gcloud run deploy를 사용합니다. 비공개 HTTP 엔드포인트라면 먼저 인증이 필요한 상태로 배포합니다.

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 트리거를 만듭니다.

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 트리거는 활성화까지 몇 분 걸릴 수 있습니다. 버킷 리전, 트리거 위치, Cloud Run 리전을 함께 설계해야 지연 시간, 데이터 위치, 비용을 통제할 수 있습니다.


로그와 확인

배포 후에는 URL만 확인하지 말고 엔드포인트와 로그를 같이 봅니다.

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

조사에 필요한 값은 남기되 안전하게 남깁니다. 이벤트 ID, 파일명, 중복 여부, 외부 API 상태, 안전한 업무 ID는 유용합니다. 토큰, 전체 메시지 본문, 개인정보는 로그에 남기지 않습니다.


흔한 함정

첫째, 멱등성 없는 재시도는 중복 부작용을 만듭니다. 결제, 이메일, 재고, 가져오기 작업에는 저장된 이벤트 ID나 업무 키가 필요합니다.

둘째, Secret Manager 접근 권한은 런타임 서비스 계정에 있어야 합니다. 배포는 성공했는데 실제 요청은 Permission denied로 실패할 수 있습니다.

셋째, 공개 HTTP 함수에는 --allow-unauthenticated 이상의 보호가 필요합니다. 공개 Webhook이라면 서명 검증, rate limit, 필요 시 API Gateway나 Cloud Armor를 고려합니다. 내부 작업은 인증된 호출을 기본으로 둡니다.

넷째, Cloud Run functions를 전체 애플리케이션 호스트로 만들지 않습니다. 많은 라우트, 긴 작업, 시스템 패키지, GPU, 세밀한 컨테이너 제어는 Cloud Run, Cloud Run jobs, Workflows가 더 맞습니다.

다섯째, 비용과 정리를 프롬프트에 포함합니다. 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은 이런 운영 체크리스트를 제품과 템플릿 그리고 팀 교육으로 정리합니다. Serverless 리뷰를 한 명의 시니어 기억에 의존하지 않고 반복 가능한 방식으로 만들 때 유용합니다.


공식 문서


결과

검증한 형태는 작지만 운영에 가깝습니다. Node.js 24 Cloud Run functions, 로컬 Functions Framework 명령, curl 기반 HTTP와 CloudEvent 확인, Secret Manager 주입, Firestore 기반 멱등성, Cloud Logging 쿼리까지 포함합니다. 실제 트래픽을 받기 전에 신원, 로그, 재시도, 리전, 비용을 확인하는 습관이 가장 중요합니다.

#claude-code #gcp #cloud-functions #typescript #serverless #pubsub
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.