9장에서 Travis라는 CI툴을 이용하여 배포 자동화 환경을 구축했습니다.
단, 배포하는 동안 애플리케이션이 종료된다는 문제가 있었습니다. 새로운 Jar가 실행되기 전 까진 기존 Jar를 종료시켜 놓기 때문에 서비스가 중단되는 현상입니다.
Jar(Java Archive)파일이란?
여러개의 자바 클래스 파일과, 클래스들이 이용하는 관련 리소스(텍스트, 그림 등) 및 메타데이터를 하나의 파일로 모아서 자바 플랫폼에 응용 소프트웨어나 라이브러리를 배포하기 위한 소프트웨어 패키지 파일 포맷입니다.
사용자는 JDK에 포함된 'jar' 명령어를 통해 파일을 압축하거나 압축을 풀 수 있습니다.
예전부터 궁금했던 내용입니다. 어떻게 서버를 닫지 않고 수정을 한 뒤 서버가 열린 상태에서 배포가 가능했을까요?
실제로 예전에는 배포를 하기 위해 날을 정해놓고 (특히 이용자가 적은 새벽 시간대)배포를 했다고 합니다.
무중단 배포를 하는 몇 가지 방법이 있습니다.
- AWS에서 블루 그린 무중단 배포
- 도커를 이용한 웹서비스 무중단 배포
- L4 스위치를 이용한 무중단 배포(고 비용)
저희가 사용할 방법은 엔진엑스(Nginx)를 이용한 무중단 배포입니다.
엔진엑스(Nginx)란?
엔진엑스는 동시 접속처리에 특화된 웹 서버 프로그램이며 웹 서버, 리버스 프록시, 캐싱, 로드 밸런싱, 미디어 스트리밍 등을 위한 오픈소스 소프트웨어입니다. 동시 접속자가 700만명 이상일 때 서버를 증설하거나, Nginx환경을 이용합니다.
그렇기 때문에 접속마다 Thread or Process를 생성하는 구조인 Apache의 자리를 빼앗기 충분했습니다.
엔진엑스가 가지고 있는 리버스 프록시는 엔진엑스가 외부의 요청을 받아 백엔드 서버로 요청을 전달하는 역할을 합니다. 그래서 실제 요청에 대한 처리는 뒷단의 웹 애플리케이션 서버들이 처리하게되는 구조입니다.
기존 사용하던 EC2에 그대로 적용이 가능하고, AWS와 같은 클라우드 인프라가 구축되어 있지 않아도 개인, 사내 서버에 구축이 가능합니다.
무중단 배포의 구조는 서버의 엔진엑스 1대와, 스프링 부트 Jar 파일을 2대 이용하는 것입니다.
저렇게 사용자는 80(http), 443(https) 포트가 할당 되어있고 해당 포트로 연결을 요청하면 현재 엔진엑스와 연결된 스프링 부트로 연결을 요청합니다.
배포가 필요하면 다른 jar파일로 작업을 하고 엔진엑스가 바라보는 jar만 바꿔주면 됩니다.
EC2로 접속하여 nginx를 설치합니다
sudo yum install nginx
설치 완료 후 엔진엑스를 실행합니다.
sudo service nginx start
그럼 Redirecting to /bin/systemctl start nginx.service
라는 메시지가 출력이 되는데, 프로세스가 실행되어있는지 확인합니다.
ps -ef | grep nginx
다음으로 엔진엑스의 포트번호를 EC2 보안 그룹에 추가합니다. 포트는 기본 80입니다.
그 후 네이버와 구글에서승인된 리디렉션 URI를 ec2 도메인에서 8080포트만 제거하고 추가합니다.
그럼 EC2 도메인에 8080을 제거하고 접속하면 다음과 같이 엔진엑스 웹페이지가 나옵니다.
이제 이것을 스프링 부트와 연결 시켜야 합니다.
위에서 말씀드렸듯이, 엔진엑스가 스프링 부트를 바라보게 해야 합니다. 프록시 설정을 하기 위해 엔진엑스 설정 파일을 열어봅니다.
sudo vim /etc/nginx/nginx.conf
그 후 server 부분에 다음의 코드를 추가합니다.
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;
}
엔진엑스로 요청이 오면, localhost:8080으로 전달하는 코드입니다. 결과적으로 엔진엑스가 스프링 부트 프로젝트를 프록시하는 것을 확인할 수 있습니다.
스크립트를 만들기 전에 배포 시 8081을 쓸지, 8082 포트를 쓸지 판단하는 API를 만듭니다.
web/ProfileController.java
package com.qweadzs.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);
}
}
그리고 이 API가 정상 작동하는지, SecurityConfig도 작동을 하는지에 대한 검증을 진행하면 됩니다. 검증은 당연히 테스트 코드를 작성하여 진행합니다!
/profile이 인증 없이도 호출될 수 있게 SecurityConfig에 다음 코드를 추가합니다.
.antMatchers("/", "/css/**", "/images/**", "/js/**", "/h2-console/**", "/profile").permitAll()
모든 테스트가 성공했다면 깃허브로 배포합니다. 그 후 브라우저에서 /profile로 접근해봅니다.
현재 EC2에서 실행되는 profile은 real뿐입니다. 해당 profile은 Travis CI 배포 자동화를 위한 profile이니 무중단 배포를 위한 profile 2개를 만들어야합니다. (real1,real2)
src/main/resouces/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
포트 번호를 저렇게 8081,8082로 바꾸어 설정합니다.
배포를 했으면 엔진엑스가 바라보고있는(프록시) 프로젝트를 바꾸어야 합니다.
sudo vim /etc/nginx/conf.d/service-url.inc
service-url.inc를 만들고 다음의 코드를 추가합니다.
set $servic_url http://127.0.0.1:8080;
엔진엑스가 사용할 수 있게 설정합니다. 다음과 같이 nginx.conf 파일을 엽니다.
sudo vim /etc/nginx/nginx.conf
location / 부분을 찾아 다음과 같이 변경합니다.
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;
}
이후 엔진엑스를 재 시작하고 브라우저가 작동하는지 확인합니다.
브라우저에 접속하려 해도 안될 때, nohup.out을 보면 이런 오류 메시지를 확인할 수 있습니다.
The Tomcat connector configured to listen on port 8080 failed to start. The port may already be in use or the connector may be misconfigured.
이미 8080 포트를 누군가 사용 중이니 연결을 해제하던가 다른 포트를 써야합니다. 그냥 현재 포트를 사용중인 프로세스를 종료 시키고 애플리케이션을 다시 구동하면 됩니다.
먼저 step3 디렉토리를 생성합니다.
mkdir ~/app/step3 && mkdir ~/app/step3/zip
step3에서는 무중단 배포를 진행합니다.
appspec.yml 역시 step3로 배포되도록 수정합니다.
무중단 배포를 진행할 스크립트들은 총 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 파일이 복사된 후 앞 스크립트들이 차례대로 실행됩니다.
모든 스크립트들을 차례로 추가합니다.
테스트 전, 잦은 배포로 Jar 파일명이 겹칠 수 있습니다. 매번 버전을 올
리는 것이 귀찮으므로 자동으로 버전값이 변경될 수 있도록 조치합니다.
build.gradle
version '1.0.1-SNAPSHOT-'+new Date().format("yyyyMMddHHmmss")
이 코드로 빌드할 때마다 그 시간이 버전에 추가됩니다.
모든 코드가 최종적으로 완성되었습니다. 깃허브로 후시한 후 CodeDeploy 로그로 잘 진행되는지 확인합니다.
tail -f /opt/codedeploy-agent/deployment-root/deployment-logs/codedeploy-agent-deployments.log
로그를 확인한뒤 step3의 nohup.out을 살펴보면 8082 포트로 실행된 것을 확인할 수 있는데, 한번 더 배포 후 8081 포트로 실행되는 것과 실시간으로 index.mustache가 바뀌는 것을 확인하면 끝입니다.
ps -ef | grep java
다음과 같이 현재 프록시 서버와 대기 중인 애플리케이션이 한개, 총 2개의 애플리케이션이 실행되고 있음을 확인할 수 있습니다.