0%

간단하게 무중단 배포 도커로 구현하기

서론

최근 도커를 활용하는 방법(도커, 레지스트리, 도커 파일)에 대한 글을 작성했습니다. 도커를 활용하면 무중단 배포를 쉽게 구현할 수 있어서 간단하게 무중단 배포를 구현하는 방법에 대해 알아보겠습니다.

무중단 배포란?

먼저 무중단 배포란 서비스를 업데이트 할 때 기존 서비스에 영향을 주지 않고 업데이트 하는 것을 말합니다. 기존에서는 서비스를 업데이트 할 때 서비스를 중단하고 업데이트를 해야 했습니다. 왜냐하면 업데이트 되지 않은 서비스가 포트를 점유하고 있기 때문에 서비스를 내린 내리지 않으면 포트를 사용할 수 없기 때문입니다. 그래서 서비스를 내린 후 업데이트를 하고 다시 서비스를 올려야 했습니다.

서비스가 내려가고 올라가는 시간 동안 사용자는 서비스를 이용할 수 없습니다. 이런 시간을 다운타임이라고 합니다. 무중단 배포는 이런 다운타임을 최소화 하기 위해 사용됩니다.

무중단 배포 방식

무중단 배포 방식은 여러가지가 있습니다. 그 중 Blue-Green 배포 방식을 알아보겠습니다.

bg

블루 그린 배포 방식은, 구버전과 동일하게 신버전을 구성을 하고 한 번에 트래픽을 신버전으로 전환하는 방식입니다.

장점은 구버전과 신버전이 존재해서 신버전에 오류가 발생하는걸 파악하면 트래픽을 구버전으로 전환해서 빠른 롤백이 가능합니다. 또한 실제 운영환경과 동일한 환경에서 테스트가 가능합니다.

하지만 구버전과 신버전이 두 개가 존재하므로 자원이 두 배로 필요한 단점이 있습니다.

무중단 배포 아이디어

제가 사용하는 서비스는 서버 한 대에 한 개의 컨테이너를 사용하고 있습니다. 컨테이너를 두 개 유지하기는 부담스러웠습니다. 그래서 Blue-Green 배포 방식에서 살짝 변형 해, 새로운 버전을 테스트 하지 않고 바로 트래픽을 전환하는 방식을 사용했습니다. 그러면 컨테이너가 두 개가 존재하는 시간이 짧아져 두 배의 자원을 사용하는 시간이 짧아집니다.

구체적인 방법은 다음과 같습니다.

  1. blue 컨테이너는 이미 실행 중이고 green 컨테이너는 실행 중이지 않습니다.
  2. green 컨테이너를 최신 버전으로 빌드합니다.
  3. green 컨테이너를 실행합니다.
  4. nginx로 기존 blue 컨테이너 요청을 green 컨테이너로 보냅니다.
  5. blue 컨테이너를 종료합니다.

blue, green의 실행 상황이 반대여도 상관이 없게 구현합니다.

주의 해야할 부분은 컨테이너 실행 명령어를 사용하더라도 컨테이너가 실행되는 시간이 있기 때문에 다운타임이 발생 할 수 있습니다. 그래서 컨테이너가 실행되는 시간을 고려해서 무중단 배포를 구현해야 합니다.

nginx에서 요청을 다른 컨테이너로 바꿀 때 재시작을 하게 되면 다운타임이 발생할 수 있습니다. 그래서 nginx의 설정파일을 바꾼 후에 reload 명령어를 사용해서 무중단 배포를 구현할 수 있습니다.

무중단 배포 구현

먼저 docker-compose.yml 파일을 작성합니다.

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
version: "3.7"

x-dang: &dang
environment:
- SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/dang
- SPRING_DATASOURCE_USERNAME=username
- SPRING_DATASOURCE_PASSWORD=password
expose:
- 8080

services:
nginx:
build: ./nginx
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/conf.d/:/etc/nginx/conf.d/
ports:
- 443:443
- 80:80

dang-green:
build: ./java/dang
<<: *dang

dang-blue:
build: ./java/dang
<<: *dang

docker-compose의 핵심 부분을 가져왔습니다.

x-dangextensions라고 불리는 기능을 사용했습니다. 간단하게 말하면 blue와 green 컨테이너의 환경 변수를 두 번 작성하지 않고 동일한 환경 변수를 사용할 수 있습니다.

dang-greendang-blue는 blue와 green 컨테이너를 나타냅니다. 두 컨테이너는 같은 이미지와 환경 변수를 사용합니다.

nginx는 nginx 컨테이너를 나타냅니다. volumes의 conf.d에는 blue와 green 컨테이너로 요청을 보내는 설정 파일이 있습니다.

blue.conf와 green.conf.tmp 파일을 작성합니다. 여기서 핵심은 두 가지 입니다.

첫 번째는 upstream을 사용해서 blue와 green 컨테이너를 나타내고 server내부의 값은 동일하게 작성합니다.

두 번째는 현재 실행 중이지 않은 컨테이너의 파일은 .tmp로 끝나게 합니다. 이렇게 하면 nginx.conf파일에서 include /etc/nginx/conf.d/*.conf;가 실행이 될 때 .tmp로 끝나는 파일은 실행되지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# conf.d/blue.conf
upstream dang-blue {
server dang-blue:8080;
}

server {
listen 443 ssl;
server_name one.marinesnow34.com;

client_max_body_size 1024M;

ssl_certificate /etc/letsencrypt/live/one.marinesnow34.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/one.marinesnow34.com/privkey.pem;

location / {
proxy_pass http://dang-blue;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# conf.d/green.conf.tmp
upstream dang-green {
server dang-green:8080;
}

server {
listen 443 ssl;
server_name one.marinesnow34.com;

client_max_body_size 1024M;

ssl_certificate /etc/letsencrypt/live/one.marinesnow34.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/one.marinesnow34.com/privkey.pem;

location / {
proxy_pass http://dang-green;
}
}

conf 파일에서는 upstream과 proxy_pass를 확인하면 됩니다. upstream에서는 컨테이너 이름과 포트를 명시하고 proxy_pass에서는 upstream을 그대로 사용합니다.

마지막으로 blue-green 배포 스크립트를 작성합니다. 스크립트는 다음과 같습니다.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#!/bin/bash
set -e

COMPOSE_FILE_PATH="/home/ubuntu/docker/docker-compose.yml"
NGINX_CONF_DIR="/home/ubuntu/docker/nginx/conf.d"
SLEEP_TIME=10 # 필요시 조정
NGINX_SERVICE_NAME="nginx" # docker-compose에서 정의된 Nginx 서비스 이름
HEALTH_CHECK_ENDPOINT="http://localhost:8080" # 헬스 체크 엔드포인트

# 현재 실행 중인 환경 확인 (무조건 한 개의 서비스는 실행 중이여야 함)
if docker-compose -f $COMPOSE_FILE_PATH ps | grep dang-blue; then
CURRENT_ENV=dang-blue
NEW_ENV=dang-green
CURRENT_CONF=blue.conf
NEW_CONF=green.conf.tmp
else
CURRENT_ENV=dang-green
NEW_ENV=dang-blue
CURRENT_CONF=green.conf
NEW_CONF=blue.conf.tmp
fi

# 새로운 환경 이미지 풀링
echo "Pulling the new environment image: $NEW_ENV"
docker-compose -f $COMPOSE_FILE_PATH pull $NEW_ENV

# 새로운 환경 이미지 빌드 (캐시 무시)
echo "Building the new environment image: $NEW_ENV with no cache"
docker-compose -f $COMPOSE_FILE_PATH build --no-cache $NEW_ENV

# 새로운 환경 시작
echo "Starting new environment: $NEW_ENV"
docker-compose -f $COMPOSE_FILE_PATH up -d --no-deps $NEW_ENV

# 새로운 환경 시작 대기
sleep $SLEEP_TIME

# 헬스 체크 대기
echo "Waiting for the new environment to be healthy..."
for i in {1..30}; do
HTTP_STATUS=$(docker-compose -f $COMPOSE_FILE_PATH exec -T $NEW_ENV curl -s -o /dev/null -w "%{http_code}" $HEALTH_CHECK_ENDPOINT)
if [ "$HTTP_STATUS" -eq 200 ]; then
echo "New environment is healthy!"
break
else
echo "Waiting for new environment to be healthy..."
sleep $SLEEP_TIME
fi
done

if [ "$HTTP_STATUS" -ne 200 ]; then
echo "New environment did not become healthy in time."
exit 1
fi

# Nginx 설정 업데이트
echo "Updating Nginx configuration..."
mv $NGINX_CONF_DIR/$CURRENT_CONF $NGINX_CONF_DIR/$CURRENT_CONF.tmp
mv $NGINX_CONF_DIR/$NEW_CONF $NGINX_CONF_DIR/${NEW_CONF%.tmp}

# Nginx 재로드
echo "Reloading Nginx..."
docker-compose -f $COMPOSE_FILE_PATH exec $NGINX_SERVICE_NAME nginx -s reload

# 이전 환경 중지 및 제거
echo "Stopping and removing the old environment: $CURRENT_ENV"
docker-compose -f $COMPOSE_FILE_PATH stop $CURRENT_ENV
docker-compose -f $COMPOSE_FILE_PATH rm -f $CURRENT_ENV

echo "Deployment complete."

주석을 달았지만 간단하게 설명하면 다음과 같습니다.

  1. 현재 실행 중인 환경이 blue인지 green인지 확인합니다.
  2. 새로운 환경 이미지를 풀링하고 빌드합니다.
  3. 새로운 환경을 시작합니다.
  4. docker-compose up이 정상 실행될때까지 잠시 대기합니다.
  5. 명시한 헬스 체크 엔드포인트가 정상 응답할 때까지 대기합니다.
  6. .tmp로 끝나는 파일을 .conf로 변경하고 .conf로 끝나는 파일을 .conf.tmp로 변경합니다.
  7. Nginx를 리로드합니다.
  8. 이전 환경을 중지하고 제거합니다.

작동 확인

blue, green 중 실행중인 컨테이너와 .conf 파일이 일치하는지 확인합니다. 그리고 스크립트를 실행합니다.

1
$ ./deploy.sh

현재 서비스는 서버 ip를 보여주는 간단한 서비스입니다. curl을 계속 실행하면서 중단 없이 서버 ip가 바뀌는지 확인합니다.

1
while true; do curl https://your-domain.com; echo "";sleep 1; done

172.10.0.2에서 오류 없이 172.10.0.5로 변경되는 것을 확인할 수 있습니다.
무중단 배포

결론

간단하게 무중단 배포를 구현하는 방법에 대해 알아보았습니다. CI과정은 현재 글에서 설명하지는 않았지만 레지스트리를 사용해서 최신 이미지를 올리고 위 스크립트를 사용하면 CI/CD를 쉽게 구축할 수 있습니다.