본문 바로가기
기술/Spring-Boot

[Ubuntu/nginx] 스프링 부트 및 우분투 환경에서 nginx 이용한 무중단배포 구현

by Zabee52 2022. 1. 5.

CI/CD

 

 

[Ubuntu/Travis-CI/CodeDeploy] SpringBoot 환경 배포 자동화 환경 구축

CI/CD 하루종일 travis와 싸웠다. travis 사이트도 말썽을 부려댔고, 깃헙도 잠깐 터졌고, 티스토리도 잘 안 되는 데다가, 시스템 구축이 감이 통 잡히지 않아 나를 괴롭게 했다. 이런 나를 이끌었던건

dazbee.tistory.com

이전 편에서 이어진다.

 

고통스러운 하루였다. 사람은 착한데 말은 안 듣는 꾸러기 nginx를 가지고 열심히 하루종일 고군분투 했다. 다 하고보니 하루나 걸릴 일인가 싶긴 한데..... 아무튼 해냈다. 당면한 문제는 언제나 스트레스이지만, 해결할 수 있기에 즐겁다. 오늘도 나의 행복 임계치가 한 꼬집 낮아졌다.

야호~

 

이전 포스트에선 몰랐는데, 이전 포스트에서 참조했던 글이 향로(이동욱 개발자님)님의 내용을 거의 그대로 붙인 것이었다. 그래서 이번엔 거슬러 올라가 원본 게시글을 보며 작업을 했다. 비교적 쉽게 구현할 수 있도록 해준 향로님께 무한한 감사의 말씀을 전한다. 인프런 방향으로 인사 한 번.

 

 

7) 스프링부트로 웹 서비스 출시하기 - 7. Nginx를 활용한 무중단 배포 구축하기

이번 시간엔 무중단 배포 환경을 구축하겠습니다. (모든 코드는 Github에 있습니다.) 7-1. 이전 시간의 문제점? 이전 시간에 저희는 스프링부트 프로젝트를 Travis CI를 활용하여 배포 자동화 환경을

jojoldu.tistory.com

 

보면서 했지만, 그럼에도 오래걸렸다. 왜 오래걸렸냐면, 아무래도 글이 과거에 작성된 글이다 보니 버전이 달랐다. 게다가 첫 번째 글부터 차례대로 밟아온게 아니라 첫 번째 글에서 세팅했던 내용이 나에게 적용되어있지 않아 그 부분도 해결하느라 시간이 소모되었다. 그래서 이번엔 step by step이 아닌 게시글의 부분부분을 짚어가며 내가 겪었던 시행착오를 쓰고자 한다.

 

why?

먼저, 정리부터 해보겠다. 그래. 나는 왜 nginx를 골랐는가?

이 부분에 대한 교과서적인 답변을 하자면,

  1. 간단한 세팅으로 구현할 수 있는 리버스 프록시

  2. 가벼움

  3. 동시접속 처리에 특화된 시스템은 로드밸런싱 시스템을 구축 시 트래픽이 많이 발생하는 커뮤니티 사이트에 적합

그 외에도 할 말이야 만들면 생기겠지만 결국 결론은 무료이기 때문이다.

내부에서 포트를 나눠 Blue-Green 배포를 시행하면 EC2를 추가로 개설할 필요도 사라지기에 사실상 50%만큼 비용이 절약된다. 살짝 맥이 빠지는 결론이긴 하지만, 비용적인 문제는 엄청나게 중요하다. 저렴한 t2.micro를 포기하기 싫어 Jenkins를 포기한 것처럼.

지금은 몇천 원 수준의 작은 비용 차이일 뿐이지만, 미래에 내가 취업할 회사에선 이 차이가 어마어마한 비용의 차이가 될 것이다. 비용의 지출을 줄이기 위해 주어진 상황 속에서 최적의 프로그램을 구축해야 하는 상황을 맞이하게 될 것을 대비한다고 생각하면 될 것이다.

 

그러면 이제 본론으로 들어가보겠다. 부분부분을 짚어가며 내가 겪었던 시행착오.. 이하생략.

위의 링크를 같이 보면 도움이 될 것 같다.

 

7-3-1. Nginx 설치

여기서 겪었던 문제는, nginx 버전이 게시글과 다르다는 점이었다. 

sudo vi /etc/nginx/nginx.conf

 

위 파일의 내용이 나와 향로님이 아예 달랐다. 여기서 시간을 조금 빼앗겼다. 결국 해결했다.

nginx 설치를 마치고 위 파일에 들어가면 이런 내용이 있을 것이다.

 

        ...
        
	# gzip_vary on;
        # gzip_proxied any;
        # gzip_comp_level 6;
        # gzip_buffers 16 8k;
        # gzip_http_version 1.1;
        # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

        ##
        # Virtual Host Configs
        ##

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
}

주목해야할 점은 include /etc/nginx/sites-enabled/* 쪽이다. nginx의 버전이 바뀌면서 /etc/nginx/sites-enabled 디렉터리에 설정을 보관하도록 되어있었다.

 

 

Install and configure Nginx | Ubuntu

Ubuntu is an open source software operating system that runs from the desktop, to the cloud, to all your internet connected things.

ubuntu.com

위의 튜토리얼과는 다르게 다른 파일을 생성하거나 할 필요는 없다. default 파일을 조금 수정해주면 된다.

 

sudo vi /etc/nginx/sites-enabled/default

 

	...

        # Add index.php to the list if you are using PHP
        index index.html index.htm index.nginx-debian.html;

        server_name _;

        include /etc/nginx/conf.d/service-url.inc;

        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 $http_host;
                # First attempt to serve request as file, then
                # as directory, then fall back to displaying a 404.
                try_files $uri $uri/ =404;
        }

	...

 

내가 찾던 location / 여기 있네요! 이 곳에 값을 넣어주면 된다. 문제 해결!

 

 

7-3-2. set1, set2 Profile 설정

여기서 겪었던 문제는, /profile 실행 시 프로필값을 제대로 받아오지 못 하는 부분이었다.

다른 프로필이 함께 존재했기 때문에 findFirst에서 set1 또는 set2가 아닌 다른 프로필이 캐치, 문제가 발생했다.

이 부분은 set 값이 존재하는 곳의 index를 잡아주는 코드로 변경해서 해결했다.

@RestController
@RequiredArgsConstructor
public class WebRestController {

    private final Environment env;

    @GetMapping("/profile")
    public String getProfile() {
        String[] str = env.getActiveProfiles();
        int idx = 0;
        for(int i = 0; i < str.length; i++){
            if(str[i].contains("set")){
                idx = i;
                break;
            }
        }

        return str[idx];
    }
}

 

추가로, 윈도우 환경이라 우분투와 같은 경로에 real-application.yml 파일을 위치시키지 못 하고, 이에 따라 로컬에서 빌드가 불가능한 문제였다. 이 부분은 Application을 수정 없이 실행시키고, real-application.yml은 프로젝트 내부에 위치, .gitignore에 추가 시켜 로컬에서 빌드 가능한 환경으로 만들고, EC2에서는 외부에서 설정 파일을 참조해 빌드하도록 만들었다.

@EnableCaching
@EnableScheduling
@EnableJpaAuditing
@SpringBootApplication
public class BackendApplication {
    public static void main(String[] args) {
        SpringApplication.run(BackendApplication.class, args);
    }
}

 

여긴 수정 없이 놔둔거다. 뭐가 바뀌었는지는 바로 다음에 나온다.

 

 

7-3-3. 배포 스크립트 작성

여기서 겪었던 문제는, 파일명.jar 과 파일명-plain.jar 파일이 같이 생성되어서 파일을 파일명-plain.jar 로 실행하려고 했던 문제였다.

해결 방법은 두 가지다.

 

1. Gradle에서 plain.jar 생성 방지

/*
	build.gradle
*/
jar {
	enabled = false
}

 

2. 파일명 정보를 더욱 명확하게 명시

sudo vi /home/ec2-user/app/nonstop/deploy.sh

 

BUILD_PATH=$(ls $BASE_PATH/springboot-webservice/build/libs/*SNAPSHOT.jar)

 

 

다음으로 겪었던 문제는 7-3-2.의 문제와 연결된다.

런타임에 application 설정을 해주지 않았다보니 application 정보가 없어 실행이 불가능한 상태였다. 물론 deploy.sh도 정상 작동하지 않는다. 외부에서 config 파일을 참조할 수 있게 설정해 해결했다.

 

sudo vi /home/ec2-user/app/nonstop/deploy.sh

 

echo "> $IDLE_PROFILE 배포"
nohup java -jar -Dspring.profiles.active=$IDLE_PROFILE -Dspring.config.location=file:///home/ec2-user/app/nonstop/application.properties,file:///home/ec2-user/app/nonstop/real-application.yml $IDLE_APPLICATION_PATH &

파일은 로컬에서 EC2로 옮겨와 직접 이동시켜줬다. 더 좋은 방법을 모색해봐야겠다.

 

 

 

다음 문제는 /health 세팅 문제였다.

 

1) 스프링부트로 웹 서비스 출시하기 - 1. SpringBoot & Gradle & Github 프로젝트 생성하기

많은 웹 서비스 구축하기 강좌들이 Python, NodeJS, Ruby, PHP만 다루고 있습니다. 국내에서 가장 많이 사용하는 언어인 Java로 웹서비스 구축 강좌는 본적이 없습니다. Java는 대부분 로컬에서 CRUD & localh

jojoldu.tistory.com

 

향로님의 관련 게시글 1번 글에서 세팅되어 있던 부분이 누락되어 /health가 작동하지 않는 문제였다. Gradle에 의존성을 추가해줘 해결했다.

 

/*
	build.gradle
*/
dependencies {
    // Health Check
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
}

 

이런데도 /health가 작동하지 않았다. 그래서 deploy.sh 파일에서 주소를 바꿔줬다.

 

sudo vi /home/ec2-user/app/nonstop/deploy.sh

 

response=$(curl -s http://localhost:$IDLE_PORT/actuator/health)

$IDLE_PORT/health -> $IDLE_PORT/actuator/health

수정하지 않아도 잘 작동하게 할 방법이 있는 것 같은데 나는 그냥 이렇게 해결했다.

 

추가로 로컬에서 작동시 Redis 관련 에러가 발생하기도 했고, 실제 기능을 작동시켰을 때 나는 Status : "UP" 하나만 보고싶은데 다른 상세 헬스체크 상태가 나타나기도 했다. 이 부분은 properties를 수정해 해결했다.

 

# application.properties
# Health check option
management.endpoint.health.show-details=never
management.health.redis.enabled=false

show-details를 always로 바꾸면 모든 상태에 대한 헬스체크가 이뤄진다.

 

여기까지 문제를 해결하고 나니 배포 과정에서는 문제 없이 잘 수행됐다. 아이좋아!

 

 

 

였는데, 권한 문제로 심볼릭 링크를 생성해주지 못 하는 문제가 발생했고, 이에 따라 빌드를 제대로 수행하지 못 하는 문제가 있었다.

 

# appspec.yml

hooks:
  AfterInstall: # 배포가 끝나면 아래 명령어를 실행
    - location: execute-deploy.sh
      runas: root

AfterInstall에 runas: root을 부여하여 해결하였다.

 

 

추가로, switch.sh 파일이 프록시 포트가 바뀌었는지 체크가 되지 않는 상황이라 확인을 위해 문장을 조금 추가해줬다.

 

sudo vi /home/ec2-user/app/nonstop/switch.sh

 

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

echo "> Nginx Reload"
sudo service nginx reload

sudo sleep 3

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

포트는 바꿔주더라도 service nginx reload가 이뤄지기 전까진 실제로 반영되지 않는다. 그렇기 때문에 반영한 뒤 한 번 더 체크할 수 있도록 구문을 넣어주었다.

 

 

마지막으로, kill -15 명령어가 sleep 시간 내로 수행 완료되지 않아 포트가 겹쳐 충돌이 발생, 프로그램을 실행하지 못 하는 문제가 있었다.

Q) kill -15랑 kill -9는 무슨 차이가 있어요?

A) kill -15는 지정된 작업을 전부 완료하고 천천히 수행하는데, kill -9는 그 자리에서 강제종료 시켜버립니다. 어차피 유휴 상태의 jar를 종료하는 것이기 때문에 그냥 즉시 강제종료 해버려도 상관이 없을거라고 판단했습니다.

 

sudo vi /home/ec2-user/app/nonstop/deploy.sh

 

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

kill -15 -> kill -9로 바꿔줌으로써 해결했다. 느려터진 친구에겐 강제종료를 드렸다.

댓글