🚀 구글 클라우드런 빠른 실습 배포키트

작성 시간: 2025-11-30 15:21 KST

📑 목차


1. 클라우드런 핵심 개념

Cloud Run이란?

구글의 완전 관리형 서버리스 컨테이너 플랫폼으로, HTTP 요청에 따라 자동으로 스케일링되며 사용한 만큼만 과금되는 서비스입니다.

💡 주요 특징 및 장점

🤔 질문: “기존 VM이나 GKE 대신 Cloud Run을 언제 사용해야 할까?”

📋 Cloud Run 핵심 장점

서버리스 컨테이너의 이점

  1. 완전 관리: 인프라 관리 불필요
  2. 자동 스케일링: 0에서 수천 개 인스턴스까지 자동 확장
  3. 비용 효율: 요청 처리 시간만 과금
  4. 빠른 배포: 컨테이너 이미지만 있으면 즉시 배포

💻 서비스 비교표

특성Cloud RunApp EngineGKECompute 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.com
gcloud services enable cloudbuild.googleapis.com
gcloud services enable containerregistry.googleapis.com
gcloud 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_env
echo "source ~/.cloudrun_env" >> ~/.zshrc  # 또는 ~/.bashrc

3. 샘플 애플리케이션들

다양한 언어별 예제

실제 배포 가능한 완전한 샘플 애플리케이션들을 제공합니다.

💡 Node.js Express 앱

🤔 질문: “가장 간단한 웹 서버부터 시작해보자”

📋 Express.js 기본 앱 생성

// 📊 package.json
{
  "name": "cloudrun-express-app",
  "version": "1.0.0",
  "description": "Cloud Run Express sample",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "cors": "^2.8.5"
  },
  "devDependencies": {
    "nodemon": "^3.0.2"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}
// 📊 server.js
const express = require('express');
const cors = require('cors');
 
const app = express();
const PORT = process.env.PORT || 8080;
 
// 미들웨어 설정
app.use(cors());
app.use(express.json());
 
// 헬스체크 엔드포인트
app.get('/', (req, res) => {
  res.json({
    message: 'Hello from Cloud Run!',
    timestamp: new Date().toISOString(),
    version: '1.0.0'
  });
});
 
// API 엔드포인트
app.get('/api/status', (req, res) => {
  res.json({
    status: 'healthy',
    uptime: process.uptime(),
    memory: process.memoryUsage(),
    environment: process.env.NODE_ENV || 'development'
  });
});
 
app.post('/api/echo', (req, res) => {
  res.json({
    received: req.body,
    headers: req.headers,
    timestamp: new Date().toISOString()
  });
});
 
// 에러 핸들링
app.use((err, req, res, next) => {
  console.error('Error:', err);
  res.status(500).json({ error: 'Internal Server Error' });
});
 
// 404 처리
app.use((req, res) => {
  res.status(404).json({ error: 'Not Found' });
});
 
// 서버 시작
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
  console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
});
 
// Graceful shutdown
process.on('SIGTERM', () => {
  console.log('Received SIGTERM, shutting down gracefully');
  process.exit(0);
});

💻 Dockerfile 작성

# 📊 Node.js용 최적화된 Dockerfile
FROM node:18-alpine
 
# 보안 강화
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
 
# 작업 디렉토리 설정
WORKDIR /app
 
# 종속성 파일 복사 (캐시 최적화)
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
 
# 애플리케이션 코드 복사
COPY . .
 
# 권한 설정
RUN chown -R nodejs:nodejs /app
USER nodejs
 
# 포트 노출
EXPOSE 8080
 
# 헬스체크
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node -e "require('http').get('http://localhost:8080/', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"
 
# 애플리케이션 실행
CMD ["npm", "start"]

💡 Python Flask 앱

📋 Flask 마이크로서비스

# 📊 requirements.txt
Flask==2.3.3
gunicorn==21.2.0
requests==2.31.0
python-dotenv==1.0.0
# 📊 app.py
from flask import Flask, request, jsonify
import os
import time
import json
from datetime import datetime
 
app = 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용 최적화된 Dockerfile
FROM 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 /app
USER appuser
 
# 포트 노출
EXPOSE 8080
 
# Gunicorn으로 실행 (프로덕션 권장)
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "2", "--timeout", "300", "app:app"]

💡 Go 웹 서버

📋 고성능 Go 애플리케이션

// 📊 main.go
package main
 
import (
    "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 stage
FROM 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 stage
FROM alpine:latest
 
# 보안 업데이트 및 CA 인증서
RUN apk --no-cache add ca-certificates tzdata
RUN adduser -D -s /bin/sh appuser
 
# 작업 디렉토리 설정
WORKDIR /root/
 
# 빌드된 바이너리 복사
COPY --from=builder /app/main .
 
# 권한 설정
RUN chown appuser:appuser main
USER appuser
 
# 포트 노출
EXPOSE 8080
 
# 실행
CMD ["./main"]
// 📊 go.mod
module cloudrun-go-app
 
go 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 Color
 
log_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 1
fi
 
# 파라미터 파싱
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 << EOF
Cloud 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
            ;;
    esac
done
 
# 필수 파라미터 검증
if [[ -z "$SERVICE_NAME" ]]; then
    log_error "Service name is required"
    show_help
    exit 1
fi
 
# 이미지 이름 구성
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 1
fi
 
# 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_CMD
log_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."
fi
 
log_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"

💡 CI/CD 파이프라인용 스크립트

📋 GitHub Actions 워크플로우

# 📊 .github/workflows/deploy-to-cloudrun.yml
name: Deploy to Cloud Run
 
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
 
env:
  PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
  SERVICE_NAME: my-cloudrun-app
  REGION: asia-northeast3
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
 
    - name: Setup Google Cloud CLI
      uses: google-github-actions/setup-gcloud@v1
      with:
        project_id: ${{ secrets.GCP_PROJECT_ID }}
        service_account_key: ${{ secrets.GCP_SA_KEY }}
        export_default_credentials: true
 
    - name: Configure Docker
      run: gcloud auth configure-docker
 
    - name: Build Docker image
      run: |
        docker build -t gcr.io/$PROJECT_ID/$SERVICE_NAME:$GITHUB_SHA .
        docker build -t gcr.io/$PROJECT_ID/$SERVICE_NAME:latest .
 
    - name: Push Docker image
      run: |
        docker push gcr.io/$PROJECT_ID/$SERVICE_NAME:$GITHUB_SHA
        docker push gcr.io/$PROJECT_ID/$SERVICE_NAME:latest
 
    - name: Deploy to Cloud Run
      run: |
        gcloud run deploy $SERVICE_NAME \
          --image gcr.io/$PROJECT_ID/$SERVICE_NAME:$GITHUB_SHA \
          --platform managed \
          --region $REGION \
          --allow-unauthenticated \
          --memory 512Mi \
          --cpu 1 \
          --concurrency 80 \
          --max-instances 100 \
          --set-env-vars "NODE_ENV=production"
 
    - name: Get service URL
      run: |
        SERVICE_URL=$(gcloud run services describe $SERVICE_NAME --region=$REGION --format="value(status.url)")
        echo "Service URL: $SERVICE_URL"
        
    - name: Test deployment
      run: |
        SERVICE_URL=$(gcloud run services describe $SERVICE_NAME --region=$REGION --format="value(status.url)")
        curl -f $SERVICE_URL || exit 1

💡 로컬 개발 도구 스크립트

📋 개발 환경 통합 스크립트

#!/bin/bash
# 📊 dev-tools.sh - 로컬 개발 도구 모음
 
set -e
 
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SERVICE_NAME="my-dev-app"
 
show_menu() {
    echo ""
    echo "🛠️  Cloud Run 개발 도구"
    echo "========================"
    echo "1. 로컬 Docker 실행"
    echo "2. Cloud Run 배포"
    echo "3. 로그 보기"
    echo "4. 서비스 삭제"
    echo "5. 트래픽 분할 설정"
    echo "6. 서비스 상태 확인"
    echo "0. 종료"
    echo ""
}
 
run_locally() {
    echo "🏃 로컬에서 Docker 컨테이너 실행..."
    
    # 이미지 빌드
    docker build -t $SERVICE_NAME:local .
    
    # 기존 컨테이너 정지 및 삭제
    docker stop $SERVICE_NAME 2>/dev/null || true
    docker rm $SERVICE_NAME 2>/dev/null || true
    
    # 새 컨테이너 실행
    docker run -d \
      --name $SERVICE_NAME \
      -p 8080:8080 \
      -e NODE_ENV=development \
      $SERVICE_NAME:local
    
    echo "✅ 컨테이너가 실행되었습니다: http://localhost:8080"
    
    # 로그 추적
    echo "📋 로그를 표시합니다 (Ctrl+C로 중단):"
    docker logs -f $SERVICE_NAME
}
 
deploy_to_cloudrun() {
    echo "☁️  Cloud Run에 배포중..."
    ./deploy-to-cloudrun.sh -n $SERVICE_NAME -t "dev-$(date +%s)"
}
 
show_logs() {
    echo "📋 Cloud Run 로그를 표시합니다..."
    gcloud logs tail cloudrun.googleapis.com/$SERVICE_NAME \
      --region=$REGION \
      --format="table(timestamp,severity,textPayload)"
}
 
delete_service() {
    echo "🗑️  서비스 삭제 확인"
    read -p "정말로 '$SERVICE_NAME' 서비스를 삭제하시겠습니까? (y/N): " -n 1 -r
    echo
    if [[ $REPLY =~ ^[Yy]$ ]]; then
        gcloud run services delete $SERVICE_NAME --region=$REGION --quiet
        echo "✅ 서비스가 삭제되었습니다."
    else
        echo "❌ 삭제가 취소되었습니다."
    fi
}
 
setup_traffic_split() {
    echo "🚦 트래픽 분할 설정"
    echo "현재 배포된 리비전들:"
    
    gcloud run revisions list --service=$SERVICE_NAME --region=$REGION
    
    echo ""
    read -p "새 리비전의 트래픽 비율을 입력하세요 (0-100): " traffic_percent
    
    if [[ "$traffic_percent" =~ ^[0-9]+$ ]] && [ "$traffic_percent" -le 100 ]; then
        gcloud run services update-traffic $SERVICE_NAME \
          --region=$REGION \
          --to-latest=$traffic_percent
        echo "✅ 트래픽 분할이 설정되었습니다."
    else
        echo "❌ 잘못된 입력입니다."
    fi
}
 
check_service_status() {
    echo "📊 서비스 상태 확인"
    
    # 서비스 기본 정보
    echo "=== 서비스 정보 ==="
    gcloud run services describe $SERVICE_NAME \
      --region=$REGION \
      --format="table(metadata.name,status.url,status.latestReadyRevisionName,spec.traffic[].percent)"
    
    echo ""
    echo "=== 최근 리비전들 ==="
    gcloud run revisions list \
      --service=$SERVICE_NAME \
      --region=$REGION \
      --limit=5 \
      --format="table(metadata.name,status.conditions[0].lastTransitionTime,spec.containerConcurrency,status.allocatedInstances)"
    
    echo ""
    echo "=== 실시간 메트릭 ==="
    SERVICE_URL=$(gcloud run services describe $SERVICE_NAME --region=$REGION --format="value(status.url)")
    if [[ -n "$SERVICE_URL" ]]; then
        echo "URL: $SERVICE_URL"
        echo "응답 테스트중..."
        
        response_time=$(curl -o /dev/null -s -w '%{time_total}' "$SERVICE_URL" || echo "failed")
        if [[ "$response_time" != "failed" ]]; then
            echo "✅ 응답 시간: ${response_time}초"
        else
            echo "❌ 서비스 응답 없음"
        fi
    fi
}
 
# 환경 변수 로드
source ~/.cloudrun_env 2>/dev/null || {
    echo "❌ 환경 설정 파일을 찾을 수 없습니다."
    echo "setup-environment.sh를 먼저 실행하세요."
    exit 1
}
 
# 메인 루프
while true; do
    show_menu
    read -p "선택하세요 (0-6): " choice
    
    case $choice in
        1) run_locally ;;
        2) deploy_to_cloudrun ;;
        3) show_logs ;;
        4) delete_service ;;
        5) setup_traffic_split ;;
        6) check_service_status ;;
        0) echo "👋 종료합니다."; exit 0 ;;
        *) echo "❌ 잘못된 선택입니다." ;;
    esac
    
    echo ""
    read -p "계속하려면 Enter를 누르세요..."
done

5. 고급 설정 및 최적화

프로덕션 준비

실제 운영 환경에서 필요한 고급 설정들과 성능 최적화 방법을 다룹니다.

💡 보안 및 IAM 설정

🤔 질문: “프로덕션에서 보안을 어떻게 강화할까?”

📋 서비스 계정 및 IAM 설정

#!/bin/bash
# 📊 setup-security.sh - 보안 설정 자동화
 
# 서비스 계정 생성
SERVICE_ACCOUNT_NAME="cloudrun-sa"
SERVICE_ACCOUNT_EMAIL="$SERVICE_ACCOUNT_NAME@$PROJECT_ID.iam.gserviceaccount.com"
 
echo "🔐 보안 설정을 시작합니다..."
 
# 1. 서비스 계정 생성
gcloud iam service-accounts create $SERVICE_ACCOUNT_NAME \
  --description="Cloud Run service account" \
  --display-name="Cloud Run Service Account"
 
# 2. 필요한 권한만 부여 (최소 권한 원칙)
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$SERVICE_ACCOUNT_EMAIL" \
  --role="roles/cloudtrace.agent"
 
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$SERVICE_ACCOUNT_EMAIL" \
  --role="roles/logging.logWriter"
 
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$SERVICE_ACCOUNT_EMAIL" \
  --role="roles/monitoring.metricWriter"
 
# 3. 키 생성 및 다운로드
gcloud iam service-accounts keys create ~/cloudrun-sa-key.json \
  --iam-account=$SERVICE_ACCOUNT_EMAIL
 
echo "✅ 서비스 계정이 생성되었습니다: $SERVICE_ACCOUNT_EMAIL"
echo "🔑 키 파일: ~/cloudrun-sa-key.json"
 
# 4. VPC Connector 설정 (Private Google Access용)
echo "🔌 VPC Connector 설정..."
gcloud compute networks vpc-access connectors create cloudrun-connector \
  --region=$REGION \
  --subnet=default \
  --subnet-project=$PROJECT_ID \
  --min-instances=2 \
  --max-instances=3
 
echo "✅ 보안 설정이 완료되었습니다."

💻 보안 강화된 배포 스크립트

#!/bin/bash
# 📊 secure-deploy.sh - 보안 강화 배포
 
SERVICE_NAME="$1"
if [[ -z "$SERVICE_NAME" ]]; then
    echo "Usage: $0 <service-name>"
    exit 1
fi
 
# 보안 설정으로 배포
gcloud run deploy $SERVICE_NAME \
  --image "gcr.io/$PROJECT_ID/$SERVICE_NAME:latest" \
  --platform managed \
  --region $REGION \
  --service-account="cloudrun-sa@$PROJECT_ID.iam.gserviceaccount.com" \
  --vpc-connector="cloudrun-connector" \
  --vpc-egress="private-ranges-only" \
  --no-allow-unauthenticated \
  --memory="1Gi" \
  --cpu="1" \
  --concurrency="80" \
  --max-instances="100" \
  --min-instances="1" \
  --timeout="300" \
  --execution-environment="gen2" \
  --cpu-throttling \
  --session-affinity \
  --set-env-vars="NODE_ENV=production" \
  --set-secrets="DB_PASSWORD=db-password:latest,API_KEY=api-key:latest"
 
echo "✅ 보안 강화된 배포가 완료되었습니다."

💡 모니터링 및 로깅 설정

📋 고급 로깅 구성

// 📊 logger.js - 구조화된 로깅
const winston = require('winston');
const { LoggingWinston } = require('@google-cloud/logging-winston');
 
const loggingWinston = new LoggingWinston({
  projectId: process.env.GOOGLE_CLOUD_PROJECT,
  keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS,
  logName: 'cloudrun-app',
  resource: {
    type: 'cloud_run_revision',
    labels: {
      service_name: process.env.K_SERVICE || 'unknown',
      revision_name: process.env.K_REVISION || 'unknown',
      location: process.env.GOOGLE_CLOUD_REGION || 'unknown'
    }
  }
});
 
const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: {
    service: process.env.K_SERVICE || 'cloudrun-app',
    version: process.env.APP_VERSION || '1.0.0'
  },
  transports: [
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()
      )
    }),
    loggingWinston
  ]
});
 
// 요청 로깅 미들웨어
const requestLogger = (req, res, next) => {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    logger.info('HTTP Request', {
      method: req.method,
      url: req.url,
      status: res.statusCode,
      duration,
      userAgent: req.get('User-Agent'),
      ip: req.ip,
      traceId: req.get('X-Cloud-Trace-Context')
    });
  });
  
  next();
};
 
module.exports = { logger, requestLogger };

💻 메트릭 수집 설정

// 📊 metrics.js - 커스텀 메트릭
const { Monitoring } = require('@google-cloud/monitoring');
 
class CloudRunMetrics {
  constructor() {
    this.client = new Monitoring.MetricServiceClient();
    this.projectPath = this.client.projectPath(process.env.GOOGLE_CLOUD_PROJECT);
  }
 
  async recordCustomMetric(metricType, value, labels = {}) {
    const dataPoint = {
      interval: {
        endTime: {
          seconds: Date.now() / 1000,
        },
      },
      value: {
        doubleValue: value,
      },
    };
 
    const timeSeries = {
      metric: {
        type: `custom.googleapis.com/${metricType}`,
        labels: labels,
      },
      resource: {
        type: 'cloud_run_revision',
        labels: {
          service_name: process.env.K_SERVICE || 'unknown',
          revision_name: process.env.K_REVISION || 'unknown',
          location: process.env.GOOGLE_CLOUD_REGION || 'unknown',
        },
      },
      points: [dataPoint],
    };
 
    const request = {
      name: this.projectPath,
      timeSeries: [timeSeries],
    };
 
    try {
      await this.client.createTimeSeries(request);
    } catch (error) {
      console.error('Failed to record metric:', error);
    }
  }
 
  // 비즈니스 메트릭 기록
  async recordBusinessMetric(action, value = 1, metadata = {}) {
    await this.recordCustomMetric('business_action', value, {
      action: action,
      ...metadata
    });
  }
 
  // 성능 메트릭 기록
  async recordPerformanceMetric(operation, duration, success = true) {
    await this.recordCustomMetric('operation_duration', duration, {
      operation: operation,
      success: success.toString()
    });
  }
}
 
module.exports = CloudRunMetrics;

💡 성능 최적화

📋 최적화된 Dockerfile

# 📊 최적화된 Node.js Dockerfile
FROM node:18-alpine AS base
WORKDIR /app
COPY package*.json ./
 
# 개발 의존성 포함 설치
FROM base AS development
RUN npm ci
COPY . .
CMD ["npm", "run", "dev"]
 
# 프로덕션 빌드
FROM base AS build
RUN npm ci --only=production && npm cache clean --force
COPY . .
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 /app
USER nodejs
 
# 프로덕션 파일만 복사
COPY --from=build --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=build --chown=nodejs:nodejs /app/dist ./dist
COPY --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 "$@"

📚 정리 및 다음 단계

⭐ 핵심 체크리스트

📋 배포 전 확인사항

프로덕션 배포 체크리스트

  1. 환경 설정
    • gcloud CLI 설치 및 인증
    • Docker 설치 및 설정
    • 환경 변수 설정
    • IAM 권한 확인
  2. 보안 설정
    • 서비스 계정 생성
    • 최소 권한 부여
    • Secret Manager 설정
    • VPC 연결 (필요시)
  3. 애플리케이션 준비
    • Dockerfile 최적화
    • 헬스체크 엔드포인트
    • 로깅 및 모니터링 설정
    • 환경별 설정 분리

📊 성능 최적화 가이드라인

항목개발환경프로덕션환경
메모리512Mi1Gi+
CPU12+
최소 인스턴스01-2
최대 인스턴스10100+
동시성80100+
타임아웃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

🚀 다음 단계 학습 로드맵

  1. 고급 주제들

    • Cloud Run Jobs (배치 처리)
    • Multi-region 배포
    • 트래픽 분할 고급 패턴
    • 커스텀 도메인 및 SSL
  2. 통합 시나리오

    • Cloud SQL 연결
    • Pub/Sub 이벤트 처리
    • Cloud Storage 파일 처리
    • BigQuery 데이터 분석
  3. DevOps 고도화

    • Terraform을 이용한 IaC
    • Skaffold 개발 워크플로우
    • GitOps with Cloud Build
    • 멀티 환경 관리

마지막 조언

Cloud Run은 서버리스의 단순함과 컨테이너의 유연함을 모두 제공하는 강력한 플랫폼입니다. 작은 프로젝트부터 시작하여 점진적으로 기능을 확장해나가는 것이 성공의 열쇠입니다.