본문으로 건너뛰기

ReverseProxy무중단배포

이번에 트레이딩 봇을 개발하면서 1초가 급박한 파생상품 시장에서
그 단1초도 놓치지 않기위해서 필수불가결하게 무중단 배포가 필요했다
예전에 Springboot + Jenkins조합으로 해봤던 기억과 기록이 있어서 다시 재현했다

포인트는

  • Nginx가 포트 리디렉션을 하는데 설정파일을 읽어서 그 값으로 리디렉션 한다
  • 포트가 변환되는 설정파일을 작성한다
  • 설정파일의 변환은 디플로이 쉘에서 작동하도록 한다
  • 디플로이시의 시간설정과 쉘 내부에서의 for문이 생각보다 중요하다
  • pgrep / wc / tee / shell if 등을 잘 숙지해야 한다

Nginx

/etc/nginx/nginx.conf

# For more information on configuration, see:
# * Official English Documentation: http://nginx.org/en/docs/
# * Official Russian Documentation: http://nginx.org/ru/docs/

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
worker_connections 1024;
}

http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 4096;

include /etc/nginx/mime.types;
default_type application/octet-stream;

# Load modular configuration files from the /etc/nginx/conf.d directory.
# See http://nginx.org/en/docs/ngx_core_module.html#include
# for more information.
include /etc/nginx/conf.d/*.conf;


server {
listen 80;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;

# 365 service Reverse_Proxy
include /etc/nginx/conf.d/service_addr.inc;
location / {
proxy_set_header HOST $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true;
proxy_pass $service_addr;
proxy_redirect off;
charset utf-8;
}

# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;

error_page 404 /404.html;
location = /40x.html {
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}

# Settings for a TLS enabled server.
#
# server {
# listen 443 ssl http2;
# listen [::]:443 ssl http2;
# server_name _;
# root /usr/share/nginx/html;
#
# ssl_certificate "/etc/pki/nginx/server.crt";
# ssl_certificate_key "/etc/pki/nginx/private/server.key";
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 10m;
# ssl_ciphers PROFILE=SYSTEM;
# ssl_prefer_server_ciphers on;
#
# # Load configuration files for the default server block.
# include /etc/nginx/default.d/*.conf;
#
# error_page 404 /404.html;
# location = /40x.html {
# }
#
# error_page 500 502 503 504 /50x.html;
# location = /50x.html {
# }
# }

}

이렇게 읽어들일것을 파일로 지정하고 로드 이 안의 주소값이 바뀐다

  • include /etc/nginx/conf.d/service_addr.inc;
  • proxy_pass $service_addr; 이렇게 proxy_pass쪽에서 위에서 불러들인 파일의 값을 사용하도록 한다

읽어들이는 파일쪽의 내용
/etc/nginx/conf.d/service_addr.inc

set $service_addr http://127.0.0.1:8080;

Java 라면

중요한 디플로이쉘

#!/bin/bash
PROFILE=$1
PROJECT=project_name
PROJECT_HOME=/develop/jenkins/${PROJECT}
JAR_PATH=${PROJECT_HOME}/build/libs/project-0.0.1-SNAPSHOT.jar
SVR_LIST=server_${PROFILE}.list
SERVERS=$(cat $SVR_LIST)
DEPLOY_PATH=/home/ec2-user/app
AWS_ID=ec2-user
DATE=$(date +%Y-%m-%d-%H-%M-%S)
JAVA_OPTS="-XX:MaxMetaspaceSize=128m -XX:+UseG1GC -Xss1024k -Xms128m -Xmx128m -Dfile.encoding=UTF-8"
PEM=your.pem
PORT=8080

echo Deploy Start
for server in $SERVERS; do
echo Target server - $server
# Target Server에 배포 디렉터리 생성
ssh -i $PEM -o "StrictHostKeyChecking no" $AWS_ID@$server "mkdir -p $DEPLOY_PATH/dist"
# Target Server에 jar 이동
echo 'Executable Jar Copying...'
scp -i $PEM $JAR_PATH $AWS_ID@$server:~/app/dist/$PROJECT-$DATE.jar
# 이동한 jar파일의 바로가기(SymbolicLink)생성
ssh -i $PEM $AWS_ID@$server "ln -Tfs $DEPLOY_PATH/dist/$PROJECT-$DATE.jar $DEPLOY_PATH/$PROJECT"
# 현재 실행중인 서버 PID 조회
runPid=$(ssh -i $PEM $AWS_ID@$server pgrep -f $PROJECT)
if [ -z $runPid ]; then
echo "No servers are running"
fi
# 현재 실행중인 서버의 포트를 조회. 추가로 실행할 서버의 포트 선정
runPortCount=$(ssh -i $PEM $AWS_ID@$server ps -ef | grep $PROJECT | grep -v grep | grep $PORT | wc -l)
if [ $runPortCount -gt 0 ]; then
PORT=8081
fi
echo "Server $PORT Starting..."
# 새로운 서버 실행
ssh -i $PEM $AWS_ID@$server "nohup java -jar -Dserver.port=$PORT -Dspring.profiles.active=$PROFILE $JAVA_OPTS $DEPLOY_PATH/dist/$PROJECT-$DATE.jar < /dev/null > std.out 2> std.err &"
# 새롭게 실행한 서버의 health check
echo "Health check $PORT"
for retry in {1..10}; do
health=$(ssh -i $PEM $AWS_ID@$server curl -s http://localhost:$PORT/actuator/health)
checkCount=$(echo $health | grep 'UP' | wc -l)
if [ $checkCount -ge 1 ]; then
echo "Server $PORT Started Normaly"
# 기존 서버 Stop / Nginx 포트 변경 후 리스타트
if [ $runPid -gt 0 ]; then
echo "Server $runPid Stop"
ssh -i $PEM $AWS_ID@$server "kill -TERM $runPid"
sleep 5
echo "Nginx Port Change"
ssh -i $PEM $AWS_ID@$server "echo 'set \$service_addr http://127.0.0.1:$PORT;' | sudo tee /etc/nginx/conf.d/service_addr.inc"
echo "Nginx reload"
ssh -i $PEM $AWS_ID@$server "sudo systemctl reload nginx"
fi
break
else
echo "Check - false"
fi
sleep 5
done
if [ $retry -eq 10 ]; then
echo "Deploy Fail"
fi
done
echo Deploy End

ssh -i $PEM $AWS_ID@$server 부분은 로컬쪽에서 ssh로 관리할경우인데 필요없으면 빼도 된다


파이썬 Flask 라면

참고로 CodeDeploy 가 scripts/deploy.sh 를 읽어들이므로 이렇게 작성했다
build 작업이 없어서 편하긴하다

#!/bin/sh

REPOSITORY=/home/ec2-user/backtest
cd $REPOSITORY

sudo pip3 install -r requirements.txt

EC2_LOG=/home/ec2-user/deploy.log
PROJECT=flask
DATE=$(date +%Y-%m-%d-%H-%M-%S)
PORT=8080

# 현재 실행중인 서버 PID 조회
runPid=$(pgrep -f $PROJECT)

if [ -z $runPid ]; then
echo "No servers are running" >> $EC2_LOG
fi

# 현재 실행중인 서버의 포트를 조회. 있으면 추가로 실행할 서버의 포트를 8081로 함
runPortCount=$(ps -ef | grep $PROJECT | grep -v grep | grep $PORT | wc -l)
if [ $runPortCount -gt 0 ]; then
# echo "현재 서버는 $PORT 로 실행중입니다"
PORT=8081
fi
echo "Server $PORT 로 시작합니다.." >> $EC2_LOG

# 새로운 서버 실행
nohup flask run --host=0.0.0.0 --port=$PORT >> $EC2_LOG 2>&1 & # EC2용

# 새롭게 실행한 서버의 health check
echo "Health check $PORT" >> $EC2_LOG

for retry in {1..10}; do
health=$(curl -s http://localhost:$PORT/health)
checkCount=$(echo $health | grep 'ok' | wc -l)
if [ $checkCount -ge 1 ]; then
echo "[$(date)] Server $PORT Started" >> $EC2_LOG

# 초기 디플로이때에는 runpid가 null이므로 그럴때는 for문을 빠져나온다
# 안쓰면 밑의 다음 if 문에서 unary operator expected 에러남. 널과 숫자 비교라서..
if [ -z $runPid ]; then
break
fi

# 기존 서버 Stop / Nginx 포트 변경 후 리스타트
if [ $runPid -gt 0 ]; then
echo "Server $runPid Stop" >> $EC2_LOG
sudo kill -TERM $runPid
sleep 1
echo "Nginx Port Change" >> $EC2_LOG
echo "set \$service_addr http://127.0.0.1:$PORT;" | sudo tee /etc/nginx/conf.d/service_addr.inc >> $EC2_LOG
echo "Nginx reload" >> $EC2_LOG
sudo systemctl reload nginx
fi
break
else
echo "Check - false" >> $EC2_LOG
fi
sleep 1
done
echo "Deploy End" >> $EC2_LOG