Claude Code로 GCP Cloud Run 배포하기: 실전 가이드
Docker, IAM, Secret Manager, 로그, 롤백까지 Node.js API를 Cloud Run에 안전하게 배포합니다.
Cloud Run은 컨테이너 기반 HTTP 서비스를 공개하고 싶지만 Kubernetes, ECS, VM 운영까지 맡고 싶지는 않을 때 좋은 선택입니다. Cloud Functions나 Lambda처럼 모든 로직을 작은 함수로 쪼갤 필요도 없습니다. Express API, BFF, webhook 수신 서버를 Docker 이미지로 만들면 Cloud Run이 HTTPS, revision, 확장, 로그를 관리합니다.
이 글은 Claude Code를 활용해 TypeScript/Express 서비스를 Cloud Run에 올리는 과정을 실전 기준으로 정리합니다. 실행 가능한 코드, Dockerfile, 로컬 테스트, Artifact Registry, gcloud run deploy, 서비스 계정, IAM, Secret Manager, concurrency, min instances, Cloud Logging, revision rollback, 비용 함정, 보안 리뷰 프롬프트를 모두 포함합니다.
용어를 쉽게 풀면 Cloud Run은 “HTTP 요청을 받는 컨테이너를 서버리스로 실행하는 플랫폼”입니다. Artifact Registry는 “Docker 이미지 저장소”, Secret Manager는 “비밀번호와 API 키 보관함”, IAM은 “누가 어떤 리소스에 접근할 수 있는지 정하는 권한 체계”입니다.
Cloud Run이 Functions나 Lambda보다 나은 경우
아주 작은 이벤트 핸들러라면 Cloud Functions나 Lambda가 더 단순할 수 있습니다. 하지만 서비스가 이미 HTTP 앱이거나 런타임 구성이 복잡하다면 Cloud Run이 더 자연스럽습니다.
| 사용 사례 | Cloud Run이 맞는 이유 |
|---|---|
| Webhook 수신 API | Stripe, GitHub, LINE 같은 POST 요청을 Express 라우트로 처리하기 쉽다 |
| 작은 BFF/API 서버 | 인증, 미들웨어, 라우팅, 검증을 하나의 Node.js 앱으로 유지할 수 있다 |
| 예약 실행용 HTTP 엔드포인트 | Cloud Scheduler에서 HTTP로 호출하면 별도 작업 프레임워크가 필요 없다 |
| 가벼운 AI/데이터 API | 네이티브 의존성이나 바이너리를 컨테이너 안에 넣을 수 있다 |
장시간 상주 worker에는 신중해야 합니다. Cloud Run은 요청 기반 서비스에 강하지만, 백그라운드 루프는 CPU 할당과 비용을 먼저 검토해야 합니다.
실행 가능한 Express 서비스
Cloud Run은 컨테이너에 PORT 환경 변수를 전달합니다. 포트를 고정하지 말고 이 값을 읽어야 합니다. /health는 배포 후 확인에 사용합니다.
{
"name": "cloud-run-claude-code-api",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"test": "node --test"
},
"dependencies": {
"express": "^4.19.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.10.0",
"tsx": "^4.19.2",
"typescript": "^5.7.2"
}
}
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"]
}
import express from "express";
const app = express();
app.use(express.json());
const requiredEnv = ["DATABASE_URL", "JWT_SECRET"];
for (const key of requiredEnv) {
if (!process.env[key]) {
console.error(`Missing required environment variable: ${key}`);
process.exit(1);
}
}
app.get("/health", (_req, res) => {
res.status(200).json({ ok: true, service: "myapp-api" });
});
app.post("/webhooks/example", (req, res) => {
console.log("webhook_received", {
eventType: req.body?.type ?? "unknown",
receivedAt: new Date().toISOString()
});
res.status(202).json({ accepted: true });
});
app.get("/config-check", (_req, res) => {
res.json({
nodeEnv: process.env.NODE_ENV ?? "development",
hasDatabaseUrl: Boolean(process.env.DATABASE_URL),
hasJwtSecret: Boolean(process.env.JWT_SECRET)
});
});
const port = Number(process.env.PORT ?? 8080);
const server = app.listen(port, () => {
console.log(`listening on ${port}`);
});
process.on("SIGTERM", () => {
console.log("SIGTERM received, closing HTTP server");
server.close(() => process.exit(0));
setTimeout(() => process.exit(1), 30000).unref();
});
먼저 로컬에서 확인합니다.
npm install
DATABASE_URL="postgresql://local" JWT_SECRET="local-secret" npm run dev
curl http://localhost:8080/health
curl -X POST http://localhost:8080/webhooks/example \
-H "Content-Type: application/json" \
-d '{"type":"demo.created"}'
Dockerfile과 Claude Code 리뷰
Claude Code에는 단순 생성이 아니라 운영 리뷰를 요청합니다. 런타임 이미지 크기, production dependency만 설치, non-root 실행, .dockerignore, PORT 대응, 보안과 비용 리스크를 함께 봐야 합니다.
claude -p "
Review and improve this Cloud Run Docker setup.
Requirements:
- Node.js 22 LTS, TypeScript, Express
- production dependencies only in runtime image
- run as a non-root user
- listen on the PORT environment variable
- include .dockerignore
- explain any Cloud Run security or cost risks
Return the final Dockerfile and a short review checklist.
"
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=8080
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER appuser
EXPOSE 8080
CMD ["node", "--max-old-space-size=384", "dist/index.js"]
node_modules
dist
.env
.env.*
*.log
.git
.gitignore
Dockerfile
README.md
docker build -t myapp-api:local .
docker run --rm -p 8080:8080 \
-e PORT=8080 \
-e DATABASE_URL="postgresql://local" \
-e JWT_SECRET="local-secret" \
myapp-api:local
curl http://localhost:8080/health
Artifact Registry와 첫 배포
Artifact Registry는 Docker 이미지 저장소입니다. 대상 리전의 Docker 인증을 설정하고, 저장소를 만든 뒤 이미지를 push합니다.
PROJECT_ID="my-project-123"
REGION="asia-northeast1"
REPOSITORY="myapp"
SERVICE="myapp-api"
IMAGE="$REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/api:v1.0.0"
gcloud config set project "$PROJECT_ID"
gcloud services enable run.googleapis.com artifactregistry.googleapis.com secretmanager.googleapis.com logging.googleapis.com
gcloud artifacts repositories create "$REPOSITORY" \
--repository-format=docker \
--location="$REGION" \
--description="Docker images for myapp"
gcloud auth configure-docker "$REGION-docker.pkg.dev"
docker build -t "$IMAGE" .
docker push "$IMAGE"
배포 전 전용 런타임 서비스 계정을 만듭니다.
gcloud iam service-accounts create myapp-run \
--display-name="Cloud Run runtime for myapp"
SERVICE_ACCOUNT="myapp-run@$PROJECT_ID.iam.gserviceaccount.com"
gcloud run deploy "$SERVICE" \
--image "$IMAGE" \
--region "$REGION" \
--platform managed \
--service-account "$SERVICE_ACCOUNT" \
--memory 512Mi \
--cpu 1 \
--concurrency 80 \
--min-instances 0 \
--max-instances 20 \
--allow-unauthenticated \
--set-env-vars NODE_ENV=production \
--port 8080
내부 API라면 --allow-unauthenticated를 빼고 필요한 호출자에게만 Cloud Run Invoker 권한을 부여합니다.
Secret Manager와 IAM
비밀번호를 --set-env-vars에 직접 쓰지 않습니다. Secret Manager에 저장하고 Cloud Run이 환경 변수로 주입하게 합니다.
echo -n "postgresql://user:password@host:5432/app" | \
gcloud secrets create DATABASE_URL --data-file=-
echo -n "replace-with-long-random-value" | \
gcloud secrets create JWT_SECRET --data-file=-
gcloud secrets add-iam-policy-binding DATABASE_URL \
--member="serviceAccount:$SERVICE_ACCOUNT" \
--role="roles/secretmanager.secretAccessor"
gcloud secrets add-iam-policy-binding JWT_SECRET \
--member="serviceAccount:$SERVICE_ACCOUNT" \
--role="roles/secretmanager.secretAccessor"
gcloud run services update "$SERVICE" \
--region "$REGION" \
--set-secrets "DATABASE_URL=DATABASE_URL:latest,JWT_SECRET=JWT_SECRET:latest"
프로젝트 전체 editor 권한 대신 secret 단위 권한을 주면 사고 범위를 줄일 수 있습니다.
Concurrency, Min Instances, 비용
Concurrency는 하나의 컨테이너 인스턴스가 동시에 처리할 수 있는 요청 수입니다. 값을 높이면 인스턴스 수는 줄 수 있지만, DB 커넥션 풀과 외부 API 제한이 먼저 막힐 수 있습니다.
gcloud run services update "$SERVICE" \
--region "$REGION" \
--concurrency 40 \
--min-instances 1 \
--max-instances 20 \
--cpu-throttling
개발 환경은 min-instances 0이 비용 면에서 낫습니다. webhook이나 본番 API처럼 첫 요청 지연이 싫다면 1 이상을 검토합니다. webhook은 20-40, BFF/API는 40-80에서 시작해 p95 latency, DB 연결 수, 오류율로 조정합니다.
Cloud Logging과 롤백
Cloud Run의 요청 로그, 컨테이너 로그, 시스템 로그는 Cloud Logging으로 들어갑니다. Node.js 앱은 stdout/stderr에 로그를 쓰면 충분한 경우가 많습니다.
gcloud run services logs read "$SERVICE" \
--region "$REGION" \
--limit 20
gcloud logging read \
"resource.type=cloud_run_revision AND resource.labels.service_name=$SERVICE" \
--limit 20 \
--format=json
배포나 설정 변경은 immutable revision을 만듭니다. 문제가 있으면 트래픽을 이전 revision으로 되돌립니다.
gcloud run revisions list \
--service "$SERVICE" \
--region "$REGION"
gcloud run services update-traffic "$SERVICE" \
--region "$REGION" \
--to-revisions myapp-api-00012-abc=100
배포 전에는 이 프롬프트로 Claude Code 리뷰를 요청합니다.
claude -p "
Act as a Cloud Run deployment reviewer.
Review package.json, Dockerfile, src/index.ts, and the gcloud commands below.
Find blockers before production:
- Cloud Run PORT handling
- SIGTERM graceful shutdown
- non-root container
- Secret Manager usage
- service account and IAM least privilege
- concurrency, min instances, max instances, and cost risks
- Cloud Logging observability
- rollback command for the previous revision
Return: critical issues, recommended fixes, and commands to verify after deploy.
"
자주 놓치는 함정
큰 이미지는 build, push, cold start를 모두 느리게 합니다. secret을 일반 환경 변수로 넣으면 명령 기록이나 CI 로그에 남을 수 있습니다. min-instances 1은 지연을 줄이지만 idle 비용이 생깁니다. concurrency를 과하게 올리면 하위 DB와 SaaS 제한이 먼저 터집니다. 롤백 명령은 장애 중이 아니라 평소에 연습해야 합니다.
참고 자료와 다음 단계
- Cloud Run 문서
- Cloud Run에 컨테이너 이미지 배포
- Artifact Registry Docker 인증
- Cloud Run Secret 설정
- Cloud Run 로그
- Cloud Run 롤백과 트래픽 이전
- Claude Code Docker 연동
- Claude Code AWS ECS/Fargate 가이드
- Claude Code 보안 모범 사례
Cloud Run은 함수 플랫폼보다 유연하고 ECS/Kubernetes보다 운영 부담이 낮은 중간 지점입니다. 팀에 Claude Code 프롬프트, 리뷰 체크리스트, 배포 가드레일을 정착시키려면 Claude Code 교육 및 상담을 참고하세요.
직접 확인한 내용
예시는 로컬 npm run dev, Docker /health, PORT 처리, 필수 환경 변수 검증까지 확인했습니다. 운영 배포 전에는 실제 프로젝트의 Artifact Registry 권한, Secret Manager IAM, Cloud Logging 쿼리, update-traffic 롤백 훈련까지 확인해야 합니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Obsidian 메모를 CLAUDE.md로 바꾸는 Claude Code 워크플로
Obsidian 작업 메모를 CLAUDE.md 운영 노트로 정리해 Claude Code 세션의 문맥 반복을 줄입니다.
Claude Code Revenue CTA Routing: 글에서 PDF, Gumroad, 상담으로 보내기
독자 의도에 따라 무료 PDF, Gumroad 상품, 상담으로 나누는 Claude Code CTA 설계입니다.
Claude Code 팀 인계 규칙: 리뷰 증거, 권한, 롤백, 수익 경로까지 넘기는 법
Claude Code 작업을 팀에 넘길 때 필요한 증거, 권한 규칙, 롤백, 무료 PDF, Gumroad, 상담 경로 체크리스트.