Project/reciping

[reciping 3차] 서비스에 맞춰 배포 전략을 직접 비교해봐요! - 장/단점 정리

S_N_Y 2025. 11. 5. 05:46

※ 기존에 기록해둔 노션 글을 옮겨적은 것으로, 노션 템플릿에 맞게 적게된 글이라 해당 링크를 통해 더 가독성있게 보실 수 있습니다.

https://www.notion.so/2690661ce62880c9ae52cd0a90c13cdd

 

서비스에 맞춰 배포 전략을 직접 비교해봐요! - 장/단점 정리 | Notion

1. 각 배포 전략별 특징 🌱

pleasant-sand-55a.notion.site


 

MSA로 분리된 서버들의 각 특징에 맞게 배포 전략을 수립하는 겸, reciping의 User-service를 기준으로 배포 전략(카나리, 블루그린, 롤링)을 각각 모니터링툴(prometheus-grafana) K6를 이용하여 직접 비교하며 장단점을 비교해보겠습니다.

 

1.  각 배포 전략별 특징 🌱

항목 카나리(Argo Rollouts) 블루-그린(Argo Rollouts) 롤링 업데이트(K8s Deployment)
리스크/블라스트 반경 매우 낮음(점진) 낮음(즉시 전환) 중간(점진이지만 전체 대상)
롤백 속도 빠름(스텝 롤백/중단) 매우 빠름(트래픽 스위치) 느린 편(이전 ReplicaSet 재기동)
지연/끊김 거의 없음(가중치 전환) 거의 없음(스위치 순간만 주의) 거의 없음(준비성 검증 필요)
전체 소요 시간 가장 김(스텝+검증) 짧음(빌드 2셋+스위치) 중간
모니터링/자동화 높음(분석 템플릿 권장) 중간(스위치 전 검증) 낮음(기본 헬스체크)
인프라 비용 중간(일시 120~150%) 높음(항상 200%) 낮음(110~130%)
구현 복잡도 중간~높음 중간 낮음
데이터 마이그레이션 확장-수축에 최적 호환 전환에 유리 주의 필요
캐시/세션 영향 단계적 검증 용이 교체 시 캐시 워밍 필요 점진 교체로 비교적 안전
트래픽 제어 정밀(가중치/헤더/쿠키) on/off 스위치 없음(컨트롤 불가)
관측 성숙도 요구 높음 중간 낮음

 

2. 사전 시나리오 및 사용할 표 템플릿 🛣️

2-1) 사전 시나리오 -요약본

  • 기준선 10~15분 수집 : RPS, 오류율, p95, CPU/메모리, 재시작
  • 부하 : k6
  • 대시보드 : 그라파나 유저팀 전용 대시보드인 user-service-overview 활용
  • [ 롤링 업데이트] : (리소스 2배 기준) 롤백은 이전 ReplicaSet 재기동으로
  • [ 블루 그린 ] : (리소스 2배 기준) 프리뷰 예열/스모크 후 스위치
  • [ 카나리 ] : 유의🚨 → SLO 기준이라 표 상으로는 시간이 가장 길 수 있음. → 현재는 Pod 비율 기반 분할

 

2-1) 사전 시나리오 - 상세본

0️⃣ [ 실험 설계 ]

1) 카나리

  • 현재 스텝을 실험형으로 조정: 30%→60%→100%, 각 단계 3~5분 관찰.
  • 실패 유도 한 번 포함(의도적 readiness 실패/느린 응답 등) → Abort 후 복구시간 실측.

승격 및 중단 커멘트 명령어들은 아래와 같습니다.

kubectl argo rollouts get rollout reciping-user-service -n reciping | cat
kubectl argo rollouts promote reciping-user-service -n reciping
kubectl argo rollouts abort reciping-user-service -n reciping
  • 롤백 시간 계측 하는 법 : Abort 시각(t0)과 오류율/지연이 기준선으로 회복된 시각(t1)을 기록

2) 블루그린

  • rollout.yaml의 strategy: blueGreen 블록 선택

예시) activeService, previewService, autoPromotionEnabled/Seconds, scaleDownDelaySeconds.

  • 실험 절차: 그린(프리뷰) 레플리카 예열 → 내부 스모크(소량 부하) → 스위치 → ALB/연결 드롭/5xx 유무 확인.
  • 롤백 시간 계측 하는 법 : 스위치 되돌리기(수 초~수십 초). t0(스위치)~t1(오류율 0% 근접·HealthyHostCount 안정) 기록

3) 롤링업데이트

  • 블루그린과 마찬가지로rollout.yaml에 strategy.rollingUpdate 블록 선택
  • 실험 포인트: 레디니스 실패율, unavailableReplicas, 배포 총소요, rollout undo 롤백 시간.

 

1️⃣ [ 계측 쿼리 ]

기존에 PromQL을 적용한 커스텀 대시보드와 동일 라벨로 정리할 것이고, 적용한 쿼리는 아래와 같습니다.

https://github.com/Reciping/reciping-k8s-resources/blob/dev/manifests/monitoring/dashboards/user-service-overview.yaml

 

reciping-k8s-resources/manifests/monitoring/dashboards/user-service-overview.yaml at dev · Reciping/reciping-k8s-resources

AI기반 통합 레시피 추천 및 검색 플랫폼 '레시핑' - Helm chart, ArgoCD, Manifest, Monitoring, kubectl - Reciping/reciping-k8s-resources

github.com

 

(PromQL 기준)

  • 오류율(%) :
"100 * ( ( sum by (reciping_service) (rate(http_server_requests_seconds_count{namespace=\"reciping\",reciping_team=\"$team\",reciping_service=\"$service\",status=~\"5..\",uri!~\"/actuator/.*\",uri=~\"$endpoint\"}[5m])) or on (reciping_service) (0 * sum by (reciping_service) (rate(http_server_requests_seconds_count{namespace=\"reciping\",reciping_team=\"$team\",reciping_service=\"$service\",uri!~\"/actuator/.*\",uri=~\"$endpoint\"}[5m])) ) ) / clamp_min( sum by (reciping_service) (rate(http_server_requests_seconds_count{namespace=\"reciping\",reciping_team=\"$team\",reciping_service=\"$service\",uri!~\"/actuator/.*\",uri=~\"$endpoint\"}[5m])), 1) )", "legendFormat": "{{reciping_service}}"
  • p95 :
"histogram_quantile(0.95, sum by (le) ( rate(http_server_requests_seconds_bucket{namespace=\"reciping\",reciping_team=\"$team\",reciping_service=\"$service\",uri!~\"/actuator/.*\",uri=~\"$endpoint\"}[5m]) ))", "legendFormat": "P95"
  • RPS :
"sum by (reciping_service) (rate(http_server_requests_seconds_count{namespace=\"reciping\",reciping_team=\"$team\",reciping_service=\"$service\",uri!~\"/actuator/.*\",uri=~\"$endpoint\"}[5m]))", "legendFormat": "{{reciping_service}}"
  • 컨테이너 재시작(5m) :
"sum by (pod) (increase(kube_pod_container_status_restarts_total{namespace=\"reciping\"}[5m]))", "legendFormat": "{{pod}}"

 

2️⃣ [ 실행·기록 체크리스트 ]

  • [ ] 베이스라인 : 배포 10~15분 전 기준 RPS/p95/p99/오류율/CPU/메모리 캡쳐
  • [ ] 배포 이벤트 타임스탬프 : 시작, 단계 승격/스위치, 종료, 롤백 시작/종료 각각 기록
  • [ ] 스파이크 캡처 : 각 이벤트±2분 윈도우의 p95 피크, 오류율 피크 수치 기록
  • [ ] 비용/자원 : 동시 가동 Pod-분(레플리카×시간) 추정치 기록

 

3️⃣ [ 사용할 최종 표 템플릿 ]

  • 트래픽 차단 기준(traffic cutoff) / SLO 회복 기준(오류율/지연이 베이스라인으로 돌아올 때)으로 분리

참고) Prometheus 5m rate/quantile 윈도우와 컨트롤러 상태(Progressing→Healthy)를 기준으로 SLO 측정

전략 배포 총소요 롤백 소요(트래픽 차단/SLO 회복) RPS 피크 RPS 평균 p95 피크(s) p95 평균(s) 오류율 피크(%) 오류율 평균(%) 비고
카나리                  
블루그린                  
롤링업데이트                  

 

3. 부하(k6) 세팅하기 🔊

부하를 주기 위해서 k6를 세팅해보겠습니다.

 

먼저 reciping-k8s-resource/test/k6-user-service.js 추가

https://github.com/Reciping/reciping-k8s-resources/blob/dev/test/k6-user-service.js

 

reciping-k8s-resources/test/k6-user-service.js at dev · Reciping/reciping-k8s-resources

AI기반 통합 레시피 추천 및 검색 플랫폼 '레시핑' - Helm chart, ArgoCD, Manifest, Monitoring, kubectl - Reciping/reciping-k8s-resources

github.com

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  scenarios: {
    steady: {
      executor: 'constant-arrival-rate',
      rate: __ENV.RATE ? parseInt(__ENV.RATE) : 300, // req/s
      timeUnit: '1s',
      duration: __ENV.DURATION || '15m',
      preAllocatedVUs: __ENV.VUS ? parseInt(__ENV.VUS) : 100,
      maxVUs: __ENV.MAX_VUS ? parseInt(__ENV.MAX_VUS) : 200,
    },
  },
  thresholds: {
    http_req_failed: ['rate<0.01'],
    http_req_duration: ['p(95)<300'], // p95 < 300ms 기본 SLO
  },
};

const BASE = __ENV.BASE || 'http://reciping-user-service.reciping:8080';
const paths = [
  '/api/v1/users/signup',
  '/api/v1/users/123/created-at',
  '/api/v1/users/me',
  '/api/v1/mypage',
  '/api/v1/mypage/bookmarks',
  '/api/v1/auth/refresh',
  '/login',
];

export default function () {
  const p = paths[Math.floor(Math.random() * paths.length)];
  const res = http.get(`${BASE}${p}`, { tags: { endpoint: p } });
  check(res, { 'status<400': (r) => r.status < 400 });
  sleep(0.05);
}

 

k6 설치하기 (윈도우 bash 기준)

choco install k6 -y

# choco 설치되어 있어야 합니다.(관리자 권한으로 실행)
# 스크립트 적용/갱신
kubectl -n reciping create configmap k6-user-script --from-file=test/k6-user-service.js --dry-run=client -o yaml | kubectl apply -f -

# 기존 잡 제거 후 실행
kubectl -n reciping delete job k6-user-baseline --ignore-not-found
cat <<'YAML' | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata:
  name: k6-user-baseline
  namespace: reciping
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: k6
          image: grafana/k6:0.46.0
          env:
            - name: RATE
              value: "200"        # 안정 확인 후 300으로 재실행
            - name: DURATION
              value: "15m"
            - name: VUS
              value: "100"
            - name: MAX_VUS
              value: "200"
          args: [ "run", "-e", "BASE=http://reciping-user-service.reciping:8080", "/scripts/k6-user-service.js" ]
          volumeMounts: [ { name: script, mountPath: /scripts } ]
      volumes:
        - name: script
          configMap:
            name: k6-user-script
            items: [ { key: k6-user-service.js, path: k6-user-service.js } ]
YAML

kubectl -n reciping logs -f job/k6-user-baseline
home@DESKTOP-0L33BAC MINGW64 ~/Desktop/groom/dev_sini/reciping-k8s-resources (dev)
$ kubectl -n reciping get job k6-user-baseline -o jsonpath='{.status.startTime}{"\n"}'
2025-08-21T17:10:01Z

# 이렇게 시간을 확인하고, 그라파나 대시보드에서 t0-1m, To: t1+1m로 설정 후 Apply

 

4. 대시보드에서 나온 데이터 값 쉽게 확인하는 방법 📊

t0-1m, To: t1+1m로 설정 후 Apply time range 클릭
각 표의 … 클릭 후 Edit 클릭
우측에서 Legend - Values에서 이런 식으로 세팅 (실제로는 Max와 Mean으로만 측정)
아래와 같이 세팅되는 것을 확인할 수 있습니다.

 

📸 배포 전략 별 비교 원본 데이터 캡쳐본

[ 카나리 ]

기존 배포 전략이 카나리로 시작했기 때문에 그대로 실행

1. 드레인 테스트 - 카나리 60%에서 실행한 결과
2. 크래시 테스트 - 카나리 60%에서 실행한 결과
3. 롤백

home@DESKTOP-0L33BAC MINGW64 ~/Desktop/groom/dev_sini/reciping-k8s-resources (dev)
$ TS=$(date -u +%Y-%m-%dT%H:%M:%SZ); echo patch_restartedAt=$TS; kubectl -n reciping patch rollout user-service-app-reciping-user-service --type=merge -p '{"spec":{"template":{"metadata":{"annotations":{"kubectl.kubernetes.io/restartedAt":"'$TS'"}}}}}' | cat; echo "waiting for Healthy..."; for i in {1..120}; do PHASE=$(kubectl -n reciping get rollout user-service-app-reciping-user-service -o jsonpath='{.status.phase}' 2>/dev/null); echo phase=${PHASE:-}; if [ "$PHASE" = "Healthy" ]; then echo t_abort1_KST=$(TZ=Asia/Seoul date '+%Y-%m-%d %H:%M:%S KST'); break; fi; sleep 2; done; kubectl -n reciping get rollout user-service-app-reciping-user-service -o jsonpath='{.status.stableRS} {"\n"}'
patch_restartedAt=2025-08-21T18:49:02Z
rollout.argoproj.io/user-service-app-reciping-user-service patched
waiting for Healthy...
phase=Progressing
phase=Progressing
phase=Progressing
phase=Progressing
phase=Progressing
phase=Progressing
phase=Progressing
phase=Progressing
phase=Progressing
.
.
.
phase=Progressing
phase=Progressing
phase=Progressing
phase=Paused
phase=Paused
phase=Paused
7d4d84ff5c 

home@DESKTOP-0L33BAC MINGW64 ~/Desktop/groom/dev_sini/reciping-k8s-resources (dev)
$ kubectl -n reciping get rollout user-service-app-reciping-user-service \
  -o jsonpath='{.status.abortedAt}{"\n"}{.status.conditions[?(@.type=="Available")].lastTransitionTime}{"\n"}'

2025-08-21T18:47:23Z


[ 블루그린 ]

1) 블루그린으로 스위치

# 차트에 반영된 값: strategy.type=blueGreen 로 바꾸고 배포
yq -i '.strategy.type = "blueGreen"' charts/reciping-user-service/values.yaml
# 변경 확인 - 굳이 안 해도 됨
git diff -- charts/reciping-user-service/values.yaml | cat

2) ArgoCD 동기화

argocd app sync user-service-app

3) 프리뷰 예열 확인

kubectl -n reciping get svc reciping-user-service reciping-user-service-preview
kubectl -n reciping get rs,po -l app=reciping-user-service -o wide

4) 스위치와 롤백 측정

  • 스위치 시작 시각(t_sw0 KST) 기록 후, 프리뷰→액티브 승격
TZ=Asia/Seoul date '+t_sw0(KST)=%Y-%m-%d %H:%M:%S KST'
# 블루그린 승격(자동승격 false라 수동 필요)
# 플러그인 없으면 서비스 셀렉터 전환으로도 가능하지만, 여기서는 Argo Rollouts 승격
# argo rollouts 플로그인 설치되어있을 경우, 아래와 같이 쉽게 승격 가능
kubectl argo rollouts promote user-service-app-reciping-user-service -n reciping
  • 스위치 완료 : svc/reciping-user-service의 Grafana에서 오류율/지연 회복 확인
  • 롤백 테스트(t_bg_rb0): 바로 반대로 되돌림
kubectl argo rollouts promote --to-revision <stable-revision> user-service-app-reciping-user-service -n reciping

블루그린 롤백 캡쳐 데이터

이런 식으로 롤링 업데이트까지 완료

 

🗂️ 최종 표 완성본

전략 배포 총소요 롤백 소요(트래픽/SLO 회복) RPS 피크 RPS 평균 p95 피크(s) p95 평균(s) 오류율 피크(%) 오류율 평균(%) 비고
카나리 10–20m 00:05 / 14:30 198 195 0.0364 0.00943 17.2 15.2 60% 단계 Abort, 5m 윈도우·컨트롤러 상태 반영
블루그린 3–5s(스위치) / 예열 포함 1–3m 00:03 / 00:45 195 190 0.006 0.003 0.3 0.1 프리뷰 2 예열, 드레인 OK 가정
롤링업데이트 1–3m 해당 없음 / 02:00 183 171 0.030 0.020 0.8 0.5 maxSurge=0, 이미지 캐시 가정

 

🔑 결론

☑️ 카나리 : SLO 회복 기준(5m 윈도우, 유도 장애, 60% 단계)이라 길게 보이긴 하지만, 트래픽 cutoff 기준이면 수 초만 걸림

☑️ 블루그린 : 프리뷰 2 예열(동일 비용) 가정일 때지만, 잘못 스위치하면 전체 영향(폭발 반경 100%)이라는 위험 존재

☑️ 롤링 : replicas=2, maxSurge=0 가정에서 1~3분이나 규모 커지면 더 늘어남(소규모(2개)면 1~3m도 가능, 대규모일수록 분·십여 분)

[ 최종 결론 ]

  • 고위험/불확실 변경(런타임, 주요 로직, 성능 민감) + 위험 최소화일 경우 : → 카나리 기본. 폭발 반경 제한, 메트릭 게이팅, 단계적 승격/중단.
  • 비호환 스키마/런타임 교체/즉시 롤백이 최우선일 경우 : → 블루그린(예열 확보+스위치, 캐시 워밍/연결 드레인만 주의)
  • 저위험/작은 변경(Stateless, 빠른 교체)일 경우 : → 롤링업데이트(maxUnavailable/surge로 속도·안정성 트레이드 오프해야 함)

 

[ reciping-user-service 자체에 대한 배포 전략 결론 ]

⇒ reciping-user-service는 유저 서비스인 만큼 사용자 체감 지연과 실패율에 민감합니다. 실측 결과, 카나리에서 단계별 p95 변동은 있었으나 오류율은 SLO 내 유지되었고, 롤백은 1분 내 복귀했습니다. 블루그린은 스위치/롤백 시간이 총합 기준으로는 가장 짧았으나, 자원 비용이 높을 수 밖에 없는 구조였습니다. 롤링 업데이트는 배포 속도가 괜찮지만 실패 시에 롤백 시간까지 고려한다면 이전 ReplicaSet 재기동해야하기 때문에 절차가 다른 배포 전략보다 어렵고 치명적이라고 판단했습니다. 이에 따라 주요 로직인 user-service의 기존 전략대로 기본 전략은 카나리로(메트릭 게이트 적용), 비호환 릴리스나 즉시롤백이 필요하다면 블루그린, 경미한 변경은 롤링업데이트로 운영해야한다고 판단하는 걸로 결론 지었습니다.