index페이지에서
<h1 id="mainTitle">Spring Boot Board Project Version 2</h1>
메인 타이틀인 Spring Boot Board Project에서 Version 2 를 추가해서 작성해서 다시 배포를 해보았다.
500에러가 발생했다 이전에 오류를 해결했던 것처럼 log를 확인했더니 8080포트가 이미 사용중이라고 deploy.sh 는 현재 구동중인 애플리케이션을 종료하고 새로운 애플리케이션을 작동시키기때문에 다시한번 deploy.sh 를 실행시키고 구동중인 애플리케이션을 확인했더니
이렇게 board_project가 두개가 실행되고있었다..
deploy.sh가 제대로 종료시키지 못하는것인지 확인해보자.
현재 구동중인 애플리케이션이 존재하고 deploy.sh의 현재 구동중인 애플리케이션에서 PID를 찾아오는 코드를 직접 실행해보았는데 아무것도 나오지 않았다.
deploy.sh 수정
CURRENT_PID=$(pgrep -fl board_project | grep jar | awk '{print $1}')
#pgrep -fl board_project | grep jar -> 애플리케이션 이름으로된 jar 프로그램 찾기
#awk '{print $1}' -> 해당 ID를 찾는다.
이부분을
CURRENT_PID=$(pgrep -f ${PROJECT_NAME}.*.jar)
이렇게 수정한다.
책을 따라서 작성했지만 전자의 코드가 어떤 의미로 작성된것인지는 알지만 왜 안나오는지는 아직 코드를 정확히 이해하지못했기 떄문에 조금더 이해하기쉬운 후자의 코드를 선택했다.
지금 실행되고있는 애플리케이션들을 모두 종료하고 다시 시작해보자.
이번에는 travis에서 진행중에 fail 이 발생했다. codedeploy를 들어가서 오류를 확인해보니
였고
nohup java -jar $REPOSITORY/jar/$JAR_NAME &
이 코드에 문제가 조금 있었다. 이 코드말고 밑에 코드를 사용해야했었다.
nohup java -jar $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &
nohup 실행 시 CodeDeploy는 무한 대기하게됩니다.
이 문제를 해결하기 위해서 nohup.out 파일을 표준 입출력용으로 별도로 사용합니다.
이렇게 하지 않으면 nohup.out파일이 생기지 않고, CodeDeploy로그에 표준 입출력이 출력됩니다.
nohup이 끝나기 전까지 CodeDeploy도 끝나지 않으니 꼭 이렇게 해야만 합니다.
관련 블로그
수정후 돌리니 정상 적으로 접근
현재 우리는 main 브랜치에 push만 해주면 자동으로 EC2까지에게 애플리케이션이 배포가 가능하게되었습니다!
하지만 한가지 문제는 배포하는 동안에는 프로젝트가 종료되어서 서비스를 이용할 수 없다는 것입니다.새로운 jar 파일이 실행되기전까지 이전에 jar를 종료시켜놓기 때문입니다.
하지만 대형 플랫폼들의 경우에는 (ex 네이버) 배포하는 동안 서비스가 정지되지않습니다.
어떻게 서비스를 중단없이 배포를 계속할 수 있는지 확인하고 적용해보자!!
무중단 배포방식에는 몇 가지가 있다.
이중에서 우리가 사용하게될것은 NGINX(엔진엑스) 이다!
NGINX는 웹 서버, 리버스 프록시, 캐싱, 로드 밸런싱, 미디어 스트리밍 등을 위한 오픈소스 소프트웨어입니다!
NGINX를 선택한 이유는 가장 저렴하고 쉽기때문입니다!
또한 기존에 쓰던 EC2를 그대로 적용할 수 있기때문에 편리합니다
추가로 꼭 AWS와 같은 클라우드 인프라가 구축되어 있지 않아도 사용할 수 있는 범용적인 방법입니다.
NGINX 1대(80,443 포트) 스프링부트 JAR 2대(8081,8082 포트) EC2 or 리눅스 서버 이렇게됩니다.
사용자는 서비스 주소로 접속합니다 (80 혹은 443 포트)
Nginx는 사용자의 요청을 받아 현재 연결된 스프링부트로 요청을 전달합니다.
스프링부트1 즉, 8081 포트로 요청을 전달한다고 가정하겠습니다.
스프링부트2는 Nginx와 연결된 상태가 아니니 요청을 받지 못합니다.
이때!
1.1 버전으로 신규 배포가 필요하면 Nginx와 연결되지 않은 스프링부트2 (8082)로 배포합니다.
배포하는 동안에도 서비스는 중단되지 않습니다.
Nginx는 스프링부트1을 바라보기 때문입니다.
배포가 끝나고 정상적으로 스프링부트2가 구동중인지 확인합니다.
스프링부트2가 정상 구동중이면 nginx reload를 통해 8081 대신에 8082를 바라보도록 합니다.
Nginx Reload는 1초 이내에 실행완료가 됩니다.
또다시 신규버전인 1.2 버전의 배포가 필요하면 이번엔 스프링부트1로 배포합니다.
현재는 스프링부트2가 Nginx와 연결되있기 때문입니다.
스프링부트1의 배포가 끝났다면 Nginx가 스프링부트1을 바라보도록 변경하고 nginx reload를 실행합니다.
1.2 버전을 사용중인 스프링부트1로 Nginx가 요청을 전달합니다.
만약 배포된 1.2 버전에서 문제가 발생한다?
그러면 바로 Nginx가 8082 포트(스프링부트2)를 보도록 변경하면 됩니다.
롤백 역시 굉장히 간단하게 처리할수 있습니다.
이렇게 구성하게된다면 우리가 지금까지 만들어 놓은 구조는 이렇게 됩니다!
다시한번 이 과정을 쉽게 접근할 수 있게 알려주신 '이동욱'님 감사드립니다.
EC2에 NGINX 설치 먼저 해줍니다.
설치관련 블로그
설치가 완료되었다면 우리는 NGINX가 EC2에 접근할 수 있도록 보안그룹의 인바운드를 편집해줍니다!
NGINX는 80포트를 사용하기때문에 80포트를 추가해줍니다.
추가해준다음에 이전에 프로젝트에 접근했던 방식처럼 퍼블릭 DNS 주소에 접근하게되면 nginx 웹페이지를 확인할 수 있습니다.
이제 스프링 부트와 연동하기 위해서는 엔진엑스의 설정파일을 수정해줘야합니다.
sudo vim /etc/nginx/nginx.conf
로 NGINX의 설정 파일에 접근합니다.
설정 파일은 이렇게 이루어져있고 저기 server밑에 location이 비워져있을것이고 저렇게 채워줍니다.
proxy_pass는 엔진엑스로 요청이오면 http://localhost:8080 으로 전달하겠다는 의미이고
proxy_set_header는 헤더에 항목들을 할당하겠다는 의미입니다.
이제 다시 퍼블릭 DNS로 접근하게되면 따로 :8080 을 붙여주지않아도 정상적으로 작동합니다!
스크립트를 작업하기전에 API 하나를 추가한다.
8081포트를 사용할지 8082를 쓸지 판단하는 기준이된다.
ProfileController
package com.example.board_project.controller;
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);
}
}
만들어진 ProfileController가 정상적으로 작동하는지 확인
& 추가로 springsecurity를 사용중이기때문에 /profile 이 접근 가능하게 하기위해서
SpringConfig에 /profile 이 인증없이 접근할 수 있게 설정
ProfileControllerTest
package com.example.board_project;
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 = "local";
ResponseEntity<String> response = restTemplate.getForEntity("/profile", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isEqualTo(expected);
}
}
ProfileControllerUnitTest
package com.example.board_project;
import com.example.board_project.controller.ProfileController;
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);
}
}
만들어진 ProfileController의 의미를 확실하게 알지 못했지만 차근차근 진행하면서 알아보도록 해보자
만들어진 ProfileController의
@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);
}
/profile 로 접근해서 profile이 잘 나오는지 확인하자!
ProfileController를 만들어주고 해당 기능이 문제가 없는지 테스트를 만들어놓은 후에 commit&push를 하니
com.example.board_project.ProfileControllerTest > profile은_인증없이_호출된다 FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:98
Caused by: org.springframework.beans.factory.BeanCreationException at AbstractAutowireCapableBeanFactory.java:1804
Caused by: javax.persistence.PersistenceException at AbstractEntityManagerFactoryBean.java:421
Caused by: org.hibernate.exception.JDBCConnectionException at SQLStateConversionDelegate.java:112
Caused by: com.mysql.cj.jdbc.exceptions.CommunicationsException at SQLError.java:175
Caused by: com.mysql.cj.exceptions.CJCommunicationsException at NativeConstructorAccessorImpl.java:-2
Caused by: java.net.ConnectException at PlainSocketImpl.java:-2
해당 오류가 발견되었다 내일 이어서 해결해보자