개요
도커를 사용해봤다면 서비스 개발 시
로컬에서 테스트를 하기 위해 docker-compose up
, docker-compose down
등의 명령어를 사용하여
새롭게 컨테이너를 띄우거나 삭제하는 과정들을 진행해봤을거다.
만약 실제 서비스가 배포 중인 production 환경에서 위와 같이 컨테이너를 내렸다 올렸다하면 어떻게 될까?
당연히 컨테이너가 재시작 되는 시점 동안 사용자들이 서비스에 접속을 못하게 될 것이다
그렇기 때문에 실제 production 환경에서는 무중단 배포를 해야한다.
무중단 배포 ?
무중단 배포란 서비스를 업데이트할 때 기존 서비스에 영향을 주지 않고 업데이트 하는 것을 의미한다.
예를 들어, A1
이라는 버전의 서비스를 도커 기반으로 운영하고 있었다고 가정해보자.
시간이 흘러 A1
서비스 개발진들은 해당 버전보다 더 개선된 버전으로 A2
를 개발 완료하였고 운영 중인 서비스를 A2
로 업데이트 하기 위해서 다음과 같은 과정을 거쳤다.
그러나 위 구성도처럼 서비스를 배포하게 되면 중단된 시간 동안 유저들은 서비스에 접속을 하지 못하게 되고,
그로 인해 트래픽이 대거 이탈될 수 있는 큰 단점이 존재한다.
이렇게 서비스가 내려가 있는 시간을 '다운 타임(Down time)' 이라고도 하는데
무중단 배포를 도입하면 이러한 다운 타임을 최소화 할 수 있다.
무중단 배포 종류
무중단 배포는 크게 두 가지로 나뉜다.
배포 종류 | 설명 |
---|---|
rolling update 배포 |
새로 배포되어야 하는 버전을 하나씩 순차적으로 적용시키면서 배포하는 방식. 한 번에 모두 배포되는 게 아니기 때문에 배포가 되는 과정에서 옛날 버전과 새로운 버전이 공존함. 그렇기에 배포 과정 중 호환성 문제가 발생할 수도 있음 |
Blue , Green 배포 |
Blue 혹은 Green 버전 둘 중 하나로 배포되어 있는 상태에서 새로운 버전을 동시에 띄우고 로드밸런서를 통해서 스위칭 하는 방식. 한 번에 두개의 버전을 동시에 띄우기 때문에 시스템 자원도 두 배로 든다는 단점이 존재함 |
또한 위 배포 과정 말고 Canary(카나리)
배포도 있다. 단계적 배포라고도 하며, 일부 사용자들에게만 새로운 버전을 배포하고 안정성을 테스트한다.
각 배포 방식에 대해 자세하게 이해해보자.
Rolling Update
로드밸런서로 연결되어있는 동일한 버전의 서버들을 순차적으로 새로운 버전으로 변환시키는 방식이다.
예를 들어, A1
버전의 애플리케이션을 서버 3대가 배포하고 있다고 가정할 때,
배포되어 있는 서버를 순차적으로 A2
버전으로 교체하는 방식이라고 생각하면 될 것 같다.
연결되어있는 서버를 하나씩 멈추고 새로운 버전으로 교체하고 있다.
이 방식의 특징은, 부가적인 서버 자원을 사용하지 않고 배포할 수 있다는 장점이 있지만
배포되는 동안에 발생하는 트래픽에 대한 부하가 더 발생할 수 있다는 단점도 있다.
위 그림의 경우에는 추가적인 서버 자원을 사용하지 않고 배포하는 방법에 대해 표현한 것이지만, 반대되는 방식도 가능하다.
업데이트를 위해 정지된 서버를 대신해서 추가적인 서버를 하나 더 띄우는 것이다.
이렇게 하면 업데이트 때문에 정지된 서버 때문에 발생하는 트래픽 부하가 걸리지 않도록 할 수 있다.
그리고 Rolling Update 에서는 기존 버전과 업데이트 하려는 버전의 호환성이 매우 중요하다
만약 기존 A1
버전에서 유저 목록을 조회하는 /users
라는 API가 있는데 A2
에서 해당 API가 제거되었다면,
사용자가 유저 목록을 볼 수도 있고, 못 볼 수도 있는 문제가 발생하게 된다.
이런 문제가 발생하는 이유는 다음과 같다.
A2
로의 업데이트 과정 중에 A1
, A2
서버가 동시에 존재하게 되고, 이 때 유저가 /users
API를 호출하게 되면 일부는 응답을 제대로 받고, 일부는 못 받는 문제가 발생하는 것이다.
Blue-Green
Blue-Green 배포 방식은 두 개의 독립적인 Blue
환경과 Green
환경을 가지고 진행되는 배포 전략이라고 한다.
여기서 Blue
는 현재 운영 중인 환경을 나타내며, Green
은 새로운 버전의 환경을 의미한다.
Green
의 배포가 준비되면 Blue
환경에서 Green
환경으로 한 번에 전환한다.
A2
가 배포된 순간부터 로드 밸런서는 A2
로 모든 트래픽을 이동시킨다.
A1
의 경우 트래픽 이동의 작업이 끝나는 즉시 종료된다.
이 때, A1
과 A2
가 동시에 켜져 있는 순간은 찰나이다.
롤링 배포와는 다르게 한 번에 버전을 업데이트하기 때문에 버전 호환에 대한 문제가 발생하지 않는다.
또, 만약 새로운 버전에 문제가 발생한다면 트래픽을 다시 Blue
환경으로 이전하면 되기 때문에 버전 롤백에 대한 문제도 쉽게 해결 가능하다.
하지만, 짧은 시간 동안이지만 두 버전이 동시에 실행되는 시간이 있기 때문에 추가적인 서버 비용이 발생한다.
Blue, Green 배포 과정
- 오늘 실습할 Blue, Green 배포 과정을 정리해보자면 아래 순서와 같다.
1. nginx -> 애플리케이션 컨테이너
2. 새로운 버전의 컨테이너를 생성
3. nginx.conf 파일 수정 후 reload (blue 컨테이너로 트래픽 이동)
4. 기존 green 컨테이너는 삭제
위 개념을 정리해보자면
새로 배포할 때마다 새로운 컨테이너들을 띄우고
nginx upstream 연결을 기존
컨테이너IP:PORT
에서 새로 띄운컨테이너IP:PORT
로 변경 후이전 컨테이너는 삭제한다.
사전 준비
- 원활한 실습을 위해
include
속성이 추가된 도커 컴포즈 v2.20.0 버전 이상 필요
무중단 배포 구현
이제 docker-compose.yaml
을 작성해보면서 위처럼 무중단 배포를 구현해보자.
docker-compose 작성
- 실습에서 배포할 앱의 경우 앱의 버전을 리턴하는 아주 간단한 앱이다.
docker-compose.yaml
x-app: &app
restart: always
expose:
- 8000
environment:
- APP_VER=1.0
services:
app-blue:
build: ./app
<<: *app
container_name: app-blue
app-green:
build: ./app
<<: *app
container_name: app-green
nginx:
build: ./nginx
restart: always
ports:
- "80:80"
volumes:
- "./nginx/conf.d/:/etc/nginx/conf.d/"
container_name: nginx
x-app
은 docker compose 3.4부터 추가된 새로운 포맷이다.
간단하게 설명하자면 blue
, green
컨테이너의 공통된 환경 변수를 각각의 컨테이너에 작성하지 않고도 동일하게 사용할 수 있다.
그리고 두 컨테이너는 동일한 이미지를 사용한다.
nginx
는 nginx 컨테이너이며, volumes
의 conf.d에는 blue와 green 컨테이너로 요청을 보내는 설정 파일이 존재한다.
nginx.conf 작성
nginx.blue.conf
와 nginx.green.conf
파일을 작성할 것이다.
여기서 핵심은 두 가지이다.
upstream
을 사용하여 blue와 green 컨테이너를 가리키고server
내부의 값은 동일하게 작성한다.- 현재 실행 중이지 않은 컨테이너의 파일은
.tmp
로 끝나도록 만든다.- 이렇게 할 경우
nginx.conf
파일에서include /etc/nginx/conf.d/*.conf;
가 실행될 때.tmp
로 끝나는 파일은 실행되지 않는다.
- 이렇게 할 경우
conf.d/blue.conf
upstream app-blue {
server app-blue:8000;
}
server {
listen 80;
location / {
proxy_pass http://app-blue;
}
}
conf.d/green.conf.tmp
upstream app-green {
server app-green:8000;
}
server {
listen 80;
location / {
proxy_pass http://app-green;
}
}
nginx.conf
파일에서는 upstream
과 proxy_pass
를 확인해주면 된다.
upstream
에서는 컨테이너 이름과 포트를 명시하고 proxy_pass
에서는 upstream
을 그대로 사용한다.
배포 스크립트 작성
- 위 두 개의
docker-compose.yaml
을 번갈아가며 띄울 수 있는deploy.sh
스크립트를 작성해보자.
deploy.sh
#!/usr/bin/bash
# require variables
COMPOSE_PATH="/home/$USER/github/blue-green/docker-compose.yaml"
NGINX_CONF_DIR="/home/$USER/github/blue-green/nginx/conf.d"
DELAY=5
NGINX_CONTAINER="nginx"
HEALTH_DELAY=15
HC_ENDPOINT="http://localhost:8000"
# Blue를 기준으로 현재 떠있는 컨테이너 추출 (무조건 한 개의 컨테이너는 실행이 되어있어야 함.)
BLUE_CT="$(docker ps --filter "name=app-blue" --filter "status=running" | grep -v "CONTAINER ID")"
# 컨테이너 스위칭
if [[ -n "$BLUE_CT" ]]; then
echo "=== BLUE => GREEN ==="
CURRENT_ENV='app-blue'
NEW_ENV='app-green'
CURRENT_NGINX_CONF='blue.conf'
NEW_NGINX_CONF='green.conf.tmp'
else
echo "=== GREEN => BLUE ==="
CURRENT_ENV='app-green'
NEW_ENV='app-blue'
CURRENT_NGINX_CONF='green.conf'
NEW_NGINX_CONF='blue.conf.tmp'
fi
# 새로운 이미지 pull
echo
echo "======================="
echo "Pulling the new env image: $NEW_ENV"
docker-compose -f "$COMPOSE_PATH" pull $NEW_ENV
echo "======================="
echo
# 새로운 이미지 빌드 (캐시 무시)
echo
echo "======================="
echo "Building the new env image: $NEW_ENV with no cache"
docker-compose -f "$COMPOSE_PATH" build --no-cache $NEW_ENV
echo "======================="
echo
# 새로운 컨테이너 시작
echo
echo "======================="
echo "Starting new env: $NEW_ENV"
docker-compose -f "$COMPOSE_PATH" up -d --no-deps $NEW_ENV
echo "======================="
echo
# 컨테이너 시작 대기
sleep $DELAY
# 헬스 체크
echo
echo "======================="
echo "Wating for the new env to be healthy"
for i in $(seq 1 $HEALTH_DELAY); do
HTTP_STATUS=$(docker-compose -f "$COMPOSE_PATH" exec -T $NEW_ENV curl -s -o /dev/null -w "%{http_code}" $HC_ENDPOINT)
done
echo "======================="
echo
if [[ "$HTTP_STATUS" -ne 200 ]]; then
echo
echo "======================="
echo "New environment did not become healthy in time."
echo "======================="
echo
exit 1
fi
# Nginx conf 업데이트
echo
echo "======================="
echo "Updating nginx configuration"
echo "$NGINX_CONF_DIR/$CURRENT_NGINX_CONF" "$NGINX_CONF_DIR/$CURRENT_NGINX_CONF.tmp"
echo "$NGINX_CONF_DIR/$NEW_NGINX_CONF" "$NGINX_CONF_DIR/${NEW_NGINX_CONF%.tmp}"
echo "======================="
echo
# Nginx 리로드
echo
echo "======================="
echo "Reloading nginx .."
docker-compose -f "$COMPOSE_PATH" exec "$NGINX_CONTAINER" nginx -s reload
echo "======================="
echo
# 이전 환경 중지 및 제거
echo
echo "======================="
echo "Stopping and removing the old environment: $CURRENT_ENV"
docker-compose -f "$COMPOSE_PATH" stop "$CURRENT_ENV"
docker-compose -f "$COMPOSE_PATH" rm -f "$CURRENT_ENV"
echo "======================="
echo
echo
echo "======================="
echo "======================="
echo "배포 완료!"
echo "======================="
echo "======================="
echo
위 스크립트를 간단하게 설명하면 다음과 같다.
- 현재 실행 중인 컨테이너가
blue
인지green
인지 검사 - 새로운 이미지 pulling - build
- 새로운 컨테이너 생성
- 컨테이너가 기동될 때까지 잠시 대기
- 명시한 헬스 체크 endpoint가 정상 응답할 때까지 대기
.tmp
로 끝나는 파일을.conf
로 변경하고.conf
로 끝나는 파일을.conf.tmp
로 변경- nginx 컨테이너 reload
- 이전 환경 중지 및 제거
작동 확인
작성한 스크립트에 실행 권한을 부여하고 스크립트를 실행
$ chmod u+x deploy.sh
$ bash deploy.sh
배포된 애플리케이션은 앱 버전을 출력하는 간단한 서비스이다.
curl
명령을 계속 실행하며 버전이 정상적으로 업데이트되는지 확인해보자.
먼저 docker-compos.yaml
파일에서 APP_VER
속성 값을 2.0으로 변경해주자.
x-app: &app
restart: always
expose:
- 8000
environment:
- APP_VER=1.0
...
새로운 터미널 탭을 열고 아래와 같은 코드를 한 줄 작성하자.
while문을 돌며 1초의 대기 시간을 두고 curl 요청을 하는 코드이다.
$ while true; do curl http://localhost; echo ""; sleep 2; done
스크립트가 실행되니 정상적으로 버전이 업데이트 된 것을 볼 수 있다.
▶︎ 마무리
지금까지 도커를 사용하는 환경에서 무중단 배포를 어떻게 하는지 기록해보았다.
오늘 이해한 내용을 기반으로 컨테이너 오케스트레이션 도구들을 공부해보면 좋을 것 같다.