본문 바로가기

DevOps/CI&CD

Jenkins를 활용한 CI/CD 구축(3/4) - Docker를 활용한 무중단 배포

728x90

1. 무중단 배포가 필요한 이유 

이전 포스팅에서 치명적인 결점이 무엇이라 생각하시나요? 배포할 때 프론트 엔드는 실시간으로 파일이 변경되는 반면, 백엔드는 새로운 Jar 파일이 실행되기 전까지 기존 Jar 파일을 종료시키기 때문에 서비스가 일시적으로 중단이 됩니다. 만약 배포하는 동안(downtime) 사용자가 해당 서비스에 접근한다면 정상적으로 작동이 안 되므로 사용자 경험에 부정적인 영향을 끼칩니다. 그러므로 무중단 배포를 통해 서비스를 지속적으로 제공함으로써 사용자 경험을 향상시키고, 서비스의 신뢰성을 높일 수 있게 하는 것이 중요합니다. 

 

2. 전략

무중단 배포 방식은 여러가지 방식이 있지만, 그중 Rolling 방식과 Blue/Green 방식을 간략히 소개하겠습니다.

1) Rolling 방식

출처: https://blog.container-solutions.com/kubernetes-deployment-strategies

Rolling 방식은 점진적으로 배포하는 방식으로, 배포 대상 서버를 하나씩 교체하면서 서비스의 중단 없이 새로운 버전을 적용하는 방법입니다.

  • 장점: 배포 과정이 단계적으로 이루어지기 때문에 오류가 발생하더라도 전체 시스템이 중단되지 않고 일부 서버에서만 문제가 발생할 가능성이 높습니다. 또한, 이전 버전과 새로운 버전을 동시에 운영할 필요가 없기 때문에 필요한 자원이 적습니다.
  • 단점: 전체 배포 과정이 길어질 수 있으며, 이전 버전과 새로운 버전이 동시에 운영되지 않기 때문에 일부 사용자는 새로운 버전을 사용하지 못할 수 있습니다.

 

2) Blue/Green 방식

출처:https://blog.container-solutions.com/kubernetes-deployment-strategies

Blue/Green 방식은 두 개의 환경을 준비하여 배포하는 방식으로, 배포 전 환경(BLUE)과 배포 후 환경(GREEN)을 구분하여 운영하는 방법입니다.

 

  • 장점: 배포 전 환경과 배포 후 환경이 분리되어 있기 때문에 새로운 버전의 서비스를 배포하기 전에 전체 시스템이 정상적으로 동작하는지 확인할 수 있습니다. 또한, 배포 전 환경과 배포 후 환경을 동시에 운영할 수 있기 때문에 모든 사용자가 새로운 버전을 사용할 수 있습니다.
  • 단점: 배포 전 환경과 배포 후 환경을 모두 준비해야 하기 때문에 자원이 더 필요합니다. 

Rolling 방식은 정합성 이슈가 발생할 위험이 있기에, 저는 Blue/Green 방식을 선택하여 안정성을 중시하겠습니다. 

 

무중단 배포 전략에 더 자세히 알고 싶은 분들은 이 블로그 글을 추천드립니다. 그림까지 자세히 나와있어서 이해하기 쉽습니다. 

 

3. Blue/Green 전략

출처:https://www.maptiler.com/news/2016/08/blue-green-deployment-with-docker-and/

Blue/ Green 배포 흐름은 아래와 같습니다.

 

  1. 새로운 버전이 GIT에 병합 시 JENKINS는 최신 버전의 JAR 파일을 빌드한다. 
  2. JAR 파일을 도커 이미지로 생성한다. 블루(9000번 포트)와 그린 (9001번 포트)의  docker compose를 작성한다. 
  3. Blue Conatiner에 Health check를 한다. 만약 Blue Container가 실행됐다면 신버전을 Green Container에 배포하면 되고, 실행되지 않았다면 Blue Container를 배포한다.
  4. 생성한 컨테이너 안에 application이 정상적으로 구동이 됐는지 확인하기 위해 10번의 핑을 보내 확인한다.
  5. 만약 정상적으로 구동이 됐으면 Nginx의 리버스 프록시 방향을 service-url.inc의 변경을 통해 새로운 container(blue or green)를 바인딩해준다.
  6. 전에 구동된 container는 삭제한다.

 

위의 흐름들을 간략히 설명하면 블루 컨테이너의 상태를 확인한 다음. 블루가 실행되어 있으면 그린 컨테이너로 스위칭을 해주고, 기존의 블루 컨테이너를 삭제한다는 의미입니다. 여러 개의 컨테이너들을 관리하기 위해 docker compose를 활용했습니다. 

 

3-1. 포트 번호 변경 

블루 혹은 그린 컨테이너로 스위칭을 해주기 위해서는 Nginx의 reverse proxy를 활용하여 실시간으로 포트 번호를 변경할 수 있게 해야 합니다. 이를 위해 service-url.inc을 만들어 줄겁니다.

 

etc/nginx/conf.d 경로에 service-url.inc을 만들고 service url을 환경변수로 등록하겠습니다.

 

cd /etc/nginx/conf.d 명령어로 해당 위치에 접근. 

sudo vim service-url.inc 명령어로 service-url.inc 파일을 생성해줍니다. 

vim으로 위처럼 내용을 입력해줍니다. 여기서 주소는 http://(배포 아이피 주소):(특정 포트)의 형태로 각자의 환경에 맞게 설정해주면 됩니다. 

 

위의 환경 변수는 나중에 deploy시 셸 스크립트에서 사용됩니다. 블루에서 그린으로 혹은 그린에서 블루로 스위칭할 때, 동적으로 주소를 변경해주는 역할을 합니다. 

 

이제 sudo vim /etc/nginx/sites-available/default 명령어로 Nginx의 proxy 설정을 위처럼 편집해 줍니다.

 

  • include /etc/nginx/conf.d/service-url.inc는 service-url.inc 파일을 불러온다는 의미입니다. 
  • proxy_pass 뒤쪽에는 $service_url로 설정하여. 나중에 주소를 동적으로 바꾸는 역할을 합니다.

 

설정을 마치면 sudo systemctl restart nginx 명령어로 nginx 재시작합니다. 

 

 

저는 green을 9001번 포트로 설정하기 위해 인바운트 규칙에 9001번 포트를 열 생각입니다.

 

aws ec2 사이트에 들어가서 보안 그룹 클릭.

 

인바운드 규칙을 편집해줍니다.

 

9001번 포트를 추가해 줍시다. 각자의 환경에 맞게끔 원하는 포트를 열어주면 됩니다. 

 

 

3-2. 도커 이미지 생성

도커 컨테이너를 통해 손쉽게 포트 변경을 변경하기 위해 먼저 도커 이미지를 생성하겠습니다. 

 

도커 이미지를 생성하는 도커 파일은 아래와 같습니다. 

 

FROM openjdk:11-jdk

ARG JAR_FILE=hospital-0.0.1-SNAPSHOT.jar

COPY ${JAR_FILE} app.jar

ENTRYPOINT ["java","-jar","app.jar"]
  • FROM openjdk:11-jdk: jar 파일을 java 11 환경에서 실행하기 위해서 11 jdk로 설정했습니다. 만약 11버전이나 17 버전 등이라면 각자의 환경에 맞게끔 변경해 주면 됩니다. 
  • ARG JAR_FILE=hospital-0.0.1-SNAPSHOT.jar: ARG 지시어는 Dockerfile 빌드 시에만 사용 가능한 환경 변수를 정의할 때 사용합니다. JAR_FILE은 환경 변수로, 값으로는 hospital-0.0.1-SNAPSHOT.jar가 할당되어 있습니다.
  • COPY ${JAR_FILE} app.jar: JAR_FILE 환경 변수에 할당된 값을 가진 파일을 app.jar로 복사하는 역할을 합니다.
  • ENTRYPOINT ["java","-jar","app.jar"]: java 명령어를 실행하고, -jar 옵션을 통해 app.jar 파일을 실행. java -jar app.jar 명령어와 같습니다.

도커 파일을 통해 도커 이미지를 생성하겠습니다. 이때 반드시 Dockerfile과 hospital-0.0.1-SNAPSOHT.jar 파일은 같은 위치에 있어야 합니다. 

 

Docker build --tag mac20010/hospital -f Dockerfile .; 명령어로 이미지를 생성합니다. 이때 mac20010은 도커 hub의 자신의 아이디이고, hospital은 이미지 이름. Dockerfile . 은 현재 위치를 뜻합니다. 각자의 상황에 맞게끔 태그 이름을 설정해주면 되겠습니다. 

ec2 환경에서 docker login을 통해 도커 허브에 접근합니다. 아이디와 패스워드를 입력하여 로그인을 합니다.

 

이제 생성한 도커 이미지를 도커 허브에 올리도록 하겠습니다. 

 

만약 이미지 이름이 형식에 안 맞게끔 저장이 됐다면, docker tag image_name:tag new image:tag 명령어를 수행하여 이미지 이름을 바꿀 수 있습니다.

 

이제 도커 허브 사이트에 접속해서 로그인 하여 repository를 생성해줍니다. 

 

생성하면 위처럼 Docker command를 볼 수 있습니다. 

 

 

docker push mac20010/hospital:버전 명령어로 이미지를 push하면 

 

성공적으로 push가 완료된 걸 확인할 수 있습니다. 

 

 

이제 이미지로 컨테이너가 정상적으로 생성됐는지 확인하기 위해 실험을 해봤더니 치명적인 문제가 있었습니다...

하나의 이미지에 PORTS가 (0.0.0.0:9001->9000/tcp, :::9001->9000/tcp) 이런 식으로 2개나 있던 것입니다... 동일한 포트에 ipv4 ipv6가 동시에 매핑이 된 겁니다. 

 

이게 문제가 되는 이유가 만약 사용자가 리뷰를 작성했더니, 한 번에 리뷰가 동시에 같은 내용으로 2개나 작성이 됩니다. 그만큼 치명적인 문제여서 검색을 해봤으나, 리소스가 극히 드물어서 문제의 원인이 뭔지도 모르던 찰나에...

 

Ubuntu 환경에서 도커 20.10 이상 버전에 오류가 발생했다는 걸 인지했습니다.(https://github.com/moby/moby/issues/42442)

 

그래서 기존의 도커를 삭제하고

 

sudo apt-get install docker-ce=[Version] 으로 특정 20.9버전으로 다운그라운드하였더니.

 

정상적으로 1개의 port만 할당된 걸 확인할 수 있었습니다.

 

 

도커가 여러 최신 버전이 나왔지만, ubuntu 환경에서 아직도 이 버그는 수정이 안 됐습니다. 누가 제발 오류 좀 고쳐주세요... 한국 사이트에는 관련 정보가 전혀 없어서 저와 같은 문제에 직면한 사람에게 도움이 됐으면 좋겠네요 ㅎㅎ  

 

3-3. 도커 컨테이너 생성

이제 여러 개의 컨테이너를 관리하기 위해 docker-compose를 작성해야 합니다.

 

저는 기본 위치에 backend 폴더 안에 docker-compose.green과 docker-compose.blue를 각각 작성했습니다.

 

먼저 블루와 그린 컨테이너에 사용할 docker network를 생성하겠습니다.

docker network create service-network

 

vim docker-compose.blue.yaml 명령어로 blue compose를 작성해봅시다.

 

version: '3.1'

services:
  api:
    image: mac20010/hospital:latest
    container_name: hospital-blue
    environment:
      - LANG=ko_KR.UTF-8
      - HTTP_PORT=9000
    ports:
      - '9000:9000'

networks:
  default:
    name: service-network
    external: true
  • version '3.1': Compose 파일의 버전을 지정합니다.
  • services: Compose에서 정의할 서비스들의 정보를 포함합니다.
  • api: 서비스의 이름을 정의합니다.
  • image: 사용할 Docker 이미지의 이름과 태그를 지정합니다.
  • container_name: 컨테이너의 이름을 지정합니다.
  • environment: 컨테이너에서 사용할 환경 변수를 지정합니다.
  • LANG: 사용할 언어셋을 지정합니다.
  • HTTP_PORT: 사용할 HTTP 포트 번호를 지정합니다.
  • ports: 컨테이너의 포트와 호스트의 포트를 연결합니다.
  • 9000:9000: 컨테이너의 9000번 포트와 호스트의 9000번 포트를 연결합니다.
  • networks: 사용할 네트워크를 지정합니다.
  • default: 기본 네트워크 이름을 지정합니다.
  • name: 네트워크 이름을 지정합니다.
  • external: 기존에 존재하는 네트워크를 사용할지 여부를 지정합니다. true로 설정하면 기존 네트워크를 사용하며, 이름은 service-network입니다.

이 Compose 파일을 사용하면 mac20010/hospital:latest 이미지를 사용하는 hospital-blue 컨테이너를 실행하고, 컨테이너의 9000번 포트를 호스트의 9000번 포트와 연결하여 사용할 수 있습니다. 

 

이제 green compose를 생성하겠습니다.

version: '3.1'

services:
  api:
    image: mac20010/hospital:latest
    container_name: hospital-green
    environment:
      - LANG=ko_KR.UTF-8
      - HTTP_PORT=9001
    ports:
      - '9001:9000'

networks:
  default:
    name: service-network
    external: true

설명은 블루와 동일하기에 생략하겠습니다. mac20010/hospital:latest 이미지를 사용하는 hospital-green 컨테이너를 실행하고, 컨테이너의 9000번 포트를 호스트의 9001번 포트와 연결하여 사용할 수 있습니다. 

 

이미지 이름과 컨테이너 이름, 포트 번호는 각자의 환경에 맞게끔 변경해 주시면 됩니다. 

 

이제 위의  docker-compose를 통해 쉘 스크립트를 사용하여 blue 컨테이너와 green 컨테이너를 스위칭하게끔 만들겠습니다. 

 

3-4. 배포 스크립트 생성

먼저 컨테이너가 정상적으로 작동하는지 확인하기 위해서 Java의 spring 파일을 아래와 같이 만들었습니다. 

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class MemberController {

    @GetMapping("/check")
    public String checkServerStatus(){
        return "check";
    }
}

http:주소:특정포트/api/check URL에 접근하여 check가 나오면 정상적으로 작동된다는 신호입니다. 

 

 

 

이제 Jenkins를 통해 실행하는 쉘 스크립트를 작성하겠습니다. 

#1
EXIST_BLUE=$(docker-compose -p hospital-blue -f docker-compose.blue.yaml ps | grep Up)

if [ -z "$EXIST_BLUE" ]; then
    docker-compose -p hospital-blue -f /home/ubuntu/backend/docker-compose.blue.yaml up -d
    BEFORE_COLOR="green"
    AFTER_COLOR="blue"
    BEFORE_PORT=9001
    AFTER_PORT=9000
else
    docker-compose -p hospital-green -f /home/ubuntu/backend/docker-compose.green.yaml up -d
    BEFORE_COLOR="blue"
    AFTER_COLOR="green"
    BEFORE_PORT=9000
    AFTER_PORT=9001
fi

echo "${AFTER_COLOR} server up(port:${AFTER_PORT})"

# 2
for cnt in {1..10}
do
    echo "서버 응답 확인중(${cnt}/10)";
    UP=$(curl -s http://localhost:${AFTER_PORT}/api/check)
    if [ -z "${UP}" ]
        then
            sleep 10
            continue
        else
            break
    fi
done

if [ $cnt -eq 10 ]
then
    echo "서버가 정상적으로 구동되지 않았습니다."
    exit 1
fi

# 3
sudo sed -i "s/${BEFORE_PORT}/${AFTER_PORT}/" /etc/nginx/conf.d/service-url.inc
sudo nginx -s reload
echo "Deploy Completed!!"

# 4
echo "$BEFORE_COLOR server down(port:${BEFORE_PORT})"
docker-compose -p hospital-${BEFORE_COLOR} -f docker-compose.${BEFORE_COLOR}.yaml down

너무 길어서 부분 부분 설명하겠습니다.

 

 

1. 컨테이너 시작

 

EXIST_BLUE=$(docker-compose -p hospital-blue -f docker-compose.blue.yaml ps | grep Up)

if [ -z "$EXIST_BLUE" ]; then
    docker-compose -p hospital-blue -f /home/ubuntu/backend/docker-compose.blue.yaml up -d
    BEFORE_COLOR="green"
    AFTER_COLOR="blue"
    BEFORE_PORT=9001
    AFTER_PORT=9000
else
    docker-compose -p hospital-green -f /home/ubuntu/backend/docker-compose.green.yaml up -d
    BEFORE_COLOR="blue"
    AFTER_COLOR="green"
    BEFORE_PORT=9000
    AFTER_PORT=9001
fi

echo "${AFTER_COLOR} server up(port:${AFTER_PORT})"

docker-compose를 이용하여 배포된 컨테이너들 중에서 새로운 컨테이너를 시작하는 스크립트입니다.

 

1) EXIST_BLUE=$(docker-compose -p hospital-blue -f docker-compose.blue.yaml ps | grep Up)
 
현재 상태가 블루인지 확인합니다. docker-compose.blue.yaml 파일을 사용하여 hospital-blue라는 이름으로 실행 중인 컨테이너가 있는지 확인하는 명령어입니다. docker-compose ps 명령어는 해당 이름으로 실행된 컨테이너들의 상태를 보여주는데, grep Up 명령어를 추가하여 실행 중인 컨테이너가 있는지 여부를 확인합니다. 결과가 존재하지 않는다면 EXIST_BLUE 변수에는 빈 문자열이 저장됩니다.
 
 
2) -z "EXIST_BLUE"
 
만약 블루 상태가 아니라면 docker-compose.blue를 실행하고, blue 상태로 전환합니다. 
 
만약 기존의 상태가 블루면 docker-compose.green을 실행하고, green 상태로 전환합니다. 
 
 
3) echo "${AFTER_COLOR} server up(port:${AFTER_PORT})"
 
현재 상태의 컬러를 나타내고, 몇 번 포트가 올라왔는지 출력합니다.
 
 
 
2. 컨테이너 상태 확인
 
# 2
for cnt in {1..10}
do
    echo "서버 응답 확인중(${cnt}/10)";
    UP=$(curl -s http://localhost:${AFTER_PORT}/api/check)
    if [ -z "${UP}" ]
        then
            sleep 10
            continue
        else
            break
    fi
done

if [ $cnt -eq 10 ]
then
    echo "서버가 정상적으로 구동되지 않았습니다."
    exit 1
fi
 
 
생성한 컨테이너 안에 Application이 구동되는지 10번의 핑을 보내서 확인합니다. 
 
 
http:주소:특정포트/api/check의 URL에 응답이 오면 반복문을 종료합니다. 
 
만약 카운트가 10이 되면, 서버가 정상적으로 구동되지 않았음을 출력하고 쉘 스크립트를 종료합니다. 
 
 
3. 포트 변경
 
# 3
sudo sed -i "s/${BEFORE_PORT}/${AFTER_PORT}/" /etc/nginx/conf.d/service-url.inc
sudo nginx -s reload
echo "Deploy Completed!!"
 
 
1) sudo sed -i "s/${BEFORE_PORT}/${AFTER_PORT}/" /etc/nginx/conf.d/service-url.inc
 
sed를 사용하여 '/etc/nginx/conf.d/service-url.inc' 파일에서 ${BEFORE_PORT} 변수가 포함된 모든 텍스트를 ${AFTER_PORT} 변수 값으로 변경한다는 의미입니다. 이를 통해 Nginx에서 사용하는 포트 번호를 변경할 수 있습니다.
 
 
 
만약 정상적으로 구동이 되면 service-url.inc에 포트를 바꿔주고 Nginx를 reload 합니다. 이러면 /api 경로에 blue or green container를 바인딩해줍니다. 
 

실제로 위의 쉘 스크립트를 실행해봤더니 9000에서 9001 바꼈음 있습니다.

 

 

4. 기존 실행된 컨테이너 삭제

# 4
echo "$BEFORE_COLOR server down(port:${BEFORE_PORT})"
docker-compose -p hospital-${BEFORE_COLOR} -f docker-compose.${BEFORE_COLOR}.yaml down

 

 

3-5. 스크립트 실행

 

기존 블루인 상태에서 쉘 스크립트를 실행해봤더니...

 

 

 

 

9001 포트 변경 완성.

 

 

9001번 포트로 신호가 정상적인 걸 확인할 수 있습니다.

 

 

 

Blue -> Green 상태로 스위칭 됐음을 확인할 수 있습니다. 

 

 

4. Jenkins로 실행하기

 

Jenkins에서도 실행이 가능하게끔 

deploy.sh, blue, green 모두 읽기 실행 권한을 부여했습니다.

 

 

그리고 이전 포스팅에서 언급된 sshpublisher를 활용하여 쉘 스크립트를 실행하게끔 만들었습니다.

 

pipeline syntax를 활용하여 스크립트 언어에 맞게끔 변형.

 

 

이제는 정말 무중단 배포가 끝난 줄 알았지만....

 

 

EC2 환경에서 쉘 스크립트를 실행시켜 정상적으로 완성된 것과 달리 Jenkins 환경에서는 정상적으로 쉘 스크립트가 구동이 안 됐습니다...

 

 

인터넷으로 검색을 해봤지만, 개개인마다 원인이 다르므로 어떤 문제인지 몰라서 도저히 어떻게 접근해야할지 감도 안 잡히는 상황이었습니다.

 

 

 

오류 로그가 나오지가 않아서 문제 접근이 아예 차단돼서 리신이 된 기분이었습니다 ㅠㅠ

 

 

 

 

Jenkins의 오류 로그만 나오면 해결될 것 같아서 검색을 해봤더니 관련 정보를 겨우 찾게 됩니다. 

 

 

sshPublisher같은 경우 verbose옵션이 있는데, 해당 옵션을 true로 주면 원격 상태에서도 콘솔 로그가 뜬다는 사실을 알게됐습니다. console output에 해당 내용이 상세하게 찍힌다는 겁니다. 

 

verbose 옵션을 true로 했더니 원격 SSH 상태에서도 console에 출력되는 걸 확인할 수 있었습니다. 그리고 무엇이 문제인지 인지하게 됐습니다. 

 

네. 경로가 잘못돼서 파일을 찾을 수 없었습니다.

 

그래서 파일의 경로를 상세히 적어줬더니.

 

 

정상적으로 구동된 걸 확인할 수 있었습니다.

 

 

blue/green 스위칭이 정상적으로 되는지 확인하려고 Jenkins로 2번 실행했더니.

 

 

정상적으로 Green blue 스위칭이 확인할 있습니다.

 

 

다음 포스팅에는 전체적인 pipeline을 구축하여 CI/CD 부분을 마무리 하겠습니다. 

 

 

5. 마무리

publish over ssh 문제 때문에 12시간 넘게 막혔습니다. 사람은 실수의 동물이라 오류가 안 날 수가 없죠. 그만큼 verbose 옵션을 true로 설정하여 오류 로그를 출력하는 게 엄청 중요한 것 같습니다. 

 

jenkins로 publish over ssh 기능을 사용하는 분들은 이것만 아셔도 많은 도움이 될거라 확신합니다. 

 

 

verbose 옵션 true. verbose 옵션 true. verbose 옵션 true. verbose 옵션 true. verbose 옵션 true. verbose 옵션 true. verbose 옵션 true. verbose 옵션 true. verbose 옵션 true. verbose 옵션 true. verbose 옵션 true. verbose 옵션 true. verbose 옵션 true. verbose 옵션 true. verbose 옵션 true.

 

 

막히신 분들에게 큰 도움이 됐으면 좋겠습니다. 감사합니다. 

 

 

참고 자료

https://devbksheen.tistory.com/entry/Jenkins-Docker-Nginx-%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0

 

Jenkins + Docker + Nginx 무중단 자동화 배포하기(Centos)

업무 상 Docker와 Nginx를 통해 Blue Green 무중단 배포를 구축하려고 한다. OS는 Centos 7.x 기준이고 Application은 Spring Boot 기준으로 정리하였다. 현재 서버는 NCP에 Server를 사용하고 있고 Container Registry에

devbksheen.tistory.com

https://hudi.blog/zero-downtime-deployment-with-jenkins-and-nginx/

 

Jenkins와 Nginx로 스프링부트 애플리케이션 무중단 배포하기

무중단 배포가 왜 필요할까? 카카오톡 서버가 새로운 버전이 배포될 때 마다 카톡을 일시적으로 사용할 수 없다면 누가 카카오톡을 사용할까? 더군다나 지금처럼 애자일하게 자주 배포하는 환

hudi.blog

 

728x90