이코에코(Eco²) Deployment Strategy: Canary Deployments

2024-12-30 | 리팩토링을 앞두고 안전한 배포 전략 수립
TL;DR
- 문제: 대규모 Clean Architecture 리팩토링을 안전하게 배포해야 함
- 선택지: Rolling Update, Blue-Green, Canary
- 결정: Canary 배포 (헤더 기반)
- 이유: 기존 Istio 인프라 활용, 비용 효율성, 점진적 검증 가능
1. 배경
1.1 현재 상황
도메인 서비스들의 Clean Architecture 리팩토링을 앞두고 있다. Auth 서비스를 시작으로 모든 도메인 서비스에 다음 변경이 예정되어 있다:
- 디렉토리 구조 전면 개편 (
models/→domain/entities/,services/→application/commands/등) - CQRS 패턴 도입 (Command/Query 분리)
- Repository 인터페이스 추가 (DIP 적용)
- 의존성 주입 개선
이는 단순한 버그 픽스가 아니라 코드베이스의 근본적인 변경이다. 프로덕션에 직접 배포하기엔 위험이 크다.
1.2 요구사항
| 점진적 검증 | 전체 트래픽에 영향 없이 새 버전 테스트 | 🔴 필수 |
| 빠른 롤백 | 문제 발생 시 즉시 복구 (< 1분) | 🔴 필수 |
| 비용 효율성 | 추가 인프라 비용 최소화 | 🟡 중요 |
| 개발자 친화성 | 쉬운 테스트 환경 | 🟢 권장 |
| 자동화 가능성 | CI/CD 파이프라인 통합 | 🟢 권장 |
1.3 현재 인프라 구성
┌─────────────────────────────────────────────────────────────────┐
│ AWS EKS Cluster │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ auth node │ │character node│ │ scan node │ ... │
│ │ (2 CPU,4GB) │ │ (2 CPU,4GB) │ │ (2 CPU,4GB) │ │
│ │ │ │ │ │ │ │
│ │ - auth-api │ │-character-api│ │ - scan-api │ │
│ │ - ext-authz │ │-character- │ │ - scan-worker│ │
│ │ - auth-relay │ │ worker │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Istio Service Mesh │ │
│ │ - Gateway (외부 트래픽 진입점) │ │
│ │ - VirtualService (라우팅 규칙) │ │
│ │ - DestinationRule (트래픽 정책) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ ArgoCD (GitOps) │ │
│ │ - ApplicationSet으로 도메인별 자동 배포 │ │
│ │ - Kustomize 오버레이 (dev/prod) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘핵심 제약사항:
- 각 노드는 2 CPU, ~4GB 메모리로 제한
- 도메인별 전용 노드로 Taint/Toleration 설정
- 추가 노드 프로비저닝 비용 부담
2. 배포 전략 비교
2.1 Rolling Update (롤링 업데이트)
Kubernetes의 기본 배포 전략이다. Pod를 하나씩 순차적으로 교체한다.
┌─────────────────────────────────────────────────────────────────┐
│ Rolling Update │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 시간 → │
│ │
│ T0: ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │v1 │ │v1 │ │v1 │ │v1 │ ← 모두 v1 │
│ └───┘ └───┘ └───┘ └───┘ │
│ │ │
│ T1: ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │v2 │ │v1 │ │v1 │ │v1 │ ← 첫 번째 교체 (v1+v2 혼재) │
│ └─┬─┘ └───┘ └───┘ └───┘ │
│ │ │
│ T2: ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │v2 │ │v2 │ │v1 │ │v1 │ ← 두 번째 교체 │
│ └───┘ └─┬─┘ └───┘ └───┘ │
│ │ │
│ ... ▼ │
│ │
│ Tn: ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │v2 │ │v2 │ │v2 │ │v2 │ ← 모두 v2 │
│ └───┘ └───┘ └───┘ └───┘ │
│ │
└─────────────────────────────────────────────────────────────────┘동작 방식
# Kubernetes Deployment 기본 설정
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 목표 replica 대비 초과 허용 Pod 수
maxUnavailable: 0 # 최소 가용 Pod 보장장점
| 장점 | 설명 |
|---|---|
| Zero Config | Kubernetes 기본 전략, 별도 설정 불필요 |
| 리소스 효율 | 추가 리소스 최소화 (maxSurge: 1이면 1개만 추가) |
| 자동화 | kubectl rollout 명령으로 상태 확인/롤백 |
단점
| 버전 혼재 | 🔴 | 배포 중 v1/v2가 동시에 서비스 → API 호환성 문제 |
| 영향 범위 | 🔴 | 첫 Pod 교체 시점부터 실제 사용자 영향 |
| 검증 불가 | 🟡 | 특정 버전만 선택적 테스트 불가능 |
| 느린 롤백 | 🟡 | 롤백도 롤링 방식 (전체 복구에 시간 소요) |
리팩토링에 부적합한 이유
Clean Architecture 리팩토링은 내부 구조가 완전히 바뀌지만 API는 동일해야 한다. 만약 리팩토링 과정에서 의도치 않은 동작 변경이 발생하면:
- Rolling Update 시작 → 첫 v2 Pod 생성
- v2 Pod가 트래픽 수신 시작 (이미 일부 사용자 영향)
- 에러 발견 → 롤백 시작
- 롤백도 롤링 방식으로 진행 → 복구에 수 분 소요
결론: 리팩토링 같은 대규모 변경에는 부적합
2.2 Blue-Green Deployment (블루-그린 배포)
두 개의 동일한 환경을 유지하고, 라우터 전환으로 즉시 배포한다.
┌─────────────────────────────────────────────────────────────────┐
│ Blue-Green Deployment │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Load Balancer │ │
│ └─────────────────┬───────────────────┘ │
│ │ │
│ ┌──────────────┴──────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Blue (Active) │ │ Green (Standby) │ │
│ │ │ │ │ │
│ │ ┌───┐ ┌───┐ ┌───┐ │ │ ┌───┐ ┌───┐ ┌───┐ │ │
│ │ │v1 │ │v1 │ │v1 │ │ │ │v2 │ │v2 │ │v2 │ │ │
│ │ └───┘ └───┘ └───┘ │ │ └───┘ └───┘ └───┘ │ │
│ │ │ │ │ │
│ │ ← 100% 트래픽 │ │ ← 0% (대기) │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
│ ═══════════════════════════════════════════════════════════ │
│ 전환 후 (Switch) │
│ ═══════════════════════════════════════════════════════════ │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Blue (Standby) │ │ Green (Active) │ │
│ │ │ │ │ │
│ │ ┌───┐ ┌───┐ ┌───┐ │ │ ┌───┐ ┌───┐ ┌───┐ │ │
│ │ │v1 │ │v1 │ │v1 │ │ │ │v2 │ │v2 │ │v2 │ │ │
│ │ └───┘ └───┘ └───┘ │ │ └───┘ └───┘ └───┘ │ │
│ │ │ │ │ │
│ │ ← 0% (롤백 대기) │ │ ← 100% 트래픽 │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘동작 방식
# Service를 selector 변경으로 전환
# 전환 전
spec:
selector:
app: my-app
version: blue # v1
# 전환 후
spec:
selector:
app: my-app
version: green # v2또는 Istio VirtualService weight 조정:
http:
- route:
- destination:
host: my-app
subset: blue
weight: 0 # 전환 후
- destination:
host: my-app
subset: green
weight: 100 # 전환 후장점
| 즉시 전환 | 라우터 변경 한 번으로 완료 (< 1초) |
| 즉시 롤백 | 문제 시 다시 Blue로 전환 (< 1초) |
| 완전한 격리 | 테스트 환경이 프로덕션과 동일 |
| 일관성 | 항상 단일 버전만 서비스 |
단점
| 2배 리소스 | 🔴 | Blue + Green 모두 프로비저닝 필요 |
| 유휴 비용 | 🔴 | Standby 환경이 항상 대기 상태 |
| DB 스키마 | 🟡 | 양쪽 환경이 동일 DB 사용 시 스키마 변경 복잡 |
| 상태 동기화 | 🟡 | 세션, 캐시 등 상태 관리 필요 |
비용 분석
현재 인프라 기준 Blue-Green 적용 시:
현재 비용 (Stable만):
- auth 노드: $X/월
- character 노드: $X/월
- scan 노드: $X/월
- ... (총 N개 노드)
= $NX/월
Blue-Green 적용 시:
- Blue 환경: $NX/월
- Green 환경: $NX/월
= $2NX/월 (100% 증가)우리 환경에서는 비용이 2배가 되므로 현실적으로 불가능.
2.3 Canary Deployment (카나리 배포)
소수의 카나리 Pod로 새 버전을 먼저 검증하고, 문제 없으면 점진적으로 확대한다.
이름의 유래: 광산에서 유독 가스 감지를 위해 카나리아 새를 사용한 것에서 유래
┌─────────────────────────────────────────────────────────────────┐
│ Canary Deployment │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────┐ │
│ │ Istio VirtualService │ │
│ │ (Header-based / Weight-based) │ │
│ └─────────────────┬───────────────────┘ │
│ │ │
│ ┌────────────────┴────────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌───────────────────────┐ ┌───────────────────────┐ │
│ │ Stable (v1) │ │ Canary (v2) │ │
│ │ │ │ │ │
│ │ ┌───┐ ┌───┐ ┌───┐ │ │ ┌───┐ │ │
│ │ │v1 │ │v1 │ │v1 │ │ │ │v2 │ │ │
│ │ └───┘ └───┘ └───┘ │ │ └───┘ │ │
│ │ │ │ │ │
│ │ 일반 트래픽 (95%) │ │ 테스트 트래픽 (5%) │ │
│ │ 또는 │ │ 또는 │ │
│ │ X-Canary 헤더 없음 │ │ X-Canary: true │ │
│ └───────────────────────┘ └───────────────────────┘ │
│ │
│ ═══════════════════════════════════════════════════════════ │
│ 점진적 확대 단계 │
│ ═══════════════════════════════════════════════════════════ │
│ │
│ Phase 1: 헤더 기반 (개발자만) 0% weight, 헤더로 접근 │
│ ↓ │
│ Phase 2: 5% 트래픽 일반 사용자 5% 노출 │
│ ↓ │
│ Phase 3: 20% 트래픽 에러율/레이턴시 모니터링 │
│ ↓ │
│ Phase 4: 50% 트래픽 대규모 검증 │
│ ↓ │
│ Phase 5: 100% 트래픽 Canary → Stable 전환 │
│ │
└─────────────────────────────────────────────────────────────────┘라우팅 방식
방식 1: 헤더 기반 (Header-based)
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
spec:
http:
# X-Canary: true 헤더가 있으면 Canary로
- match:
- headers:
x-canary:
exact: 'true'
route:
- destination:
subset: canary
# 그 외는 Stable로
- route:
- destination:
subset: stable방식 2: 가중치 기반 (Weight-based)
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
spec:
http:
- route:
- destination:
subset: stable
weight: 95
- destination:
subset: canary
weight: 5방식 3: 하이브리드 (우리가 선택한 방식)
http:
# 1순위: 헤더가 있으면 무조건 Canary
- match:
- headers:
x-canary:
exact: 'true'
route:
- destination:
subset: canary
# 2순위: 나머지는 Stable (나중에 weight 추가 가능)
- route:
- destination:
subset: stable장점
| 최소 리소스 | Canary Pod 1-2개만 추가 (+10~15% 비용) |
| 점진적 검증 | 단계별 트래픽 확대로 위험 최소화 |
| 선택적 테스트 | 헤더 기반으로 개발자만 테스트 가능 |
| 빠른 롤백 | Canary Pod 제거 또는 weight 0으로 즉시 복구 |
| 실제 트래픽 | 프로덕션 환경에서 실제 사용자 패턴으로 검증 |
단점
| 설정 복잡도 | 🟡 | VirtualService, DestinationRule 이해 필요 |
| 모니터링 필요 | 🟡 | 버전별 메트릭 분리 및 비교 필요 |
| 디버깅 복잡 | 🟢 | 두 버전 동시 운영 시 로그 추적 복잡 |
비용 분석
현재 비용 (Stable만):
- N개 노드, 각 노드에 Pod 2-3개
= 기준 비용 $B/월
Canary 적용 시:
- 기존 Stable Pod 유지
- 서비스당 Canary Pod 1개 추가
- 추가 리소스: CPU 0.2코어, 메모리 256MB (노드당)
= $B × 1.1~1.15/월 (10~15% 증가)3. 최종 선택: Canary (헤더 기반)
3.1 결정 매트릭스
| 점진적 검증 | 30% | 1 | 2 | 5 | Canary만 단계별 검증 가능 |
| 빠른 롤백 | 25% | 2 | 5 | 5 | BG/Canary 모두 즉시 가능 |
| 비용 효율성 | 25% | 5 | 1 | 4 | BG는 2배 비용 |
| 설정 용이성 | 10% | 5 | 3 | 2 | Rolling이 가장 간단 |
| 기존 인프라 활용 | 10% | 3 | 2 | 5 | Istio 이미 구축됨 |
| 총점 | 100% | 2.8 | 2.4 | 4.5 |
3.2 Istio 활용 이점
우리 클러스터에는 이미 Istio Service Mesh가 구축되어 있다:
# 이미 존재하는 리소스들
- Gateway: istio-system/eco2-gateway
- VirtualService: 각 도메인별 라우팅 규칙
- DestinationRule: 일부 서비스에 존재 (character, my, ext-authz)Canary 배포를 위해 추가로 필요한 것:
# 추가/수정이 필요한 리소스
1. DestinationRule: stable/canary subset 정의 (신규 또는 수정)
2. VirtualService: X-Canary 헤더 매칭 규칙 추가 (수정)
3. Deployment: Canary 버전 (version: v2 라벨) 추가 (신규)핵심: 새로운 인프라 구성 요소 없이 기존 Istio로 구현 가능
3.3 구현 결정사항
라우팅 전략: 헤더 기반 우선
처음에는 가중치 기반이 아닌 헤더 기반으로 시작:
# Phase 1: 개발자 테스트 (헤더 기반)
http:
- match:
- headers:
x-canary:
exact: 'true'
route:
- destination:
subset: canary
- route:
- destination:
subset: stable이유:
- 리팩토링 초기에는 개발자(본인, Opus)만 테스트
- 안정성 확인 후 가중치 기반으로 전환
- 가중치 전환은 YAML 수정만으로 가능
버전 라벨링 전략
# Stable Deployment
metadata:
labels:
app: auth-api
version: v1 # ← subset 매칭에 사용
# Canary Deployment
metadata:
labels:
app: auth-api
version: v2 # ← subset 매칭에 사용
release: canary이미지 태그 전략
Stable: mng990/eco2:{service}-dev-latest
Canary: mng990/eco2:{service}-dev-canaryCI/CD에서 -canary 태그로 빌드하면 자동으로 Canary Deployment에 적용.
4. 구현 상세
4.1 적용 범위
API 서비스 (9개)
| auth-api | auth | ✅ | ✅ (신규) | ✅ |
| character-api | character | ✅ | ✅ (수정) | ✅ |
| chat-api | chat | ✅ | ✅ (신규) | ✅ |
| image-api | image | ✅ | ✅ (신규) | ✅ |
| location-api | location | ✅ | ✅ (신규) | ✅ |
| my-api | my | ✅ | ✅ (수정) | ✅ |
| scan-api | scan | ✅ | ✅ (신규) | ✅ |
| sse-gateway | sse-consumer | ✅ | ✅ (신규) | ✅ |
| ext-authz | auth | - (gRPC) | ✅ (수정) | ✅ |
Worker 서비스 (4개)
HTTP 라우팅이 불필요하므로 Canary Deployment만 추가:
| auth-relay | auth | ✅ |
| character-worker | character | ✅ |
| scan-worker | scan | ✅ |
| my-worker | my | ✅ |
4.2 파일 구조
workloads/
├── domains/{service}/base/
│ ├── deployment.yaml # Stable (version: v1)
│ ├── deployment-canary.yaml # Canary (version: v2) ← 신규
│ ├── destination-rule.yaml # stable/canary subset ← 신규/수정
│ ├── service.yaml
│ ├── configmap.yaml
│ └── kustomization.yaml # canary 리소스 포함 ← 수정
│
└── routing/{service}/base/
└── virtual-service.yaml # X-Canary 헤더 라우팅 ← 수정4.3 배포 워크플로우
┌─────────────────────────────────────────────────────────────────┐
│ Canary 배포 워크플로우 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 코드 변경 │
│ └─→ feature/xxx 브랜치에서 작업 │
│ │
│ 2. Canary 이미지 빌드 │
│ └─→ docker build -t mng990/eco2:{service}-dev-canary │
│ └─→ docker push mng990/eco2:{service}-dev-canary │
│ │
│ 3. ArgoCD 자동 배포 │
│ └─→ Canary Deployment가 새 이미지로 업데이트 │
│ └─→ Canary Pod 생성 (version: v2) │
│ │
│ 4. 개발자 테스트 (헤더 기반) │
│ └─→ curl -H "X-Canary: true" https://api.growbin.app/... │
│ └─→ 로그/메트릭 확인 │
│ │
│ 5. 문제 발견 시 │
│ └─→ 코드 수정 후 2번부터 반복 │
│ └─→ 또는 Canary Pod 스케일 0으로 비활성화 │
│ │
│ 6. 테스트 통과 │
│ └─→ 옵션 A: 가중치 기반으로 전환 (5% → 20% → 50% → 100%) │
│ └─→ 옵션 B: Stable 이미지를 Canary로 교체 │
│ │
│ 7. 완전 전환 │
│ └─→ Canary 이미지를 latest 태그로 재태깅 │
│ └─→ Stable Deployment 업데이트 │
│ └─→ Canary Pod 스케일 다운 │
│ │
└─────────────────────────────────────────────────────────────────┘5. 사용 가이드
5.1 Canary 버전 테스트
# cURL
curl -H "X-Canary: true" https://api.growbin.app/api/v1/auth/me
# HTTPie
http https://api.growbin.app/api/v1/auth/me X-Canary:true
# 브라우저: ModHeader 확장 프로그램 사용5.2 로그 확인
# Canary Pod 로그
kubectl logs -n auth -l version=v2 --tail=100 -f
# Stable Pod 로그
kubectl logs -n auth -l version=v1 --tail=100 -f5.3 메트릭 구분
Jaeger/Grafana에서 서비스명으로 구분:
- Stable:
auth-api - Canary:
auth-api-canary
5.4 롤백
# 즉시 롤백: Canary Pod 제거
kubectl scale deployment auth-api-canary --replicas=0 -n auth
# 또는 VirtualService에서 canary route 제거6. 결론
선택 요약
ubuntu@k8s-master:~$ echo '=== Check Canary Deployments ==='
echo 'auth namespace:'
kubectl get deploy -n auth | grep -E 'canary|NAME'
echo ''
echo 'character namespace:'
kubectl get deploy -n character | grep -E 'canary|NAME'
echo ''
echo 'scan namespace:'
kubectl get deploy -n scan | grep -E 'canary|NAME'
=== Check Canary Deployments ===
auth namespace:
NAME READY UP-TO-DATE AVAILABLE AGE
auth-api-canary 1/1 1 1 19m
auth-relay-canary 0/1 1 0 19m
ext-authz-canary 0/1 1 0 19m
character namespace:
NAME READY UP-TO-DATE AVAILABLE AGE
character-api-canary 1/1 1 1 19m
character-worker-canary 1/1 1 1 20m
scan namespace:
NAME READY UP-TO-DATE AVAILABLE AGE
scan-api-canary 1/1 1 1 19m
scan-worker-canary 1/1 1 1 19m| 배포 전략 | Canary Deployment |
| 라우팅 방식 | 헤더 기반 (X-Canary: true) |
| 구현 기술 | Istio VirtualService + DestinationRule |
| 적용 범위 | API 서비스 9개 + Worker 4개 |
| 비용 증가 | ~10-15% (Pod 1개/서비스 추가) |
기대 효과
- 안전한 리팩토링: 실제 클러스터 배포 환경에서 검증 후 전환
- 빠른 피드백: 문제 발견 시 즉시 롤백 가능
- 개발자 경험 향상: 헤더 하나로 새 버전 테스트
- 비용 효율성: Blue-Green 대비 85% 비용 절감
관련 커밋
4efee1e2- feat(canary): add header-based canary deployment for auth serviced3a67f61- feat(canary): add header-based canary deployment for all workloads