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