💡 CICD 과정LINK의 개요에 따라 작성되었습니다.
순서를 따르면 순조롭게 진행할 수 있습니다.

blue-green 방식으로 무중단 배포하기

기존 배포 방식은 작동하던 Docker Container를 내리고 새로운 이미지를 받아와 새로운 Docker Container를 띄우는 방식이었다. 문제 없이 동작하는 듯 하지만 사용자가 서비스를 이용하다가 배포가 일어나면 해당 서비스 전체가 다운된다.

따라서 blue-green방식을 이용해 서비스의 중단이 일어나지 않게 해보자.

blue-green이라는 이름만 들으면 굉장히 거창할 것 같지만 사실은 단순하다. 방식은 다음과 같다.

  1. 기존 컨테이너를 종료하지 않고 새로운 컨테이너를 띄운다.
  2. 새로운 컨테이너의 정상 작동이 확인되면 리버스 프록시에 새로운 컨테이너를 연결한다.
  3. 기존 컨테이너를 종료한다.

nginx의 reload는 굉장히 빠른 시간 내에 일어나기 때문에 사용자가 인지하기 어렵다. 따라서 서비스의 중단 없는 배포가 가능해진다.

이제 실제로 무중단 배포가 이루어 지기 위한 설정을 해보자.

server에 nginx 관련 설정하기

blue-green방식을 이용하기 위해 nginx를 Docker Container로 띄워 이용할 예정이다. 새로운 컨테이너를 띄우고 nginx의 설정파일을 reload하는 방식으로 진행된다.

따라서 nginx Docker Container상에 있는 설정 디렉토리에 접근해야한다.

우리는 nginx의 설정파일이 존재하는 디렉토리를 호스트로 끌어와 쉽게 수정하고 싶다. Docker Container를 실행할 때, nginx의 설정 파일이 존재하는 /etc/nginx디렉토리와 호스트의 임의의 디렉토리를 -v옵션으로 실행하면 될 것 같지만, 실제로 실행해보면 호스트의 디렉토리는 비어있게 되고, 컨테이너의 /etc/nginx도 호스트의 디렉토리를 따라 빈 디렉토리로 남기 때문에 nginx container는 /etc/nginx 디렉토리가 비었다는 로그와 함께 Exited (1)로 종료되게 된다.

Docker의 volume mapping은 두 가지 방법이 존재한다. volumebind mount인데 위의 방법이 bind mount이다. 공식 문서에 따르면 bind mount방법을 사용할 시 컨테이너의 디렉토리에 존재하는 모든것이 가려지게 된다.

따라서 우리는 volume을 사용하여야 한다. volume을 사용하면 호스트 디렉토리를 덮어쓰는 것이 아니라 동기화 한다. 옵션을 추가해 ~/nginx-config 디렉토리를 device로 설정한다.

먼저 volume을 만들고 -v옵션에 절대경로 대신 volume의 이름을 입력하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
$ cd ~
$ mkdir nginx-config

# volume 생성
$ docker volume create --driver local \
      --opt type=none \
      --opt device=~/nginx-config \
      --opt o=bind \
      nginx-config-volume

# volume 정보 확인
$ docker volume inspect nginx-config-volume

위의 명령어로 volume을 생성하고 정보를 확인하자.

위처럼 volume이 잘 생성된 것을 확인할 수 있다.

1
$ docker run -d --name nginx -p 80:80 -v nginx-config-volume:/etc/nginx nginx:latest

위의 명령어로 docker container를 띄운다.

~/nginx-config를 확인해보면 컨테이너 내부에 /etc/nginx 디렉터리가 동기화 되는 것을 확인할 수 있다.

마지막으로 우리가 관심 있는 파일은 conf.d/default.conf이다. 이 파일을 교체해 Nginx의 설정을 변경할 예정이다. 해당 파일의 권한은 rw-r--r--로 설정되어 있기 때문에 jenkins에서 교체를 시도하면 권한오류가 뜰 것이다. 이를 변경해주자.

1
$ sudo chmod 777 ~/nginx-config/conf.d/default.conf

위의 명령어를 입력하면 권한이 변경된다.

기존 컨테이너 종료

이제 blue-green 방식의 배포를 사용하기 위해 기존에 작동중이던 컨테이너의 작동을 중지시키자.

1
$ docker rm -f springboot

위의 명령어로 기존의 컨테이너를 종료시킨다.

nginx.conf 작성

이제 Nginx의 설정을 할 수 있는 nginx.conf파일을 작성해 보자. 프로젝트 루트 디렉토리에 nginx.conf 파일을 만들고 다음과 같이 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
upstream api {
    // server 172.17.0.1:${EXTERNAL_PORT};
}

access_log /var/log/nginx/access.log;

server {
    listen 80;

    location / {
        proxy_pass http://api;
        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
    }
}

Nginx 설정법은 다른 포스트에서 소개하도록 하고 간단하게 내용만 알아보자.

2번째 줄은 deploy.sh의 명령에 따라 server 172.17.0.2:${EXTERNAL_PORT}로 변경될 예정이다. 80포트를 어느 포트에 연결해 줄것인지 결정한다. blue가 켜질 경우 blue에 맞는 외부포트와 연결되고, green일 경우 green에 맞는 외부포트와 연결되고 reload를 통해 새로운 컨테이너를 nginx가 바라보게 한다.

나머지는 log관련 설정과 nginx를 통과하기 전 client 정보를 헤더에 넣어 전달해 주는 역할을 한다.

deploy.sh 작성

Jenkins에서 ssh에 접속해 deploy.sh파일과 nginx.conf파일을 넘기고, deploy.sh를 실행해 배포를 진행되게 할 예정이다.

이제 blue-green 배포를 수행하는 deploy.sh파일을 작성해보자. 프로젝트 루트 디렉토리에 deploy.sh파일을 생성하고 작성하자.

작성 전, 간단하게 deploy.sh의 내부 동작을 알아보자.

  1. nginx 컨테이너가 정상 작동하는지 확인하고 동작하고 있지 않다면 nginx 컨테이너를 실행시킨다.
  2. 기존에 떠 있던 springboot의 포트가 A포트로 작동되고 있는지, B포트로 작동되고 있는지 확인한다.
  3. 기존 포트의 반대 포트로 springboot container를 작동시킨다.
  4. 새로 띄운 springboot container의 health check를 진행한다.
  5. health check가 통과되었다면, 미리 작성한 nginx.conf파일의 포트 번호를 변경하고 교체한다.
  6. nginx를 reload시켜 변경된 포트로 연결되게 한다.
  7. 기존 포트로 작동되고 있는 springboot container를 종료한다.

위의 과정을 거치면 컨테이너를 교체하는 과정에서 공백이 없기 때문에 중단없는 배포가 가능해진다.

하나하나 구현이 필요한 순서대로 deploy.sh를 작성해보자.

변수 선언

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash  
NGINX_CONTAINER_CONF_DIR=/etc/nginx  
  
NGINX_CONF_VOLUME=nginx-config-volume  
HOST_NGINX_CONF_DIR=~/nginx-config  
  
NGINX_CONF_FILE_DIR=~  
  
DOCKER_CONTAINER_NAME_PREFIX=springboot  
  
# 받아올 변수  
# INTERNAL_PORT  
# EXTERNAL_PORT_GREEN  
# EXTERNAL_PORT_BLUE  
# DOCKER_IMAGE_NAME  
# OPERATION_ENV

가장 먼저 필요한 변수들을 정리해 둔다.

  • NGINX_CONTAINER_CONF_DIR : nginx 컨테이너 내부 설정파일의 위치이다. nginx가 정상동작하고 있지 않는 경우 docker 명령어로 nginx를 켤때 볼륨 매핑을 위해 사용한다.

  • NGINX_CONF_DIR : 호스트에서 만들어둔 volume의 이름이다. 마찬가지로 nginx가 정상동작 하고 있지 않을 때, docker 명령어로 volume 매핑을 위해 사용한다.

  • HOST_NGINX_CONF_DIR : 호스트에서 만든 볼륨의 실제 위치이며 jenkins에서 전송한 nginx설정파일이 이동할 위치이다.

  • NGINX_CONF_FILE_DIR : jenkins에서 전송할 nginx설정파일이 위치할 경로이다. 이 경로의 설정파일을 호스트의 볼륨으로 이동시킨다.

  • DOCKER_CONTAINER_NAME_PREFIX : 컨테이너 교체 시, 두개의 컨테이너를 동시에 켜놓고 새로운 컨테이너가 정상작동이 판단되면 교체를 진행한다. 이 때, 컨테이너의 이름으로 어떤 컨테이너를 nginx에 연결할지 판단한다. springboot-bluespringboot-green으로 두 컨테이너의 이름을 정할 예정이다. 따라서 앞에 붙을 prefix를 정해 변수화 시킨다.

  • INTERNAL_PORT : 컨테이너가 작동할 내부 포트이다. 일반적으로 springboot 프로젝트의 application.yml에 작동할 포트 번호를 명시한다. deploy.sh에서 다시 포트번호를 명시하면 포트번호를 관리할 곳이 두곳이나 생긴다. 따라서 Jenkins에서 빌드 전 application.yml에서 포트번호를 파싱해서 실제 서버에 전달할 예정이다. Jenkins에서 조금의 추가작업이 필요하고 deploy.sh를 작성한 후 해당 파싱 작업을 Jenkins 파이프라인에 작성해보자.

  • EXTERNAL_PORT_GREEN, EXTERNAL_PORT_BLUE : 컨테이너가 작동할 외부 포트이다. 예를 들어 8080과 8081이라면 nginx.conf파일을 새로 띄울 컨테이너의 외부 포트에 맞게 변경해주고 reload해야한다. 포트번호는 보안과 관계있기 때문에 Jenkins에서 credential로 관리하고 서버에 변수로 주입해주자.

  • DOCKER_IMAGE_NAME : 도커 허브에서 내려받을 이미지의 이름이다. 해당 이름은 Jenkinsfile에서 push를 위해 필요하고 또한 deploy.sh파일에서도 pull을 위해 필요하다. 두 곳에서 산재하면 관리가 어려우므로 Jenkins에서 변수를 넘겨 작업하는 것이 깔끔할 것이다.

  • OPERATION_ENV: 어떤 branch에서 시작된 배포 과정인지 전달한다. proddev가 있으며 docker container를 실행할 때 어떤 프로파일로 작동할지 SPRING_PROFILES_ACTIVE에 값을 넣어준다.

Nginx 작동 확인 및 실행

1
2
3
4
5
6
7
8
9
10
11
12
13
# nginx 컨테이너가 정상 작동하는지 확인  
IS_NGINX_RUNNING=$(docker inspect -f '{{.State.Status}}' nginx | grep running)  
if [ -z "$IS_NGINX_RUNNING" ]; then  
  # 정상 작동하지 않을 시 nginx를 완전히 내린 후 다시 구동  
  echo "nginx container is not running. run nginx container"  
  docker rm -f nginx  
  docker run -d --name nginx \  
          -v ${NGINX_CONF_VOLUME}:${NGINX_CONTAINER_CONF_DIR} \  
          -p 80:80 nginx:latest  
  sleep 3  
else  
  echo "nginx is already running"  
fi

nginx가 정상적으로 작동하는지 확인하고 작동하지 않을 시 nginx를 완전히 내린 후 다시 구동한다. 이 과정에서 변수로 선언 해 놓았던 volume의 이름과 container 내부의 경로가 필요하다.

실행 명령어를 내리고 3초 sleep을 주어 구동시간을 확보해준다.

반대 컨테이너 구동

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
IS_BLUE_RUNNING=$(docker inspect -f '{{.State.Status}}' ${DOCKER_CONTAINER_NAME_PREFIX}-blue | grep running)  
  
if [ -n "$IS_BLUE_RUNNING" ]; then  
  echo "green up"  
  
  docker rm -f ${DOCKER_CONTAINER_NAME_PREFIX}-green  
  docker run -d --name ${DOCKER_CONTAINER_NAME_PREFIX}-green \  
          -p ${EXTERNAL_PORT_GREEN}:${INTERNAL_PORT} \  
          -e "SPRING_PROFILES_ACTIVE=${OPERATION_ENV}" \  
          ${DOCKER_IMAGE_NAME}:latest  
  
  BEFORE_COLOR=blue  
  AFTER_COLOR=green  
  
  EXTERNAL_PORT=${EXTERNAL_PORT_GREEN}  
else  
  echo "blue up"  
  
  docker rm -f ${DOCKER_CONTAINER_NAME_PREFIX}-blue  
  docker run -d --name ${DOCKER_CONTAINER_NAME_PREFIX}-blue \  
          -p ${EXTERNAL_PORT_BLUE}:${INTERNAL_PORT} \  
          -e "SPRING_PROFILES_ACTIVE=${OPERATION_ENV}" \  
          ${DOCKER_IMAGE_NAME}:latest  
  
  BEFORE_COLOR=green  
  AFTER_COLOR=blue  
  
  EXTERNAL_PORT=${EXTERNAL_PORT_BLUE}  
fi  
  
sleep 3

springboot-blue가 구동중인지 확인하고, 정상적으로 구동이 판단되면 springboot-green을 구동시킨다.

springboot-blue가 구동중이 아니라면 springboot-green이 작동중이라고 판단하고 springboot-blue를 구동시킨다.

그리고 그 결과를 BEFORE_COLOR, AFTER_COLOR, EXTERNAL_PORT에 담는다. 여기서 EXTERNAL_PORT에 값을 담으면 nginx를 reload할 때 nginx.conf에서 EXTERNAL_PORT의 값을 참조해 포트가 변경되게 된다.

새로운 컨테이너 Health Check

새로 띄워진 컨테이너가 정상 작동하는지 판단하고, 정상 작동한다면 default.conf를 변경 후 nginx가 새로운 컨테이너를 바라보도록 reload한다.

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
echo "Health Check Start!"  
for i in {1..10}  
do  
  sleep 3  
  RESPONSE=$(curl -I http://localhost:${EXTERNAL_PORT} | grep HTTP)  
  
  if [ -n "${RESPONSE}" ]; then  
    echo "> Health check 성공"  
    sed -i "2s/.*/server 172.17.0.1:${EXTERNAL_PORT};/g" ${NGINX_CONF_FILE_DIR}/nginx.conf
    yes | cp -rf ${NGINX_CONF_FILE_DIR}/nginx.conf ${HOST_NGINX_CONF_DIR}/conf.d/default.conf  
  
    docker exec nginx nginx -s reload  
  
    sleep 3  
  
    docker rm -f ${DOCKER_CONTAINER_NAME_PREFIX}-${BEFORE_COLOR}  
    echo ${BEFORE_COLOR} down  
  
    echo "배포를 성공적으로 종료합니다"  
    exit 0  
  fi  
done  
  
echo "Health Check 실패"  
  
docker rm -f ${DOCKER_CONTAINER_NAME_PREFIX}-${AFTER_COLOR}  
echo "이전 컨테이너가 유지됩니다."  
exit 1

새로운 컨테이너의 EXTERNAL_PORThttp://localhost에 3초마다 요청을 보내 요청 결과에 HTTP라는 단어가 있는지 판단한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 정상 작동 시
$ curl -I http://localhost:[PORT]/
HTTP/1.1 404
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 12 Feb 2023 08:39:45 GMT

# 비정상 작동 시
$ curl -I http://localhost:[PORT]/
curl: (7) Failed to connect to localhost port [PORT]: Connection refused

위 처럼 정상적으로 컨테이너가 동작하고 있다면 HTTP라는 단어가 결과에 포함되어 있음을 할 수 있다. 따라서 HTTP라는 문자열을 grep해 사용한다.

sed라는 리눅스 기본 유틸리티를 이용해 nginx.conf의 두번째 줄을 server 172.17.9.1:${EXTERNAL_POORT};로 교체한다. 이렇게 하고 nginx를 reload하면 새로운 컨테이너를 바라보게 된다.

sed라는 유틸리티를 사용한 이유는 nginx설정파일에서 공식적으로 환경변수를 지원하지 않고, envsubst라는 유틸리티를 통해 우회해야하기 때문에 기본 유틸리티인 sed를 사용하는 방법을 선택했다.

이제 nginx.conf volume으로 복사하고 Nginx를 reload한다. reload 후에는 기존에 동작하던 컨테이너를 내리고 배포를 종료한다.

최종 deploy.sh

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#!/bin/bash  
NGINX_CONTAINER_CONF_DIR=/etc/nginx  
  
NGINX_CONF_VOLUME=nginx-config-volume  
HOST_NGINX_CONF_DIR=~/nginx-config  
  
NGINX_CONF_FILE_DIR=~  
  
DOCKER_CONTAINER_NAME_PREFIX=springboot  
  
# 받아올 변수  
# INTERNAL_PORT  
# EXTERNAL_PORT_GREEN  
# EXTERNAL_PORT_BLUE  
# DOCKER_IMAGE_NAME  
# OPERATION_ENV  
  
# nginx 컨테이너가 정상 작동하는지 확인  
IS_NGINX_RUNNING=$(docker inspect -f '{{.State.Status}}' nginx | grep running)  
if [ -z "$IS_NGINX_RUNNING" ]; then  
  # 정상 작동하지 않을 시 nginx를 완전히 내린 후 다시 구동  
  echo "nginx container is not running. run nginx container"  
  docker rm -f nginx  
  docker run -d --name nginx \  
          -v ${NGINX_CONF_VOLUME}:${NGINX_CONTAINER_CONF_DIR} \  
          -p 80:80 nginx:latest  
  sleep 3  
else  
  echo "nginx is already running"  
fi  
  
  
  
IS_BLUE_RUNNING=$(docker inspect -f '{{.State.Status}}' ${DOCKER_CONTAINER_NAME_PREFIX}-blue | grep running)  
  
if [ -n "$IS_BLUE_RUNNING" ]; then  
  echo "green up"  
  
  docker rm -f ${DOCKER_CONTAINER_NAME_PREFIX}-green  
  docker run -d --name ${DOCKER_CONTAINER_NAME_PREFIX}-green \  
          -p ${EXTERNAL_PORT_GREEN}:${INTERNAL_PORT} \  
          -e "SPRING_PROFILES_ACTIVE=${OPERATION_ENV}" \  
          ${DOCKER_IMAGE_NAME}:latest  
  
  BEFORE_COLOR=blue  
  AFTER_COLOR=green  
  
  EXTERNAL_PORT=${EXTERNAL_PORT_GREEN}  
else  
  echo "blue up"  
  
  docker rm -f ${DOCKER_CONTAINER_NAME_PREFIX}-blue  
  docker run -d --name ${DOCKER_CONTAINER_NAME_PREFIX}-blue \  
          -p ${EXTERNAL_PORT_BLUE}:${INTERNAL_PORT} \  
          -e "SPRING_PROFILES_ACTIVE=${OPERATION_ENV}" \  
          ${DOCKER_IMAGE_NAME}:latest  
  
  BEFORE_COLOR=green  
  AFTER_COLOR=blue  
  
  EXTERNAL_PORT=${EXTERNAL_PORT_BLUE}  
fi  
  
sleep 3  
  
echo "Health Check Start!"  
for i in {1..10}  
do  
  sleep 3  
  RESPONSE=$(curl -I http://localhost:${EXTERNAL_PORT} | grep HTTP)  
  
  if [ -n "${RESPONSE}" ]; then  
    echo "> Health check 성공"  
    sed -i "2s/.*/server 172.17.0.1:${EXTERNAL_PORT};/g" ${NGINX_CONF_FILE_DIR}/nginx.conf
    yes | cp -rf ${NGINX_CONF_FILE_DIR}/nginx.conf ${HOST_NGINX_CONF_DIR}/conf.d/default.conf  
  
    docker exec nginx nginx -s reload  
  
    sleep 3  
  
    docker rm -f ${DOCKER_CONTAINER_NAME_PREFIX}-${BEFORE_COLOR}  
    echo ${BEFORE_COLOR} down  
  
    echo "배포를 성공적으로 종료합니다"  
    exit 0  
  fi  
done  
  
echo "Health Check 실패"  
  
docker rm -f ${DOCKER_CONTAINER_NAME_PREFIX}-${AFTER_COLOR}  
echo "이전 컨테이너가 유지됩니다."  
exit 1

위의 deploy.sh파일을 프로젝트 최상단에 위치시킨다.

Jenkinsfile 작성

기존에 Deploy to Server Stage에서는 직접 ssh로 접속해 docker image를 pull받고 기존에 떠있는 docker container을 내린 후 다시 이미지를 띄웠다.

이 과정을 이제 다음과 같이 변경해 주어야 한다.

  1. nginx.conf, deploy.sh를 전송한다.
  2. 필요한 환경변수를 설정해준다.
  3. deploy.sh를 실행시킨다.

가장 먼저 전달해야하는 변수는 다음과 같다.

  • INTERNAL_PORT
  • EXTERNAL_PORT_GREEN
  • EXTERNAL_PORT_BLUE
  • DOCKER_IMAGE_NAME
  • OPERATION_ENV

INTERNAL_PORT는 이전에 application.yml에서 yq를 통해 변수화 시켜놓아서 그냥 전달하면 된다. 또한 OPERATION_ENV도 담아두어서 그냥 전달하기만 하면 된다. EXTERNAL_PORT_GREENEXTERNAL_PORT_BLUE는 도커가 작동하는 외부포트이다. 이 변수는 소스코드내에서 관리되는 것이 아니라서 Jenkins에 Credential에 등록하고 전달한다. 여기서 DOCKER_IMAGE_NAME도커허브아이디/도커이미지이름의 형태이다. 따라서 해당 형태로 만들어 전달한다.

가장 먼저 EXTERNAL_PORT_GREENEXTERNAL_PORT_BLUE를 Credential에 등록해 보자. cicd-test라는 item에 국한된 변수이므로 cicd-test 도메인에 Credential을 생성한다.

위와 같이 credential을 생성해준다. GREEN이 8080이라면 BLUE가 8081인 식으로 서로 다르게 등록하자.

가장 먼저 EXTERNAL_PORT_BLUEEXTERNAL_PORT_GREEN을 Credentials에서 꺼내와 변수에 넣어놓자.

environment에서 포트 관련한 정보를 변수에 저장한다.

1
2
3
4
5
6
7
environment {  
    GPG_SECRET_KEY = credentials('GPG_SECRET_KEY')  
  
    // PORT  
    EXTERNAL_PORT_BLUE = credentials('EXTERNAL_PORT_BLUE')  
    EXTERNAL_PORT_GREEN = credentials('EXTERNAL_PORT_GREEN')  
}

위와 같이 선언하면 다른 스테이지에서도 "${EXTERNAL_PORT_BLUE}"와 같이 포트 정보에 접근이 가능하다.

INTERNAL_PORT는 이미 Parse Internal Port Stage에서 yq를 이용해 내부 포트를 변수에 넣어주었다.

1
2
3
4
5
6
7
stage('Parse Interal Port') {  
    steps {  
        script {  
            INTERNAL_PORT = sh(script: "yq e '.server.port' ./src/main/resources/application-${OPERATION_ENV}.yml", returnStdout: true).trim();  
        }  
    }  
}

returnStdout은 sh메서드가 yq의 작동 결과를 리턴하게 한다. returnStdoutfalse라면 성공 시 0, 실패 시 1을 리턴하여 포트정보는 소실되게 된다.

이제 환경변수가 될 정보를 변수에 담았으니 실제로 배포 관련 파일들을 옮기고 실행해주기만 하면 된다. Deploy to Server 스테이지를 다음과 같이 변경한다.

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
stage('Deploy to Server') {  
    steps {  
        echo 'Deploy to Server'  
        withCredentials([  
            usernamePassword(credentialsId: DOCKER_HUB_CREDENTIAL_ID,  
                                usernameVariable: 'DOCKER_HUB_ID',  
                                passwordVariable: 'DOCKER_HUB_PW'),  
            sshUserPrivateKey(credentialsId: SSH_CREDENTIAL_ID,  
                                keyFileVariable: 'KEY_FILE',  
                                passphraseVariable: 'PW',  
                                usernameVariable: 'USERNAME'),  
            string(credentialsId: SSH_HOST_CREDENTIAL_ID, variable: 'HOST'),  
            string(credentialsId: SSH_PORT_CREDENTIAL_ID, variable: 'PORT')]) {  

            script {  
                def remote = [:]  
                remote.name = OPERATION_ENV  
                remote.host = HOST  
                remote.user = USERNAME  
                remote.password = PW  
                // remote.identity = KEY_FILE  
                remote.port = PORT as Integer  
                remote.allowAnyHosts = true  
                  
                sshCommand remote: remote, command:  
                    'docker pull ' + DOCKER_HUB_ID + '/' + DOCKER_IMAGE_NAME + ":latest"  
                  
                sshPut remote: remote, from: './deploy.sh', into: '.'  
                sshPut remote: remote, from: './nginx.conf', into: '.'  
                  
                sshCommand remote: remote, command:  
                    ('export OPERATION_ENV=' + OPERATION_ENV + ' && '  
                    + 'export INTERNAL_PORT=' + INTERNAL_PORT + ' && '  
                    + 'export EXTERNAL_PORT_GREEN=' + EXTERNAL_PORT_GREEN + ' && '  
                    + 'export EXTERNAL_PORT_BLUE=' + EXTERNAL_PORT_BLUE + ' && '  
                    + 'export DOCKER_IMAGE_NAME=' + DOCKER_HUB_ID + '/'
                    + DOCKER_IMAGE_NAME + ' && '  
                    + 'chmod +x deploy.sh && '  
                    + './deploy.sh')
            }  
        }  
    }  
}  

기존 Docker Container를 내리고 새로운 Container를 띄우는 과정을 삭제하고 다음과 같은 내용을 추가했다.

가장 먼저 docker 이미지를 pull해준다. 그리고 다음 명령으로 deploy.shnginx.conf를 전송한다. 그리고 환경변수를 전달해 export하게 되면 해당 환경변수에 이전에 담아놨던 값들이 담기게 된다. 마지막으로 deploy.sh를 실행 가능한 권한으로 변경하고 deploy.sh를 실행한다.

이제 blue-green 무중단 배포를 시행해보자. 변경 내용을 커밋하고 push하자.

1
2
3
$ git add .
$ git commit -m "chore: Blue Green 무중단 배포 적용"
$ git push

Jenkins를 확인해보자.

배포가 정상적으로 진행되었다. /hello URL에 접속해보면 정상작동을 확인할 수 있다.

빌드 번호를 누르고 Replay버튼을 눌러 다시 빌드해본다. 빌드가 진행되는 와중에 /hello URL에서 새로고침을 계속 눌러봐도 서비스가 끊기는 구간이 없다. 정상적으로 무중단배포가 진행된 것이다.

또한 실제로 서버에 들어가서 컨테이너를 확인해 보면

Replay 전과 후로 blue에서 green으로 컨테이너가 변경된 것을 확인할 수 있다.

이로써 성공적으로 blue-green방식의 무중단 배포를 마쳤다. 다음 포스트에서는 Jenkinsfile에 간단한 메서드 작성으로 빌드 stage마다의 성공 또는 실패 결과를 실시간으로 받아보자.

댓글남기기