9장까지 만든 서비스의 단점은 배포하는 과정에서 새로운 Jar가 실행되기전까진 기존 Jar를 종료시켜야한다는 점때문에 서비스가 중단되는 문제가 발생한다.
그러나, 24시간 서비스를 하는 네이버,카카오톡등은 배포과정에서 서비스가 정지되지 않는데 이번 장에선 그 방법에 대해 알아볼 계획이다.
10.1 무중단 배포 소개
과거에는 배포를 하기 위해서, 사용자가 적은 시간대에 개발자들이 출근을 해서 배포를 진행했다고 한다. 서비스를 일시적으로 정지해야한다는 단점, 롤백이 어렵다는 단점들이 존재했다.
따라서, 무중단 배포를 위한 방법을 연구했고 여러가지 방법이 존재한다.
- AWS에서 블루 그린(Blue-Green)을 통한 무중단 배포
- 도커를 이용한 웹서비스 무중단 배포
- L4 스위치를 이용한 무중단 배포 ( 고가의 장비라 대기업 외에는 사용하지 않음 )
이번장에서 우리는, NGINX를 이용할 계획이다.
엔진엑스란?
- 웹 서버, 리버스 프록시, 캐싱, 로드 밸런싱, 미디어 스트리밍등을 위한 오픈소스 소프트웨어이다.
리버스 프록시란?
- 엔진엑스가 외부의 요청을 받아 이것을 백엔드 서버로 요청을 전달하는 행위를 의미함.
EC2에 적용할 계획이며 별도의 인프라를 구축할 필요가 없음. 개인서버/사내서버에도 적용가능
EC2 / 리눅스 서버에 1대의 엔진엑스와 2대의 스프링부트 Jar를 2대 사용하는 것.
- 엔진엑스 80(http), 443(https)포트를 할당한다.
- 스프링부트1은 8081
- 스프링부트 2는 8082 포트로 실행한다.
1. 사용자는 서비스주소 (80,443 포트)로 접속한다.
2. 엔진엑스는 해당 요청을 연결된 스프링부트로 요청을 전달한다.
3. 연결되지 않은 요청은 연결된 상태가 아니므로 요청을 못받는다.
이 과정에서 새로운 버전의 신규 배포가 필요하면 엔진엑스와 연결되지 않은 포트로 배포를 진행한다. 그러면 배포중 서비스가 중단되지 않고, 정상 구동을 할 수 있고 포트 연결을 수정해 업데이트를 할 수 있는 것이다.
10.2 엔진엑스 설치 및 스프링부트 연동하기
EC2에 엔진엑스를 설치하자.
책에서는 다음과 같은 명령어를 사용하라했는데 에러가 떴다.
sudo yum install nginx
nginx패키지가 존재하지 않으니 다른 명령어를 제시하라는 이야기 같았다.
sudo amazon-linux-extras install nginx1
위 명령어로 설치를 진행했다. 진행후 다음과 같은 코드로 엔진엑스를 실행했는데
sudo service nginx start
예상은 Starting nginx: [ OK ]가 나와야하는데
Redirecting to /bin/systemctl start nginx.service가 나왔다.
이게 이상해서 다른 블로그에 구글링을 해봤는데 맞게 된거라고 한다.
https://smpark1020.tistory.com/m/240
조금 의심되긴하지만 진행을 계속했다.
EC2 보안그룹에 80포트로 모두가 접근할 수 있는 IP 0.0.0.0/0, ::/0으로 추가를 진행한다.
이후엔 구글, 네이버 인증 페이지에서 승인된 리디렉션 url에 8080를 제외한 도메인을 추가해준다.
8080을 제외한 도메인에 등록하면 다음과 같은 NGINX 화면이 등장한다.
책과 다르게 위 페이지가 떠서 무언가 문제가 있을 것이라 생각했다. Further configuration이면 추가 구성이 필요하다는건데 뭘까..? nginx.org를 들어가도 해답을 찾을 수 없었다. 우선, 다음 설정이 configuration과 관련된 부분이라서 해당 내용부터 진행을 해봤다.
sudo vim /etc/nginx/nginx.conf
에 접속해 server - location 부분에 아래 코드를 추가해주자.
location /{
proxy_pass http://localhost:8080;
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;
}
proxy_pass : nginx 요청이 오면 해당 url로 요청을 전달하라. 즉, http://localhost:8080으로 요청을 전달한다.
proxy_set_header XXX : 실제 요청 데이터를 header의 각 항목에 할당하라
ex : proxy_set_header X-Real-IP $remote_addr: Request header의 X-Real-Ip에 요청자의 IP를 저장하자.
책에서는 맨 윗 줄만 추가하면 되는 것 같은데 나는 아예 해당 파트가 존재하지 않아서 직접 추가해줬다.
이후 restart를 통해 nginx를 재시작하고 8080을 제외한 EC2 퍼블릭 도메인으로 접근하면 정상적으로 서비스가 진행되는 것을 확인할 수 있다.
10.3 무중단 배포 스크립트 만들기
무중단 배포 스크립트 작업전 배포 시 8081, 8082 포트중 어느 포트를 쓸지 판단하는 API를 만들자.
ProfileController를 다음과 같이만들어주자.
package com.jojoldu.book.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);
}
}
env.getActiveProfiles()
- 현재 실행중인 ActiveProfile을 모두 가져오는 메소드.
- real,oauth,real-db등이 활성화 되어있다면 3개를 모두 담겨있다.
- real,real1,real2는 배포에 사용될 profile이라 이 중 하나라도 있으면 그 값을 반환해주자.
이어서 테스트 코드를 작성할 것이다. 해당 컨트롤러는 스프링환경이 필요하지 않기에 @SpringBootTest를 사용하지 않는다.
package com.jojoldu.book.springboot.web;
import org.junit.Test;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.AssertionsForClassTypes.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, Environment 모두 자바 클래스이기에 쉽게 테스트할 수 있었다. Environment는 인터페이스라 가짜 구현체 MockEnvironment를 사용해 테스트할 수 있다.
이어서, /profile이 인증없이도 호출될 수 있게 SecurityConfig클래스에 제외 코드를 추가하자.
.antMatchers("/","/css/**","/images/**","/js/**","/h2-console/**","/profile").permitAll()
즉, /profile의 url요청이 들어오면 무조건적인 허용을 해야한다는 뜻이다.
그러나, 이 과정에서 스프링 테스트를 진행했으나, 오류가 발생했다.
package com.jojoldu.book.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);
}
}
permitAll() 이니 default가 등장해야하는데 실제로는 oauth가 나오고 있다는 이야기이다.
/profile로 접속해도 로그인이 필요하다고 나온다.
뭐가 문제일까
- 테스트 코드 문제? : 다른 블로그의 MockMvc를 통한 테스트 코드로 확인했는데도 같은 결과가 나왔기에 로직의 문제는 아니다.
- ProfileController 문제? : UnitTest가 정상으로 진행되었기에 아닐것이라고 생각.
- SecurityConfig : 얘가 유력하긴한데 코드 상 잘못된 부분이 안보인단 말이지.
https://github.com/jojoldu/freelec-springboot2-webservice/issues/451
해당 이슈를 참고해서 해결함.
test의 application.properties의 spring.profiles.include='oauth'를 지워줬더니 해결되었다.
------------------------------------------ 스 터 디 마 감 ------------------------------------------------
해당 내용까지는 인텔리제이 테스트 코드 문제였다.
그러나 마냥 EC2에 배포를 하고 실행을 했을때 url에 /profile을 입력해주면 Oauth 권한이 필요해 로그인을 하라는 이야기가 나왔다.
관련 시도
/profile 관련 컨트롤러와 테스트 코드를 유심히 살펴봤다.
컨트롤러 코드는 존재하는 프로필을 받아온 후, 내용이 비어있으면 default, 아니면 존재하는 내용을 보여주는 코드이다.
SecurityConfig에 /profile 요청이 오면 모든 권한을 허용하라는 코드를 작성해줬다.
.antMatchers("/","/css/**","/images/**","/js/**","/h2-console/**","/profile").permitAll()
관련 우리가 만든 테스트에서도 모든 권한이 허용되면 default가 나오는지를 확인해야하는데 위에서 안나오는 부분을 해결했다.
EC2에 배포하고 travis에서 빌드를 하면 /profile에도 정상적용이 되어야하는데 안됐다.
왜그럴까? 쉘 스크립트에서 zip파일을 만드는걸 전체파일이 아닌 따로 설정을 해서일까? 고민을하다가 스터디를 진행했는데 윤정님께서 ec2를 중지/시작하고 nginx를 다시 start해보라하셨다.
그러니까 해결이 되었다...;; 뭐임
암튼 해결~~
real1,real2,profile생성
EC2 환경에서 실행되는 profile은 real밖에 없는데 real은 Travis CI 배포 자동화를 위한 profile이다.
따라서, 무중단 배포를 위한 real1, real2를 src/main/resource에 추가해주자.
application-real1.properties 파일이다 real, real2와는 포트만 다르다.
server.port=8081
spring.profiles.include=oauth,real-db
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.sesssion.store-type=jdbc
해당 코드를 추가하고 github에 푸시를 했는데 ec2-...url이 정상적으로 작동하지 않는다. Travis에서 build는 pass되었는데 어디서 막힌걸까?
EC2를 재시작해도 똑같네.. 뭐지
우선, 어디서부터 문제였을까 싶어서 엔진엑스 설정 수정한 스크립트를 다시 원상복구했다.
Travis CI에서 빌드 후 배포가 된다는 점을 이용해 Travis 빌드 히스토리를 이용해 잘못된 시점을 찾아보고자 했다.
또한 real1,real2프로필을 생성하기 이전의 빌드를 다시 Travis에서 실행하고 ec2-~~ url로 접속을 했더니 접속 정상적으로 되었다.
이어서, 이제 real1,real2 생성 이후의 빌드를 Travis에서 다시 빌드하니 실패했다.
이때 엔진엑스 설정을 수정해보자.
우선,, 수정을 했는데 server 윗단의 include를 실수로 지워서 기억이 안남...ㅎ;;;
이게 근데 메인 문제가 아닌거같은게 real1,real2 프로필 생성하고 푸시했을때 ec2 url 정상적으로 작동이 안되었어서 코드 문젠가? 뭐가 문젤까 ㅁ;ㅏㅇ널;나ㅣㅜ
동하님 깃헙에 푸시내용을 봤다. 왜인지는 모르겠지만 rea1,real2, real파일에 real이 들어가있었다.
spring.profiles.include=real,real-db,oauth
책하고 내용이 다르긴한데 일단 따라서 코드를 입력하고 푸시해봤다. 그런데 안됐다.
하하하하
음. real1,2를 추가하기전엔 제대로 작동이 됐어, real1,2를 추가하고 엔진엑스 설정 수정을 했지. 그런데 안돼 그러면 이 둘중에 문제가 있겠지? 근데 Travis에서 빌드는 통과를 하네? 빌드가 통과됐다는게 무슨말일까?
0314
일단 github에 이슈를 남겨보니 해당 명ㅇ령어를 통해 nginx 에러 로그를 확인해보라고 했다.
sudo vim /var/log/nginx/error.log
로그를 확인해보니 다음과 같은 에러로그가 있었다.
upstream timed out 에러인데 nginx 서버가 upstream 서버에서 응답을 받을 수 없어서 생기는 에러라고 한다.
ChatGPT가 말하길 내 upstream 서버는 http://127.0.0.1:8080이라고 하는데 하긴 proxy_pass에 service_url을 설정했으니 이 주소로 무언가가 작동하게 되어야한다. 근데 http://127.0.0.1:8080에서 뭐 작동하게 넣었던 적이 있ㄴ나?
딱히 없는 것 같은데... 일단 책을 한 번 다시 읽어봐야겠다.
'BE > 6기 코테이토 - Spring Study' 카테고리의 다른 글
5회. Ch9 - 코드가 푸시되면 자동으로 배포해보자- Travis CI 배포 자동화 (0) | 2023.02.22 |
---|---|
4회(2). Ch8 - EC2서버에 프로젝트를 배포해보자 (0) | 2023.02.17 |
4.5회. Ch8. 다시도전 (그래도 실패를 곁들인) (0) | 2023.02.17 |
4회(1). Ch8 - EC2서버에 프로젝트를 배포해보자 (0) | 2023.02.15 |
4회. Ch7 - AWS에 데이터베이스 환경을 만들어보자. - AWS RDS (0) | 2023.02.14 |