이종관
Back to Posts

Blue/Green 배포 (4): 운영 및 최적화

모니터링, 비용 최적화, 네트워킹 고려사항, 트러블슈팅 가이드와 실제 사례 연구

2026년 1월 27일·15 min read·
infra
deployment
blue-green
monitoring
prometheus
grafana
cost-optimization
troubleshooting

이 글은 Blue/Green 배포 시리즈의 마지막 글입니다.

  1. 기초와 전략
  2. Kubernetes 구현
  3. 클라우드 & CI/CD 통합
  4. 운영 및 최적화 (현재 글)

1. 전환/롤백 체크리스트

1.1 전환 전 (Green 검증)

PRE-DEPLOYMENT CHECKLIST

Infrastructure

  • Green Pod/인스턴스 모두 Running & Ready
  • Health check 엔드포인트 응답 확인 (HTTP 200)
  • 리소스 사용량 정상 범위 (CPU < 80%, Memory < 80%)

Application

  • Smoke test 통과
  • Critical API 엔드포인트 테스트
  • 의존성 서비스 연결 확인 (DB, Redis, 외부 API)

Database

  • 마이그레이션 스크립트 성공 실행
  • 스키마 하위 호환성 확인
  • 필요 시 데이터 백필 완료

Monitoring

  • 알림 채널 동작 확인 (Slack, PagerDuty)
  • 대시보드 접근 가능
  • 롤백 담당자 대기

1.2 전환 직후 (5분 모니터링)

POST-DEPLOYMENT MONITORING

Critical Metrics (첫 5분)

  • HTTP 5xx 에러율 < 0.1%
  • HTTP 4xx 에러율 급증 없음
  • P99 레이턴시 < 기존 대비 20% 증가
  • 요청 처리량(RPS) 정상 유지

Application Logs

  • ERROR/CRITICAL 레벨 로그 급증 없음
  • Exception stack trace 없음
  • Connection refused/timeout 없음

Business Metrics

  • 주요 전환율(로그인, 결제) 급감 없음
  • 사용자 세션 유지됨

1.3 롤백 결정 기준

지표임계값조치
5xx 에러율> 1% (5분간)즉시 롤백
P99 레이턴시> 2x 기존값조사 후 결정
핵심 기능 장애결제/로그인 실패즉시 롤백
CPU 사용률> 90% (지속)스케일 업 또는 롤백
OOM Kill발생즉시 롤백

2. 모니터링 설정

2.1 핵심 SLI/SLO 정의

# SLO 예시
slos:
  availability:
    target: 99.9%  # 월 43분 다운타임 허용
    window: 30d
    indicator: |
      sum(rate(http_requests_total{status!~"5.."}[5m])) /
      sum(rate(http_requests_total[5m]))

  latency:
    target: 95%    # 95%의 요청이 200ms 이내
    window: 30d
    indicator: |
      histogram_quantile(0.95,
        sum(rate(http_request_duration_seconds_bucket[5m])) by (le)
      )

  error_budget:
    monthly: 43.2m  # 99.9% SLO = 43.2분/월
    alert_threshold: 50%  # 예산 50% 소진 시 알림

2.2 Prometheus 알림 규칙

# prometheus-rules.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: blue-green-alerts
spec:
  groups:
  - name: deployment
    rules:
    # 5xx 에러율 급증
    - alert: HighErrorRate
      expr: |
        sum(rate(http_requests_total{status=~"5.."}[5m])) /
        sum(rate(http_requests_total[5m])) > 0.01
      for: 2m
      labels:
        severity: critical
        action: rollback
      annotations:
        summary: "High error rate detected after deployment"
        description: "Error rate is {{ $value | humanizePercentage }} (threshold: 1%)"
        runbook_url: "https://wiki.example.com/runbooks/high-error-rate"

    # 레이턴시 급증
    - alert: HighLatency
      expr: |
        histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
        > 0.5
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "P99 latency exceeded 500ms"
        description: "Current P99: {{ $value | humanizeDuration }}"

    # Pod 재시작 감지
    - alert: PodRestartLoop
      expr: |
        increase(kube_pod_container_status_restarts_total{
          pod=~"my-app-.*"
        }[15m]) > 3
      labels:
        severity: critical
        action: rollback
      annotations:
        summary: "Pod restart loop detected"

    # 배포 후 롤백 감지
    - alert: DeploymentRollback
      expr: |
        kube_deployment_status_observed_generation{deployment="my-app"}
        < kube_deployment_metadata_generation{deployment="my-app"}
      for: 1m
      labels:
        severity: info
      annotations:
        summary: "Deployment rollback in progress"

2.3 Grafana 대시보드

{
  "dashboard": {
    "title": "Blue/Green Deployment Dashboard",
    "panels": [
      {
        "title": "Traffic Distribution",
        "type": "piechart",
        "targets": [
          {
            "expr": "sum(rate(http_requests_total{color=\"blue\"}[5m]))",
            "legendFormat": "Blue"
          },
          {
            "expr": "sum(rate(http_requests_total{color=\"green\"}[5m]))",
            "legendFormat": "Green"
          }
        ]
      },
      {
        "title": "Error Rate by Version",
        "type": "timeseries",
        "targets": [
          {
            "expr": "sum(rate(http_requests_total{status=~\"5..\",color=\"blue\"}[5m])) / sum(rate(http_requests_total{color=\"blue\"}[5m]))",
            "legendFormat": "Blue 5xx"
          },
          {
            "expr": "sum(rate(http_requests_total{status=~\"5..\",color=\"green\"}[5m])) / sum(rate(http_requests_total{color=\"green\"}[5m]))",
            "legendFormat": "Green 5xx"
          }
        ]
      },
      {
        "title": "P99 Latency Comparison",
        "type": "timeseries",
        "targets": [
          {
            "expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{color=\"blue\"}[5m])) by (le))",
            "legendFormat": "Blue P99"
          },
          {
            "expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{color=\"green\"}[5m])) by (le))",
            "legendFormat": "Green P99"
          }
        ]
      },
      {
        "title": "Deployment Events",
        "type": "annotations",
        "datasource": "-- Grafana --",
        "query": "tags=deployment"
      }
    ]
  }
}

2.4 배포 이벤트 기록

# deploy_marker.py - 배포 이벤트를 Grafana에 기록
import requests
from datetime import datetime

def mark_deployment(version: str, environment: str, color: str):
    """Grafana에 배포 이벤트 마커 생성"""
    grafana_url = "https://grafana.example.com"
    api_key = os.environ["GRAFANA_API_KEY"]

    annotation = {
        "time": int(datetime.now().timestamp() * 1000),
        "tags": ["deployment", environment, color],
        "text": f"Deployed {version} to {color} ({environment})"
    }

    response = requests.post(
        f"{grafana_url}/api/annotations",
        json=annotation,
        headers={"Authorization": f"Bearer {api_key}"}
    )
    return response.status_code == 200

3. 비용 최적화

3.1 비용 분석

3.2 Spot/Preemptible 인스턴스 활용

# Green 환경에 Spot 인스턴스 사용 (AWS EKS)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-green
spec:
  template:
    spec:
      nodeSelector:
        node.kubernetes.io/lifecycle: spot
      tolerations:
      - key: "node.kubernetes.io/lifecycle"
        operator: "Equal"
        value: "spot"
        effect: "NoSchedule"
      # Spot 중단 대비 graceful shutdown
      terminationGracePeriodSeconds: 30
      containers:
      - name: app
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 10"]

3.3 자동 스케일링 설정

# green-hpa.yaml - Green 환경 최소 유지
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-app-green
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app-green
  minReplicas: 1  # 평상시 최소 유지
  maxReplicas: 10 # 배포 시 확장
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0  # 즉시 스케일 업
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15
    scaleDown:
      stabilizationWindowSeconds: 300  # 5분 대기 후 스케일 다운

3.4 배포 시간 외 Green 환경 축소

#!/bin/bash
# scale-green.sh - 업무 시간 외 Green 환경 축소

HOUR=$(date +%H)
DAY=$(date +%u)

# 업무 시간 (월-금 09:00-18:00) 외에는 축소
if [ $DAY -gt 5 ] || [ $HOUR -lt 9 ] || [ $HOUR -gt 18 ]; then
  echo "Off-hours: Scaling down Green environment"
  kubectl scale deployment my-app-green --replicas=0
else
  echo "Business hours: Maintaining Green environment"
  kubectl scale deployment my-app-green --replicas=1
fi
# CronJob으로 자동화
apiVersion: batch/v1
kind: CronJob
metadata:
  name: scale-green-down
spec:
  schedule: "0 19 * * 1-5"  # 평일 19:00
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: deployment-scaler
          containers:
          - name: kubectl
            image: bitnami/kubectl
            command:
            - kubectl
            - scale
            - deployment/my-app-green
            - --replicas=0
          restartPolicy: OnFailure
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: scale-green-up
spec:
  schedule: "0 8 * * 1-5"  # 평일 08:00
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: deployment-scaler
          containers:
          - name: kubectl
            image: bitnami/kubectl
            command:
            - kubectl
            - scale
            - deployment/my-app-green
            - --replicas=1
          restartPolicy: OnFailure

4. 네트워킹 고려사항

4.1 DNS TTL 관리

# Route53 TTL 설정 예시
aws route53 change-resource-record-sets \
  --hosted-zone-id Z123456 \
  --change-batch '{
    "Changes": [{
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "api.example.com",
        "Type": "A",
        "TTL": 60,
        "ResourceRecords": [{"Value": "1.2.3.4"}]
      }
    }]
  }'

4.2 CDN 캐시 무효화

# CloudFront 캐시 무효화
aws cloudfront create-invalidation \
  --distribution-id E123456789 \
  --paths "/*"

# Fastly 캐시 퍼지
curl -X POST "https://api.fastly.com/service/{service_id}/purge_all" \
  -H "Fastly-Key: $FASTLY_API_KEY"

4.3 Connection Draining

# Kubernetes Service - Connection draining 설정
apiVersion: v1
kind: Service
metadata:
  name: my-app
  annotations:
    # AWS ALB
    service.beta.kubernetes.io/aws-load-balancer-connection-draining-enabled: "true"
    service.beta.kubernetes.io/aws-load-balancer-connection-draining-timeout: "60"
spec:
  # ...
# Pod - Graceful shutdown
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      terminationGracePeriodSeconds: 60
      containers:
      - name: app
        lifecycle:
          preStop:
            exec:
              # 새 연결 중단, 기존 연결 완료 대기
              command:
              - /bin/sh
              - -c
              - |
                # Health check 실패시키기
                touch /tmp/shutdown
                # 기존 연결 완료 대기
                sleep 30

4.4 Sticky Session 처리

# Redis를 통한 세션 외부화 (Spring Boot)
# application.yml
spring:
  session:
    store-type: redis
    redis:
      namespace: spring:session
  redis:
    host: redis.example.com
    port: 6379

---
# Kubernetes에서 Session Affinity 사용 시
apiVersion: v1
kind: Service
metadata:
  name: my-app
spec:
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 3600

5. 트러블슈팅 가이드

5.1 일반적인 문제와 해결책

문제 1: Green 환경이 Ready 상태가 되지 않음

# 진단
kubectl get pods -l color=green
kubectl describe pod my-app-green-xxx

# 일반적인 원인
# 1. 이미지 Pull 실패
kubectl get events --field-selector reason=Failed

# 2. Readiness Probe 실패
kubectl logs my-app-green-xxx

# 3. 리소스 부족
kubectl describe node | grep -A5 "Allocated resources"

문제 2: 전환 후 5xx 에러 급증

# 즉시 롤백
kubectl patch svc my-app -p '{"spec":{"selector":{"color":"blue"}}}'

# 원인 분석
# 1. 로그 확인
kubectl logs -l color=green --tail=100

# 2. 의존성 연결 확인
kubectl exec -it my-app-green-xxx -- curl -v http://db-service:5432

# 3. 환경 변수 비교
kubectl get deployment my-app-blue -o json | jq '.spec.template.spec.containers[0].env'
kubectl get deployment my-app-green -o json | jq '.spec.template.spec.containers[0].env'

문제 3: 세션 유실

# Redis 세션 확인
redis-cli KEYS "spring:session:*" | wc -l

# 해결책: 세션 마이그레이션 확인
# 1. 세션 스토어 연결 확인
kubectl exec my-app-green-xxx -- nc -zv redis.example.com 6379

# 2. 세션 키 포맷 확인 (버전 간 호환성)
redis-cli GET "spring:session:sessions:xxx"

5.2 트러블슈팅 플로우차트

6. 실제 사례 연구

6.1 사례 1: E-commerce 플랫폼 (결제 시스템)

상황: 결제 모듈 업데이트 시 Blue/Green 배포

환경

항목스펙
인프라AWS EKS (Kubernetes 1.28)
배포 도구Argo Rollouts
트래픽10,000 RPS (피크)

배포 전략

결과

지표
다운타임0초
롤백 필요없음
총 소요 시간55분

6.2 사례 2: API 서버 (DB 스키마 변경 포함)

상황: 사용자 테이블에 새 컬럼 추가

배포 계획 (Expand & Contract)

문제 발생 및 해결

단계내용
문제Phase 1에서 v2 배포 후 일부 API에서 null 에러
원인구 클라이언트가 새 컬럼 없이 데이터 전송
해결기본값 설정 추가 후 재배포

교훈

  • DB 스키마 변경은 최소 2주 배포 계획 필요
  • 클라이언트 호환성 테스트 필수

6.3 사례 3: 마이크로서비스 (다중 서비스 동시 배포)

상황: 3개 서비스 동시 업데이트 (API Gateway, Auth, User)

배포 순서 (의존성 고려)

자동화 (GitHub Actions)

단계설명
빌드서비스별 병렬 빌드
배포순차적 배포 (의존성 순서)
승인각 단계 수동 승인

문제 발생 및 해결

단계내용
문제User 서비스 전환 후 Auth 호출 실패
원인Auth 새 버전의 API 변경을 User가 인지 못함
해결즉시 User 롤백, Auth API 하위 호환 패치 후 재배포

교훈

  • 마이크로서비스 동시 배포 시 API 계약 테스트 필수
  • Consumer-Driven Contract Testing 도입 권장

7. 시리즈 요약

핵심 포인트

주제핵심 내용
기초두 환경 동시 운영, 즉시 롤백 가능
KubernetesArgo Rollouts 또는 Istio 사용 권장
클라우드ALB Weight, Cloud Run Tags, App Service Slots
CI/CD자동화된 검증 → 수동 승인 → 모니터링
운영SLO 기반 모니터링, 비용 최적화, 체크리스트

권장 사항

  1. 시작은 간단하게: 순수 Kubernetes Service selector로 시작
  2. 점진적 고도화: Argo Rollouts → Istio 순으로 도입
  3. 자동화 필수: CI/CD 파이프라인에 Blue/Green 통합
  4. 모니터링 우선: 배포 전 SLO/알림 체계 구축
  5. 롤백 연습: 정기적인 롤백 훈련 실시

다음 학습 주제

  • Canary 배포: Blue/Green의 점진적 버전
  • GitOps: Argo CD를 통한 선언적 배포
  • Chaos Engineering: 배포 안정성 검증
  • Feature Flags: 코드 수준의 릴리스 제어

이전 글: Blue/Green 배포 (3) - 클라우드 & CI/CD ←


이 시리즈가 Blue/Green 배포를 이해하고 구현하는 데 도움이 되었기를 바랍니다. 질문이나 피드백은 언제든 환영합니다.