롤링 배포를 사용한 모아온의 무중단 배포 전략

2025. 10. 27. 03:56·개발/DevOps

무중단 배포 방식

모아온 운영 서버는 2개의 EC2 인스턴스를 활용한 롤링 배포 방식을 채택했습니다.

관련 코드는 아래 링크에서 확인 가능합니다.

https://github.com/woowacourse-teams/2025-moaon/pull/460

 

feat(BE): 무중단 배포 by yesjuhee · Pull Request #460 · woowacourse-teams/2025-moaon

🎯 이슈 번호 close 무중단 배포 #459 ✅ 체크 리스트 Target Branch를 올바르게 설정했나요? Reviewers/Assignees/Labels을 알맞게 설정했나요? 🏁 작업 내용 인프라 구조 변경 기존 구조 nginx가 public subnet에

github.com

https://github.com/woowacourse-teams/2025-moaon/pull/514

 

feat(BE): prod-2 실패 시 prod-1 롤백 로직 구현 by yesjuhee · Pull Request #514 · woowacourse-teams/2025-moaon

🎯 이슈 번호 close 무중단 배포 롤백 로직 개선 #509 ✅ 체크 리스트 Target Branch를 올바르게 설정했나요? Reviewers/Assignees/Labels을 알맞게 설정했나요? 🏁 작업 내용 기존 로직에서 prod-1이 성공하고

github.com

 

롤링 배포 vs 블루 그린 배포

무중단 배포 방식을 선택하는 과정에서 롤링 배포와 블루-그린 배포를 비교 검토했습니다. 두 방식은 각각 장단점이 명확하며, 프로젝트의 상황과 우선순위에 따라 적합한 방식이 달라집니다.

하위 호환성 고려사항

블루-그린 배포는 신규 버전의 인스턴스 그룹을 완전히 새로 구성한 후, 트래픽을 한 번에 전환하는 방식입니다. 이 과정에서 구버전과 신버전이 동시에 요청을 처리하는 상황이 발생하지 않기 때문에 버전 간 호환성을 고려할 필요가 없습니다.

반면 롤링 배포는 인스턴스를 순차적으로 교체하는 방식이므로, 배포 과정에서 구버전과 신버전의 인스턴스가 동시에 운영됩니다. 이로 인해 API 스펙 변경, 데이터베이스 스키마 변경 등이 발생할 경우 양쪽 버전 모두에서 정상 동작하도록 하위 호환성을 보장해야 합니다. 이는 개발 시 추가적인 고려사항과 테스트를 필요로 하며, 배포 전략의 복잡도를 높입니다.

비용 효율성

동일한 수의 운영 인스턴스를 유지하는 환경에서, 롤링 배포가 비용 측면에서 유리합니다.

블루-그린 배포는 배포 시점에 기존 운영 중인 인스턴스(그린)와 동일한 규모의 신규 인스턴스(블루)를 추가로 생성해야 합니다. 이는 배포 과정에서 일시적으로 2배의 인스턴스를 운영해야 함을 의미하며, 비용 부담이 증가합니다. 비용을 최소화하려면 배포 시에만 신규 인스턴스를 생성하고, 배포 완료 후 즉시 구버전 인스턴스를 삭제하는 자동화된 프로세스가 필요합니다.

롤링 배포는 기존 인스턴스를 순차적으로 교체하는 방식이므로 추가 인스턴스 없이 배포가 가능하며, 이는 운영 비용 절감으로 이어집니다.

구현 복잡도와 인프라 의존성

최종적으로 롤링 배포를 선택한 가장 큰 이유는 AWS 인프라에 대한 의존성을 최소화하고, 배포 메커니즘을 로우 레벨에서 직접 구현하며 학습하기 위함입니다.

블루-그린 배포를 비용 효율적으로 구현하려면 인스턴스의 동적 생성 및 삭제가 필수적입니다. 그러나 우테코 환경에서는 인스턴스 생성/삭제 권한에 제약이 있어, 이를 우회하기 위해서는 다음과 같은 변경이 불가피합니다:

  • 배포 도구를 GitHub Actions에서 AWS CodeDeploy로 전환
  • 로드밸런서를 Nginx에서 AWS ALB(Application Load Balancer)로 교체
  • AWS 관리형 서비스에 대한 의존도 증가

이러한 변경은 AWS 생태계에 대한 결합도를 높이고, 배포 프로세스의 내부 동작을 블랙박스화할 수 있습니다. 반면 롤링 배포는 Nginx와 GitHub Actions 기반의 현재 인프라를 유지하면서도 무중단 배포를 구현할 수 있으며, 배포 과정의 각 단계를 직접 제어하고 이해할 수 있다는 학습적 가치가 있습니다.

선택 근거

하위 호환성 관리라는 개발 복잡도 증가에도 불구하고, 비용 효율성과 인프라 독립성, 그리고 배포 메커니즘에 대한 깊은 이해를 얻을 수 있다는 점에서 롤링 배포를 최종 선택했습니다.

운영 서버 인프라 아키텍처

롤링 배포를 구현하기 위해 2개의 EC2 인스턴스를 Nginx 기반 로드밸런서로 연결하는 아키텍처를 구성했습니다.

로드밸런싱 전략

Nginx는 라운드 로빈(Round Robin) 알고리즘을 기본 로드밸런싱 방식으로 사용하며, 수신된 요청을 두 인스턴스에 순차적으로 분배합니다. 이와 함께 헬스체크 메커니즘을 통해 비정상 인스턴스로의 트래픽 전달을 자동으로 차단합니다.

upstream backend {
    # ec2-prod-1의 Private IP 주소
    # max_fails=3: 3번 연속 요청 실패 시 unhealthy 상태로 전환
    # fail_timeout=10s: unhealthy 상태에서 10초간 트래픽 차단 후 재시도
    server 10.0.20.28:80 max_fails=3 fail_timeout=10s;
    
    # ec2-prod-2의 Private IP 주소
    server 10.0.20.212:80 max_fails=3 fail_timeout=10s;
}

server {
    listen 80;
    
    location / {
        proxy_pass <http://backend>;
        # 추가 프록시 설정...
    }
}

이러한 구성을 통해 배포 중 한 인스턴스가 재시작되더라도, Nginx가 자동으로 정상 인스턴스로만 트래픽을 라우팅하여 서비스 중단 없는 배포가 가능합니다.

 


롤링 배포 시나리오

전체 흐름도

롤링 배포 프로세스는 다음과 같이 순차적으로 진행되며, 각 단계에서 헬스체크를 통해 배포 성공 여부를 판단합니다.

배포 성공 시나리오

초록색 경로는 모든 인스턴스가 정상적으로 배포되는 이상적인 시나리오를 나타냅니다.

1. Prod CD Start

배포 파이프라인이 시작됩니다. 일반적으로 main 브랜치로의 머지나 수동 트리거를 통해 실행됩니다.

2. Build & Push Image

GitHub Actions의 GitHub-hosted Runner에서 최신 소스코드를 기반으로 Docker 이미지를 빌드하고, Docker Hub에 푸시합니다. 이 이미지는 두 인스턴스에서 동일하게 사용됩니다.

3. Deploy prod-1

ec2-prod-1의 Self-hosted Runner가 새로운 Docker 이미지를 pull하고 docker compose up 명령어로 컨테이너를 재시작합니다.

무중단 보장: prod-1이 재시작되는 동안 발생하는 모든 사용자 요청은 Nginx가 자동으로 prod-2로 라우팅하여 서비스 중단 없이 처리합니다.

4. Is prod-1 Up?

Spring Boot Actuator의 /actuator/health 엔드포인트를 주기적으로 호출하여 prod-1의 상태를 확인합니다.

  • 응답 성공(UP): 다음 단계로 진행
  • 응답 실패: 롤백 시나리오로 분기

5. Deploy prod-2

prod-1의 배포가 성공적으로 완료되면, 동일한 방식으로 ec2-prod-2에 배포를 진행합니다.

무중단 보장: prod-2가 재시작되는 동안 모든 트래픽은 이미 업데이트된 prod-1이 처리합니다.

6. Is prod-2 Up?

prod-2의 헬스체크를 수행합니다.

  • 응답 성공(UP): 배포 완료
  • 응답 실패: 롤백 시나리오로 분기

7. Deploy Success

두 인스턴스 모두 새로운 버전으로 정상 구동되며, 배포 프로세스가 성공적으로 완료됩니다.

배포 실패 시나리오

파란색 경로는 헬스체크 실패 시 자동으로 이전 버전으로 롤백하는 시나리오를 나타냅니다.

롤백 트리거

각 인스턴스의 배포 후 헬스체크에서 실패 응답을 받으면 즉시 롤백 프로세스가 시작됩니다. 롤백은 이전 버전의 Docker 이미지로 컨테이너를 재시작하는 방식으로 진행됩니다.

시나리오 1: prod-1 배포 실패

  1. prod-1 배포 후 헬스체크 실패
  2. prod-1을 이전 버전으로 롤백
  3. 롤백 후 재헬스체크:
    • 성공: 배포 프로세스 종료 (Deploy Failed / Rollback Success)
    • 실패: 롤백 실패 상태로 전환

시나리오 2: prod-2 배포 실패

  1. prod-1 배포 성공
  2. prod-2 배포 후 헬스체크 실패
  3. prod-2를 이전 버전으로 롤백
  4. prod-1도 이전 버전으로 롤백 (버전 일관성 유지)
  5. 두 인스턴스 모두 재헬스체크:
    • 모두 성공: 배포 프로세스 종료 (Deploy Failed / Rollback Success)
    • 하나라도 실패: 롤백 실패 상태로 전환

무중단 보장: 롤백 과정에서도 최소 하나의 인스턴스가 정상 동작하므로, Nginx를 통해 서비스가 지속적으로 제공됩니다.

롤백 실패 시나리오

빨간색 경로는 롤백마저 실패하여 수동 개입이 필요한 최악의 시나리오를 나타냅니다.

부분 장애: 한 인스턴스만 롤백 실패

prod-1이 롤백에 실패하더라도 prod-2가 정상 동작한다면, 서비스는 단일 인스턴스로 계속 제공됩니다. 이 경우:

  • 서비스 중단은 발생하지 않으나, 가용성과 처리 용량이 50% 감소
  • 개발자는 장애 알림을 받고 prod-1의 문제를 수동으로 해결해야 함

전체 장애: 두 인스턴스 모두 롤백 실패

최악의 경우 prod-1과 prod-2 모두 롤백에 실패하면 완전한 서비스 중단이 발생합니다. 만약 롤백이 실패했을 경우, 해당 서버는 서비스가 불가능 합니다. 이때는 개발자가 수동으로 오류를 해결해야 합니다.

추가적인 구현으로 이 케이스를 막을 수 있습니다.


버전 정합성 문제

롤링 배포의 특성상 배포 과정에서 서로 다른 버전의 인스턴스가 동시에 트래픽을 처리하는 상황이 발생합니다. 이는 API 호환성, 데이터베이스 스키마, 세션 관리 등에서 예기치 않은 오류를 유발할 수 있습니다.

정상 배포 시나리오에서의 버전 혼재

발생 상황

정상적인 배포 프로세스에서도 다음과 같은 짧은 시간 동안 두 버전이 공존합니다:

[배포 타임라인]
prod-1: v1.0 ──────▶ [재시작 중] ──────▶ v1.1 ───────────────────▶
prod-2: v1.0 ───────────────────────────▶ [재시작 중] ──────▶ v1.1

        ├─────────── 구간 A ──────────┤├──── 구간 B ────┤

영향도 평가

현재 2개의 인스턴스만 운영하는 환경에서는 버전 혼재 시간이 짧고, Nginx의 라운드 로빈 방식이 요청을 균등 분배하므로 대부분의 경우 무시할 수 있는 수준입니다. 그러나 다음 상황에서는 주의가 필요합니다:

  • API 스펙 변경: 요청/응답 구조가 변경된 경우
  • 새로운 필수 필드 추가: 구버전이 인식하지 못하는 데이터
  • 상태를 공유하는 기능: 세션, 캐시 등

데이터베이스 스키마 변경 시 고려사항

데이터베이스 스키마 변경은 롤링 배포에서 가장 주의가 필요한 영역입니다. 구버전과 신버전이 동시에 동일한 데이터베이스를 접근하기 때문입니다.

위험 시나리오

예시 1: 컬럼 삭제

-- v1.1에서 user.phone 컬럼 삭제
ALTER TABLE user DROP COLUMN phone;
  • 배포 중 v1.0 인스턴스가 phone 컬럼을 조회하려 시도 → SQL 오류 발생
  • 롤백 시 v1.1로 업그레이드된 DB를 v1.0 코드가 접근 → 여전히 오류 발생

예시 2: 필수 컬럼 추가

-- v1.1에서 user.email 컬럼을 NOT NULL로 추가
ALTER TABLE user ADD COLUMN email VARCHAR(255) NOT NULL;
  • 배포 중 v1.0 인스턴스가 사용자 생성 시 email 없이 INSERT → 제약 조건 위반

안전한 스키마 변경 전략

1단계 배포: 하위 호환성 유지 (Backward Compatible)

-- 컬럼 추가 시 nullable로 먼저 추가
ALTER TABLE user ADD COLUMN email VARCHAR(255) NULL;

-- 또는 기본값 설정
ALTER TABLE user ADD COLUMN email VARCHAR(255) DEFAULT 'unknown@example.com';
  • 구버전 코드는 새 컬럼을 무시하고 정상 동작
  • 신버전 코드는 새 컬럼을 사용

2단계 배포: 제약 조건 추가

-- 모든 인스턴스가 v1.1로 업데이트된 후
ALTER TABLE user MODIFY COLUMN email VARCHAR(255) NOT NULL;

컬럼 삭제 시 3단계 접근

-- 1단계: 코드에서 해당 컬럼 사용 중단 (v1.1 배포)
-- 2단계: 충분한 시간 경과 후 컬럼 삭제 (v1.2 배포)
ALTER TABLE user DROP COLUMN phone;

'개발 > DevOps' 카테고리의 다른 글

깃허브에 노출된 환경변수를 완전히 제거하는 현실적인 방법  (0) 2026.01.22
'개발/DevOps' 카테고리의 다른 글
  • 깃허브에 노출된 환경변수를 완전히 제거하는 현실적인 방법
yesjuhee
yesjuhee
Dopamine Driven Developer
  • yesjuhee
    나랑 노랑
    yesjuhee
  • 전체
    오늘
    어제
    • 분류 전체보기 (29)
      • 개발 (11)
        • DevOps (2)
        • Java & Spring (4)
        • AI (1)
        • DB (1)
        • 기타 (3)
      • 후기 or 회고 (15)
        • 우아한테크코스 (11)
        • 기타 (4)
      • 독서 (2)
      • 기타 (1)
      • 초록 스터디 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    mysql
    초록 스터디
    모아온
    spring
    레벨2
    독서
    레벨3
    초록 밋업
    바킹독
    레벨4
    SCG
    우테코
    coderabbit
    QueryDSL
    우아콘
    DispatcherServlet
    후기
    Ai
    소프티어 부트캠프
    claude code
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
yesjuhee
롤링 배포를 사용한 모아온의 무중단 배포 전략
상단으로

티스토리툴바