[DevOps] Nginx를 이용한 무중단 배포 공부하기 - Hype

2023. 4. 2. 17:28프로젝트/학습


💡본문에 들어가기 앞서...
최근 이전에 수행했던 프로젝트인 Hype을 리팩토링 및 추가 기능 구현하고 있습니다. 그래서 기존 환경을 조금 수정하고 있는데 여기서는 Nginx를 WAS와 따로 분리하고 무중단 배포를 붙인 부분에 대해서 적어보겠습니다.  

기존 서버 구조

저희 서비스의 기존 구조는 아래 그림과 같았습니다.

즉, Nginx가 ec2 인스턴스 내부에 WAS랑 같이 존재하고 외부에서 접근하면 nginx를 통해서 was로 붙도록 구현했는데 그 이유가 https를 붙이기 위해서 이런 방식을 채택했습니다. 그리고 ec2 인스턴스는 micro freetier를 이용했습니다.

기존 서버에서 발생한 문제점 및 구조 변경

기존 서버를 운영하면서 문제가 발생했었는데 ec2 인스턴스의 크기로 인한 문제였습니다. ec2가 micro이다 보니까 트래픽이 조금만 많이 생기면 cpu 사용량이 100퍼센트를 찍으면서 인스턴스가 죽는 문제가 발생했습니다. 이런 문제와 또 차후의 운영을 생각했을 때 ec2를 스케일 업시키는 것이 좋을 것 같다는 생각을 했습니다.

그리고 nginx를 was에서 따로 빼내서 다른 서버로 둬야겠다는 생각을 했습니다. 나중에 스케일 아웃할 때도 좋을 것 같았고 reverse proxy를 제대로 이용하기 위해선 nginx랑 was 서버의 ip가 다른 것이 맞지 않나하는 생각이 들어서 이렇게 결정했습니다.

마지막으로 redis를 다른 서버에 두자는 생각을 했습니다. redis가 인메모리 저장소니까 서버 메모리를 초과할 경우 서버에 문제가 생길 수 있다고 생각했고 서버의 메모리를 공유하게 되니까 메모리 공간을 많이 사용할 수 있다는 생각이 들어서 redis도 다른 서버로 빼내서 사용하는 구조를 생각하게 되었습니다.

아래는 변경된 서비스 구조입니다.


※ 해당 게시글에서는 Nginx에 대한 내용만을 다룰 것임으로 AWS 관련 설정에 대한 내용은 포함되지 않습니다.

Nginx 설정 파일 수정.

먼저 nginx 서버에 대한 내용부터 말해보겠습니다. nginx 서버는 ec2 micro의 인스턴스를 하나 만들고 그 위에 nginx를 설치했습니다. 그리고 https를 위한 설정과 reverse proxy 역할을 할 수 있게 설정했습니다.

user www-data; # 프로세스의 실행 권한.
worker_processes auto; # 몇개의 워커 프로세스를 생성할 것인지 지정하는 지시어.
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events { # 접속 처리에 관한 설정을 하는 블록
        worker_connections 1024; # 워커 프로세스 한 개당 동시 접속 수 지정
}

http { # 웹, 프록시 관련 서버 설정

        access_log /var/log/nginx/access.log; # log 관련 설
        error_log /var/log/nginx/error.log;

        server {
          listen 80;
          return 301 https://$host$request_uri;
        }

        server {
          listen 443 ssl;
          server_name hype.r-e.kr;

          include /etc/nginx/conf.d/service_url.inc;

          ssl_certificate /etc/letsencrypt/live/hype.r-e.kr/fullchain.pem;
          ssl_certificate_key /etc/letsencrypt/live/hype.r-e.kr/privkey.pem;

          ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

          ssl_prefer_server_ciphers on;
          ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;

          add_header Strict-Transport-Security "max-age=31536000" always;

          ssl_session_cache shared:SSL:10m;
          ssl_session_timeout 10m;

          location / {
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            # 현재까지 거쳐온 서버의 IP에 대한 정보
            proxy_set_header X-Forwared_Proto $scheme;
            # 클라이언트 요청 프로토콜
            proxy_set_header X-Real-IP $remote_addr;
            # 클라이언트 IP
            proxy_set_header Host $http_host;
            # 서버의 도메인 네임

            proxy_pass $service_url;
          }
        }

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
}

server 블록에서 80포트로 들어오는 요청에 대해서 https로의 요청으로 리다이렉트를 응답해줍니다. 그리고 443 포트에서 ssl 인증서를 이용하여 https 통신이 가능하도록 설정해줍니다.

그리고 location 블록에서 proxy_pass를 이용하여 특정 서버로 포워딩해주는 방식으로 reverse proxy 서버 역할을 설정해줍니다.

Nginx를 이용한 무중단 배포 구현

위의 설정 파일을 보면

include /etc/nginx/conf.d/service_url.inc;

요런 구문이 존재합니다. 해당 파일 내부에서는 아래와 같이 환경 변수를 설정해줍니다.

set $service_url xxx.xxx.xxx.xxx:8080;

 

만약 이런 식으로 파일로 포워딩할 목적지를 설정하지 않는다면 하나의 목적지로만 보낼 수 밖에 없습니다. 그럼 무중단 배포에서 저희는 포트 번호를 바꾸면서 포워딩할 위치를 설정해줘야 blue인 상태의 서버들을 바꿔서 포워딩할 수 있게 됩니다.

 

그리고 기존 ec2의 보안 그룹에 설정된 인바운드 규칙도 열어줘야 해당 포트로의 외부 접근이 가능하게 만들어 주는 것을기억해주세요! (저는 이거 때문에 시간을 많이 써버려서,,,)

 

그럼 저 포트를 바꾸는 행위는 어느 시점에 일어나는 것이 좋을까요?? 무중단 배포라는 말은 배포시에 다운타임을 없애는 것을 의미하니까 배포 시에 일어나야 합니다. 저는 code deploy를 이용해서 배포를 하기 때문에 appspec.yml 파일을 통해서 code deploy agent가 실행할 shell 파일을 수정해서 무중단 배포를 구현해야합니다. 순서는 다음과 같습니다.

  1. 먼저 jar 파일을 github actions를 이용해서 s3에 저장 후 code deploy에게 배포해주세요 하고 넘깁니다.
  2. code deploy는 설정되어 있는 ec2의 agent에게 jar 파일을 넘깁니다.
  3. ec2의 code deploy agent는 appspec.yml을 실행합니다.
  4. 그럼 appspec.yml에 설정되어 있는 shell을 실행하게 되고 거기서 배포가 일어납니다. 

그래서 저는 아래와 같이 배포를 위한 yml 파일을 구성했습니다.

name: deploy-release

on:
  push:
    branches: [ release ]
jobs:
  build:
    runs-on: ubuntu-22.04

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: make secret envfiles for prod
        run: |
          echo "${{secrets.PROD_DB_ENV}}" > ./prod_db_info.env
          echo "${{secrets.SENSITIVE_RELEASE_ENV}}" > ./sensitive.env

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: 17
          distribution: 'temurin'
          cache: gradle

      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew
        shell: bash

      - name: Build with Gradle
        run: ./gradlew build
        shell: bash

      - name: Run Checkstyle
        run: ./gradlew checkstyleMain

      - name: Make zip file
        run: zip -qq -r ./$GITHUB_SHA.zip .
        shell: bash

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1-node16
        with:
          aws-access-key-id: ${{ secrets.ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.ACCESS_KEY_SECRET }}
          aws-region: ap-northeast-2

      - name: Upload to AWS S3
        run: |
          aws deploy push \
            --application-name hype-deploy \
            --ignore-hidden-files \
            --s3-location s3://devcourse-pearls-bucket/$GITHUB_SHA.zip \
            --source .

      - name: Code Deploy
        run: aws deploy create-deployment --application-name hype-deploy
          --deployment-config-name CodeDeployDefault.AllAtOnce
          --deployment-group-name hype-prod-group-1
          --s3-location bucket=devcourse-pearls-bucket,bundleType=zip,key=$GITHUB_SHA.zip

먼저 github actions를 이용하여 ci가 일어나고 위에서 말한 cd가 일어나는 부분입니다.

그리고 위에서 말한 appspec.yml을 아래와 같이 구성했습니다.

version: 0.0
os: linux

files:
  - source: /
    destination: /home/ubuntu/build
    overwrite: yes

permissions:
  - object: /home/ubuntu
    pattern: '**'
    owner: ubuntu
    group: ubuntu

hooks:
  ApplicationStart:
    - location: scripts/deploy_no_stop.sh
      timeout: 1000
      runas: ubuntu

배포 시 ApplicationStart 시점에서 deploy_no_stop.sh 파일을 실행하게 만들었습니다.

 

그리고 나서 이제 deploy_no_stop.sh 파일에 대해서 알아보겠습니다. 이거는 이제 진짜 배포를 수행하기 위한 파일이고 순서는 아래와 같습니다.

  1. 먼저 저희는 8080, 8081 포트를 이용하여 무중단 배포를 구현할 것입니다. 앞서 말했던 service_url.inc에 설정되어 있는 포트 번호를 가져옵니다. 가져온 포트 번호는 현재 was가 띄워져 있는 포트일 것 입니다.
  2. 만약 8080 포트를 가져왔다면 8081 포트는 쉬고 있는 포트일 것이니까 여기에다가 배포할 jar를 띄우는 것입니다.
  3. jar가 잘 띄워졌는지 check를 한 후, nginx 서버의 설정 파일을 현재 포트번호로 수정해줍니다.
  4. 그리고 나서 이전에 띄워져 있던 jar를 죽입니다.

요렇게 설정을 하면 새로 배포된 jar 파일이 띄워지고 나서 nginx를 이쪽 포트로 돌린 후에 기존 띄워져 있던 was를 죽이니까 다운타임이 엄청 짧다 거의 없다라고 생각할 수 있게 됩니다. 

SUCCESS_HEALTH=health
WAS_IP=$(cat /home/ubuntu/build/was_ip.env)
NGINX_IP=$(cat /home/ubuntu/build/nginx_ip.env)

CURRENT_PORT=$(sudo ssh -i /home/ubuntu/build/build/libs/hype-ec2-key.pem ubuntu@${NGINX_IP} sudo cat /etc/nginx/conf.d/service_url.inc | grep -Po '[0-9]+' | tail -1)
TARGET_PORT=0
if [ ${CURRENT_PORT} -eq 8080 ]
then
  TARGET_PORT=8081
elif [ ${CURRENT_PORT} -eq 8081 ]
then
  TARGET_PORT=8080
else
  echo "> No WAS is connected to nginx"
  exit 1
fi

TARGET_PID=$(sudo lsof -ti tcp:${TARGET_PORT})
if [ ! -z ${TARGET_PID} ]
then
  echo "> Kill WAS running at ${TARGET_PORT}"
  sudo kill ${TARGET_PID}
fi

echo "> New WAS runs at ${TARGET_PORT}"
sudo nohup java -jar -Dserver.port=${TARGET_PORT} -Dspring.profiles.active=prod -Dspring.config.import=optional:file:/home/ubuntu/build/prod_info.env[.properties] /home/ubuntu/build/build/libs/pearls-1.0.jar &

for RETRY in {1..10}
do
  HEALTH_COUNT=$(curl -s http://${WAS_IP}:${TARGET_PORT}/health | grep ${SUCCESS_HEALTH} | wc -l)
  if [ ${HEALTH_COUNT} -ge 1  ]
  then
    echo "> Health Check Success"
    echo "set \$service_url http://${WAS_IP}:${TARGET_PORT};" | sudo ssh -i /home/ubuntu/build/build/libs/hype-ec2-key.pem ubuntu@${NGINX_IP} sudo tee /etc/nginx/conf.d/service_url.inc
    sudo ssh -i /home/ubuntu/build/build/libs/hype-ec2-key.pem ubuntu@${NGINX_IP} sudo service nginx reload
    break
  elif [ ${RETRY} -eq 10 ]
  then
    echo "> Health Check Fail"
    exit 1
  fi
  echo "> Health Check Retry..."
  sleep 10
done

CURRENT_PID=$(sudo lsof -ti tcp:${CURRENT_PORT})
if [ -z ${CURRENT_PID} ]
then
  echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
  echo "> kill -15 ${CURRENT_PID}"
  sudo kill -15 ${CURRENT_PID}
  sleep 5
fi

마무리

이 프로젝트를 진행했을 당시에 제가 CI/CD를 구성하지도 않았고 AWS 설정도 거의 안했어서 여기에 대한 지식이 거의 무지했는데 이를 리팩토링 해보면서 AWS도 다뤄보고 CI/CD도 흐름을 파악하고 코드도 완전 갈아엎어 보면서 좋은 경험이었고 재미있었다고 생각이 듭니다.

그리고 무중단 배포를 위해서 Nginx가 아닌 CodeDeploy에서 제공하는 Blue/Green 방식의 배포를 이용할 수 있지만 어디서 문제가 발생했는지에 대한 부분을 log로 찍어서 배포가 실패하는 경우도 모니터링을 구성하기 위해 Nginx로 세부적인 무중단 배포의 흐름을 shell 파일로 직접 구성했습니다. 이를 통해서 무중단 배포가 내부적으로 일어나는 과정 또한 이해할 수 있었습니다.  

마지막으로 현재 음악 검색 API가 WAS 내부에 들어있고 해당 API를 호출하면 여기서 애플뮤직 API를 호출하는 방식으로 구현되어 있습니다. 요 부분을 따로 분리해서 서버로 따로 빼내서 reverse proxy에서 음악 검색 API 호출 시 그 서버로 날라가게 구현하고 해당 서버에서 여러 포트에 음악 검색을 위한 was를 띄워서 로드 밸런싱을 하면 현재 애플 뮤직 API가 초당 20번 요청을 할 수 있는 제한이 있는데 이런 부분도 해결할 수 있지 않을까 생각이 듭니다.

 

Reference

https://blogshine.tistory.com/432

 

[AWS] Github Actions, CodeDeploy, Nginx 로 무중단 배포하기 - 4

총 4개의 시리즈 글로 진행될 것입니다. 1) Github Actions과 AWS S3 연동 2) EC2 설정과 CodeDeploy 적용 3) EC2와 RDS 4) Nginx 설치와 배포 스크립트 (이번 글) ▶ 전체 흐름도 우선 전반적인 흐름은 다음과 같습

blogshine.tistory.com

https://tecoble.techcourse.co.kr/post/2022-11-01-blue-green-deployment/

 

무중단 배포

무중단 배포를 도입하게 된 계기 프로젝트를 론칭한 이후, 사용자의 피드백을 반영하다보니 운영 환경으로의 배포가 점차 잦아졌습니다. 문제는 배포를 할 때마다 기존 서버를 내리고 새롭게

tecoble.techcourse.co.kr

https://soso-hyeon.tistory.com/59

 

[우당탕탕 개발 일지] Spring Boot 배포하기: Nginx 로 CICD

안녕하세요. 방금 우당탕탕 어찌저찌 무중단 배포로 CICD 세팅 완료하고 왔읍니다 ! [CHORE] CICD 세팅 - CD by thguss · Pull Request #39 · Team-Smeme/Smeme-server-renewal Related issue 🚀 closed #37 Work Description 💚 CICD

soso-hyeon.tistory.com