포스트

배포시간 단축: 블루-그린 배포 도입기

복잡한 툴 없이, Shell Script와 actuator/health로 완성한 1초 트래픽 전환 시스템

배포시간 단축: 블루-그린 배포 도입기

새로운 서비스를 개발하며 배포 과정을 자동화하는 CI/CD 구축은 필수적인 과제였습니다. 하지만 기존에 사용하던 배포 방식은 몇 가지 고질적인 문제점을 안고 있었고, 이로 인해 더 안정적이고 효율적인 배포 전략을 모색하게 되었습니다.

첫째, 배포 중 간헐적으로 발생하는 에러가 문제였습니다. 명확한 원인 파악이 어려워 재현조차 힘든 에러는 배포의 안정성을 떨어뜨리는 주범이었습니다.
둘째, 배포 실패 시 전체 서버가 다운되는 치명적인 위험이 있었습니다. 이는 곧 서비스 중단으로 이어져 사용자에게 직접적인 피해를 줄 수 있는 매우 심각한 문제였습니다.
마지막으로, 로드 밸런서에서 서버를 제외하고 다시 연결하는 과정에서 발생하는 딜레이는 전체 배포 시간을 늘려 불필요한 비효율을 초래했습니다.

이러한 문제들을 해결하고 완벽한 무중단 배포를 실현하기 위해, 새로운 배포 전략인 블루-그린(Blue-Green) 배포를 도입하기로 결정했습니다. 이 글을 통해 기존 방식의 한계와 새로운 배포 전략의 도입 과정을 자세히 공유하고자 합니다.

현재 사용 중인 배포 방식 : 롤링 업데이트 배포(Rolling Update Deployment)

기존 서비스에서는 HAProxy를 통해 로드 밸런싱을 하고 있는데 서버 반을 로드밸런싱에서 제외하고 배포 후 다시연결 나머지를 제외하고 배포 후 연결하는 방식으로 배포하고 있는데 이러한 방식을 롤링 업데이트 배포라고 합니다.

현재 사용 중인 배포 방식의 처리 흐름

/img/blue-green/deploy01.jpg

  1. 로드밸런서에서 서버의 반을 제외(4대인경우 2대를 제외)합니다.
  2. 제외된 서버에 배포합니다. /img/blue-green/deploy02.jpg
  3. 배포완료 후 로드밸런서에 연결합니다.
  4. 나머지 배포전인 서버를 로드밸런서에서 제외합니다.
  5. 나머지 서버에 배포힙니다. /img/blue-green/deploy03.jpg
  6. 로드 밸런서에 연결합니다.

기존 배포방식의 문제점

기존의 서비스들을 수차례 배포해보면서 다음과 같은 세가지 문제점을 인식하게 되었습니다.

  1. 드물게 발생하는 에러 : 배포할때만 간헐적으로 발생하는 에러가 있는데 로드 밸런서가 서버를 제외하거나 다시 연결하는 타이밍에 에러가 발생하는 듯 합니다.
    • 아래와 같은 에러가 발생했습니다.
      1
      2
      3
      
      [2024-11-12 07:04:13.886] [ERROR] [/] - org.xnio.listener invokeChannelListener - XNIO001007: A channel event listener threw an exception
      java.lang.NoClassDefFoundError: io/undertow/server/protocol/http/HttpReadListener$1
      at io.undertow.server.protocol.http.HttpReadListener.sendBadRequestAndClose(HttpReadListener.java:286)
      

      에러의 원인을 AI에게 문의해보니 배포 관련으로 인한 문제 일수도 있다고 나왔습니다.

      제미나이 답변 내용

      부분적 업데이트: 롤링 업데이트 같은 배포 방식에서 기존 클래스 파일과 새로운 클래스 파일이 섞여서 로드될 때, 버전 충돌로 인해 발생할 수 있습니다. 예를 들어, 로드 밸런서에서 트래픽을 제외하기 전에 요청이 들어오거나, 오래된 애플리케이션 컨테이너가 새 클래스 파일을 잘못 캐싱했을 때 발생합니다.

  2. 배포 에러 발생 시의 위험성
    • 배포 하다가 에러 발생 시 로드밸런서에서 제외된 서버들은 다운된 상태이고 기동 서버가 반으로 줄어 부하가 증가합니다.
    • 배포 스크립트 문제인지 에러가 발생했음에도 계속 진행되어 서버 전체가 다운되는 문제가 발생했었습니다.
      • 서버 전체가 다운되어 서비스 이용 불가가 되었습니다.
    • 여기서 가장 문제는 복구하기 위해서는 소스를 기존으로 롤백하고 다시 배포를 해야 하기에 시간이 걸린다는 것이었습니다.
      • 복구 시간 동안 서비스를 이용 못하게 되고 관련해서 에러가 계속 발생하여 아찔한 순간이었습니다.
  3. 긴 배포 시간
    • 로드 밸런서에서 제외하고 다시 연결하는 과정에 시간이 소모되었습니다.
    • 서버 별로 나눠서 배포를 진행하게 되어 서버 수가 늘어날수록 배포 시간은 더 길어질 것으로 보여집니다.
    • 롤백 할때에도 다시 배포해야하기에 배포시간의 영향을 받게 됩니다.

위의 문제점들을 개선하고자 배포 방식 변경을 검토하게 되었습니다.

새로운 해결책 : 블루-그린 배포

기존과 다른 배포 방식으로 찾게 된 것이 블루-그린 배포였습니다.

블루-그린 배포는 무중단 배포기법의 하나로 블루는 구버전, 그린은 신버전으로 신버전인 그린에 배포 및 기동을 완료한 후에 트래픽을 전환하는 방식의 배포 전략입니다.

주요 특징에서 아래 3가지가 있었습니다.

  • 무중단 배포 : 서비스가 중단되지 않고 새로운 버전으로 전환되어 서비스 제공에 영향을 주지 않습니다.
  • 트래픽 변경 없음 : 배포시에도 동일한 서버대 수 유지되어 서버별 부하에는 변동이 없습니다.
  • 빠른 롤백 : 문제 발생시 로드밸런서 설정을 되돌려 기존 버전으로 트래픽을 전환하는 것으로 신속한 복구가 가능합니다.

위의 특징들이 기존 배포 방식의 문제점 개선에 적합하다고 생각되어 블루-그린 배포 방식을 적용하게 되었습니다.

블루-그린 배포 도입에 중요한 포인트

  • 블루(Blue) 환경 : 현재 운영 중인 애플리케이션 버전이 실행 중인 환경
    • 서버 내에서 고정으로 포트 2개를 지정하여 사용 중인 포트의 서비스를 블루로 지정했습니다.
  • 그린(Green) 환경: 새롭게 배포될 애플리케이션 버전이 실행되는 별도의 환경
    • 미사용중인 포트의 서비스를 그린으로 지정했습니다.
  • 트래픽 전환 : 새로운 버전을 테스트하고 준비한 후, 트래픽을 기존 블루 환경에서 그린 환경으로 전환
    • Nginx에서 프록시 해주는 포트 번호를 바꾸는 방식으로 구현했습니다.

블루-그린 배포 구현 방법

  • Nginx 설정
    • 서버내 로컬에서 지정한 포트의 서비스를 외부에서 호스팅 되도록 설정했습니다
  • 배포 서비스 설정
    • 신/구버전의 서비스가 포트가 다르게 구동되도록 설정했습니다.
    • 대상 서비스는 java spring boot 기반의 서비스로 서비스별 사용할 포트 두개는 임의로 각각 8092, 8093로 하고 설명 진행하겠습니다. (서버에서 사용중인 아닌 포트라면 어떤 번호도 문제 없습니다.)
  • 배포 스크립트 작성
    • 실행 중인 포트를 자동으로 확인하고, 미사용인 포트로 서비스를 배포하고 트래픽을 새로운 포트로 전환하는 스크립트를 작성하였습니다.

구현한 블루-그린 배포처리 흐름

/img/blue-green/newdeploy01.jpg

  1. 포트 체크 : 포트의 기동 여부를 체크하여 사용하지 않는 포트를 확인합니다.
  2. 그린 환경 준비 : 새 버전의 애플리케이션을 1에서 확인된 사용하지 않는 포트(8093)의 서비스에 배포하고 기동합니다.
  3. 헬스 체크 : 배포 후 테스트와 검증 수행합니다.
    • 문제없이 기동 되었는지와 api에 대한 호출 체크를 합니다.

/img/blue-green/newdeploy02.jpg

  1. 트래픽 전환 : Nginx의 프록시 대상 포트를 기존포트(8092)에서 새 포트(8093)로 변경합니다.
  2. Nginx 반영 : Nginx 설정을 반영(nginx reload)합니다.
  3. 블루 환경 종료 : 사용 중이었던 포트(8092)의 서비스를 기동 정지합니다.
  4. 확인 : 모니터링 및 안정성 확인합니다.

여기서 배포하고 문제가 발생한 경우, 정지된 포트의 서비스를 기동하고 nginx의 프록시 대상 포트를 바꿔주는 것으로 롤백이 가능합니다.

배포스크립트를 통해 보는 배포 흐름

  1. 빌드를 실행하여 jar파일이 생성되도록 합니다.
    1
    
    ./gradlew --build-cache :api:bootJar
    
  2. 기동중인 포트 확인: 서비스 이름에 포트를 포함하여 서비스 활성화 확인하여 사용 중인 포트(SERVER_PORT_USE)와 미사용인 포트(SERVER_PORT)를 각각의 변수에 저장합니다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    # 서비스 이름에 포트를 포함
    SERVICE_NAME="${SERVICE_UNIT_NAME}-${SERVER_PORT_BLUE}"
    # ssh를 사용해서 해당포트의 서비스가 사용중인지 체크
    # server_ip : 해당하는 서버의 ip
    if ssh -p포트번호 계정@${server_ip} "sudo systemctl is-active --quiet ${SERVICE_NAME}"; then
    echo "Port ${SERVER_PORT_BLUE} on ${server_ip} is in use."
    SERVER_PORT=${SERVER_PORT_GREEN}
    SERVER_PORT_USE=${SERVER_PORT_BLUE}
    else
    echo "Port ${SERVER_PORT_BLUE} on ${server_ip} is not in use."
    SERVER_PORT=${SERVER_PORT_BLUE}
    SERVER_PORT_USE=${SERVER_PORT_GREEN}
    fi
    
  3. 기동 중이 아닌 포트의 서비스 이름을 변수에 저장합니다.
    1
    2
    
    # 사용중이지 않은 포트로 서비스 이름 재설정
    SERVICE_NAME="${SERVICE_UNIT_NAME}-${SERVER_PORT}"
    
  4. 서비스 배포: 소스를 빌드하고 배포합니다.
    1
    2
    3
    4
    5
    6
    7
    
    ssh -p포트번호 계정@${server_ip} "
    mkdir -p -v ${pipeline_dir}
    sudo find ${project_dir}/ -mtime +1 -delete || ls -l ${project_dir}/*/*
    "
    scp -P포트번호 -p api/build/libs/*.jar 계정@${server_ip}:${pipeline_dir}
    scp -P포트번호 -p scripts/* 계정@${server_ip}:${project_dir}
    ssh -p포트번호 계정@${server_ip} "sudo cp -v ${pipeline_dir}/*.jar /var/bootapp/${SERVICE_NAME}.jar"
    
  5. 서비스 기동 : 새로운 포트의 서비스를 기동합니다.
    1
    
    ssh -p포트번호 계정@${server_ip} "sudo systemctl restart ${SERVICE_NAME}"
    
  6. 서비스 기동 확인 : 새로운 포트의 서비스 기동이 제대로 되었는지 확인하고 실패시 에러 메시지를 출력하고 기동했던 서비스를 정지하고 배포 처리를 종료합니다.
    1
    2
    3
    
    # 기동완료되었는지 확인
    ssh -p포트번호 계정@${server_ip} "TRACE=${CI_DEBUG_TRACE} sh $project_dir/check-started.sh ${SERVICE_UNIT_NAME} ${SERVER_PORT} ${SERVICE_NAME}"
    [[ $? -ne 0 ]] && ssh -p포트번호 계정@${server_ip} "sudo systemctl stop ${SERVICE_NAME}" && exit 1
    
    • 기동 체크
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      
      r=0
      while [[ ${r} -lt 60 ]] ;
      do
       result=$(sudo curl -s http://localhost:${SERVER_PORT}/actuator/health)
       if [[ "${result}" == *"UP"* ]]; then
       echo "#서비스 구동 완료 - ${SERVICE_UNIT_NAME} "; exit ${status}
       fi
       ((r++))
       sleep 1
      done
      echo "#서비스 구동 오류 (타임아웃) - ${SERVICE_UNIT_NAME} ${status} "; exit 1;;
      
    • curl로 heath체크를 하는데 대기 시간이 있으므로 반복문으로 실행했습니다.
    • 기동에 오래걸리는 서비스는 아니라서 최대 60초로 설정했습니다.
  7. Nginx 설정 파일 업데이트: sed 명령어를 사용하여 Nginx 설정 파일에서 현재 사용 중인 포트(SERVER_PORT_USE)를 이번에 사용할 포트(SERVER_PORT)로 변경합니다.
    1
    
    ssh -p포트번호 계정@${server_ip} "sudo sed -i 's/${SERVER_PORT_USE}/${SERVER_PORT}/g' ${NGINX_PATH}"
    
    • NGINX_PATH : nginx 설정 파일의 파일명을 포함한 경로 입니다.
  8. Nginx 설정 검사 및 적용: 변경된 설정 파일이 올바른지 nginx -t로 문법 검사를 수행한 후, 성공하면 Nginx를 reload하여 새로운 포트로 프록시 되도록 업데이트합니다.
    1
    2
    3
    4
    5
    6
    7
    
    # 변경된 설정 파일 문법 검사
    ssh -p포트번호 계정@${server_ip} "sudo nginx -t"
    # 문법 검사 통과 시 설정을 reload, 그렇지 않으면 오류 메시지 출력
    if [ $? -eq 0 ]; then
    echo "Nginx configuration is valid. Reloading Nginx..."
    ssh -p포트번호 계정@${server_ip} "sudo nginx -s reload"
    echo "Nginx successfully reloaded. Proxy pass updated to port: ${SERVER_PORT}"
    
  9. 서비스 정지: Nginx reload 후에 기존에 사용하던 포트의 서비스를 정지합니다.
    1
    2
    
    sleep 5
    ssh -p포트번호 계정@${server_ip} "sudo systemctl stop ${SERVICE_UNIT_NAME}-${SERVER_PORT_USE}"
    
    • sleep 5: 포트 전환 후에 이미 들어와 있는 호출이 있을 경우 처리할 시간을 주기 위해 딜레이를 주었습니다.

직접 구현하며 느낀 블루-그린 배포의 장점

  • 배포 시간 단축: 로드밸런서의 제외/연결에 소요되는 시간 절약되어서 그런지 배포 시간이 많이 단축되었습니다.
    • 배포 방식 변경 전 : 13분 30초 걸렸습니다. (평균적으로 10분 정도 걸렸습니다.)
    • 변경 후 : 1분 54초 걸렸습니다. (2분 가량 걸렸습니다.)
    • 소요 시간이 5분의 1로 단축 되었습니다.
  • 빠른 롤백: 롤백이 필요한 경우 구버전 기동 후 nginx설정 변경으로 즉시 롤백 가능합니다.
  • 테스트 용이성: 포트만 달리하여 구/신버전을 동시 기동이 가능하여 버전 비교 테스트도 가능했습니다.
  • 배포 실패시 안정성: 배포 중 서버 기동 실패 발생해도 기존 서비스는 기동 중이고 nginx에서 포트 전환은 되지 않기에 서비스 이용에는 영향이 없습니다.
  • 무중단 배포: Jmeter로 api 호출 테스트를 해보았는데 단일 서버 테스트였음에도 서비스가 끊김 없이 호출되었습니다. (*부록-배포 테스트)
부록 - 배포 테스트

Jmeter를 통해 배포 중에 api호출시 에러가 발생하는지 확인하였습니다.

다음과 같은 Thread를 설정하고 기동과 함께 jmeter를 실행했습니다. /img/blue-green/jmeter01.jpg

  • threads : 60
    • 접속 유저 수라고 보시면 됩니다.
  • period : 1
    • 지정한 시간까지 threads 에 설정한 트래픽이 증가 됩니다. (설정대로면 1초가 되면 60 유저가 진입완료)
  • loop count : 20
    • 반복수입니다. (설정대로면 60명의 유저가 각각 20번 호출 하는 것으로 총 1200번 호출됩니다.)

결과 레포트에서 에러가 발생하지 않은 것을 확인할 수 있었습니다. /img/blue-green/jmeter02.jpg

  • samples : 총 호출 수로 1200번 호출 되었습니다.
  • Error% : 에러 발생 비율로 에러가 발생하지 않아서 0%입니다.

마무리하며: 단순함 속에서 찾은 혁신

이번 배포 방식 개선을 통해 배포 중 발생하는 에러를 최소화하고, 배포 시간을 획기적으로 단축하는 성과를 거두었습니다.

특히, 복잡한 신기술이나 고비용의 툴 도입 없이 기존에 사용하던 Nginx의 설정을 활용하여 이러한 안정성과 효율을 확보했다는 것이 가장 큰 소득이었습니다. 이는 인프라 환경 변경 없이도 배포 프로세스를 개선할 수 있는 가능성이라고 생각합니다.

앞으로 신규 서비스는 물론, 기존 서비스에도 블루-그린 전략을 점진적으로 적용하여 전체 시스템의 무중단 가용성과 안정성을 한층 높일 계획입니다. 긴 글 읽어주셔서 감사합니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.