개발하는 자몽

롤링 무중단 배포 본문

Architecture & Tool

롤링 무중단 배포

jaamong 2024. 4. 19. 08:54

적용 전 준비할 코드

nginx.conf

worker_processes auto;

events {
    worker_connections 1024;
}

http {
    ...

    # for load balancing
    upstream project_server {
        server project_server2:8000;
        server project_server1:8000;
        keepalive 1024;
    }

    server {
        listen 80;  
        
        location / {
            proxy_pass http://project_server/;    # upstream 이름
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            ...
        }

        ...
    }
}

무중단 배포의 전제조건은 이중화(로드밸런서)로, `upstream` 지시어로 설정할 수 있다. 

 

docker-compose.yml

version: "3.5"

services:
  _build_image:
    image: project_django:4.2.6
    build: .
    
  project_server1: 
    image: project_django:4.2.6
    restart: always
    command: gunicorn project_server.backend.wsgi --bind 0.0.0.0:8000 --log-level debug --timeout=120
    ports:
      - "8000"  
    depends_on:
      - _build_image

  project_server2: 
    image: project_django:4.2.6
    restart: always
    command: gunicorn project_server.backend.wsgi --bind 0.0.0.0:8000 --log-level debug --timeout=120
    ports:
      - "8000" 
    depends_on:
      - _build_image

  nginx:
    image: project-nginx:latest    # tag the image with the name "nginx"
    container_name: project-nginx
    build: ./nginx  # Dockerfile location for Nginx
    restart: alway
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro  # read-only
    depends_on:
      - project_server1
      - project_server2
    ports:
      - "8000:80"
...

 

 

수동으로 하는 방법

✅ 전체 과정

  1. 배포 전 상태로 project_server1, project_server2 모두 로드밸런서(nginx)에 연결되어 있다.
  2. project_server2를 로드밸런서에서 제거하고, project_server2에 배포를 진행한다.
  3. project_server2에 배포가 완료되면, 다시 project_server2를 로드밸런서에 연결한다.
  4. project_server1을 로드밸런서에서 제거하고, project_server1에 배포를 진행한다.
  5. project_server1에 배포가 완료되면, 다시 project_server1을 로드밸런서에 연결한다.

 

Step1.

배포가 진행되는 동안 서비스가 멈추지 않는지(무중단) 확인하기 위해서 터미널 하나를 열여서 주기적으로 Nginx에 요청을 보낸다.

# 1초 간격으로 요청 (무한루프)
$ while true; do curl localhost:4100; ehco ""; sleep1; done

 

Step2.

project_server2를 로드밸런서에서 제거한다. nginx.conf의 `upstream` 서버 그룹에 아래와 같이 `down`을 추가한다.

...

upstream project_server {
    server project_server2:8000 down; 
    server project_Server1:8000;
}

...

 

Nginx 컨테이너에 아래와 같은 명령어를 전달하여 멈추지 않고 설정을 리로드 한다. 리로드 시 `exec`의 다음 인자는 docker-compose.yml의 `container_name`을 적어야 한다.

$ docker compose exec project-nginx service nginx reload

Step1에서 열어둔 터미널에서 project_server1에서만 응답이 오는 것을 확인하자.

 

project_server2의 배포를 진행한다.

$ docker compose up --build -d project_server2

 

Step3.

project_server2의 배포가 완료되면 project_server2를 로드밸런서에 연결하고 Nginx를 리로드 한다.

...

upstream project_server {
    server project_server2:8000; 
    server project_Server1:8000;
}

...
$ docker compose exec project-nginx service nginx reload

리로드 이후, Step1에서 열어둔 터미널에서 project_server1, project_server2로부터 응답이 오는 것을 확인한다.

 

Step4.

이번에는 project_server1의 배포를 진행한다. nginx.conf의 `upstream` 부분을 다음과 같이 수정하고 Nginx를 리로드 한다.

...

upstream project_server {
    server project_server2:8000; 
    server project_Server1:8000 down;
}

...

마찬가지로 Step1에서 열어둔 터미널에서 project_server2에서만 응답이 오는 것을 확인한다.

 

project_server1이 로드밸런서에서 제거됐으므로 project_server1의 배포를 진행한다.

$ docker compose up --build -d project_server1

 

Step5.

project_server1의 배포가 완료되었으므로 다시 로드밸런서에 연결한다.

...

upstream project_server {
    server project_server2:8000; 
    server project_Server1:8000;
}

...

 

마지막으로 Nginx를 리로드 한다.

$ docker compose exec project-nginx service nginx reload

 

먼저 열어둔 터미널에서 두 서버로부터 응답이 오는지 확인한다. 응답이 잘 온다면 모든 과정이 끝났다!

 

 

Deploy Shell Script

위에서 수동으로 진행한 롤링 무중단 배포 과정을 bash shell script로 작성했다. 수동으로 할 필요 없이 배포해야 할 때 아래와 같이 터미널에 입력하면 된다.

$ bash {쉘 스크립트 위치}/{쉘 스크립트 파일 이름}.sh
$ bash ./deploy.sh
#!/bin/bash

# cut connection to a server from the load-balancer(nginx)
# args: server name, server port, docker container name for nginx
cut_reload_nginx() {
	src="$1:$2"

	dest=$src
	dest+=" down;"

	src+=";"

	sed -i "s/$src/$dest/g" ./nginx/nginx.conf
	docker compose exec $3 nginx -s reload 
	sleep 10s
}

# connect a server to the load-balancer(nginx)
# args: server name, server port, docker container name for nginx
connect_reload_nginx() {
	dest="$1:$2"

	src=$dest
	src+=" down;"

	dest+=";"

	sed -i "s/$src/$dest/g" ./nginx/nginx.conf
	docker compose exec $3 nginx -s reload 
	sleep 10s
}

# deploy a server
# args: container name, env file path
deploy_server() {
	# 1. stop container 
	# 2. remove container
	# 3. up and build container

	echo ">> [deploy_server] Stop & Remove $1..."
	
	docker compose stop $1
	sleep 5s
	
	docker compose rm $1
	sleep 10s

	echo ">> [deploy_server] Up $1..."

	sudo docker compose --env-file $2 up $1 --build -d 
}

# health check for a container(server)
# args: url for health check(pass a url which is located at after localhost/)
health_check() {
	UP_CHECK=""
	URL="http://localhost/$1"

	echo ">> [health_check] URL=$URL"
	echo ">> [health_check] start..."

	while [ -z "$UP_CHECK" ]
	do 
		UP_CHECK=$(curl -s $URL) 
	done 

	echo ">> [health_check] finish..."
}

echo ""
echo ""
echo "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"
echo ""
echo "Rolling Deployment Start"
echo "Author: Jaamong"
echo "Last Updated Date: 2024-0X-XX"
echo ""
echo "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"
echo ""
echo ""


# ----- for project_server2 container -----
echo "> Nginx: Cut connection to project_server2..."
cut_reload_nginx "project_server2" "8000" "project-nginx"
echo ""

echo "> Deploy only project_server2..."
deploy_server "project_server2" "./project_server/config/.env"
echo ""

echo "> Health Check for project_server2..."
health_check "api/v1/health-check"
echo ""

echo "> Nginx: Connect to project_server2..."
connect_reload_nginx "project_server2" "8000" "project-nginx"
echo ""


sleep 10s


# ----- for project_server1 container -----
echo "> Nginx: Cut connection to project_server1..."
cut_reload_nginx "project_server1" "8000"
echo ""

echo "> Deploy only project_server1..."
deploy_server "project_server1" "./project_server/config/.env"
echo ""

echo "> Health Check for project_server1..."
health_check "api/v1/health-check"
echo ""

echo "> Nginx: Connect to project_server1..."
connect_reload_nginx "project_server1" "8000"
echo ""


sleep 10s


echo ""
echo "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"
echo ""
echo "Rolling Deploy Finish"
echo ""
echo "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"
echo ""
echo ""

 

주의 사용 시 인자(argument)의 순서를 지켜서 입력해야 한다. 본인의 nginx.conf, docker-compose.yml과 맞지 않는 내용이 있을 수도 있으므로 반드시 스크립트 코드를 확인할 것.

  • cut_reload_nginx args
    • server name: nginx와 연결을 끊을 서버 이름(컨테이너 명)
    • server port: nginx와 연결을 끊을 서버 port
    • nginx container name: docker-compose.yml에서 정의된 Nginx용 도커 컨테이너 이름
  • connect_reload_nginx args
    • server name: nginx와 연결할 서버 이름(컨테이너 명)
    • server port: nginx와 연결할 서버 port
    • nginx container name: docker-compose.yml에서 정의된 Nginx용 도커 컨테이너 이름
  • deploy_server args
    • container name: 배포할 컨테이너의 이름
    • env file path: `.env` 파일 위치
  • health_check args
    • health check URL: 헬스체크용 URL

 

 

 

 

 

 

Comments