diff --git a/WebServiceBySpringBootAndAWS/WebServiceBySpringBootAndAWS.md b/WebServiceBySpringBootAndAWS/WebServiceBySpringBootAndAWS.md index c09fd70..bbc5235 100644 --- a/WebServiceBySpringBootAndAWS/WebServiceBySpringBootAndAWS.md +++ b/WebServiceBySpringBootAndAWS/WebServiceBySpringBootAndAWS.md @@ -8,4 +8,5 @@ - [Chapter6. AWS 서버 환경을 만들어보자 - AWS EC2](https://github.com/banjjoknim/TIL/blob/master/WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter6.md) - [Chapter7. AWS에 데이터베이스 환경을 만들어보자 - AWS RDS](https://github.com/banjjoknim/TIL/blob/master/WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter7.md) - [Chapter8. EC2 서버에 프로젝트를 배포해 보자](https://github.com/banjjoknim/TIL/blob/master/WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter8.md) -- [Chapter9. 코드가 푸시되면 자동으로 배포해 보자 - Travis CI 배포 자동화](https://github.com/banjjoknim/TIL/blob/master/WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter9.md) \ No newline at end of file +- [Chapter9. 코드가 푸시되면 자동으로 배포해 보자 - Travis CI 배포 자동화](https://github.com/banjjoknim/TIL/blob/master/WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter9.md) +- [Chapter10. 24시간 365일 중단 없는 서비스를 만들자](https://github.com/banjjoknim/TIL/blob/master/WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter10.md) \ No newline at end of file diff --git a/WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter10.md b/WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter10.md new file mode 100644 index 0000000..c7540ae --- /dev/null +++ b/WebServiceBySpringBootAndAWS/src/ChapterDescription/Chapter10.md @@ -0,0 +1,681 @@ +# Chapter10. 24시간 365일 중단 없는 서비스를 만들자 +`Travis CI`를 활용하여 배포 자동화 환경을 구축해 보았습니다. 하지만 배포하는 동안 애플리케이션이 종료된다는 문제가 남았습니다. 긴 기간은 아니지만, **새로운 Jar가 실행되기 전까진 기존 Jar를 종료시켜 놓기 때문에** 서비스가 중단됩니다. + +--- + +## 10.1 무중단 배포 소개 +서비스를 정지하지 않고, 배포할 수 있는 방법을 **무중단 배포**라고 합니다. + +무중단 배포 방식에는 몇 가지가 있습니다. + +- `AWS`에서 블루 그린(`Blue-Green`) 무중단 배포 +- 도커를 이용한 웹서비스 무중단 배포 + +이 외에도 `L4 스위치`를 이용한 무중단 배포 방법도 있지만, `L4`가 워낙 고가의 장비이다 보니 대형 인터넷 기업 외에는 쓸 일이 거의 없습니다. + +여기서 진행할 방법은 **엔진엑스(Nginx)** 를 이용한 무중단 배포입니다. 엔진엑스는 웹 서버, 리버스 프록시, 캐싱, 로드 밸런싱, 미디어 스트리밍 등을 위한 오픈소스 소프트웨어입니다. + +엔진엑스가 가지고 있는 여러 기능 중 `리버스 프록시`가 있습니다. `리버스 프록시`란 엔진엑스가 **외부의 요청을 받아 백엔드 서버로 요청을 전달**하는 행위를 이야기합니다. 리버스 프록시 서버(엔진엑스)는 요청을 전달하고, 실제 요청에 대한 처리는 뒷단의 웹 애플리케이션 서버들이 처리합니다. + +엔진엑스를 이용한 무중단 배포를 하는 이유는 간단합니다. **가장 저렴하고 쉽기 때문**입니다. + +기존에 쓰던 `EC2`에 그대로 적용하면 되므로 배포를 위해 `AWS EC2 인스턴스`가 하나 더 필요하지 않습니다. 추가로 이 방식은 꼭 `AWS`와 같은 클라우드 인프라가 구축되어 있지 않아도 사용할 수 있는 범용적인 방법입니다. 즉, 개인 서버 혹은 사내 서버에서도 동일한 방식으로 구축할 수 있으므로 사용처가 많습니다. + +구조는 간단합니다. 하나의 `EC2` 혹은 리눅스 서버에 엔진엑스 1대와 **스프링 부트 Jar를 2대** 사용하는 것입니다. + +- 엔진엑스는 `80(http)`, `443(https)` 포트를 할당합니다. +- `스프링 부트1`은 `8081`포트로 실행합니다. +- `스프링 부트2`는 `8082`포트로 실행합니다. + +**엔진엑스 무중단 배포 1**은 다음과 같은 구조가 됩니다. + +![Chapter10_nginx_무중단배포_1](https://user-images.githubusercontent.com/68052095/101275875-79c9a600-37ec-11eb-98e8-8bf136781f9a.png) + +운영 과정은 다음과 같습니다. + +- ① 사용자는 서비스 주소로 접속합니다(`80` 혹은 `443` 포트). +- ② 엔진엑스는 사용자의 요청을 받아 현재 연결된 스프링 부트로 요청을 전달합니다. + - 스프링 부트1 즉, 8081 포트로 요청을 전달한다고 가정합니다. +- ③ 스프링 부트2는 엔진엑스와 연결된 상태가 아니니 요청받지 못합니다. + +1.1 버전으로 신규 배포가 필요하면, 엔진엑스와 연결되지 않은 스프링 부트2(8082 포트)로 배포합니다(아래 사진). + +![Chapter10_nginx_무중단배포_2](https://user-images.githubusercontent.com/68052095/101275874-79310f80-37ec-11eb-85ce-4684565138c6.png) + +- ① 배포하는 동안에도 서비스는 중단되지 않습니다. + - 엔진엑스는 스프링 부트1을 바라보기 때문입니다. +- ② 배포가 끝나고 정상적으로 스프링 부트2가 구동 중인지 확인합니다. +- ③ 스프링 부트2가 정상 구동 중이면 `nginx reload` 명령어를 통해 `8081` 대신에 `8082`를 바라보도록 합니다. +- ④ `nginx reload`는 0.1초 이내에 완료됩니다. + +이후 1.2 버전 배포가 필요하면 이번에는 스프링 부트1로 배포합니다(아래 사진). + +![Chapter10_nginx_무중단배포_3](https://user-images.githubusercontent.com/68052095/101275873-79310f80-37ec-11eb-85e5-f727663f69ca.png) + +- ① 현재는 엔진엑스와 연결된 것이 스프링 부트2입니다. +- ② 스프링 부트1의 배포가 끝났다면 엔진엑스가 스프링 부트1을 바라보도록 변경하고 `nginx reload`를 실행합니다. +- ③ 이후 요청부터는 엔진엑스가 스프링 부트 1로 요청을 전달합니다. + +이렇게 구성하게 되면 전체 시스템 구조는 다음과 같습니다. + +![Chapter10_nginx_무중단배포_전체_구조](https://user-images.githubusercontent.com/68052095/101275872-77ffe280-37ec-11eb-88eb-c1f6e69b8fa6.png) + +기존 구조에서 `EC2` 내부의 구조만 변경된 것이니 크게 걱정하지 않아도 됩니다. + +사진 출처 : [기억보단 기록을](https://jojoldu.tistory.com/267) + +--- + +## 10.2 엔진엑스 설치와 스프링 부트 연동하기 +가장 먼저 `EC2`에 엔진엑스를 설치하겠습니다. + +#### 엔진엑스 설치 +`EC2`에 접속해서 다음 명령어로 엔진엑스를 설치합니다. + +>sudo yum install nginx + +설치가 완료되었으면 다음 명령어로 엔진엑스를 실행합니다. + +>sudo service nginx start + +엔진엑스가 잘 실행되었다면 다음과 같은 메시지를 볼 수 있습니다. + +>Starting nginx: [ OK ] + +>###### 학습중 발생 오류 추가 +>![Chapter10_nginx_service_start_error](https://user-images.githubusercontent.com/68052095/101275172-e346b600-37e6-11eb-94d4-2274484363a6.PNG) +>명령어로 `sudo service nginx start` 대신 `sudo systemctl start nginx` 를 사용 +>멈추고 싶다면 `sudo systemctl stop nginx` 명령어를 이용하면 된다. +>상태를 확인하고 싶다면 `sudo systemctl status nginx` 명령어를 이용한다. +> +>참고 링크 : [AWS EC2에 NGINX 설치 및 사용하기](https://msyu1207.tistory.com/entry/AWS-EC2%EC%97%90-NGINX-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0) + +외부에서 잘 노출되는지 확인해 보겠습니다. + +#### 보안 그룹 추가 +먼저 엔진엑스의 포트번호를 보안 그룹에 추가하겠습니다. 엔진엑스의 포트번호는 기본적으로 `80`입니다. 해당 포트 번호가 보안 그룹에 없으니 `[EC2 -> 보안 그룹 -> EC2 보안 그룹 선택 -> 인바운드 편집]`으로 차례로 이동해서 변경합니다. + +![80번 포트를 보안 그룹에 추가] + +![Chapter10_nginx_inbound_add](https://user-images.githubusercontent.com/68052095/101275268-b0e98880-37e7-11eb-8060-22478c0c4da5.PNG) + +#### 리다이렉션 주소 추가 +`8080`이 아닌 `80`포트로 주소가 변경되니 구글과 네이버 로그인에도 변경된 주소를 등록해야만 합니다. 기존에 등록된 리디렉션 주소에서 `8080` 부분을 제거하여 추가 등록합니다. 앞서 진행된 `Chapter8`을 참고하여 구글과 네이버에 차례로 등록합니다. + +![Chapter10_nginx_google_domain_add](https://user-images.githubusercontent.com/68052095/101275320-1ccbf100-37e8-11eb-904a-d92e28c42419.png) + +![Chapter10_nginx_naver_domain_add](https://user-images.githubusercontent.com/68052095/101275369-67e60400-37e8-11eb-80b3-b45109d9b84c.png) + +추가한 후에는 `EC2`의 도메인으로 접근하되, **8080 포트를 제거하고** 접근해 봅니다. 즉, 포트번호 없이 도메인만 입력해서 브라우저에서 접속합니다. + +>`80번 포트는 기본적으로 도메인에서 포트번호가 제거된 상태입니다.` + +그럼 다음과 같이 엔진엑스 웹페이지를 볼 수 있습니다. + +![Chapter10_nginx_home](https://user-images.githubusercontent.com/68052095/101275799-da0c1800-37eb-11eb-97b5-dc7cd1db5c99.PNG) + +이제 스프링 부트와 연동해 보겠습니다. + +#### 엔진엑스와 스프링 부트 연동 +엔진엑스가 현재 실행 중인 스프링 부트 프로젝트를 바라볼 수 있도록 프록시 설정을 하겠습니다. 엔진엑스 설정 파일을 열어봅니다. + +>sudo vim /etc/nginx/nginx.conf + +설정 내용 중 `server` 아래의 `location /` 부분을 찾아서 다음과 같이 추가합니다. + +![Chapter10_nginx_location](https://user-images.githubusercontent.com/68052095/101276215-85b66780-37ee-11eb-820a-dc838291c482.png) + +>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** +- 엔진엑스로 요청이 오면 `http://localhost:8080`로 전달합니다. + +**② proxy_set_header XXX** +- 실제 요청 데이터를 `header`의 각 항목에 할당합니다. +- 예) `proxy_set_header X-Real-IP $remote_addr` : `Request Header`의 `X-Real-IP`에 요청자의 `IP`를 저장합니다. + +##### ----------------------- + +수정이 끝났으면 `:wq` 명령어로 저장하고 종료해서, 엔진엑스를 재시작 하겠습니다. + +>sudo service nginx restart + +>###### 학습중 발생 오류 추가 +> +>위의 동작, 정지와 같은 오류로 인해 `sudo systemctl restart nginx` 사용하여 해결. + + +다시 브라우저로 접속해서 엔진엑스 시작 페이지가 보이면 화면을 새로고침합니다. + +엔진엑스가 스프링 부트 프로젝트를 프록시하는 것이 확인됩니다(기본 페이지가 보입니다). 본격적으로 무중단 배포 작업을 진행해 보겠습니다. + +--- + +## 10.3 무중단 배포 스크립트 만들기 +무중단 배포 스크립트 작업 전에 API를 하나 추가하겠습니다. 이 API는 이후 배포 시에 `8081`을 쓸지, `8082`를 쓸지 판단하는 기준이 됩니다. + +### profile API 추가 +`ProfileController`를 만들어 다음과 같이 간단한 API 코드를 추가합니다. + +```java +package com.banjjoknim.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 profiles = Arrays.asList(env.getActiveProfiles()); // ① + + List 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`, `real1`, `real2`는 모두 배포에 사용될 `profile`이라 이 중 하나라도 있으면 그 값을 반환하도록 합니다. +- 실제로 이번 무중단 배포에서는 `real1`과 `real2`만 사용되지만, `step2`를 다시 사용해볼 수도 있으니 `real`도 남겨둡니다. +##### ----------------------- + +이 코드가 잘 작동하는지 테스트 코드를 작성해 보겠습니다. 해당 컨트롤러는 특별히 **스프링 환경이 필요하지는 않습니다.** 그래서 `@SpringBootTest` 없이 테스트 코드를 작성합니다. + +```java +package com.banjjoknim.book.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); + } + +} +``` +`ProfileController`나 `Environment` 모두 **자바 클래스(인터페이스)**이기 때문에 쉽게 테스트할 수 있습니다. `Environment`는 인터페이스라 가짜 구현체인 `MockEnvironment`(스프링에서 제공)를 사용해서 테스트하면 됩니다. + +이렇게 해보면 **생성자 DI가 얼마나 유용한지** 알 수 있습니다. 만약 `Environment`를 `@Autowired`로 `DI` 받았다면 **이런 테스트 코드를 작성하지 못 했습니다.** 항상 스프링 테스트를 해야했을 것입니다. 앞의 테스트가 다 통과했다면 컨트롤러 로직에 대한 이슈는 없습니다. + +그리고 이 `/profile`이 **인증 없이도 호출될 수 있게** `SecurityConfig` 클래스에 제외 코드를 추가합니다. + +```java +.antMatchers("/", "/css/**", "/images/**", "/js/**", "/h2-console/**", "/profile").permitAll(); +``` +##### -----코드설명----- +**① permitAll 마지막에 "/profile"이 추가됩니다.** +##### ---------------------- + +그리고 `SecurityConfig` 설정이 잘 되었는지도 테스트 코드로 검증합니다. 이 검증은 스프링 시큐리티 설정을 불러와야 하니 `@SpringBootTest`를 사용하는 테스트 클래스(`ProfileControllerTest`)를 하나 더 추가합니다. + +```java +package com.banjjoknim.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은_인증없이_호출된다() { + String expected = "default"; + + ResponseEntity response = restTemplate.getForEntity("/profile", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(expected); + } +} +``` +여기까지 모든 테스트가 성공했다면 깃허브로 푸시하여 배포 합니다. 배포가 끝나면 브라우저에서 `/profile`로 접속해서 `profile`이 잘 나오는지 확인합니다. + +여기까지 잘 되었으면 잘 구성된 것이니 다음으로 넘어갑니다. + +### real1, real2 profile 생성 +현재 `EC2` 환경에서 실행되는 `profile`은 `real`밖에 없습니다. 해당 `profile`은 **Travis CI 배포 자동화를 위한** `profile`이니 무중단 배포를 위한 `profile` 2개(`real1`, `real2`)를 `src/main/resources` 아래에 추가합니다. + +```java +//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 +``` + +```java +//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 +``` + +2개의 `profile`은 `real profile`과 크게 다른 점은 없지만, 한 가지가 다릅니다. + +`server.port`가 `8080`이 아닌 `8081/8082`로 되어 있습니다. 이 부분만 주의해서 생성하고 생성된 후에는 깃허브로 푸시하면서 마무리합니다. + +### 엔진엑스 설정 수정 +무중단 배포의 핵심은 **엔진엑스 설정**입니다. 배포 때마다 엔진엑스의 프록시 설정(스프링 부트로 요청을 흘려보내는)이 순식간에 교체됩니다. 여기서 프록시 설정이 교체될 수 있도록 설정을 추가하겠습니다. + +엔진엑스 설정이 모여있는 `/etc/nginx/conf.d/ `에 `service-url.inc`라는 파일을 하나 생성합니다. + +>sudo vim /etc/nginx/conf.d/service-url.inc + +그리고 다음 코드를 입력합니다. + +>set \$service_url http://127.0.0.1:8080; + +저장하고 종료한 뒤(`:wq`) 해당 파일은 엔진엑스가 사용할 수 있게 설정합니다. 다음과 같이 `nginx.conf` 파일을 열겠습니다. + +>sudo vim /etc/nginx/nginx.conf + +`location / `부분을 찾아 다음과 같이 변경합니다. + +>include /etc/nginx/conf.d/service-url.inc; +> +>location / { +> proxy_pass \$service_url; +>} + +저장하고 종료한 뒤(`:wq`) **재시작**합니다. + +> sudo service nginx restart + +다시 브라우저에서 정상적으로 호출되는지 확인합니다. 확인되었다면 엔진엑스 설정까지 잘 된 것입니다. + +### 배포 스크립트들 작성 +먼저 `step2`와 중복되지 않기 위해 `EC2`에 `step3` 디렉토리를 생성합니다. + +>mkdir ~/app/step3 && mkdir ~/app/step3/zip + +무중단 배포는 앞으로 `step3`를 사용하겠습니다. 그래서 `appspec.yml` 역시 `step3`로 배포되도록 수정합니다. + +```sh +version: 0.0 +os: linux +files: + - source: / + destination: /home/ec2-user/app/step3/zip/ + overwrite: yes +``` +무중단 배포를 진행할 스크립트들은 총 5개입니다. + +- `stop.sh` : 기존 엔진엑스에 연결되어 있진 않지만, 실행 중이던 스프링 부트 종료 +- `start.sh` : 배포할 신규 버전 스프링 부트 프로젝트를 `stop.sh`로 종료한 `profile`로 실행 +- `health.sh` : `start.sh`로 실행시킨 프로젝트가 정상적으로 실행됐는지 체크 +- `switch.sh` : 엔진엑스가 바라보는 스프링 부트를 최신 버전으로 변경 +- `profile.sh` : 앞선 4개 스크립트 파일에서 공용으로 사용할 `profile` 과 포트 체크 로직 + +`appspec.yml`에 앞선 스크립트를 사용하도록 설정합니다. + +``` +hooks: + AfterInstall: + - location: stop.sh # 엔진엑스와 연결되어 있지 않은 스프링 부트를 종료합니다. + timeout: 60 + runas: ec2-user + ApplicationStart: + - location: start.sh # 엔진엑스와 연결되어 있지 않은 Port로 새 버전의 스프링 부트를 시작합니다. + timeout: 60 + runas: ec2-user + ValidateService: + - location: health.sh # 새 스프링 부트가 정상적으로 실행됐는지 확인합니다. + timeout: 60 + runas: ec2-user +``` +`Jar` 파을이 복사된 이후부터 차례로 앞선 스크립트들이 실행된다고 보면 됩니다. 다음은 각 스크립트입니다. 이 스크립트들 역시 `scripts` 디렉토리에 추가합니다. + +![Chapter10_scripts](https://user-images.githubusercontent.com/68052095/101280263-7db8f080-380b-11eb-9ac8-d0e81bb7397c.PNG) + +#### `profile.sh` +```sh +#!/usr/bin/env bash + +# 쉬고 있는 profile 찾기: real1이 사용 중이면 real2가 쉬고 있고, 반대면 real1이 쉬고 있음 + +function find_idle_profile() { + RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/profile) ① + + if [ ${RESPONSE_CODE} -ge 400 ] # 400 보다 크면(즉, 40x/50x 에러 모두 포함) + 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 +} +``` +##### -----코드설명----- +**① `$(curl -s -o /dev/null -w "%{http_code}" http://localhost/profile)`** +- 현재 엔진엑스가 바라보고 있는 스프링 부트가 정상적으로 수행 중인지 확인합니다. +- 응답값을 `HttpStatus`로 받습니다. +- 정상이면 `200`, 오류가 발생한다면 `400 ~ 503` 사이로 발생하니 `400` 이상은 모두 예외로 보고 `real2`를 **현재 profile로 사용**합니다. + +**② `IDLE_PROFILE`** +- 엔진엑스와 연결되지 않은 `profile`입니다. +- 스프링 부트 프로젝트를 이 `profile`로 연결하기 위해 반환합니다. + +**③ `echo "${IDLE_PROFILE}"`** +- `bash`라는 스크립트는 **값을 반환하는 기능이 없습니다.** +- 그래서 **제일 마지막 줄에 echo로 결과를 출력** 후, 클라이언트에서 그 값을 잡아서 (`$(find_idle_profile)`) 사용합니다. +- 중간에 `echo`를 사용해선 안 됩니다. +##### ---------------------- + +#### **`stop.sh`** +```sh +#!/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 +``` +##### -----코드설명----- +**① `ABSDIR=$(dirname $ABSPATH)`** +- 현재 `stop.sh`가 속해 있는 경로를 찾습니다. +- 하단의 코드와 같이 `profile.sh`의 경로를 찾기 위해 사용됩니다. + +**② `source ${ABSDIR}/profile.sh`** +- 자바로 보면 일종의 `import` 구문입니다. +- 해당 코드로 인해 `stop.sh`에서도 `profile.sh`의 여러 `function`을 사용할 수 있게 됩니다. +##### ----------------------- + +#### **`start.sh`** +```sh +#!/usr/bin/env bash + +ABSPATH=$(readlink -f $0) +ABSDIR=$(dirname $ABSPATH) +source ${ABSDIR}/profile.sh + +REPOSITORY=/home/ec2-user/app/step3 +PROJECT_NAME=SpringBootWebService + +echo "> Build 파일 복사" +echo "> cp $REPOSITORY/zip/*.jar $REPOSITORY/" + +cp $REPOSITORY/zip/*.jar $REPOSITORY/ + +echo "> 새 애플리케이션 배포" +JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -n 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 & +``` + +##### -----코드설명----- +**① `기본적인 스크립트는 step2의 deploy.sh와 유사합니다`** +**② `다른 점이라면 IDLE_PROFILE을 통해 properties 파일을 가져오고(application-$IDLE_PROFILE.properties), active profile을 지정하는 것(-Dspring.profiles.active=$IDLE_PROFILE) 뿐입니다.`** +**③ `여기서도 IDLE_PROFILE을 사용하니 profile.sh을 가져와야 합니다.`** +##### ---------------------- + +#### **`health.sh`** +```sh +#!/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_proxy`)합니다.** +**③ 엔진엑스 프록시 설정 변경은 `switch.sh`에서 수행합니다.** +##### ---------------------- + +#### `switch.sh` + +```sh +#!/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 +} +``` +##### -----코드설명----- +**① `echo "set \$service_url http://127.0.0.1:${IDLE_PORT};"`** +- 하나의 문장을 만들어 파이프라인(`|`)으로 넘겨주기 위해 `echo`를 사용합니다. +- 엔진엑스가 변경할 프록시 주소를 생성합니다. +- 쌍따옴표(`"`)를 사용해야 합니다. +- 사용하지 않으면 `$service_url`을 그대로 인식하지 못하고 변수를 찾게 됩니다. + +**② `| sudo tee /etc/nginx/conf.d/service-url.inc`** +- 앞에서 넘겨준 문장을 `service-url.inc`에 덮어씌웁니다. + +**③ `sudo service nginx reload`** +- 엔진엑스 설정을 다시 불러옵니다. +- **`restart`와는 다릅니다.** +- `restart`는 잠시 끊기는 현상이 있지만, `reload`는 끊김 없이 다시 불러옵니다. +- 다만, 중요한 설정들은 반영되지 않으므로 `restart`를 다시 사용해야 합니다. +- 여기선 **외부의 설정 파일**인 `service-url`을 다시 불러오는 거라 `reload`로 가능합니다. + +스크립트까지 모두 완성했습니다. 그럼 실제로 무중단 배포를 진행해 보겠습니다. + +--- + +## 10.4 무중단 배포 테스트 +배포 테스트를 하기 전, 한 가지 추가 작업을 진행하도록 하겠습니다. 잦은 배포로 `Jar` 파일명이 겹칠 수 있습니다. 매번 버전을 올리는 것이 귀찮으므로 자동으로 버전값이 변경될 수 있도록 조치하겟습니다. + +#### `build.gradle` +```java +version '1.0.1-SNAPSHOT-'+new Date().format("yyyyMMddHHmmss") +``` +##### -----코드설명----- +**① `build.gradle`은 `Groovy` 기반의 빌드툴입니다.** +**② 당연히 `Groovy` 언어의 여러 문법을 사용할 수 있는데, 여기서는 `new Date()`로 빌드할 때마다 그 시간이 버전에 추가되도록 구성하였습니다.** +##### ---------------------- + +여기까지 구성한 뒤 최종 코드를 깃허브로 푸시합니다. 배포가 자동으로 진행되면 `CodeDeploy` 로그로 잘 진행되는지 확인해 봅니다. + +>`tail -f /opt/codedeploy-agent/deployment-root/deployment-logs/codedeploy-agent-deployments.log` + +그럼 다음과 같은 메시지가 차례로 출력됩니다. + +![Chapter10_deployments_log](https://user-images.githubusercontent.com/68052095/101282400-53216480-3818-11eb-9d7b-c844c5f37f3f.PNG) + +스프링 부트 로그도 보고 싶다면 다음 명령어로 확인할 수 있습니다. + +>`vim ~/app/step3/nohup.out` + +그럼 스프링 부트 실행 로그를 직접 볼 수 있습니다. 한 번 더 배포하면 그때는 `real2`로 배포됩니다. 이 과정에서 브라우저 새로고침을 해보면 전혀 중단 없는 것을 확인할 수 있습니다. 2번 배포를 진행한 뒤에 다음과 같이 자바 애플리케이션 실행 여부를 확인합니다. + +>`ps -ef | grep java` + +다음과 같이 2개의 애플리케이션(`real1`, `real2`)이 실행되고 있음을 알 수 있습니다. + +![Chapter10_grep_java](https://user-images.githubusercontent.com/68052095/101282469-aa273980-3818-11eb-87e5-3fde7e548517.png) + +이제 이 시스템은 마스터 브랜치에 푸시가 발생하면 자동으로 서버 배포가 진행되고, 서버 중단 역시 전혀 없는 시스템이 되었습니다. + +--- + +#### 추가사항 +실습 중간중간에 스크립트에 문제가 있는 것인지 새롭게 배포를 할 때마다 기존에 실행되어있던 프로젝트가 종료되지 않았고, 그로 인해 `java.net.BindException: Address already in use (Bind failed)` 에러가 발생하여 제대로 되지 않았다. 이 에러는 8080포트로 이미 실행되어 있는 `PID`를 찾아서 종료해준 뒤 배포하면 제대로 배포가 되는 것을 볼 수 있다. + +참고 링크 +- [Address already in use 혹은 Bind failed 에러 해결하기](https://fishpoint.tistory.com/3746) +- [Address already in use (Bind failed) 에러 해결하기](https://philip1994.tistory.com/6) +- [netstat 명령어를 통한 네트워크 상태 확인 방법](http://blog.naver.com/PostView.nhn?blogId=ncloud24&logNo=221388026417&parentCategoryNo=&categoryNo=79&viewDate=&isShowPopularPosts=false&from=postView) \ No newline at end of file diff --git a/WebServiceBySpringBootAndAWS/src/images/Chapter10_deployments_log.PNG b/WebServiceBySpringBootAndAWS/src/images/Chapter10_deployments_log.PNG new file mode 100644 index 0000000..7fa750d Binary files /dev/null and b/WebServiceBySpringBootAndAWS/src/images/Chapter10_deployments_log.PNG differ diff --git a/WebServiceBySpringBootAndAWS/src/images/Chapter10_grep_java.png b/WebServiceBySpringBootAndAWS/src/images/Chapter10_grep_java.png new file mode 100644 index 0000000..bd209b1 Binary files /dev/null and b/WebServiceBySpringBootAndAWS/src/images/Chapter10_grep_java.png differ diff --git a/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_google_domain_add.png b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_google_domain_add.png new file mode 100644 index 0000000..e1a5241 Binary files /dev/null and b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_google_domain_add.png differ diff --git a/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_home.PNG b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_home.PNG new file mode 100644 index 0000000..bad34be Binary files /dev/null and b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_home.PNG differ diff --git a/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_inbound_add.PNG b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_inbound_add.PNG new file mode 100644 index 0000000..52ca21b Binary files /dev/null and b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_inbound_add.PNG differ diff --git a/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_location.png b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_location.png new file mode 100644 index 0000000..399892b Binary files /dev/null and b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_location.png differ diff --git a/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_naver_domain_add.png b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_naver_domain_add.png new file mode 100644 index 0000000..34f1407 Binary files /dev/null and b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_naver_domain_add.png differ diff --git a/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_service_start_error.PNG b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_service_start_error.PNG new file mode 100644 index 0000000..244f45d Binary files /dev/null and b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_service_start_error.PNG differ diff --git a/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_무중단배포_1.png b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_무중단배포_1.png new file mode 100644 index 0000000..f1d73a6 Binary files /dev/null and b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_무중단배포_1.png differ diff --git a/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_무중단배포_2.png b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_무중단배포_2.png new file mode 100644 index 0000000..ad95c6c Binary files /dev/null and b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_무중단배포_2.png differ diff --git a/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_무중단배포_3.png b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_무중단배포_3.png new file mode 100644 index 0000000..a9b42f2 Binary files /dev/null and b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_무중단배포_3.png differ diff --git a/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_무중단배포_전체_구조.png b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_무중단배포_전체_구조.png new file mode 100644 index 0000000..1fc1a04 Binary files /dev/null and b/WebServiceBySpringBootAndAWS/src/images/Chapter10_nginx_무중단배포_전체_구조.png differ diff --git a/WebServiceBySpringBootAndAWS/src/images/Chapter10_scripts.PNG b/WebServiceBySpringBootAndAWS/src/images/Chapter10_scripts.PNG new file mode 100644 index 0000000..63c2ac4 Binary files /dev/null and b/WebServiceBySpringBootAndAWS/src/images/Chapter10_scripts.PNG differ