구글의 완전 관리형 서버리스 컨테이너 플랫폼으로, HTTP 요청에 따라 자동으로 스케일링되며 사용한 만큼만 과금되는 서비스입니다.
💡 주요 특징 및 장점
🤔 질문: “기존 VM이나 GKE 대신 Cloud Run을 언제 사용해야 할까?”
📋 Cloud Run 핵심 장점
서버리스 컨테이너의 이점
완전 관리: 인프라 관리 불필요
자동 스케일링: 0에서 수천 개 인스턴스까지 자동 확장
비용 효율: 요청 처리 시간만 과금
빠른 배포: 컨테이너 이미지만 있으면 즉시 배포
💻 서비스 비교표
특성
Cloud Run
App Engine
GKE
Compute Engine
관리 복잡도
낮음
낮음
높음
매우 높음
스케일링
자동 (0-1000)
자동
수동/자동
수동
비용
사용량 기반
사용량 기반
상시 과금
상시 과금
컨테이너 지원
네이티브
커스텀 런타임
네이티브
수동 설정
Cold Start
있음
있음
없음
없음
📊 적합한 사용 사례
Cloud Run 최적 활용 시나리오
✅ 적합한 경우:
API 서버, 웹 애플리케이션
이벤트 처리, 배치 작업
마이크로서비스 아키텍처
트래픽 변동이 큰 서비스
❌ 부적합한 경우:
지속적인 연결이 필요한 서비스 (WebSocket 장시간)
높은 메모리/CPU가 지속적으로 필요
로컬 파일시스템에 의존적
15분 이상의 장시간 처리
2. 환경 설정 및 준비
사전 준비 사항
Google Cloud 계정, gcloud CLI, Docker가 필요합니다. 순서대로 설정해보겠습니다.
💡 필수 도구 설치 및 설정
🤔 질문: “처음 시작할 때 어떤 도구들을 설치해야 할까?”
📋 macOS 환경 설정 (Homebrew 사용)
필수 도구 설치
# Google Cloud CLI 설치brew install google-cloud-sdk# Docker Desktop 설치 (GUI로 설치 권장)brew install --cask docker# 기타 유용한 도구들brew install jq # JSON 파싱brew install httpie # HTTP 클라이언트brew install dive # Docker 이미지 분석
💻 Google Cloud 초기 설정
# 📊 Google Cloud 인증 및 프로젝트 설정# 1. Google Cloud 로그인gcloud auth login# 2. 프로젝트 설정 (기존 프로젝트 사용 또는 신규 생성)# 프로젝트 목록 확인gcloud projects list# 프로젝트 설정export PROJECT_ID="your-project-id"gcloud config set project $PROJECT_ID# 3. 필수 API 활성화gcloud services enable run.googleapis.comgcloud services enable cloudbuild.googleapis.comgcloud services enable containerregistry.googleapis.comgcloud services enable artifactregistry.googleapis.com# 4. 기본 리전 설정gcloud config set run/region asia-northeast3 # 서울 리전
📊 환경 변수 설정
# 📋 배포 스크립트용 환경 변수cat > ~/.cloudrun_env << 'EOF'# Google Cloud 설정export PROJECT_ID="your-project-id"export REGION="asia-northeast3"export SERVICE_ACCOUNT_EMAIL="your-service-account@your-project.iam.gserviceaccount.com"# Container Registry 설정export REGISTRY_URL="gcr.io"export ARTIFACT_REGISTRY_URL="asia-northeast3-docker.pkg.dev"# Cloud Run 기본 설정export DEFAULT_MEMORY="512Mi"export DEFAULT_CPU="1"export DEFAULT_CONCURRENCY="80"export DEFAULT_TIMEOUT="300"EOF# 환경 변수 로드source ~/.cloudrun_envecho "source ~/.cloudrun_env" >> ~/.zshrc # 또는 ~/.bashrc
# 📊 app.pyfrom flask import Flask, request, jsonifyimport osimport timeimport jsonfrom datetime import datetimeapp = Flask(__name__)@app.route('/')def home(): return jsonify({ 'message': 'Hello from Python Flask on Cloud Run!', 'timestamp': datetime.now().isoformat(), 'python_version': f"{os.sys.version}", 'version': '1.0.0' })@app.route('/api/health')def health_check(): return jsonify({ 'status': 'healthy', 'uptime': time.time() - start_time, 'environment': os.environ.get('FLASK_ENV', 'production') })@app.route('/api/env')def show_env(): # 환경 변수 표시 (민감한 정보 제외) safe_env = {k: v for k, v in os.environ.items() if not any(secret in k.lower() for secret in ['password', 'key', 'secret', 'token'])} return jsonify(safe_env)@app.route('/api/process', methods=['POST'])def process_data(): try: data = request.get_json() # 간단한 데이터 처리 시뮬레이션 processed = { 'input': data, 'processed_at': datetime.now().isoformat(), 'result': f"Processed {len(str(data))} characters" } return jsonify(processed) except Exception as e: return jsonify({'error': str(e)}), 400@app.errorhandler(404)def not_found(error): return jsonify({'error': 'Not found'}), 404@app.errorhandler(500)def internal_error(error): return jsonify({'error': 'Internal server error'}), 500# 시작 시간 기록start_time = time.time()if __name__ == '__main__': port = int(os.environ.get('PORT', 8080)) debug = os.environ.get('FLASK_ENV') == 'development' app.run(host='0.0.0.0', port=port, debug=debug)
# 📊 Python용 최적화된 DockerfileFROM python:3.11-slim# 시스템 업데이트 및 필수 패키지RUN apt-get update && apt-get install -y \ gcc \ && rm -rf /var/lib/apt/lists/*# 보안 강화RUN groupadd -r appuser && useradd -r -g appuser appuser# 작업 디렉토리 설정WORKDIR /app# Python 종속성 설치COPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txt# 애플리케이션 코드 복사COPY . .# 권한 설정RUN chown -R appuser:appuser /appUSER appuser# 포트 노출EXPOSE 8080# Gunicorn으로 실행 (프로덕션 권장)CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "2", "--timeout", "300", "app:app"]
💡 Go 웹 서버
📋 고성능 Go 애플리케이션
// 📊 main.gopackage mainimport ( "context" "encoding/json" "fmt" "log" "net/http" "os" "os/signal" "runtime" "syscall" "time")type Response struct { Message string `json:"message"` Timestamp string `json:"timestamp"` Version string `json:"version"`}type HealthResponse struct { Status string `json:"status"` Uptime float64 `json:"uptime"` GoVersion string `json:"go_version"` NumGoroutine int `json:"num_goroutine"` Memory runtime.MemStats `json:"memory"`}var startTime = time.Now()func main() { port := os.Getenv("PORT") if port == "" { port = "8080" } mux := http.NewServeMux() // 라우트 설정 mux.HandleFunc("/", homeHandler) mux.HandleFunc("/api/health", healthHandler) mux.HandleFunc("/api/process", processHandler) // 미들웨어 추가 handler := loggingMiddleware(corsMiddleware(mux)) server := &http.Server{ Addr: ":" + port, Handler: handler, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } // Graceful shutdown 설정 go func() { log.Printf("Server starting on port %s", port) if err := server.ListenAndServe(); err != http.ErrServerClosed { log.Fatalf("Server failed to start: %v", err) } }() // 종료 신호 대기 quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Server is shutting down...") ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { log.Fatalf("Server forced to shutdown: %v", err) } log.Println("Server exited")}func homeHandler(w http.ResponseWriter, r *http.Request) { response := Response{ Message: "Hello from Go on Cloud Run!", Timestamp: time.Now().Format(time.RFC3339), Version: "1.0.0", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response)}func healthHandler(w http.ResponseWriter, r *http.Request) { var memStats runtime.MemStats runtime.GC() runtime.ReadMemStats(&memStats) response := HealthResponse{ Status: "healthy", Uptime: time.Since(startTime).Seconds(), GoVersion: runtime.Version(), NumGoroutine: runtime.NumGoroutine(), Memory: memStats, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response)}func processHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var data map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&data); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } result := map[string]interface{}{ "input": data, "processed_at": time.Now().Format(time.RFC3339), "server": "Go Cloud Run", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(result)}func corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } next.ServeHTTP(w, r) })}func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start)) })}
# 📊 Go용 멀티스테이지 Dockerfile# Build stageFROM golang:1.21-alpine AS builder# 보안 패키지 설치RUN apk add --no-cache git ca-certificates tzdata# 작업 디렉토리 설정WORKDIR /app# Go modules 파일 복사COPY go.mod go.sum ./RUN go mod download# 소스 코드 복사COPY . .# 바이너리 빌드RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .# Production stageFROM alpine:latest# 보안 업데이트 및 CA 인증서RUN apk --no-cache add ca-certificates tzdataRUN adduser -D -s /bin/sh appuser# 작업 디렉토리 설정WORKDIR /root/# 빌드된 바이너리 복사COPY --from=builder /app/main .# 권한 설정RUN chown appuser:appuser mainUSER appuser# 포트 노출EXPOSE 8080# 실행CMD ["./main"]
// 📊 go.modmodule cloudrun-go-appgo 1.21
4. 배포 스크립트 모음
원클릭 배포 스크립트
복잡한 명령어들을 자동화하여 빠르고 안전하게 배포할 수 있는 스크립트들입니다.
💡 범용 배포 스크립트
🤔 질문: “매번 긴 명령어를 치기 번거로운데, 스크립트로 자동화할 수 없을까?”
📋 메인 배포 스크립트
#!/bin/bash# 📊 deploy-to-cloudrun.sh - 범용 Cloud Run 배포 스크립트set -e # 오류 발생시 스크립트 중단# 색상 출력 함수RED='\033[0;31m'GREEN='\033[0;32m'YELLOW='\033[1;33m'BLUE='\033[0;34m'NC='\033[0m' # No Colorlog_info() { echo -e "${BLUE}[INFO]${NC} $1"; }log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }log_error() { echo -e "${RED}[ERROR]${NC} $1"; }# 환경 변수 로드if [[ -f ~/.cloudrun_env ]]; then source ~/.cloudrun_env log_info "Environment variables loaded"else log_error "Environment file not found. Run setup first." exit 1fi# 파라미터 파싱SERVICE_NAME=""IMAGE_TAG="latest"MEMORY="512Mi"CPU="1"CONCURRENCY="80"MAX_INSTANCES="100"MIN_INSTANCES="0"TIMEOUT="300"ENV_VARS=""ALLOW_UNAUTHENTICATED="true"show_help() { cat << EOFCloud Run 배포 스크립트사용법: $0 [옵션]필수 파라미터: -n, --name SERVICE_NAME 서비스 이름선택적 파라미터: -t, --tag TAG 이미지 태그 (기본값: latest) -m, --memory MEMORY 메모리 할당 (기본값: 512Mi) -c, --cpu CPU CPU 할당 (기본값: 1) --concurrency NUM 동시 요청 수 (기본값: 80) --max-instances NUM 최대 인스턴스 (기본값: 100) --min-instances NUM 최소 인스턴스 (기본값: 0) --timeout SECONDS 타임아웃 (기본값: 300) --env-vars "KEY1=VALUE1,KEY2=VALUE2" 환경 변수 --no-allow-unauthenticated 인증 필요 설정 -h, --help 도움말 표시예시: $0 -n my-app -t v1.0.0 -m 1Gi --env-vars "NODE_ENV=production,DEBUG=false"EOF}# 파라미터 파싱while [[ $#--gt-0-| -gt 0 ]]; do case $1 in -n|--name) SERVICE_NAME="$2" shift 2 ;; -t|--tag) IMAGE_TAG="$2" shift 2 ;; -m|--memory) MEMORY="$2" shift 2 ;; -c|--cpu) CPU="$2" shift 2 ;; --concurrency) CONCURRENCY="$2" shift 2 ;; --max-instances) MAX_INSTANCES="$2" shift 2 ;; --min-instances) MIN_INSTANCES="$2" shift 2 ;; --timeout) TIMEOUT="$2" shift 2 ;; --env-vars) ENV_VARS="$2" shift 2 ;; --no-allow-unauthenticated) ALLOW_UNAUTHENTICATED="false" shift ;; -h|--help) show_help exit 0 ;; *) log_error "Unknown option $1" show_help exit 1 ;; esacdone# 필수 파라미터 검증if [[ -z "$SERVICE_NAME" ]]; then log_error "Service name is required" show_help exit 1fi# 이미지 이름 구성IMAGE_NAME="gcr.io/$PROJECT_ID/$SERVICE_NAME:$IMAGE_TAG"log_info "Starting deployment..."log_info "Service: $SERVICE_NAME"log_info "Image: $IMAGE_NAME"log_info "Region: $REGION"# 1. Dockerfile 존재 확인if [[ ! -f "Dockerfile" ]]; then log_error "Dockerfile not found in current directory" exit 1fi# 2. Docker 이미지 빌드log_info "Building Docker image..."docker build -t "$IMAGE_NAME" .log_success "Docker image built successfully"# 3. 이미지 푸시log_info "Pushing image to Container Registry..."docker push "$IMAGE_NAME"log_success "Image pushed successfully"# 4. Cloud Run 배포 명령어 구성DEPLOY_CMD="gcloud run deploy $SERVICE_NAME \ --image $IMAGE_NAME \ --platform managed \ --region $REGION \ --memory $MEMORY \ --cpu $CPU \ --concurrency $CONCURRENCY \ --max-instances $MAX_INSTANCES \ --min-instances $MIN_INSTANCES \ --timeout $TIMEOUT"# 환경 변수 추가if [[ -n "$ENV_VARS" ]]; then DEPLOY_CMD="$DEPLOY_CMD --set-env-vars $ENV_VARS"fi# 인증 설정if [[ "$ALLOW_UNAUTHENTICATED" == "true" ]]; then DEPLOY_CMD="$DEPLOY_CMD --allow-unauthenticated"fi# 5. 배포 실행log_info "Deploying to Cloud Run..."eval $DEPLOY_CMDlog_success "Deployment completed successfully"# 6. 서비스 URL 확인SERVICE_URL=$(gcloud run services describe $SERVICE_NAME --region=$REGION --format="value(status.url)")log_success "Service is available at: $SERVICE_URL"# 7. 배포 정보 출력log_info "Deployment Summary:"echo " Service Name: $SERVICE_NAME"echo " Image: $IMAGE_NAME"echo " Region: $REGION"echo " URL: $SERVICE_URL"echo " Memory: $MEMORY"echo " CPU: $CPU"echo " Concurrency: $CONCURRENCY"# 8. 헬스 체크 (선택적)log_info "Testing service..."if curl -s "$SERVICE_URL" > /dev/null; then log_success "Service is responding correctly"else log_warning "Service might not be ready yet. Please check manually."filog_success "Deployment completed! 🚀"
💻 개발환경용 빠른 배포 스크립트
#!/bin/bash# 📊 quick-deploy.sh - 개발용 빠른 배포set -e# 기본 설정SERVICE_NAME=${1:-"my-dev-app"}IMAGE_TAG=${2:-"dev-$(date +%s)"}# 환경 변수 로드source ~/.cloudrun_env 2>/dev/null || { echo "Error: Environment file not found" exit 1}echo "🚀 Quick Deploy to Cloud Run"echo "Service: $SERVICE_NAME"echo "Tag: $IMAGE_TAG"# 원클릭 배포gcloud run deploy $SERVICE_NAME \ --source . \ --platform managed \ --region $REGION \ --allow-unauthenticated \ --memory 512Mi \ --cpu 1 \ --concurrency 80 \ --max-instances 10 \ --set-env-vars "NODE_ENV=development,DEBUG=true"# URL 출력URL=$(gcloud run services describe $SERVICE_NAME --region=$REGION --format="value(status.url)")echo "✅ Deployed: $URL"# 브라우저에서 열기 (macOS)[[ "$OSTYPE" =~ ^darwin ]] && open "$URL"
# 📊 최적화된 Node.js DockerfileFROM node:18-alpine AS baseWORKDIR /appCOPY package*.json ./# 개발 의존성 포함 설치FROM base AS developmentRUN npm ciCOPY . .CMD ["npm", "run", "dev"]# 프로덕션 빌드FROM base AS buildRUN npm ci --only=production && npm cache clean --forceCOPY . .RUN npm run build# 프로덕션 런타임FROM node:18-alpine AS production# 보안 업데이트RUN apk update && apk upgrade && apk add --no-cache dumb-init# 사용자 생성RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001# 작업 디렉토리 설정WORKDIR /app# 소유권 변경RUN chown nodejs:nodejs /appUSER nodejs# 프로덕션 파일만 복사COPY --from=build --chown=nodejs:nodejs /app/node_modules ./node_modulesCOPY --from=build --chown=nodejs:nodejs /app/dist ./distCOPY --from=build --chown=nodejs:nodejs /app/package*.json ./# 헬스체크HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD node -e "require('http').get('http://localhost:8080/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"# 포트 노출EXPOSE 8080# 신호 처리를 위한 init 시스템 사용ENTRYPOINT ["dumb-init", "--"]CMD ["node", "dist/server.js"]
💻 Cold Start 최적화
// 📊 cold-start-optimization.js// Cold Start 최적화 기법들class ColdStartOptimizer { constructor() { this.isWarmedUp = false; this.warmupConnections = new Map(); // 애플리케이션 시작시 즉시 워밍업 this.warmupApplication(); } async warmupApplication() { console.log('🔥 Warming up application...'); // 1. 데이터베이스 연결 풀 미리 생성 await this.warmupDatabaseConnections(); // 2. 외부 API 연결 테스트 await this.warmupExternalConnections(); // 3. 캐시 데이터 미리 로드 await this.preloadCache(); this.isWarmedUp = true; console.log('✅ Application warmed up successfully'); } async warmupDatabaseConnections() { // 데이터베이스 연결 풀 미리 생성 const db = require('./database'); await db.testConnection(); } async warmupExternalConnections() { // 외부 서비스 연결 테스트 const services = ['auth-service', 'payment-service']; for (const service of services) { try { const connection = await this.createConnection(service); this.warmupConnections.set(service, connection); } catch (error) { console.warn(`Failed to warmup ${service}:`, error.message); } } } async preloadCache() { // 자주 사용되는 데이터 캐시에 미리 로드 const cache = require('./cache'); await cache.preload([ 'frequently-used-config', 'user-preferences-default' ]); } // 요청 처리 시 워밍업 상태 확인 isReady() { return this.isWarmedUp; } // 헬스체크에서 워밍업 상태 포함 getHealthStatus() { return { status: this.isWarmedUp ? 'ready' : 'warming-up', uptime: process.uptime(), memory: process.memoryUsage(), warmedConnections: Array.from(this.warmupConnections.keys()) }; }}// 전역 인스턴스const optimizer = new ColdStartOptimizer();// Express 미들웨어const warmupMiddleware = (req, res, next) => { if (!optimizer.isReady() && req.path !== '/health') { return res.status(503).json({ error: 'Service warming up', retryAfter: 5 }); } next();};module.exports = { optimizer, warmupMiddleware };
🎯 실전 배포 예시: 종합 시나리오
완전한 배포 파이프라인
실제 프로덕션 환경에서 사용할 수 있는 종합적인 배포 예시를 통해 모든 개념을 통합해봅니다.
💻 통합 배포 시나리오
#!/bin/bash# 📊 production-deployment.sh - 프로덕션 완전 배포 스크립트set -euo pipefail# 설정 변수들SERVICE_NAME="ecommerce-api"ENVIRONMENT="production"VERSION_TAG="v$(date +%Y%m%d-%H%M%S)"HEALTH_CHECK_URL="/api/health"# 색상 출력readonly RED='\033[0;31m'readonly GREEN='\033[0;32m'readonly YELLOW='\033[1;33m'readonly BLUE='\033[0;34m'readonly NC='\033[0m'log() { echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"; }success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }error() { echo -e "${RED}[ERROR]${NC} $1"; }# 환경 검증validate_environment() { log "환경 검증 중..." # 필수 도구 확인 for tool in gcloud docker jq; do if ! command -v "$tool" &> /dev/null; then error "$tool이 설치되지 않았습니다." exit 1 fi done # 환경 변수 확인 if [[ -z "${PROJECT_ID:-}" || -z "${REGION:-}" ]]; then error "PROJECT_ID와 REGION 환경 변수가 설정되지 않았습니다." exit 1 fi # 현재 프로젝트 확인 current_project=$(gcloud config get-value project 2>/dev/null || echo "") if [[ "$current_project" != "$PROJECT_ID" ]]; then warning "현재 프로젝트($current_project)와 설정된 PROJECT_ID($PROJECT_ID)가 다릅니다." gcloud config set project "$PROJECT_ID" fi success "환경 검증 완료"}# 코드 품질 검증validate_code_quality() { log "코드 품질 검증 중..." # Dockerfile 검증 if [[ ! -f "Dockerfile" ]]; then error "Dockerfile을 찾을 수 없습니다." exit 1 fi # 보안 스캔 (예: hadolint) if command -v hadolint &> /dev/null; then hadolint Dockerfile || warning "Dockerfile 린트 검사에서 경고가 발생했습니다." fi # 패키지 취약점 검사 (Node.js 예시) if [[ -f "package.json" ]]; then npm audit --audit-level=high || warning "보안 취약점이 발견되었습니다." fi success "코드 품질 검증 완료"}# 이미지 빌드 및 최적화build_and_optimize_image() { log "Docker 이미지 빌드 및 최적화 중..." local image_name="gcr.io/$PROJECT_ID/$SERVICE_NAME:$VERSION_TAG" local latest_image="gcr.io/$PROJECT_ID/$SERVICE_NAME:latest" # 멀티스테이지 빌드 docker build \ --target production \ --build-arg BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ --build-arg VERSION="$VERSION_TAG" \ --tag "$image_name" \ --tag "$latest_image" \ . # 이미지 크기 확인 local image_size=$(docker image inspect "$image_name" --format='{{.Size}}' | numfmt --to=iec) log "빌드된 이미지 크기: $image_size" # 보안 스캔 (Trivy 사용) if command -v trivy &> /dev/null; then trivy image --severity HIGH,CRITICAL "$image_name" || warning "보안 취약점이 발견되었습니다." fi # 이미지 푸시 docker push "$image_name" docker push "$latest_image" success "이미지 빌드 및 배포 완료: $image_name" echo "$image_name" # 다음 단계에서 사용하기 위해 반환}# 스테이징 환경 배포deploy_to_staging() { local image_name="$1" log "스테이징 환경에 배포 중..." local staging_service="${SERVICE_NAME}-staging" gcloud run deploy "$staging_service" \ --image "$image_name" \ --platform managed \ --region "$REGION" \ --service-account="cloudrun-sa@$PROJECT_ID.iam.gserviceaccount.com" \ --memory="512Mi" \ --cpu="1" \ --concurrency="80" \ --max-instances="10" \ --min-instances="0" \ --timeout="300" \ --allow-unauthenticated \ --set-env-vars="NODE_ENV=staging,LOG_LEVEL=debug" \ --tag="staging-$VERSION_TAG" \ --quiet local staging_url=$(gcloud run services describe "$staging_service" \ --region="$REGION" --format="value(status.url)") success "스테이징 배포 완료: $staging_url" echo "$staging_url"}# 스테이징 테스트test_staging_deployment() { local staging_url="$1" log "스테이징 환경 테스트 중..." # 헬스체크 local health_url="${staging_url}${HEALTH_CHECK_URL}" local max_attempts=30 local attempt=1 while [[ $attempt -le $max_attempts ]]; do if curl -sf "$health_url" > /dev/null; then success "헬스체크 통과 ($attempt/$max_attempts)" break fi if [[ $attempt -eq $max_attempts ]]; then error "헬스체크 실패" exit 1 fi sleep 10 ((attempt++)) done # 기능 테스트 local response=$(curl -s "$staging_url" | jq -r '.message // "No message"') if [[ "$response" == *"Hello"* ]]; then success "기능 테스트 통과" else error "기능 테스트 실패: $response" exit 1 fi # 성능 테스트 (간단한 부하 테스트) log "간단한 성능 테스트 실행 중..." for i in {1..10}; do curl -s "$staging_url" > /dev/null & done wait success "스테이징 테스트 완료"}# 프로덕션 배포 (카나리/블루-그린)deploy_to_production() { local image_name="$1" log "프로덕션 환경에 카나리 배포 중..." # 새 리비전 배포 (트래픽 0%) gcloud run deploy "$SERVICE_NAME" \ --image "$image_name" \ --platform managed \ --region "$REGION" \ --service-account="cloudrun-sa@$PROJECT_ID.iam.gserviceaccount.com" \ --vpc-connector="cloudrun-connector" \ --vpc-egress="private-ranges-only" \ --memory="1Gi" \ --cpu="2" \ --concurrency="100" \ --max-instances="100" \ --min-instances="2" \ --timeout="300" \ --cpu-throttling \ --no-allow-unauthenticated \ --set-env-vars="NODE_ENV=production,LOG_LEVEL=info" \ --tag="canary-$VERSION_TAG" \ --no-traffic \ --quiet local prod_url=$(gcloud run services describe "$SERVICE_NAME" \ --region="$REGION" --format="value(status.url)") success "프로덕션 카나리 배포 완료 (트래픽 0%): $prod_url" # 카나리 테스트 log "카나리 버전 테스트 중..." local canary_url="${prod_url}--canary-${VERSION_TAG}---${PROJECT_ID}.cloudfunctions.net" # 카나리 헬스체크 if curl -sf "${canary_url}${HEALTH_CHECK_URL}" > /dev/null; then success "카나리 헬스체크 통과" else error "카나리 헬스체크 실패" exit 1 fi # 점진적 트래픽 증가 local traffic_percentages=(10 25 50 100) for percentage in "${traffic_percentages[@]}"; do log "트래픽 ${percentage}%로 증가 중..." gcloud run services update-traffic "$SERVICE_NAME" \ --region="$REGION" \ --to-revisions="LATEST=$percentage" \ --quiet # 모니터링 대기 시간 local wait_time=300 # 5분 log "${wait_time}초 동안 모니터링 중..." sleep "$wait_time" # 에러율 체크 (간단한 예시) local error_count=$(gcloud logging read "resource.type=cloud_run_revision AND severity>=ERROR" \ --limit=10 --format="value(timestamp)" | wc -l) if [[ $error_count -gt 5 ]]; then error "에러율이 높습니다. 롤백을 실행합니다." rollback_deployment exit 1 fi success "트래픽 ${percentage}% 안정화 완료" done success "프로덕션 배포 완료!" echo "$prod_url"}# 롤백 기능rollback_deployment() { log "이전 버전으로 롤백 중..." gcloud run services update-traffic "$SERVICE_NAME" \ --region="$REGION" \ --to-revisions="LATEST=0" \ --quiet warning "롤백이 완료되었습니다."}# 배포 후 모니터링 설정setup_monitoring() { log "모니터링 및 알림 설정 중..." # 커스텀 대시보드 생성을 위한 설정 파일 cat > monitoring-config.json << EOF{ "displayName": "${SERVICE_NAME} Dashboard", "mosaicLayout": { "tiles": [ { "width": 6, "height": 4, "widget": { "title": "Request Count", "xyChart": { "dataSets": [{ "timeSeriesQuery": { "timeSeriesFilter": { "filter": "resource.type=\"cloud_run_revision\" resource.label.service_name=\"${SERVICE_NAME}\"", "aggregation": { "alignmentPeriod": "60s", "perSeriesAligner": "ALIGN_RATE" } } } }] } } } ] }}EOF success "모니터링 설정 완료"}# 메인 실행 함수main() { log "🚀 프로덕션 배포 시작: $SERVICE_NAME ($VERSION_TAG)" validate_environment validate_code_quality local image_name image_name=$(build_and_optimize_image) local staging_url staging_url=$(deploy_to_staging "$image_name") test_staging_deployment "$staging_url" # 배포 승인 요청 echo "" warning "스테이징 테스트가 완료되었습니다." read -p "프로덕션 배포를 진행하시겠습니까? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then log "배포가 중단되었습니다." exit 0 fi local prod_url prod_url=$(deploy_to_production "$image_name") setup_monitoring success "🎉 모든 배포가 성공적으로 완료되었습니다!" echo "" echo "📊 배포 요약:" echo " Service: $SERVICE_NAME" echo " Version: $VERSION_TAG" echo " Staging URL: $staging_url" echo " Production URL: $prod_url" echo " Image: $image_name" log "배포 로그는 Google Cloud Console에서 확인하세요."}# 스크립트 실행main "$@"
📚 정리 및 다음 단계
⭐ 핵심 체크리스트
📋 배포 전 확인사항
프로덕션 배포 체크리스트
환경 설정
gcloud CLI 설치 및 인증
Docker 설치 및 설정
환경 변수 설정
IAM 권한 확인
보안 설정
서비스 계정 생성
최소 권한 부여
Secret Manager 설정
VPC 연결 (필요시)
애플리케이션 준비
Dockerfile 최적화
헬스체크 엔드포인트
로깅 및 모니터링 설정
환경별 설정 분리
📊 성능 최적화 가이드라인
항목
개발환경
프로덕션환경
메모리
512Mi
1Gi+
CPU
1
2+
최소 인스턴스
0
1-2
최대 인스턴스
10
100+
동시성
80
100+
타임아웃
300초
300초
💡 트러블슈팅 가이드
📋 자주 발생하는 문제들
일반적인 문제 해결
Cold Start 문제:
# 최소 인스턴스 설정으로 해결--min-instances=1
메모리 부족:
# 메모리 증가--memory=1Gi
권한 오류:
# 서비스 계정 권한 확인gcloud iam service-accounts get-iam-policy SERVICE_ACCOUNT_EMAIL
네트워킹 문제:
# VPC 연결 상태 확인gcloud compute networks vpc-access connectors describe CONNECTOR_NAME --region=REGION
🚀 다음 단계 학습 로드맵
고급 주제들
Cloud Run Jobs (배치 처리)
Multi-region 배포
트래픽 분할 고급 패턴
커스텀 도메인 및 SSL
통합 시나리오
Cloud SQL 연결
Pub/Sub 이벤트 처리
Cloud Storage 파일 처리
BigQuery 데이터 분석
DevOps 고도화
Terraform을 이용한 IaC
Skaffold 개발 워크플로우
GitOps with Cloud Build
멀티 환경 관리
마지막 조언
Cloud Run은 서버리스의 단순함과 컨테이너의 유연함을 모두 제공하는 강력한 플랫폼입니다. 작은 프로젝트부터 시작하여 점진적으로 기능을 확장해나가는 것이 성공의 열쇠입니다.