본문 바로가기
Tool/🤵Jenkins

Nginx와 shell을 이용한 무중단 배포 (SSL 적용된 상태)

by 발개발자 2022. 12. 25.
반응형

  작업개요 

현재 개인적으로 운영중인 사이트가 배포시 약 1분간 서버가 중단 상태가 된다.

그래서 째려보기만 하고 아껴두던 그녀석,,, 드디어 꺼낼 때가 됐다. 무중단 배포...!

무중단 배포란 이렇게 본인처럼 서버를 중단시키는 것을 방지하고 배포를 계속하는 작업이다.

무중단 배포를 적용해서 서비스가 중단되지 않고 변경된 소스를 반영시켜보자.

 

 

  Nginx를 사용한 무중단 배포

무중단 배포에도 여러가지 방법이 있는데, 본인은 Nginx를 사용하여 무중단 배포를 진행할 계획이다.

왜냐하면... 무료기 때문이다. 손가락 빨고 있는 주식판의 노예는 어쩔수 없는 선택이다.

 

 

자! 이제 Nginx를 사용할 경우 구현되는 구조를 알아보자.

1. 사용자는 서비스 주소로 접속한다. (Nginx)

2. Nginx는 현재 연결된 서비스로 redirect 시켜준다. (8081로 가정)

3. 신규 배포가 진행시 8082로 서비스 실행.

4. 배포가 정상적으로 완료될 시, 8082가 구동중인지 Check.

5. 8082가 구동중이면, nginx reload를 통해 8081의 연결을 끊고 8082로 연결시켜준다.

6. Nginx reload는 1초 이내에 실행이 완료되기 때문에 사용자 입장에서 중단되는 느낌을 거의 못 갖게 된다.

7. 한번 더 배포를 실행할 경우 위의 과정을 8081포트로 다시 재실행하게 된다.

8. 롤백시 바라보는 포트만 변경.

 

 

  작업시작

 

본인은 SSL작업할 때 nginx를 설치해놓은 상태여서 nginx 설치는 명령어로 넘어간다.

// nginx 설치
sudo yum install nginx

// nginx start
sudo service nginx start

// nginx service 확인
ps -ef | grep nginx

 

Spring profile 설정

application.yml

spring:
  profiles:
    active: local #기본환경 선택
  config:
    use-legacy-processing: true

#local 환경
---
spring:
  profiles: local
... 각자 설정내용
server:
  port: '8080'


#운영 환경
---
spring:
  profiles: real
... 각자 설정내용
server:
  port: '8080'

config:use-legacy-processing:true 부분은 위와같이 profile를 세팅하는 구조는 곧 deprecate 되지만, 현재 지원하기 위한 설정이다. 추후 설정을 바꿔줘야 한다.

바꿀 경우 아래의 url을 참고하면 좋을 것 같다!

 

http://honeymon.io/tech/2021/01/16/spring-boot-config-data-migration.html

 

 

MainRestController.class

 

@RestController
@RequiredArgsConstructor
public class MainRestController {

    private final Environment env;

    //profile 조회
    @GetMapping(value="/profile")
    public String getProfile(){
        return Arrays.stream(env.getActiveProfiles())
                .findFirst()
                .orElse("");
    }
}

 

Test.class

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MainControllerTest {
    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void Profile확인 () {
        //when
        String profile = restTemplate.getForObject("/profile", String.class);

        //then
        Assertions.assertThat(profile).isEqualTo("local");
    }
}

 

url을 /profile로 요청하면 local이라는 문자열을 받는지 확인하는 테스트 코드이다.

이 컨트롤러를 통해 현재 구동중인 서비스를 파악할 수 있다.

 

 

운영환경 yml 생성

배포환경에서 본인이 원하는 디렉토리에 real-application.yml을 생성한다.

# 운영 환경
---
spring.profiles: set1
spring.profiles.include: real

server:
  port: 8081

---
spring.profiles: set2
spring.profiles.include: real

server:
  port: 8082
~
~
~

프로젝트 내부에 운영환경의 yml을 포함시키는걸 권장하지않는다.

만일 운영환경 내부에서 yml 파일을 github에 그대로 commit할 경우 DB access 정보나, 접속정보, 암호화키 등 많은 크리티컬한 정보가 유출되기 때문이다.

그래서 위와 같이 외부로 빼내어 생성하는 것이다.

 

Applicataion.class

@SpringBootApplication
@EnableScheduling
@EnableCaching
public class Application {

	public static final String APPLICATION_LOCATIONS = "spring.config.location="
			+ "classpath:application.yml,"
			+ " /home/ec2-user/was/config/drbot/real-application.yml";


	public static void main(String[] args) {
		new SpringApplicationBuilder(DrbotApplication.class)
				.properties(APPLICATION_LOCATIONS)
				.run(args);
	}

}

이제 Springboot가 구동될 때 해당 application.yml을 읽을 수 있게 위와 같이 수정해준다.

이렇게 될 경우 프로젝트 내부의 application.yml파일과 외부로 빼놓은 real-application.yml파일을 다 읽어서 서버를 구동할 수 있다.

이렇게 yml파일을 다읽어서 실행하기 때문에, real-application.yml에서 설정한 spring.profiles.include: real 구문을 통해 프로젝트 내부에 있는 spring profile이 real 설정을 포함시킬 수 있다.

 

배포 스크립트 생성

deploy.sh

#!/bin/bash
BASE_PATH=/home/ec2-user/was
BUILD_PATH=$(ls /var/lib/jenkins/workspace/drbot/build/libs/*.jar)
JAR_NAME=$(basename $BUILD_PATH)
echo "> build 파일명: $JAR_NAME"

echo "> build 파일 복사"
DEPLOY_PATH=$BASE_PATH/jar/
cp $BUILD_PATH $DEPLOY_PATH

echo "> 현재 구동중인 Set 확인"
CURRENT_PROFILE=$(curl -s http://localhost/profile)
echo "> $CURRENT_PROFILE"

# 쉬고 있는 set 찾기: set1이 사용중이면 set2가 쉬고 있고, 반대면 set1이 쉬고 있음
if [ $CURRENT_PROFILE == set1 ]
then
  IDLE_PROFILE=set2
  IDLE_PORT=8082
elif [ $CURRENT_PROFILE == set2 ]
then
  IDLE_PROFILE=set1
  IDLE_PORT=8081
else
  echo "> 일치하는 Profile이 없습니다. Profile: $CURRENT_PROFILE"
  echo "> set1을 할당합니다. IDLE_PROFILE: set1"
  IDLE_PROFILE=set1
  IDLE_PORT=8081
fi

echo "> application.jar 교체"
IDLE_APPLICATION=$IDLE_PROFILE-drbot.jar
IDLE_APPLICATION_PATH=$DEPLOY_PATH$IDLE_APPLICATION

ln -Tfs $DEPLOY_PATH$JAR_NAME $IDLE_APPLICATION_PATH

echo "> $IDLE_PROFILE 에서 구동중인 애플리케이션 pid 확인"
IDLE_PID=$(pgrep -f $IDLE_APPLICATION)

if [ -z $IDLE_PID ]
then
  echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
  echo "> kill -15 $IDLE_PID"
  sudo kill -15 $IDLE_PID
  sleep 5
fi

echo "> $IDLE_PROFILE 배포"
echo "> $IDLE_PROFILE $IDLE_APPLICATION_PATH 경로"
sudo nohup java -jar -Dspring.profiles.active=$IDLE_PROFILE $IDLE_APPLICATION_PATH 1>/dev/null 2>&1 &

echo "> $IDLE_PROFILE 10초 후 Health check 시작"
echo "> curl -s http://localhost:$IDLE_PORT/health "
sleep 10

for retry_count in {1..10}
do
  response=$(curl -s http://localhost:$IDLE_PORT/health)
  up_count=$(echo $response | grep 'UP' | wc -l)

  if [ $up_count -ge 1 ]
 then # $up_count >= 1 ("UP" 문자열이 있는지 검증)
      echo "> Health check 성공"
      break
  else
      echo "> Health check의 응답을 알 수 없거나 혹은 status가 UP이 아닙니다."
      echo "> Health check: ${response}"
  fi

  if [ $retry_count -eq 10 ]
  then
    echo "> Health check 실패. "
    echo "> Nginx에 연결하지 않고 배포를 종료합니다."
    exit 1
  fi

  echo "> Health check 연결 실패. 재시도..."
  sleep 10
done

echo "> 스위칭"
sleep 10
sudo sh /home/ec2-user/was/switch.sh
  • 본인은 젠킨스를 통해 빌드를 하기 때문에 해당 경로에 빌드된 jar파일을 BUILD_PATH로 잡았다.
  • 이제 본인이 배포폴더로 잡을 곳을 BASE_PATH로 설정한다.
  • BASE_PATH/jar 폴더를 만들어 준 후 해당 폴더로 jar파일을 복사시킨다.
  • curl -s을 통해 현재 구동중인 profile을 체크한다. 여기서 본인은 이미 SSL발급을 해놓고 nginx를 통해 80포트를 막아놓은 상태기 때문에 실제론 https://본인 도메인/profile 이렇게 설정하여 체크하였다.
  • 구동되지 않고 있는 서비스쪽에 변수들과 jar를 할당한 후 서비스 구동.
  • 실행한 서비스의 Health체크를 실행한다.
implementation 'org.springframework.boot:spring-boot-starter-actuator'
  • 해당 의존성을 추가한다면, /health의 결과로 {"status":"UP"}의 결과를 받을 수 있다.
  • 정상적으로 서비스가 구동되었다면 스위칭 스크립트 실행 ( 8081 -> 8082 )

 

nginx의 conf 수정

 

include service-url.inc를 추가해준다.

$service-url은 해당 inc파일에서 추출해온 변수값이라 보면된다.

즉, nginx로 요청오면 본인이 지정한 변수 값에 해당하는 url로 redirect 시켜준다.

 

 

service-url.inc 파일 생성.

sudo vim /etc/nginx/conf.d/service-url.inc

// 파일 오픈 후 아래 소스 입력
set $service_url http://127.0.0.1:8081;

nginx relaod

sudo service nginx restart

 

 

스위칭 스크립트 생성

switch.sh

echo "> 현재 구동중인 Port 확인"
CURRENT_PROFILE=$(curl -s http://localhost/profile)

# 쉬고 있는 set 찾기: set1이 사용중이면 set2가 쉬고 있고, 반대면 set1이 쉬고 있음
if [ $CURRENT_PROFILE == set1 ]
then
  IDLE_PORT=8082
elif [ $CURRENT_PROFILE == set2 ]
then
  IDLE_PORT=8081
else
  echo "> 일치하는 Profile이 없습니다. Profile: $CURRENT_PROFILE"
  echo "> 8081을 할당합니다."
  IDLE_PORT=8081
fi

echo "> 전환할 Port: $IDLE_PORT"
echo "> Port 전환"
echo "set \$service_url http://127.0.0.1:${IDLE_PORT};" |sudo tee /etc/nginx/conf.d/service-url.inc

PROXY_PORT=$(curl -s http://localhost/profile)
echo "> Nginx Current Proxy Port: $PROXY_PORT"

echo "> Nginx Reload"
sudo service nginx reload

echo "반대쪽 service 종료"
sudo kill -9 `pgrep -f $CURRENT_PROFILE`

권한부여

chmod +x switch.sh

여기서도 curl -s http://localhost/profile을 본인이 ssl을 발급하고 nginx를 통해 80포트가 막혀있다면, 해당 conf를 수정해주거나 curl -s https://본인도메인/profile로 바꿔주면 된다.

 

그리고 본인은 ec2 free tier 환경이기 때문에 2개의 서비스를 계속 켜놓는 상태가 아니라, 정상적으로 switching 될 경우 유휴 서비스를 종료시켜주었다. 메모리를 줄이기 위해 어쩔 수 없이 진행하였다.

 

이제 deploy.sh를 통해 set1과 set2를 실행시킨 후, switch.sh를 실행시키면 서비스가 switching되는 걸 확인할 수 있다.

 

이제 본인은 jenkins의 pipeline에서 deploy를 위에 생성한 deploy.sh로 바꿔주었다.

pipeline {
    agent any 
    stages {
        stage('GitHub Repository Clone') { 
            steps {
                  git branch: 'master', credentialsId: '', url: ''
            }
        }
        stage('Gradle build') { 
            steps {
                  sh '''
                    echo 'Gradle Build'
                    sudo chmod 777 ./gradlew
                    sudo ./gradlew clean bootJar --no-daemon
                 '''    
            }
        }
        stage('Deploy') { 
            steps {
                sh '''
                   sudo sh '/home/ec2-user/was/deploy.sh'
                '''

            }
        }

    }
}

 

 

이제 배포시 무중단 배포가 가능해졌다.

뿌듯!

 

 

아래 링크에 이동욱 센세께서 엄청나게 잘 정리해놓으셨다.

 

참고

https://github.com/jojoldu/springboot-webservice/blob/master/tutorial/7_NGINX_SSL_%EB%AC%B4%EC%A4%91%EB%8B%A8%EB%B0%B0%ED%8F%AC.md

 

GitHub - jojoldu/springboot-webservice: 스프링부트로 웹서비스 구축하기 시리즈

스프링부트로 웹서비스 구축하기 시리즈. Contribute to jojoldu/springboot-webservice development by creating an account on GitHub.

github.com

 

반응형

댓글