2022. 3. 9. 09:11ㆍ개발/실습
0. 구성
2개의 애플리케이션을 사용한다. 사용자는 포트 80번을 사용하는 nginx로 요청을 보내고 nginx는 현재 연결된 애플리케이션을 사용자에게 제공하도록 한다.
만약 포트 8082를 사용하는 애플리케이션이 신규로 배포되었다면 nginx는 포트 8082의 애플리케이션을 사용자에게 제공할것이다.
1. nginX 설치 및 수정
sudo amazon-linux-extras install nginx1 명령어를 통해 nginx를 설치한다.
sudo systemctl nginx start로 nginx를 시작하며 sudo systemctl status nginx로 현재 nginx의 상태를 확인할 수 있다.
nginx의 기본 포트는 80이기 때문에 EC2의 보안그룹의 인바운드를 편집한다.
nginx가 정상적으로 동작하는지 확인한다. 주소에 포트번호를 생략하면 80번 포트로 접속하게 된다.
/etc/nginx 경로에 있는 nginx.conf 파일을 연다.
아래의 사진을 보면서 nginx에서 사용하는 context에 대해서 간단히 알아본다.
events context는 전역(Global) 옵션을 설정하는 Context다. NginX Configuration을 작성할때 하나만 존재하도록 해야한다.
http context는 events contex와 형제 context이며 main context를 부모로 가진다 따라서 http와 event context는 아래 처럼 side-by-side하게 작성하는것이 옳다.
server context는 http context내에 정의된다. server context는 multiple한 정의가 가능하다. 선언한 server context의 개수에 따라 NginX는 가상서버를 만들어 준다.
server context에 있는 listen은 server에 대한 접근 조건을 관리한다. IP와 Port로 이뤄져있다.
server context에 있는 server_name은 서로 다른 server에 똑같은 listen이 작성되어 있을때 사용된다. 만약 서로 다른 server에 같은 listen이 작성되어 있다면 NginX는 server_name과 request의 헤더에 있는 Host 필드를 확인한다. 만약 server_name과 Host 필드가 같다면 해당 요청을 해당 server가 처리할 수 있도록 한다.
location context는 server context 하위에 multiple한 정의가 가능하다. 사용자의 요청과 매칭이 되는 location이 선택되면 하위에 작성된 경로로 forwarding 해준다.
location context를 보면 /로 들어오는 모든 요청은 localhost의 8080포트로 포워딩됨을 알 수 있다.
하지만 위처럼 작성하면 새로운 proxy_pass를 동적으로 제어하지 못한다. 따라서 아래와 같이 service-url.inc 파일을 import해서 proxy_pass에서 사용할 url을 불러오는 방식으로 코드를 변경한다.
/etc/nginx/conf.d에 있는 service-url.inc의 내용을 작성한다. nginx.conf파일에서 사용할 service_url 변수 값을 여기서 설정한다. 이런식으로 코드를 구성하면 nginx.conf의 proxy_pass를 runtime시에 변경이 가능하다.
2. profile 생성
application-real1.yml과 application-real2.yml을 생성한다. application-{profile명}.yml 규칙에 따라 profile 명은 real1과 real2가 된다
두 profile의 차이점은 server의 포트뿐이다. real1은 8081, real2는 8082를 사용하도록 했다.
3. 셸 스크립트 작성
codedeploy를 통해 배포를 수행할 예정이므로 appspec.yml을 다음과 같이 작성한다.
version: 0.0
os: linux
files:
- source: / # 어떤 파일들을 보낼것인가. 루트 하위에 있는 모든 파일을 보내겠다
destination: /home/ec2-user/toy-project-board/deploy/project # 타겟 EC2의 어떤 경로에 저장할지
overwrite: yes # 기존에 파일이 있다면 덮어쓴다.
hooks:
AfterInstall:
- location: scripts/stop.sh
timeout: 60
runas: root
ApplicationStart:
- location: scripts/start.sh
timeout: 300
runas: root
ValidateService:
- location: scripts/health.sh
timeout: 300
runas: root
appspec.yml에 있는 스크립트뿐만아니라 쉬고 있는 profile과 port를 찾기위한 profile.sh, nginx의 포워딩 주소 변경을 위한 switch.sh을 추가로 작성한다.
먼저 profile.sh를 살펴보면, find_idle_profile 함수에선 현재 사용중이지 않은 profile을 찾고, find_idle_port 함수에서는 현재 사용중이지 않은 port를 찾는다.
#!/usr/bin/env bash
function find_idle_profile()
{
# 현재 활성화된 profile 찾기
RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/profile)
# 400 보다 크면(에러)다면
if [ ${RESPONSE_CODE} -ge 400 ]
then
CURRENT_PROFILE=real2 # 현재 활성화된 profile이 없다면 real2로 매핑한다
else
CURRENT_PROFILE=$(curl -s http://localhost/profile) # 현재 활성화된 profile
fi
if [ ${CURRENT_PROFILE} == real1 ] # 현재 활성화된 profile이 real1이면
then
IDLE_PROFILE=real2 # real2가 쉬는중
else
IDLE_PROFILE=real1
fi
echo "${IDLE_PROFILE}"
}
# 쉬고 있는 profile의 port 찾기
function find_idle_port()
{
IDLE_PROFILE=$(find_idle_profile) # 현재 쉬고 있는 profile
if [ ${IDLE_PROFILE} == real1 ]
then
echo "8081"
else
echo "8082"
fi
}
stop.sh를 통해서 현재 쉬고 있는 애플리케이션을 찾아 중지하도록 한다.
#!/usr/bin/env bash
ABSPATH=$(readlink -f $0) # 현재파일 절대경로 (링크가 있다면 실제 경로를 찾도록한다)
ABSDIR=$(dirname $ABSPATH) # ABSPATH가 있는 디렉토리 (ABSPATH는 파일명이 포함된 경로이기때문)
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
stop.sh를 통해서 현재 쉬고 있는 애플리케이션을 종료 했다면 start.sh를 통해서 현재 쉬고 있는 애플리케이션을 다시 실행시킨다.
Dspring.profiles.active를 통해 애플리케이션을 실행할때 활성화할 profile을 선택한다.
Dspring.config.location을 통해 사용할 profile들을 불러온다. 별다른 설정을 하지 않았다면 classpath: 는 src/main/resources/ 와 src/main/java/ 가 된다.
#!/usr/bin/env bash
JAR_PATH=/home/ec2-user/toy-project-board/deploy
PROJECT_PATH=/home/ec2-user/toy-project-board/deploy/project
ABSPATH=$(readlink -f $0) # 현재파일 절대경로 (링크가 있다면 실제 경로를 찾도록한다)
ABSDIR=$(dirname $ABSPATH) # ABSPATH가 있는 디렉토리 (ABSPATH는 파일명이 포함된 경로이기때문)
source ${ABSDIR}/profile.sh # profile.sh를 실행한다.
echo "> Start Build"
cd $PROJECT_PATH # 프로젝트가 저장된 경로로 이동
sudo chmod ugo+rwx gradlew # 권한부여
sudo ./gradlew build # 빌드시작
echo "> copy Jar"
cp $PROJECT_PATH/build/libs/*jar $JAR_PATH/ # jar 파일을 복사
JAR_NAME=$(ls -tr $JAR_PATH/*.jar | tail -n 1) # 실행할 Jar명 가져오기
echo "> Run $JAR_NAME"
IDLE_PROFILE=$(find_idle_profile)
echo ">Run $JAR_NAME with IDLE_PROFILE"
echo ">IDLE_PROFILE $IDLE_PROFILE"
# application-db.yml 로딩 및 $IDLE_PROFILE profile 선택
nohup java -jar -Dspring.config.location=/home/ec2-user/toy-project-board/application-db.yml, \
classpath:/application-$IDLE_PROFILE.yml \
-Dspring.profiles.active=$IDLE_PROFILE $JAR_NAME > $JAR_PATH/nohup.out 2>&1 &
health.sh에선 start.sh에서 실행한 애플리케이션이 정상적으로 동작하는지 확인한 후 nginx에서 start.sh에서 실행한 애플리케이션을 바라 볼수 있도록 switch.sh에 있는 switch_proxy 함수를 실행시킨다.
#!/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는 tee 명령어를 사용해 echo에 입력된 문자열을 service-url.inc에 그대로 덮어쓰기 한다.
#!/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 "> 엔진엑스 재시작"
sudo systemctl nginx reload
}
동작확인
Jenkins를 통해 2회 이상 코드를 배포하면 아래 사진처럼 2개의 애플리케이션이 모두 실행중인 것을 확인할 수 있다.
다만 service-url.inc를 확인해보면 nginx는 8081번 포트와 연결되어 있음을 확인할 수 있다. 즉, 8082번 포트는 현재 사용자에게 서비스가 되고 있지 않는 상태이다.
발생한 문제
jenkins를 통해 새로운 코드를 배포를 해도 codedeploy가 계속 과거에 있던 코드를 빌드하는 현상이 있었다. 그래서 /opt/codedeploy-agent/deployment-root/6bb1bb58-3259-442f-a653-53a0ca1a6d89 배포그룹 하위에 있는 기존 소스를 모두 지웠다.
이후 codedeploy시 아래와 같은 에러가 발생했다. 이때 메시지에 있는 경로를 신규로 만들고 appspec.yml 파일을 함께 만들어줬더니 배포시 신규 코드로 잘 작동했다.
참고 서적 및 사이트
Nginx Configuration 파일 분석
SC2086
classpath에 대한 개념 정리 사이트
'개발 > 실습' 카테고리의 다른 글
게시판 실습 복기2 (ft. EC2, Jenkins, Codedeploy, S3 ) (0) | 2022.03.02 |
---|---|
게시판 실습 복기 (ft. Spring Security, Junit ) (0) | 2022.02.25 |
GraphViz를 통해 함수 호출 그래프 그리기 (0) | 2021.10.13 |