1. 시작하며
사이드 프로젝트를 운영하면서 백엔드 코드 변경이 빈번하게 발생했습니다. 신규 기능 추가뿐만 아니라 리팩토링 작업과 성능 개선을 포함한 지속적인 코드 품질 향상을 위해 이루어진 작업이었습니다. 하지만 배포 단계에서 서비스 중단이 발생할 가능성이 큰 문제가 되었습니다.
서비스 중단은 사용자 경험에 직접적인 영향을 미칠 수 있으며, 특히 실시간으로 사용되는 서비스라면 이는 치명적일 수 있습니다. 서비스 운영 중에도 배포가 이루어지더라도 사용자에게 중단 없는 경험을 제공하는 것이 목표였습니다.
이를 위해 무중단 배포 전략을 도입하기로 결정했습니다. 기존에는 간단히 Docker 컨테이너를 종료하고 새 컨테이너를 띄우는 방식으로 배포를 진행했지만, 이 과정에서 서비스가 일시적으로 중단되는 문제가 있었습니다. 이를 해결하기 위해 도입한 전략이 바로 Blue-Green 배포 전략입니다.
Blue-Green 배포는 두 개의 환경(Blue, Green)을 번갈아가며 운영 및 배포하는 방식으로, 배포 중에도 현재 운영 중인 환경을 유지하여 사용자에게 영향을 주지 않는 장점이 있습니다. 이번 글에서는 Jenkins, Docker Compose, Nginx를 활용하여 Blue-Green 배포를 구현하는 과정, 문제를 해결하기 위한 삽질들, 그리고 최종적으로 무중단 배포를 성공적으로 구축한 경험을 공유하고자 합니다.
1.1 프로젝트 구조와 목표
이 프로젝트는 무중단 배포를 구현하는 것을 목표로 다음과 같은 구조를 가지고 있습니다. 프로젝트 구조를 간략하게 표현하면 다음과 같습니다.
NowDoBoss
├── BackEnd
│ ├── FastApiServer
│ └── SpringBootServer
├── FrontEnd
│ ├── src
│ ├── public
│ ├── docker-compose-frontend.yml
│ └── React.Dockerfile
├── CICD
│ ├── Nginx
│ │ ├── default.conf
│ │ ├── nginx.conf
│ │ └── Nginx.Dockerfile
... 이외 생략
- BackEnd
- FastAPI와 Spring Boot 두 개의 서버로 구성
- Spring Boot는 Docker Compose로 Blue/Green 컨테이너 관리
- FrontEnd
- React로 구현된 정적 SPA 파일
- Nginx 컨테이너를 통해 정적 파일 제공
- CI/CD
- Jenkins 파이프라인을 통해 배포 자동화
- Nginx는 리버스 프록시 역할을 하며 정적 파일과 API를 연결
2. Blue-Green 배포란?
Blue-Green 배포는 무중단 배포를 위한 대표적인 전략으로, 두 개의 환경(Blue, Green)을 번갈아가며 배포 및 트래픽을 전환하는 방식입니다.
- Blue 환경: 현재 운영 중인 안정적인 환경
- Green 환경: 새로운 버전의 코드가 배포되는 환경
운영 중인 Blue 환경에 영향을 주지 않고 Green 환경에 새로운 변경 사항을 적용한 뒤, Green 환경으로 트래픽을 전환합니다. 만약 Green 환경에서 문제가 발생하면, 다시 Blue 환경으로 롤백할 수 있어 안정성과 신속한 배포를 동시에 제공합니다.
2.1 기존 Blue-Green 배포 방식과 해당 사이드 프로젝트에서 적용한 방식의 차이점
일반적인 Blue-Green 배포는 한 번의 배포로 새로운 Green 환경으로 전환한 뒤, Blue 환경을 비활성화합니다. 이후 변경된 코드를 또 배포한다면 다시 새로운 Green 환경이 아닌 기존의 Blue 환경을 비활성화된 상태로 준비시키는 방식입니다.
하지만 제가 구현한 방식에서는 Blue와 Green 환경을 번갈아 가며 지속적으로 재활용합니다. 새로운 코드가 배포될 때마다 현재 활성화된 환경(Blue 또는 Green)을 확인한 뒤, 비활성화된 환경에 새 버전을 배포합니다. 이후 트래픽을 새 환경으로 전환하고, 이전 환경을 제거하는 방식으로 무중단 배포를 구현했습니다.
2.2 해당 방식의 Blue-Green 배포 흐름
- 현재 활성 환경 확인
- 현재 동작 중인 환경이 Blue인지 Green인지 파악
- 새 버전 배포
- 비활성화된 환경(예: Green)에 새로운 코드를 배포
- Nginx 프록시 전환
- Nginx 설정을 통해 트래픽을 새로운 환경으로 전환
- 이전 환경 종료
- 이전 환경(예: Blue)을 종료 및 제거
2.3 일반적인 Blue-Green 배포와 비교
항목 | 일반적인 Blue-Green 배포 | 제가 구현한 방식 |
---|---|---|
환경 구조 | 새로운 환경(Green)을 생성하고 기존 환경(Blue)을 비활성화한 뒤 전환. 두 환경(Blue와 Green)을 항상 동시에 유지 | 두 환경(Blue, Green)을 번갈아 사용하며, 현재 비활성화된 환경에 새 버전을 배포한 뒤 활성화된 환경과 교체. 하나의 환경은 항상 유지되어 서비스가 중단되지 않음 |
환경 재활용 여부 | 비활성화된 환경은 유지하며, 필요 시 재활용 가능 | 비활성화된 환경을 종료 및 제거 후, 동일한 이름으로 새로운 버전을 배포하여 재활용. 제한된 리소스 내에서 효율적으로 운영 가능 |
트래픽 전환 방식 | Green 환경으로 트래픽을 전환한 후 Blue 환경을 비활성화 | Docker alias를 활용해 컨테이너 간 동적으로 이름을 전환. Nginx는 항상 고정된 도메인(alias)만 바라보도록 설정하여 트래픽 전환을 간소화 |
컨테이너 관리 | 새로운 버전을 Green 환경에 생성한 뒤 기존 Blue 환경은 비활성화 상태로 유지. 두 컨테이너를 항상 띄워 관리 | 비활성화된 컨테이너는 삭제하여 리소스를 회수하고, 새로운 컨테이너를 실행. 제한된 리소스 내에서 운영 가능한 구조로 Blue와 Green 컨테이너를 반복적으로 재활용 |
2.4 왜 이런 방식으로 구현했을까?
제가 구현한 방식은 다음과 같은 이유로 선택되었습니다.
- 리소스 효율성
- 일반적인 Blue-Green 배포 방식에서는 새로운 환경(Green)을 매번 생성하고 기존 환경(Blue)을 유지하는 방식이기에, 동시에 두 환경이 모두 리소스를 소비하게 됩니다. 그러나 제가 구현한 방식은 비활성화된 환경을 재사용하여 추가적인 컨테이너 생성을 최소화했습니다.
- 특히, 이 프로젝트는 라즈베리파이 5 리눅스 서버에서 운영되며, 해당 서버는 상대적으로 제한된 CPU와 메모리 리소스를 가지고 있습니다. 이런 환경에서 불필요한 리소스 낭비를 줄이는 것은 안정적인 서비스 운영을 위해 필수적입니다.
- 따라서 Blue-Green 환경을 번갈아 재활용함으로써 제한된 리소스 내에서 효율적인 배포를 구현할 수 있었습니다.
- Nginx 설정의 간소화
- Nginx는 항상 고정된 도메인(alias)만 바라보도록 설정하고, 배포 스크립트가 트래픽 전환만 담당하도록 구성하여 설정의 복잡도를 낮췄습니다. 이를 통해 추가적인 Nginx 설정 변경 없이 배포 프로세스를 간소화할 수 있었습니다.
- 무중단 배포의 구현
- Blue와 Green 환경을 번갈아 사용하며, 컨테이너가 실행되는 동안 항상 하나의 환경이 활성화되어 있으므로 배포 중에도 서비스 중단이 없습니다. 이는 사용자 경험을 유지하면서도 안정적인 배포를 가능하게 합니다.
2.5 Blue-Green 배포 구조의 시각적 설명
해당 이미지들은 현재 프로젝트에서 적용한 Blue-Green 배포 구조를 시각적으로 나타낸 것입니다.
- Blue와 Green 컨테이너는 내부적으로 동일한 포트(8080)를 사용하여 애플리케이션을 서비스합니다.
- 외부적으로는 각각 8081, 8082 포트를 통해 접근이 가능합니다.
- 배포 과정에서 Nginx는 항상 고정된 alias를 통해 트래픽을 라우팅하며, 활성화된 컨테이너에만 트래픽이 전달됩니다.
3. 설정 과정
3.1 Docker Compose 파일 구성
백엔드(Spring Boot) 서비스의 docker-compose-springboot.yml
파일은 다음과 같이 작성했습니다.
x-spring-boot-environment: &spring_boot_environment
TZ: Asia/Seoul
SPRING_BOOT_PORT: ${SPRING_BOOT_PORT}
# 기타 환경 변수...
services:
nowdoboss_springboot_blue_service:
container_name: nowdoboss-backend-springboot-blue
build:
context: .
dockerfile: SpringBootServer.Dockerfile
image: nowdoboss-backend-springboot-img:blue
restart: always
ports:
- "${SPRING_BOOT_BLUE_PORT}:${SPRING_BOOT_PORT}"
environment:
<<: *spring_boot_environment
networks:
- nowdoboss-net
nowdoboss_springboot_green_service:
container_name: nowdoboss-backend-springboot-green
build:
context: .
dockerfile: SpringBootServer.Dockerfile
image: nowdoboss-backend-springboot-img:green
restart: always
ports:
- "${SPRING_BOOT_GREEN_PORT}:${SPRING_BOOT_PORT}"
environment:
<<: *spring_boot_environment
networks:
- nowdoboss-net
networks:
nowdoboss-net:
name: nowdoboss-net
driver: bridge
3.2 문제가 발생한 Nginx 설정
Blue-Green 배포를 위한 Nginx의 동적 업스트림 설정을 작성했습니다.
초기 default.conf
코드
upstream backend_blue {
server nowdoboss-backend-springboot-blue:8081; # Blue 환경
}
upstream backend_green {
server nowdoboss-backend-springboot-green:8082; # Green 환경
}
Nginx 설정 파일인 default.conf
에 해당 동적 업스트림 설정을 한 뒤 Nginx를 재실행 하니 다음과 같은 문제가 발생했습니다.
nginx: [emerg] host not found in upstream "nowdoboss-backend-springboot-green"
확인해보니 nowdoboss-backend-springboot-green 컨테이너가 실행되지 않은 상태에서 Nginx가 업스트림을 찾지 못해 에러 발생하였습니다.
3.3 문제 해결: Docker의 alias를 이용한 동적 도메인 설정
Docker의 alias
를 활용해 컨테이너 이름을 동적으로 설정하는 방법으로 문제를 해결했습니다.
최종 default.conf
코드
# Blue/Green 환경에 따라 동적으로 설정되는 API 프록시
location /api {
proxy_pass http://nowdoboss-backend-springboot:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Nginx가 항상 동일한 컨테이너 이름(혹은 alias) nowdoboss-backend-springboot
만 바라보도록 고정시켰습니다. 이를 위해 Docker의 alias 기능을 활용해 컨테이너 이름을 동적으로 관리했습니다.
그 다음으로 deploy_blue_green.sh
파일을 작성해 Jenkins Pipeline에서 배포를 자동화했습니다.
#!/bin/bash
DOCKER_COMPOSE_FILE="docker-compose-springboot.yml"
ENV_FILE="src/main/resources/backend-env/.env-springboot"
# 현재 활성화된 환경 확인
if docker ps --filter "name=nowdoboss-backend-springboot-blue" --filter "status=running" --format "{{.Names}}" | grep -q blue; then
CURRENT_ENV="blue"
else
CURRENT_ENV="green"
fi
echo "현재 동작 중인 환경: $CURRENT_ENV"
if [ "$CURRENT_ENV" == "blue" ]; then
echo "Blue -> Green 전환을 진행합니다."
# 1) Green 컨테이너 실행
docker-compose -f $DOCKER_COMPOSE_FILE --env-file $ENV_FILE up --build -d nowdoboss_springboot_green_service
# 2) 기존 Blue alias 해제 및 Green alias 연결
echo "Green 컨테이너에 네트워크 alias 연결 중..."
docker network disconnect nowdoboss-net nowdoboss-backend-springboot-blue || true
# 3) 새로 띄워진 Green 컨테이너의 기존 네트워크 연결을 해제하고, alias를 사용해 도메인 이름(nowdoboss-backend-springboot)으로 네트워크에 다시 연결
docker network disconnect nowdoboss-net nowdoboss-backend-springboot-green
docker network connect --alias nowdoboss-backend-springboot nowdoboss-net nowdoboss-backend-springboot-green
# 4) Blue 컨테이너 중지 및 제거
docker stop nowdoboss-backend-springboot-blue || true
docker rm nowdoboss-backend-springboot-blue || true
echo "Green 환경 전환 완료. (alias nowdoboss-backend-springboot -> Green)"
else
echo "Green -> Blue 전환을 진행합니다."
# 1) Blue 컨테이너 실행
docker-compose -f $DOCKER_COMPOSE_FILE --env-file $ENV_FILE up --build -d nowdoboss_springboot_blue_service
# 2) 기존 Green alias 해제 및 Blue alias 연결
echo "Blue 컨테이너에 네트워크 alias 연결 중..."
docker network disconnect nowdoboss-net nowdoboss-backend-springboot-green || true
# 3) 새로 띄워진 Blue 컨테이너의 기존 네트워크 연결을 해제하고, alias를 사용해 도메인 이름(nowdoboss-backend-springboot)으로 네트워크에 다시 연결
docker network disconnect nowdoboss-net nowdoboss-backend-springboot-blue
docker network connect --alias nowdoboss-backend-springboot nowdoboss-net nowdoboss-backend-springboot-blue
# 3) Green 컨테이너 중지 및 제거
docker stop nowdoboss-backend-springboot-green || true
docker rm nowdoboss-backend-springboot-green || true
echo "Blue 환경 전환 완료. (alias nowdoboss-backend-springboot -> Blue)"
fi
3.4 Jenkins Pipeline 설정
Jenkins Pipeline의 배포 단계에 다음 스크립트를 추가했습니다.
stage('Deploy with Docker Compose') {
steps {
script {
dir('BackEnd/SpringBootServer') {
echo "Blue/Green 전략을 이용한 배포 진행"
sh "chmod +x deploy_blue_green.sh"
sh "./deploy_blue_green.sh"
}
}
}
}
성공적으로 배포된 뒤, 아래와 같이 Docker 컨테이너 상태를 확인할 수 있습니다.
현재 활성화된 컨테이너는 nowdoboss-backend-springboot-blue이며, Nginx는 자동으로 이 활성 환경(Blue)으로 트래픽을 라우팅하도록 설정되어 있습니다. 이후 백엔드 코드가 변경되어 다시 배포될 경우, Green 환경이 새 버전을 수용한 뒤 활성화되며, 트래픽은 Green 환경으로 전환됩니다.
4. Blue-Green 배포 전략의 다양한 방식
4.1 도커 레벨에서 “활성 컨테이너 이름”을 고정하고, 배포 시 컨테이너 이름을 재정의(혹은 alias)하는 방법
- Nginx는 항상 고정된 이름만 바라보고, 실제 컨테이너 간의 alias를 배포 스크립트에서 조정
4.2 Nginx upstream 블록을 이용한 수동/자동 스위칭 + Nginx Reload(혹은 무중단 Reload)
- Nginx 설정에 두 개의 업스트림(Blue, Green)을 모두 선언하고 배포 시점에 활성화된 서버만 남기고 reload
5. 삽질과 교훈
5.1 문제점
- Nginx 업스트림에서 컨테이너가 실행되지 않은 상태로 참조
- DNS 캐싱 이슈로 인해 502 에러 발생
5.2 해결 방법
- Docker alias를 활용한 컨테이너 이름 동적 변경
- Nginx 설정을 고정하고 alias만 조정하여 무중단 배포 구현
6. 결론
이번 Blue-Green 배포를 통해 서비스의 무중단 배포를 성공적으로 구현할 수 있었습니다. 다양한 문제를 해결하는 과정에서 배운 교훈은 다음과 같습니다.
- 문제의 원인 파악: 로그 분석과 Docker 네트워크의 동작 방식을 이해.
- 자동화의 중요성: Jenkins 파이프라인으로 배포 과정을 자동화하여 실수를 최소화.
- 구성의 유연성: Docker alias와 Nginx 설정의 고정화를 통해 효율적인 배포 환경 구축.
앞으로도 DevOps와 관련된 새로운 기술과 전략을 연구하며, 더 나은 배포 환경을 만들어가겠습니다.