반응형

docker와 docker-compose, nginx를 이용해서 spring boot를 무중단 배포 해보겠습니다.

절차는 다음과 같습니다.

1. 리버스 프록시로 nginx의 80포트를 스프링 앱으로 연결시켜줄 것이다. 초기에는 8080포트를 바라보고 있다고 가정한다.

2. 스프링 앱은 8080포트 또는 8081포트로 배포할 것이다.

3. 만약 스프링 A(8080)로 배포 중 이었다면, 스프링 B(8081)를 새로 실행한다.

4. 스프링 B가 정상적으로 다 실행되면, nginx가 8081포트를 바라보도록 하고, nginx를 재시작한다.

5. 구동 중이었던 스프링 A(8080)는 종료한다.

6. 만약 스프링 B(8081)로 배포 중 이었다면, 스프링 A(8080)를 새로 실행한다.

7. 스프링 A가 정상적으로 다 실행되면, nginx가 8080포트를 바라보도록 하고, nginx를 재시작한다.

8. 구동 중이었던 스프링 B(8081)는 종료한다.

nginx의 재시작은 빠르게 일어나므로 순간적으로 다른 스프링 앱으로 교체하는 과정입니다.

먼저, 스프링 부트가 잘 실행되었는지 확인하기 위해 다음 dependency를 추가해주겠습니다.

implementation 'org.springframework.boot:spring-boot-starter-actuator'

actuator를 이용하면 스프링 앱 서버의 상태를 확인할 수 있습니다.

 

잘 구동되고 있는지의 여부만 확인할 것이므로 별다른 설정은 하지 않고, endpoint만 설정해주겠습니다.

기본적으로 "/actuator/health"로 접속하면 서버의 정보를 확인할 수 있습니다.

하지만 추가적인 설정을 하면, 민감한 정보가 조회될 수도 있으니 경로를 바꿔줄 수 있습니다.

# application.yml
management:
  endpoint:
  endpoints:
    web:
      base-path: /kuke

저는 /kuke/health로 접속하면 서버의 상태가 조회되도록 하겠습니다.

 

스프링 앱을 구동하고, 해당 경로로 접속해보겠습니다.

{"status":"UP"}

정상적으로 스프링 앱이 구동되면, 다음과 같은 결과를 응답해줄 것입니다.

이제 nginx를 설정하겠습니다.

배포할 인스턴스에 nginx를 설치한 뒤, 다음과 같이 서버 블록을 작성합니다.

/etc/nginx/sites-available/kuke.conf 를 작성하겠습니다.

# /etc/nginx/sites-available/kuke.conf

server {
        include /etc/nginx/conf.d/service-url.inc;
        server_name     ...;
        charset         utf-8;

        location / {
                proxy_pass      $service_url;
                proxy_set_header        X-Real-Ip $remote_addr;
                proxy_set_header        x-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header        Host $host;
        }
}

다른 설정은 기본적인 nginx의 리버스 프록시 설정과 같습니다.

서버 블록 내에 include /etc/nginx/conf.d/service-url.inc; 라는 코드가 있는데,

service-url.inc는 구동 중인 스프링 앱의 url을 기입해둔 파일입니다.

해당 파일을 include하면, 그 파일에 작성된 변수를 이용할 수 있습니다.

location / {
       proxy_pass      $service_url;
       ...
}

위 설정 파일의 location 블록을 보면,

이처럼 proxy_pass에 service_url이라는 변수를 값으로 지정해주는데, service-url에 선언되어있는 값입니다.

 

# /etc/nginx/conf.d/service-url.inc

set $service_url http://127.0.0.1:8080;

serivce-url.inc는 위와 같습니다.

배포해줄 스프링 앱의 url이 기록되어있습니다.

이 값이 kuke.conf 에서 proxy_pass의 값으로 사용됩니다.

여기까지 되었으면, ln -s 로 kuke.conf의 심볼릭링크를 sites-enabled에 등록해주고 nginx를 시작해줍니다.

이제 spring boot 프로젝트의 루트경로에 Dockerfile을 작성해보겠습니다.

FROM openjdk:11-jdk

ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} kuke.jar

ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "-jar", "/kuke.jar"]

빌드된 jar 파일을 가지고 실행하게 됩니다.

이어서 스프링 프로젝트 루트 경로에 docker-compose 파일을 두 개 작성해보겠습니다.

# docker-compose.a.yml

version: "3"
services:
  kukemeet:
    build: .
    restart: on-failure
    ports:
      - 8080:8080

networks:
  default:
    external:
      name: kukenetwork
# docker-compose.b.yml

version: "3"
services:
  kukemeet:
    build: .
    restart: on-failure
    ports:
      - 8081:8080

networks:
  default:
    external:
      name: kukenetwork

둘의 하는 일은 포트를 제외하고는 완전히 동일합니다.

그저 a는 8080포트로 매핑시켜주고, b는 8081포트로 매핑시켜주는 차이가 있을 뿐입니다.

일반적으로 docker-compose로 실행하면 default로 새로 네트워크를 생성해주고, docker-compose를 종료할 때, 네트워크도 제거됩니다. 같은 네트워크에 묶여있으면 컨테이너 명으로 통신할 수 있습니다.

docker-compose 파일의 networks 부분은 default 네트워크로, 미리 docker 명령어로 생성해둔 외부 네트워크를 이용한다는 의미입니다.

 

이를 위해 네트워크를 미리 생성해두겠습니다.

$ docker network create kukenetwork
$ docker network ls

ls 명령어로 확인해보시면, 생성된 네트워크가 조회될 것입니다.

이제 마지막으로 배포를 위한 스크립트 파일 deploy.sh를 작성해보겠습니다.

자세한 내용은 주석을 참고 바랍니다.

#!/bin/bash

sudo chmod +x ./gradlew # gradlew 읽기 권한 부여
sudo ./gradlew bootJar # jar 파일 생성

# 실행 중인 도커 컴포즈 확인
EXIST_A=$(sudo docker-compose -p kukemeet-a -f docker-compose.a.yml ps | grep Up)

if [ -z "${EXIST_A}" ] # -z는 문자열 길이가 0이면 true. A가 실행 중이지 않다는 의미.
then
        # B가 실행 중인 경우
        START_CONTAINER=a
        TERMINATE_CONTAINER=b
        START_PORT=8080
        TERMINATE_PORT=8081
else
        # A가 실행 중인 경우
        START_CONTAINER=b
        TERMINATE_CONTAINER=a
        START_PORT=8081
        TERMINATE_PORT=8080
fi

echo "kukemeet-${START_CONTAINER} up"

# 실행해야하는 컨테이너 docker-compose로 실행. -p는 docker-compose 프로젝트에 이름을 부여
# -f는 docker-compose파일 경로를 지정
sudo docker-compose -p kukemeet-${START_CONTAINER} -f docker-compose.${START_CONTAINER}.yml up -d --build

for cnt in {1..10} # 10번 실행
do
        echo "check server start.."

        # 스프링부트에 등록했던 actuator로 실행되었는지 확인
        UP=$(curl -s http://127.0.0.1:${START_PORT}/kuke/health | grep 'UP')
        if [ -z "${UP}" ] # 실행되었다면 break
        then
                echo "server not start.."       
        else
                break
        fi

        echo "wait 10 seconds" # 10 초간 대기
        sleep 10
done

if [ $cnt -eq 10 ] # 10번동안 실행이 안되었으면 배포 실패, 강제 종료
then
        echo "deployment failed."
        exit 1
fi

echo "server start!"
echo "change nginx server port"

# sed 명령어를 이용해서 아까 지정해줬던 service-url.inc의 url값을 변경해줍니다.
# sed -i "s/기존문자열/변경할문자열" 파일경로 입니다.
# 종료되는 포트를 새로 시작되는 포트로 값을 변경해줍니다.
sudo sed -i "s/${TERMINATE_PORT}/${START_PORT}/" /etc/nginx/conf.d/service-url.inc

# 새로운 포트로 스프링부트가 구동 되고, nginx의 포트를 변경해주었다면, nginx 재시작해줍니다.
echo "nginx reload.."
sudo service nginx reload

# 기존에 실행 중이었던 docker-compose는 종료시켜줍니다.
echo "kukemeet-${TERMINATE_CONTAINER} down"
sudo docker-compose -p kukemeet-${TERMINATE_CONTAINER} -f docker-compose.${TERMINATE_CONTAINER}.yml down
echo "success deployment"

deploy.sh에 실행 권한을 주고 실행해봅시다.

스크립트를 실행할 때마다 스프링 컨테이너가 바뀌기 때문에, 외부에서는 nginx를 통해 서비스의 중단 없이 이용할 수 있습니다.

반응형

+ Recent posts