티스토리 뷰

반응형

서비스를 운영하다 보면 반드시 마주치는 질문이 있다. "배포할 때 사용자가 에러를 마주치지 않으려면 어떻게 해야 할까?" 그 해답이 바로 무중단 배포(Zero-Downtime Deployment) 전략이다.

✏️  무중단 배포란?

전통적인 배포 방식은 단순하다. 서버를 내리고, 새 버전을 올린다. 하지만 서버가 내려가 있는 동안 사용자는 503 에러를 만나게 된다. 트래픽이 적은 새벽 시간을 노려 배포하는 방식도 있지만, 글로벌 서비스라면 "새벽"이라는 개념 자체가 없다.

무중단 배포는 서비스를 중단하지 않고 새 버전을 배포하는 방법론이다. 크게 세 가지 전략이 존재한다.

  • 롤링 업데이트(Rolling Update)
  • 블루-그린 배포(Blue-Green Deployment)
  • 카나리 배포(Canary Deployment)

각각 장단점이 뚜렷하게 다르고, 서비스의 상황에 따라 적합한 전략이 달라진다.

 

✏️ 1. 롤링 업데이트 (Rolling Update)

 

1-1. 핵심 아이디어

롤링 업데이트는 한 번에 다 바꾸지 말고, 조금씩 교체하자는 아이디어에서 출발한다. 전체 인스턴스를 한꺼번에 내리지 않고, 일부만 종료 → 새 버전 기동 → 정상 확인 → 다음 인스턴스 순서로 순차적으로 교체해나간다.

로드 밸런서는 새 버전 인스턴스가 헬스체크를 통과하기 전까지 해당 인스턴스로 트래픽을 보내지 않는다. 덕분에 배포 중에도 서비스는 계속 살아있다.

1-2. 구체적인 운영 흐름

예시 상황: 쇼핑몰 서비스, 인스턴스 4개, 새벽이 아닌 낮 시간에 배포해야 한다.

[배포 전]
인스턴스1(v1) ← 트래픽
인스턴스2(v1) ← 트래픽
인스턴스3(v1) ← 트래픽
인스턴스4(v1) ← 트래픽

[Step 1] 인스턴스1 교체
인스턴스1: 로드밸런서에서 제외 → graceful shutdown(처리 중인 요청 마무리) → v2로 교체 → 헬스체크 통과 → 재등록
인스턴스2(v1) ← 트래픽
인스턴스3(v1) ← 트래픽
인스턴스4(v1) ← 트래픽

[Step 2] 인스턴스2 교체
인스턴스1(v2) ← 트래픽
인스턴스2: 교체 중
인스턴스3(v1) ← 트래픽
인스턴스4(v1) ← 트래픽

... (반복)

[배포 완료]
인스턴스1(v2) ← 트래픽
인스턴스2(v2) ← 트래픽
인스턴스3(v2) ← 트래픽
인스턴스4(v2) ← 트래픽

이때 Graceful Shutdown이 핵심이다. 인스턴스를 무작정 kill하면 처리 중이던 요청이 502로 떨어진다. 스프링 부트에서는 server.shutdown=graceful 설정으로 처리 중인 요청이 모두 완료된 후에 종료되도록 할 수 있다.

# application.yml
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s  # 최대 30초 대기

1-3. 주의해야 할 상황 — 구/신 버전 혼재

롤링 업데이트의 가장 큰 함정은 배포 중에 v1과 v2가 동시에 운영된다는 점이다.

실제로 문제가 생기는 케이스

상황: 장바구니 API 응답 형식이 변경됨
  v1 응답: { "items": [...] }
  v2 응답: { "cartItems": [...] }  ← 필드명 변경

사용자 A가 v2 인스턴스에서 장바구니 조회 → "cartItems" 필드로 응답받음
다음 요청이 v1 인스턴스로 라우팅됨 → "items" 필드로 응답받음
→ 프론트엔드가 undefined를 받아 UI 깨짐

이 문제를 피하려면 하위 호환성(Backward Compatibility) 을 항상 유지해야 한다. 필드명을 바꿀 때는 바로 삭제하지 말고, 구 필드와 신 필드를 일정 기간 함께 반환하는 방식으로 점진적으로 전환한다.

// 하위 호환 유지 예시 (일정 기간 두 필드 모두 반환)
public CartResponse getCart() {
    List<CartItem> cartItems = cartService.findItems();
    return CartResponse.builder()
        .items(cartItems)      // 기존 필드 (deprecated)
        .cartItems(cartItems)  // 신규 필드
        .build();
}

1-4 DB 스키마 변경이 있을 때

롤링 업데이트 중 DB 컬럼을 추가하거나 삭제해야 하는 상황이 생길 수 있다.

잘못된 방법 (절대 하지 말 것)

배포 중에 기존 컬럼 이름 변경 또는 삭제
→ v1 인스턴스들이 사라진 컬럼을 참조하려다가 에러 발생

올바른 방법 — Expand and Contract 패턴

Step 1: 새 컬럼 추가 (배포 A)
  → 새 컬럼을 추가하되, 아직 사용하지 않음. v1도 v2도 새 컬럼을 그냥 무시함

Step 2: 새 컬럼 사용 시작 (배포 B)
  → v2가 새 컬럼에 데이터를 쓰기 시작. 동시에 기존 컬럼에도 계속 씀

Step 3: 기존 컬럼 제거 (배포 C, v2 완전 전환 후)
  → 이제 v1이 완전히 사라진 뒤에 기존 컬럼 제거

1-5. Kubernetes에서의 설정

maxUnavailable: 0으로 설정하면 기존 파드를 종료하기 전에 반드시 새 파드가 먼저 기동되고 헬스체크를 통과해야 한다. 트래픽이 끊기는 순간이 없다.

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # 동시에 추가로 생성할 수 있는 파드 수
      maxUnavailable: 0  # 배포 중 항상 replicas 수 유지 (0개도 줄이지 않음)
  replicas: 4

1-6. Readiness Probe의 중요성

쿠버네티스에서 롤링 업데이트가 제대로 작동하려면 Readiness Probe가 반드시 설정되어 있어야 한다. 새 파드가 실제로 요청을 받을 준비가 됐는지 쿠버네티스가 확인하는 수단이기 때문이다. Readiness Probe가 없으면 파드가 기동은 됐지만 아직 DB 연결이나 캐시 워밍업이 안 된 상태에서 트래픽을 받게 되고, 사용자가 에러를 경험한다.

readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  initialDelaySeconds: 10   # 기동 후 10초 뒤부터 확인 시작
  periodSeconds: 5          # 5초마다 확인
  failureThreshold: 3       # 3번 연속 실패 시 트래픽 제외

장점 정리

  • 추가 인프라 비용이 없다. 기존 서버 수를 유지하면서 교체한다.
  • Kubernetes에서는 기본 전략이라 별도 도구 없이 사용 가능하다.
  • 배포 중에도 전체 capacity에 가까운 처리량을 유지한다.

단점 정리

  • 배포 중 구/신 버전이 동시에 운영된다. API 호환성을 항상 신경 써야 한다.
  • 문제 발생 시 롤백도 다시 롤링 방식으로 진행된다. 느리다.
  • 인스턴스 수가 많을수록 배포 완료까지 시간이 길어진다

 

✏️ 2. 블루-그린 배포 (Blue-Green Deployment)

2-1. 핵심 아이디어

블루-그린은 구버전을 바로 내리지 말고, 새 버전을 완전히 준비한 뒤에 한 번에 전환하자는 전략이다. 항상 두 개의 동일한 환경(Blue, Green)을 유지한다. 한쪽이 운영 중이면 다른 쪽은 유휴 상태 또는 이전 버전을 그대로 유지한다.

트래픽 전환은 로드 밸런서(또는 DNS)에서 타겟 그룹만 바꾸는 방식으로 이루어진다. 사용자 입장에서는 아무 일도 없었던 것처럼 신버전으로 넘어간다.

2-2. 구체적인 운영 흐름

예시 상황: 핀테크 서비스(결제/송금). 조금이라도 에러가 나면 치명적이다. 즉각 롤백이 가능해야 한다.

[평소]
사용자 → 로드밸런서 → Blue (v1, 인스턴스 4개) ← 트래픽 100%
                    → Green (v1, 인스턴스 4개) ← 유휴 상태

[Step 1] Green에 새 버전 배포
사용자 → 로드밸런서 → Blue (v1) ← 트래픽 100%
                    → Green (v2 배포 중, 트래픽 없음)
  → Green 4개 모두 새 버전으로 교체
  → DB 마이그레이션 실행 (스키마 변경)
  → 내부 QA 팀이 Green 환경 직접 접속해 스모크 테스트

[Step 2] 트래픽 전환 (몇 초 걸림)
사용자 → 로드밸런서 → Blue (v1) ← 트래픽 0% (대기)
                    → Green (v2) ← 트래픽 100%

[문제 발생 시]
사용자 → 로드밸런서 → Blue (v1) ← 트래픽 100% 즉시 복구
                    → Green (v2) ← 격리

2-3. 스모크 테스트 

블루-그린에서 가장 큰 강점은 실제 운영과 동일한 환경에서 사전 검증이 가능하다는 점이다. Green 환경이 완전히 준비됐지만 트래픽은 없는 상태이므로, QA 팀이나 자동화 테스트가 실 DB(또는 그것과 동일한 스테이징 DB)를 대상으로 충분히 검증할 수 있다. CI/CD 파이프라인에서 이 과정을 자동화하면 배포 실수를 크게 줄일 수 있다.

스모크 테스트 항목 예시
  주요 API 정상 응답 (회원 로그인, 조회, 결제)
  DB 마이그레이션 결과 확인
  외부 서비스 연동 확인 (PG사, SMS)
  평균 응답 시간 이상 없음
  에러 로그 없음
→ 전부 통과 후에만 트래픽 전환

2-4. AWS에서 구현하는 방법

AWS에서는 Application Load Balancer(ALB)의 타겟 그룹을 두 개 만들어 전환하거나, CodeDeploy를 사용해 블루-그린을 자동화한다. CodeDeploy를 사용한 무중단 배포 방식은 해당 게시글에서 확인할 수 있다.

https://pooreumjung.tistory.com/565

 

[AWS] - AWS CodeDeploy와 Blue-Green 배포로 무중단 배포 환경 구축하기

✏️ 1. 배포 방식에 대한 고민사람들과 팀 플젝을 하면서 느꼈던 문제점 중 하나는 서버를 배포할 때마다 서비스가 잠깐씩 멈춘다는 점이었다.기존에는 단일 EC2 서버에서 애플리케이션을 교체

pooreumjung.tistory.com

[ALB 타겟 그룹 전환 방식]
ALB Listener Rule:
  기존: forward → target-group-blue (v1)
  전환: forward → target-group-green (v2)

→ AWS CLI로 전환:
aws elbv2 modify-listener \
  --listener-arn arn:aws:elasticloadbalancing:... \
  --default-actions Type=forward,TargetGroupArn=arn:...:target-group-green

ECS를 사용하는 경우 CodeDeploy와 연동하면 배포 파이프라인에서 자동으로 블루-그린 전환을 처리해준다.

// appspec.yaml (ECS CodeDeploy)
{
  "version": 0.0,
  "Resources": [{
    "TargetService": {
      "Type": "AWS::ECS::Service",
      "Properties": {
        "TaskDefinition": "<TASK_DEFINITION>",
        "LoadBalancerInfo": {
          "ContainerName": "my-app",
          "ContainerPort": 8080
        }
      }
    }
  }],
  "Hooks": [
    { "BeforeAllowTraffic": "LambdaSmokeTestFunction" },  // 트래픽 전환 전 스모크 테스트
    { "AfterAllowTraffic": "LambdaVerifyFunction" }        // 전환 후 검증
  ]
}

DB 마이그레이션 처리 — 가장 어려운 부분

블루-그린에서 가장 까다로운 부분이 DB 처리다. Blue와 Green이 같은 DB를 바라보는 경우가 많기 때문이다.

[잘못된 케이스]
Green에 v2 배포 후, v2가 새 스키마로 DB를 마이그레이션
→ Blue(v1)가 변경된 스키마를 참조하다가 에러 발생
→ 롤백도 못 하고 장애 상황
[올바른 케이스 — Expand and Contract]
배포 A (Blue → Green 전환 전):
  컬럼 추가만 진행. 기존 컬럼은 그대로 유지.
  v1도 v2도 신규 컬럼을 그냥 무시하거나 함께 사용.

배포 B (정식 전환):
  Green(v2)으로 트래픽 전환.
  v2가 신규 컬럼에 데이터를 쓰기 시작.

배포 C (정리):
  Blue도 완전히 v2로 교체된 후, 기존 컬럼 삭제.

세션 처리 주의사항

트래픽 전환 순간, 기존에 Blue와 통신 중이던 사용자의 세션이 문제가 될 수 있다. 특히 세션을 서버 메모리에 저장하는 방식이면 Green 전환 후 세션을 잃는다.

이를 방지하려면 세션을 Redis 같은 외부 저장소에 저장해야 한다. 

스프링에서는 Spring Session을 활용하면 간단하게 해결된다. 설정하면 Blue → Green 전환 시 세션이 끊기지 않는다.

// build.gradle
implementation 'org.springframework.session:spring-session-data-redis'

// application.yml
spring:
  session:
    store-type: redis

장점 정리

  • 즉각적인 롤백이 가능하다. 로드 밸런서 타겟만 바꾸면 수 초 내 복구.
  • 배포 중 구/신 버전 혼재 문제가 없다.
  • 트래픽 전환 전 실 환경과 동일한 조건에서 충분한 검증이 가능하다.

단점 정리

  • 인프라 비용이 약 2배다. 유휴 상태의 Green을 항상 유지해야 한다. (비용 절감을 위해 평소에는 Green을 최소 사양으로 유지하다가 배포 직전에 스케일업하는 방법도 있다.)
  • DB 마이그레이션이 복잡하다. Expand and Contract 패턴을 반드시 이해해야 한다.
  • 트래픽 전환이 한 번에 일어나므로, 신버전에 숨어있던 대규모 버그가 전체 사용자에게 동시에 영향을 줄 수 있다.

 

✏️ 3. 카나리 배포 (Canary Deployment)

핵심 아이디어

카나리 배포는 전체 사용자에게 한 번에 배포하지 말고, 소수에게 먼저 배포해서 검증하자는 전략이다. 과거 광부들이 일산화탄소를 감지하기 위해 카나리아 새를 먼저 광산에 들여보낸 데서 이름이 유래했다.

롤링이나 블루-그린과 다르게, 카나리는 실제 운영 트래픽을 이용해 신버전의 안정성을 검증한다. 테스트 환경에서 재현하기 어려운 문제를 실 트래픽으로 먼저 발견할 수 있다는 게 가장 큰 차별점이다.

구체적인 운영 흐름

예시 상황: 월간 활성 사용자 500만 명의 SNS 서비스. 피드 알고리즘을 전면 교체하는 대규모 변경을 배포해야 한다. 한 번에 모든 사용자에게 배포했다가 알고리즘에 버그가 있으면 500만 명이 이상한 피드를 보게 된다. 만약 한 번에 100%로 배포했다면 500만 명 전체가 그 버그를 경험했을 것이다.

[Day 1] 카나리 1% 오픈
  트래픽 99% → v1 (기존 알고리즘)
  트래픽  1% → v2 (새 알고리즘) = 약 5만 명
  
  모니터링 지표 확인:
    에러율: v1 0.01% / v2 0.02% → 허용 범위
    평균 응답시간: v1 120ms / v2 118ms → 이상 없음
    피드 클릭률: v1 3.2% / v2 3.8% → 오히려 개선됨!
    이탈률: 큰 차이 없음

[Day 3] 카나리 10%로 확대
  트래픽 90% → v1
  트래픽 10% → v2 = 약 50만 명
  
  모니터링 지표 확인:
    에러율: v2에서 특정 지역 사용자 에러율 0.5% → 이슈 발견!
    → 원인 파악: 특정 언어 설정에서 문자열 파싱 오류
    → v2 핫픽스 후 재배포 (영향받은 사용자: 50만 명 중 일부)

[Day 7] 핫픽스 완료 후 20% → 50% → 100% 단계적 확대
  → 최종 배포 완료

 

트래픽 분배 방법들

1. 비율 기반 (가장 일반적)

# Nginx upstream 설정
upstream backend {
  server v1-server weight=95;  # 95%
  server v2-server weight=5;   # 5%
}

2. 헤더 기반 (내부 테스트)

X-Canary: true 헤더가 있는 요청만 v2로 라우팅
→ QA팀이나 내부 직원만 새 버전을 사용
→ 일반 사용자는 전혀 영향받지 않음
# Kubernetes Ingress (NGINX)
annotations:
  nginx.ingress.kubernetes.io/canary: "true"
  nginx.ingress.kubernetes.io/canary-by-header: "X-Canary"

3. 사용자 ID 기반 (점진적 출시)
이 방법은 같은 사용자가 항상 같은 버전을 경험한다는 장점이 있다. 비율 기반은 같은 사용자가 요청마다 다른 버전을 경험할 수 있다.

// 특정 사용자 그룹에게만 새 기능 노출
public boolean isCanaryUser(Long userId) {
    // 전체 사용자의 5%에 해당하는 ID만 카나리 적용
    return userId % 100 < 5;
}

4. 지역(Region) 기반

1단계: 서울 리전만 v2 배포 → 국내 사용자 반응 확인
2단계: 이상 없으면 도쿄, 싱가포르로 확대
3단계: 전 세계 확대
→ 넷플릭스, 우버 같은 글로벌 서비스에서 자주 쓰는 방식

모니터링 

카나리 배포는 모니터링 없이는 의미가 없다. 단순히 5%에게 배포했다고 끝이 아니라, v1과 v2의 지표를 실시간으로 비교해야 한다. v2의 에러율이 v1보다 유의미하게 높아지면 자동으로 카나리를 롤백하는 자동 롤백 정책을 설정하는 것이 좋다.

Datadog, Prometheus + Grafana, AWS CloudWatch 같은 모니터링 도구를 함께 사용한다.

[핵심 모니터링 지표]

기술 지표 (서비스 안정성):
  - 에러율 (5xx 응답 비율)
  - 평균 응답시간(Latency) / P99 응답시간
  - CPU/메모리 사용률
  - DB 쿼리 소요시간

비즈니스 지표 (실제 영향):
  - 전환율(구매 완료율 등)
  - 세션 당 이탈률
  - 특정 기능 사용률
  - 사용자 불만/오류 신고 건수

Argo Rollouts로 자동화

Kubernetes에서는 Argo Rollouts를 사용하면 카나리 배포를 자동화할 수 있다. 지표가 임계값을 초과하면 자동 롤백, 정상이면 자동 확대까지 가능하다.

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: my-app
spec:
  replicas: 10
  strategy:
    canary:
      steps:
        - setWeight: 5          # 5% 카나리 오픈
        - pause: { duration: 10m }   # 10분 대기 후 자동 진행
        - setWeight: 20
        - pause: { duration: 30m }
        - setWeight: 50
        - pause: {}             # 빈 pause = 수동 승인 필요
        - setWeight: 100
      
      # 자동 롤백 조건
      analysis:
        templates:
          - templateName: error-rate-check
        args:
          - name: service-name
            value: my-app
# 분석 템플릿 (에러율이 1% 초과하면 자동 롤백)
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: error-rate-check
spec:
  metrics:
    - name: error-rate
      interval: 1m
      successCondition: result[0] < 0.01  # 에러율 1% 미만
      failureLimit: 3
      provider:
        prometheus:
          address: http://prometheus:9090
          query: |
            sum(rate(http_requests_total{status=~"5.."}[1m]))
            /
            sum(rate(http_requests_total[1m]))

카나리 vs A/B 테스트

카나리와 A/B 테스트는 비슷해 보이지만 목적이 다르다.
두 가지를 동시에 활용하는 경우도 많다. 카나리로 기술적 안정성을 먼저 검증하고, 이후 A/B 테스트로 비즈니스 효과를 측정하는 방식이다.

구분 카나리 배포 A/B 테스트
목적 기술적 안정성 검증 비즈니스 효과 측정
질문 v2가 안정적으로 동작하는가? 버전 A vs B 중 클릭률이 높은 것은 무엇인가
기간 며칠~1주일 통계적 유의성이 나올 때까지 (보통 2~4주)
성공 기준 에러율, 응답시간 전환율, 클릭률, 이탈률

 

장점 정리

  • 실제 트래픽으로 신버전을 검증한다. 테스트 환경에서 재현 안 되는 문제를 먼저 잡을 수 있다.
  • 영향 범위가 제한된다. 5%에게만 배포하면 문제가 생겨도 5%만 영향받는다.
  • 비즈니스 지표까지 사전에 검증할 수 있다.

단점 정리

  • 구/신 버전 혼재 문제가 존재한다. API 하위 호환성을 신경 써야 한다.
  • 모니터링 인프라가 반드시 필요하다. 모니터링이 없으면 카나리의 의미가 없다.
  • 완전한 배포 완료까지 시간이 오래 걸린다. 단계마다 안정성을 확인하기 때문이다.
  • 설정과 운영이 복잡하다. 팀의 DevOps 역량이 어느 정도 필요하다.

 

✏️ 세 가지 전략 비교

항목 롤링 업데이트 블루-그린 카나리
추가 인프라 비용 없음 2배 약간
롤백 속도 느림 매우 빠름 (초 단위) 빠름
구/신 버전 혼재 있음 없음 있음
구현 복잡도 낮음 중간 높음
실트래픽 검증 불가 불가 가능
사전 검증 환경 없음 있음 (Green) 없음
A/B 테스트 불가 불가 가능
API 하위 호환 필요 필수 불필요 필수
주요 사용 사례 소규모, 비용 중시 결제/금융 등 핵심 서비스 대규모, 점진적 출시

 

✏️ 실무에서는 어떻게 적용하는 것이 좋을까?

단계에 따라 전략을 바꾸는 것이 현실적이다.

스타트업 초기 (팀 3~10명): 롤링 업데이트로 시작한다. 인프라 비용이 적고, 구현도 쉽다. Kubernetes를 쓴다면 기본 설정만으로 충분하다.

서비스가 성장하면서 (팀 10~50명): 결제, 인증 같은 핵심 도메인은 블루-그린으로 분리한다. 배포 실수 한 번이 큰 손실로 이어지는 서비스는 즉각 롤백 능력이 중요하다.

대규모 서비스 (팀 50명+): 카나리 배포를 도입한다. 모니터링 인프라(Prometheus, Datadog 등)가 갖춰진 뒤에 도입하는 것이 순서다. Netflix, Google, Meta 등이 이 방식을 주로 사용한다.

전략을 섞어 쓰는 것도 자연스럽다. 예를 들어 카나리로 5%에게 먼저 배포 → 안정성 확인 → 나머지 95%는 블루-그린으로 한 번에 전환하는 방식도 가능하다.

 

✏️ 마무리

무중단 배포는 단순히 서버를 안 끄는 것이 아니다. 사용자 경험을 지키면서 빠르게 배포할 수 있는 엔지니어링 문화의 일부다.

  • 롤링: 비용 효율적이고 간단하지만, API 하위 호환성을 항상 챙겨야 한다.
  • 블루-그린: 즉각 롤백이 핵심 강점. DB 마이그레이션과 세션 처리를 주의하자.
  • 카나리: 실 트래픽으로 검증하는 가장 안전한 방식. 모니터링 인프라가 전제 조건이다.

세 가지 전략을 이해하고, 서비스의 규모와 요구사항에 맞게 선택하는 것이 중요하다.

 

반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/07   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함