본문으로 바로가기
728x90

Chapter 10

24시간 365일 중단없는 서비스를 만들자

 

 

무중단 배포 소개

 

- 무중단 배포 : 서비스를 정지하지 않고, 배포할 수 있는 방법

  대표적인 방법 : 엔진엑스, 도커, AWS에서 블루-그린

 

 

엔진엑스 설치와 스프링 부트 연동하기

 

  •  엔진엑스 설치

- EC2에 엔진엑스 설치

sudo yum install nginx

- 엔진엑스 실행

 

  • 보안 그룹 추가

엔진엑스의 포트번호는 기본적으로 80

EC2->보안 그룹->EC2 보안그룹 선택->인바운드 편집으로 이동해 변경

 

  • 리다이렉션 주소 추가

구글과 네이버 로그인에도 변경된 주소를 등록 -> 기존의 리디렉션 주소에 8080 제거해 추가 등록

 

추가한 이후, EC2의 도메인으로 접근하되 8080 포트를 제거하고 접근해보기

 

  • 엔진엑스와 스프링 부트 연동

엔진엑스가 현재 실행중인 스프링 부트 프로젝트를 바라볼 수 있도록 프록시 설정을 하기

먼저, 엔진엑스의 설정파일 열기

sudo vim /etc/nginx/nginx.conf

내용 중에서 server 아래의 location / 부분을 찾아 아래와 같이 추가

 

수정 이후, 엔진엑스를 아래 명령어로 재시작

sudo service nginx restart

이제 아까의 url로 이동하여 새로고침을 해보자.

 

무중단 배포 스크립트 만들기

 

- profile API 추가(배포시 8081, 8082를 쓸지의 기준이 됨)

 

ProfileController 생성

 

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
package com.qwon.springboot.web;
 
import lombok.RequiredArgsConstructor;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
import java.util.Arrays;
import java.util.List;
 
@RequiredArgsConstructor
@RestController
public class ProfileController {
    private final Environment env;
    
    @GetMapping("/profile")
    public String profile(){
        List<String> profiles= Arrays.asList(env.getActiveProfiles());
        List<String> realProfiles=Arrays.asList("real","real1","real2");
        String defaultProfile=profiles.isEmpty()?"default":profiles.get(0);
        return profiles.stream()
                .filter(realProfiles::contains)
                .findAny()
                .orElse(defaultProfile);
    }
}
 
 
 

 

테스트 코드 작성

 

ProfileControllerUnitTest 생성

 

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
package com.qwon.springboot.web;
 
import org.junit.Test;
import org.springframework.mock.env.MockEnvironment;
 
import static org.assertj.core.api.Assertions.assertThat;
 
public class ProfileControllerUnitTest {
 
    @Test
    public void real_profile이_조회된다(){
        //given
        String expectedProfile="real";
        MockEnvironment env=new MockEnvironment();
        env.addActiveProfile(expectedProfile);
        env.addActiveProfile("oauth");
        env.addActiveProfile("real-db");
 
        ProfileController controller=new ProfileController(env);
 
        //when
        String profile=controller.profile();
 
        //then
        assertThat(profile).isEqualTo(expectedProfile);
 
    }
 
    @Test
    public void real_profile이_없으면_첫번째가_조회된다(){
        //given
        String expectedProfile="oauth";
        MockEnvironment env=new MockEnvironment();
        env.addActiveProfile(expectedProfile);
        env.addActiveProfile("real-db");
 
        ProfileController controller=new ProfileController(env);
 
        //when
        String profile=controller.profile();
 
        //then
        assertThat(profile).isEqualTo(expectedProfile);
    }
 
    @Test
    public void active_profile이_없으면_default가_조회된다(){
        //given
        String expectedProfile="default";
        MockEnvironment env=new MockEnvironment();
        ProfileController controller=new ProfileController(env);
 
        //when
        String profile=controller.profile();
 
        //then
        assertThat(profile).isEqualTo(expectedProfile);
    }
}
 
 
 

 

이 /profile이인증 없이도 호출될 수 있게 SecurityConfig 클래스에 제외코드 추가

.antMatchers("/", "/css/**", "/images/**", "/js/**", "/h2-console/**", "/profile").permitAll()

 

테스트 코드로 검증

 

ProfileControllerTest 생성

 

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
package com.qwon.springboot.web;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
 
import static org.assertj.core.api.Assertions.assertThat;
 
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment =
        SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ProfileControllerTest {
 
    @LocalServerPort
    private int port;
 
    @Autowired
    private TestRestTemplate restTemplate;
 
    @Test
    public void profile은_인증없이_호출된다() throws Exception{
        String expected="default";
 
        ResponseEntity<String> response=
                restTemplate.getForEntity("/profile",String.class);
        assertThat(response.getStatusCode()).
                isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).isEqualTo(expected);
    }
}
 
 
 

 

모든 테스트가 성공했다면 커밋 & 푸시

 

이후, 브라우저에서 ~/profile로 접속해 확인해보자.

 

- real1, real2 profile 생성

현재 profile은 Travis CI 배포 자동화를 위한 profile이다.

따라서, 무중단 배포를 위한 profile 2개(real1, real2)를 src/main/resources 아래에 추가

 

 

application-real1.properties

server.port=8081
spring.profiles.include=oauth,real-db
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.session.store-type=jdbc

 

application-real2.properties

server.port=8082
spring.profiles.include=oauth,real-db
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.session.store-type=jdbc

 

 

- 엔진엑스 설정 수정

 

엔진엑스 설정이 모여있는 /etc/nginx/conf.d/에 service-url.inc 파일 생성

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

파일 이동 후, 아래 내용 입력

 

해당 파일을 엔진엑스가 사용할 수 있도록 설정하기 위해 nginx.conf 파일 열기

sudo vim /etc/nginx/nginx.conf

 

저장 종료(:wq)이후 재시작

sudo service nginx restart

 

- 배포 스크립트들 작성

EC2에 step3 디렉토리 생성

mkdir ~/app/step3 && mkdir ~/app/step3/zip

무중단 배포는 앞으로 step3만 사용하려고 함

appspec.yml를 step3로 변경

무중단 배포를 진행할 스크립트

 

- stop.sh : 기존 엔진엑스에 연결x, 실행 중이던 스프링 부트 종료

- start.sh : 배포할 신규 버전을 stop.sh로 종료한 'profile'로 실행

- health.sh : 'start.sh'로 실행한 프로젝트가 정상적으로 실행됬는지 체크

- switch.sh : 엔진엑스가 바라보는 스프링 부트를 최신버전으로 변경

- profile.sh : 앞선 4개의 파일에서 공용으로 사용할 'profile'과 포트 체크 로직

 

 

appspec.yml 스크립트 수정

 

 

- profile.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
#! /usr/bin/env bash
 
function find_idle_profile()
{
  RESPONSE_CODE=$(curl --/dev/null -"%{http_code}"
  http://localhost/profile)
 
  if [ ${RESPONSE_CODE} -ge 400 ]
  then
    CURRENT_PROFILE=real2
  else
    CURRENT_PROFILE=$(curl -s http://localhost/profile)
  fi
 
  if [ ${CURRENT_PROFILE} == real1 ]
 
  then
    IDLE_PROFILE=real2
  else
    IDLE_PROFILE=real1
  fi
 
  echo "${IDLE_PROFILE}"
 }
 
# 쉬고 있는 profile의 port 찾기
function find_idle_port()
{
  IDLE_PROFILE=$(find_idle_profile)
 
  if [ ${IDLE_PROFILE} == real1 ]
  then
    echo "8081"
  else
    echo "8082"
  fi
 
}
 
 
 
 

 

- stop.sh

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env bash
 
ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh
 
IDLE_PORT=$(find_idle_port)
 
echo "> $IDLE_PORT 에서 구동중인 애플리케이션 pid 확인"
IDLE_PID=$(lsof -ti tcp:${IDLE_PORT})
 
if [ -z ${IDLE_PID} ]
then
  echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
  echo "> kill -15 $IDLE_PID"
  kill -15 ${IDLE_PID}
  sleep 5
fi
 
 

 

 

- start.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
#!/usr/bin/env bash
 
ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh
 
REPOSITORY=/home/ec2-user/app/step3
PROJECT_NAME=freelec-springboot2-webservice
 
echo "> Build 파일 복사"
echo "> cp $REPOSITORY/zip/*.jar $REPOSITORY/"
 
cp $REPOSITORY/zip/*.jar $REPOSITORY/
 
echo "> 새 어플리케이션 배포"
JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -1)
 
echo "> JAR Name: $JAR_NAME"
 
echo "> $JAR_NAME 에 실행권한 추가"
 
chmod +x $JAR_NAME
 
echo "> $JAR_NAME 실행"
 
IDLE_PROFILE=$(find_idle_profile)
 
echo "> $JAR_NAME 를 profile=$IDLE_PROFILE 로 실행합니다."
nohup java -jar \
    -Dspring.config.location=classpath:/application.properties,classpath:/application-$IDLE_PROFILE.
properties,/home/ec2-user/app/application-oauth.properties,/home/ec2-user/app/application-real-db.properties \
    -Dspring.profiles.active=$IDLE_PROFILE \
    $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &
 
 

 

- health.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
#!/usr/bin/env bash
 
ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh
source ${ABSDIR}/switch.sh
 
IDLE_PORT=$(find_idle_port)
 
echo "> Health Check Start!"
echo "> IDLE_PORT: $IDLE_PORT"
echo "> curl -s http://localhost:$IDLE_PORT/profile "
sleep 10
 
for RETRY_COUNT in {1..10}
do
  RESPONSE=$(curl -s http://localhost:${IDLE_PORT}/profile)
  UP_COUNT=$(echo ${RESPONSE} | grep 'real' | wc -l)
 
  if [ ${UP_COUNT} -ge 1 ]
  then # $up_count >= 1 ("real" 문자열이 있는지 검증)
      echo "> Health check 성공"
      switch_proxy
      break
  else
      echo "> Health check의 응답을 알 수 없거나 혹은 실행 상태가 아닙니다."
      echo "> Health check: ${RESPONSE}"
  fi
 
  if [ ${RETRY_COUNT} -eq 10 ]
  then
    echo "> Health check 실패. "
    echo "> 엔진엑스에 연결하지 않고 배포를 종료합니다."
    exit 1
  fi
 
  echo "> Health check 연결 실패. 재시도..."
  sleep 10
done
 
 

 

- switch.sh

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env bash
 
ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh
 
function switch_proxy() {
    IDLE_PORT=$(find_idle_port)
 
    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
 
    echo "> 엔진엑스 Reload"
    sudo service nginx reload
}
 
 
 
 

 

 

무중단 배포 테스트

 

배포 테스트 전, 매번 버전을 올리는 번거로움을 피하기 위해 자동으로 버전값 변경하기

- build.gradle 버전 변경

- CodeDeploy 로그로 확인

tail -f /opt/codedeploy-agent/deployment-root/deployment-logs/codedeploy-agent-deployments.log

스프링 부트 로그로 확인하고 싶으면 아래 명령어

vim ~/app/step3/nohup.out

 

만약 2번 배포를 진행한 뒤에 다음과 같이 자바 애플리케이션 실행 여부를 확인

ps -ef | grep java

 

이제 이 시스템은 마스터 브랜치에 푸시에 발생하면 자동으로 서버 배포가 진행되고, 서버 중단 역시 전혀 없는 시스템이 되었다.

728x90