이전 포스팅에서 Travis CI로 소스를 빌드하고 S3, AWS CodeDeploy를 통해 빌드한 파일을 EC2 서버로 전송하는 과정까지를 진행하였습니다. EC2에 전송한 JAR파일을 실행시키려면 현재 구동 중인 프로젝트를 중지하고 다시 기동 해야 하는데 이렇게 배포 과정에서 서비스를 중지해야 한다면, 배포 시 오류 발생 상황에 대처하기 힘들어지고 서비스 사용자 입장에서도 서버가 중지되어 불편함을 겪게 됩니다.
이러한 상황을 대비해 서비스를 중지하지 않고 배포할 수 있는 무중단 배포환경 구축할 수 있습니다. 무중단 배포 방식에는 AWS Blue Green, Docker를 이용한 무중단 배포, L4 스위치를 이용한 무중단 배포 등 여러가지가 있습니다. 이번 포스팅에서는 오픈소스 소프트웨어인 Nginx를 활용한 무중단 배포 환경을 구축하는 과정을 정리하였습니다.
1. nginx 설치
아마존 리눅스2에서는 yum을 통한 nginx 설치를 지원하지 않습니다.
아래의 명령어를 통해 nginx 설치 패키지를 조회하고 설치를 진행합니다.
$ amazon-linux-extras list | grep nginx
조회한 nginx 설치를 진행합니다.
$ sudo amazon-linux-extras install -y nginx1
nginx 설치 확인
$ nginx -v
nginx 설치를 완료한 뒤 서비스를 실행시킵니다.
$ sudo service nginx start
2. AWS 보안그룹 추가 -> 인바운드 규칙에 80 포트 개방
nginx로 접속 시 80포트로 접속하기 때문에 aws 보안 그룹 인바운드 규칙에 80 코드를 개방시켜주어야 합니다.
3. nginx <-> Springboot 연동
nginx가 현재 구동중인 스프링 부트를 바라보도록 프록시 설정을 해주어야 합니다.
먼저 nginx 설정들이 모여있는 /etc/nginx/conf.d 에 service-url.inc 파일을 생성합니다.
sudo vim /etc/nginx/conf.d/service-url.inc
그다음 nginx.conf 파일을 설정해줍니다. 위에서 생성한 service-url.inc파일을 include 해주고 server 섹션의 location 부분에다가 proxy_pass를 설정해줍니다.
sudo vim /etc/nginx/nginx.conf
server {
listen 80;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;
# Load configuration files for the default server block.
# include /etc/nginx/default.d/*.conf;
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;
}
error_page 404 /404.html;
location = /404.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
proxy_pass : nginx 요청이 오면 service-url에 설정한 url로 전달합니다.
수정을 마친 뒤 nginx 서비스를 재기동해주고 포트 번호 없이 브라우저로 접속하면 nginx 프록시 접속이 잘 되는 것을 확인할 수 있습니다.
sudo service nginx restart
4. 무중단 배포 환경 구성
무중단 배포를 위한 profile을 2개 추가해줍니다
profile는하나의 프로젝트 코드를 여러 개의 환경으로 실행시킬 수 있게 해주는 설정 기능입니다. 8081, 8082 포트로 접속할 수 있는 설정 파일(real1, real2)을 각각 생성하면 후에 실행하고 싶은 환경으로 프로젝트 코드를 실행시킬 수 있습니다.
application-real1.properties
server.port=8081
...
application-real1.properties
server.port=8082
...
API 추가 (ProfileController)
프로젝트에 현재 구동중인 환경 정보를 가져와 구분하는 API를 추가해줍니다. 나중에 배포 후 real1 환경으로 접속 시 "real1"값을, real2 환경으로 접속 시 "real2"값을 화면에 반환합니다.
ProfileController.class
@RestController
@RequiredArgsConstructor
public class ProfileController {
private final Environment env;
@GetMapping("/profile")
public String profile(){
List<String> profiles = Arrays.asList(env.getActiveProfiles());
List<String> realProfiles = Arrays.asList("real1","real2");
String defaultProfile = profiles.isEmpty() ? "default" : profiles.get(0);
return profiles.stream()
.filter(realProfiles::contains)
.findAny()
.orElse(defaultProfile);
}
}
Environment : 스프링부트 환경설정에 대한 정보를 제공하는 인터페이스입니다. Environment 인터페이스를 통해서 profiles에 접근할 수 있습니다. 참고로 설정값 변경은 불가능합니다.
getActiveProfiles() : 현재 활성화 된 profile을 String 값으로 반환합니다.
EC2에 무중단 배포 디렉토리 생성
무중단 배포 시 파일을 업로드 할 디렉터리를 EC2환경에 생성해줍니다.
mkdir ~/app/step3 && mkdir ~/app/step3/zip
appspec.yml 배포 경로 수정
appsecc.yml 파일에서 위에서 생성한 디렉토리(step3)로 배포 대상 경로를 수정해줍니다.
version: 0.0
os: linux
files:
- source: /
destination: /home/ec2-user/app/step4/zip
overwrite: yes
무중단 배포 스크립트 생성
프로젝트에 scripts 디렉토리 하위에 무중단 배포를 위한 스크립트 파일을 생성해줍니다.
- stop.sh : 실행중인 스프링 부트 종료
- start.sh : 새로 배포할 스프링부트를 실행 중이지 않은 profile로 실행
- health.sh : start.sh로 실행시킨 프로젝트의 상태 체크
- switch.sh : 엔진엑스가 바라보는 스프링부트를 최신 버전으로 변경
- profile.sh : 포트 체크 로직. 위의 4개의 스크립트 파일에서 임포트 하여 사용한다.
stop.sh
#!/usr/bin/env bash
ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH) # stop.sh 가 속한 경로 찾기. -> profile.sh의 경로를 찾기 위해 사용
source ${ABSDIR}/profile.sh # 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
start.sh
#!/usr/bin/env bash
ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh
REPOSITORY=/home/ec2-user/app/step3
PROJECT_NAME=miniproject
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.yml,classpath:/application-$IDLE_PROFILE.properties \
-Dspring.profiles.active=$IDLE_PROFILE \
$JAR_NAME > $REPOSITORY/nohup.out 2>&1 &
health.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.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
}
profile.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) # nginx가 바라보고 있는 스프링 부트가 정상적으로 수행중인지 확인
if [ ${RESPONSE_CODE} -ge 400 ] # 400 보다 크면 (즉, 40x/50x 에러 모두 포함) / 정상 : 200, 오류 400~503
then
CURRENT_PROFILE=real2
else
CURRENT_PROFILE=$(curl -s http://localhost/profile)
fi
if [ ${CURRENT_PROFILE} == real1 ]
then # IDLE_PROFILE : nginx와 연결되지 않은 profile
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
}
appspec.yml 스크립트 실행 설정 추가
appspec.yml 의 hooks 세션에 위에서 생성한 스크립트를 실행하는 이벤트들을 설정합니다.
참고로 appspec.yml에서 hooks 세션은 해당 배포 컴퓨팅 환경에 따라서 역할이 달라집니다. EC2/온프레미스 환경에서의 hooks 세션은 배포의 수명주기나 하나 이상의 스크립트 실행을 연결시키는 매핑을 포함합니다.
version: 0.0
os: linux
files:
- source: /
destination: /home/ec2-user/app/step3/zip
overwrite: yes
permissions:
- object: /
pattern: "**"
owner: ec2-user
group: ec2-user
## 배포 스크립트 관련 설정 추가 ##
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
hooks 섹션 부분은 Jar파일이 복사된 이후부터 차례대로 실행됩니다.
5. 무중단 배포 테스트
CodeDeploy 로그 확인
tail -f /opt/codedeploy-agent/deployment-root/deployment-logs/codedeploy-agent-deployments.log
[2022-04-01 13:31:57.314] [d-OET4S1CWF]LifecycleEvent - AfterInstall
[2022-04-01 13:31:57.314] [d-OET4S1CWF]Script - stop.sh
[2022-04-01 13:31:57.343] [d-OET4S1CWF][stdout]> 8081 에서 구동중인 애플리케이션 pid 확인
[2022-04-01 13:31:57.388] [d-OET4S1CWF][stderr]/opt/codedeploy-agent/deployment-root/8364edf8-f041-4472-8f42-dffbbdb0388d/d-OET4S1CWF/deployment-archive/stop.sh: line 17: kill: 21441됨: arguments must be process or job IDs
[2022-04-01 13:31:57.388] [d-OET4S1CWF][stdout]> kill -15 21441됨
[2022-04-01 13:32:02.555] [d-OET4S1CWF]LifecycleEvent - ApplicationStart
[2022-04-01 13:32:02.555] [d-OET4S1CWF]Script - start.sh
[2022-04-01 13:32:02.570] [d-OET4S1CWF][stdout]> Build 파일 복사
[2022-04-01 13:32:02.570] [d-OET4S1CWF][stdout]> cp /home/ec2-user/app/step3/zip/*.jar /home/ec2-user/app/step3/
[2022-04-01 13:32:02.583] [d-OET4S1CWF][stdout]> 새 어플리케이션 배포
[2022-04-01 13:32:02.586] [d-OET4S1CWF][stdout]> JAR Name: /home/ec2-user/app/step3/miniproject-1.0.1-SNAPSHOT-20220401133105.jar
[2022-04-01 13:32:02.586] [d-OET4S1CWF][stdout]> /home/ec2-user/app/step3/miniproject-1.0.1-SNAPSHOT-20220401133105.jar 에 실행권한 추가
[2022-04-01 13:32:02.587] [d-OET4S1CWF][stdout]> /home/ec2-user/app/step3/miniproject-1.0.1-SNAPSHOT-20220401133105.jar 실행
[2022-04-01 13:32:02.598] [d-OET4S1CWF][stdout]> /home/ec2-user/app/step3/miniproject-1.0.1-SNAPSHOT-20220401133105.jar 를 profile=real1 로 실행합니다.
[2022-04-01 13:32:03.611] [d-OET4S1CWF]LifecycleEvent - ValidateService
[2022-04-01 13:32:03.611] [d-OET4S1CWF]Script - health.sh
[2022-04-01 13:32:03.750] [d-OET4S1CWF][stdout]> Health Check Start!
[2022-04-01 13:32:03.750] [d-OET4S1CWF][stdout]> IDLE_PORT: 8081
[2022-04-01 13:32:03.750] [d-OET4S1CWF][stdout]> curl -s http://localhost:8081/profile
[2022-04-01 13:32:13.765] [d-OET4S1CWF][stdout]> Health check 성공
[2022-04-01 13:32:13.777] [d-OET4S1CWF][stdout]> 전환할 Port: 8081
[2022-04-01 13:32:13.777] [d-OET4S1CWF][stdout]> Port 전환
[2022-04-01 13:32:13.788] [d-OET4S1CWF][stdout]set $service_url http://127.0.0.1:8081;
[2022-04-01 13:32:13.790] [d-OET4S1CWF][stdout]> 엔진엑스 Reload
[2022-04-01 13:32:13.811] [d-OET4S1CWF][stderr]Redirecting to /bin/systemctl reload nginx.service
[2022-04-01 13:38:22.877] [d-8TE4ATCWF]LifecycleEvent - AfterInstall
[2022-04-01 13:38:22.877] [d-8TE4ATCWF]Script - stop.sh
[2022-04-01 13:38:22.914] [d-8TE4ATCWF][stdout]> 8082 에서 구동중인 애플리케이션 pid 확인
[2022-04-01 13:38:22.962] [d-8TE4ATCWF][stderr]/opt/codedeploy-agent/deployment-root/8364edf8-f041-4472-8f42-dffbbdb0388d/d-8TE4ATCWF/deployment-archive/stop.sh: line 17: kill: 됨: arguments must be process or job IDs
[2022-04-01 13:38:22.962] [d-8TE4ATCWF][stdout]> kill -15 됨
[2022-04-01 13:38:28.145] [d-8TE4ATCWF]LifecycleEvent - ApplicationStart
[2022-04-01 13:38:28.145] [d-8TE4ATCWF]Script - start.sh
[2022-04-01 13:38:28.160] [d-8TE4ATCWF][stdout]> Build 파일 복사
[2022-04-01 13:38:28.160] [d-8TE4ATCWF][stdout]> cp /home/ec2-user/app/step3/zip/*.jar /home/ec2-user/app/step3/
[2022-04-01 13:38:28.172] [d-8TE4ATCWF][stdout]> 새 어플리케이션 배포
[2022-04-01 13:38:28.175] [d-8TE4ATCWF][stdout]> JAR Name: /home/ec2-user/app/step3/miniproject-1.0.1-SNAPSHOT-20220401133732.jar
[2022-04-01 13:38:28.175] [d-8TE4ATCWF][stdout]> /home/ec2-user/app/step3/miniproject-1.0.1-SNAPSHOT-20220401133732.jar 에 실행권한 추가
[2022-04-01 13:38:28.176] [d-8TE4ATCWF][stdout]> /home/ec2-user/app/step3/miniproject-1.0.1-SNAPSHOT-20220401133732.jar 실행
[2022-04-01 13:38:28.194] [d-8TE4ATCWF][stdout]> /home/ec2-user/app/step3/miniproject-1.0.1-SNAPSHOT-20220401133732.jar 를 profile=real2 로 실행합니다.
[2022-04-01 13:38:29.211] [d-8TE4ATCWF]LifecycleEvent - ValidateService
[2022-04-01 13:38:29.211] [d-8TE4ATCWF]Script - health.sh
[2022-04-01 13:38:29.376] [d-8TE4ATCWF][stdout]> Health Check Start!
[2022-04-01 13:38:29.376] [d-8TE4ATCWF][stdout]> IDLE_PORT: 8082
[2022-04-01 13:38:29.376] [d-8TE4ATCWF][stdout]> curl -s http://localhost:8082/profile
[2022-04-01 13:38:39.626] [d-8TE4ATCWF][stdout]> Health check 성공
[2022-04-01 13:38:39.673] [d-8TE4ATCWF][stdout]> 전환할 Port: 8082
[2022-04-01 13:38:39.673] [d-8TE4ATCWF][stdout]> Port 전환
[2022-04-01 13:38:39.695] [d-8TE4ATCWF][stdout]set $service_url http://127.0.0.1:8082;
[2022-04-01 13:38:39.697] [d-8TE4ATCWF][stdout]> 엔진엑스 Reload
[2022-04-01 13:38:39.720] [d-8TE4ATCWF][stderr]Redirecting to /bin/systemctl reload nginx.service
자바 어플리케이션 실행 여부 확인
ps -ef | grep java
아래와 같이 2개의 어플리케이션이 실행되는 것을 확인할 수 있습니다.
java -jar -Dspring.config.location=...-Dspring.profiles.active=real1 /home/ec2-user/app/step3/~~.jar
java -jar -Dspring.config.location=...-Dspring.profiles.active=real2 /home/ec2-user/app/step3/~~.jar
무중단 배포 성공!
'Dev > AWS' 카테고리의 다른 글
ECS(Elastic Container Service) 기본 개념 (0) | 2022.08.28 |
---|---|
DynamoDB 기본 개념 (0) | 2022.07.31 |
Travis CI,CodeDeploy,S3,Nginx로 EC2에 무중단 배포하기(2) - Travis CI,S3,CodeDeploy 연동 (0) | 2022.04.06 |
Travis CI,CodeDeploy,S3,Nginx로 EC2에 무중단 배포하기(1) - Travis CI,S3 연동 (0) | 2022.04.05 |
[AWS] CloudWatch를 활용한 docker 이미지 로그 관리하기 (1) | 2022.03.06 |
댓글