1. 무중단 배포를 위한 Nginx

이전에 github actions를 이용해서 자동배포를 만들었다

하지만 자동배포를 할때 다운타임 이 발생한다

 

여기서 다운타임이란 기존에있던 서버가 내려가고 새로운 서버가 올라갈때

그때 생기는 잠깐의 시간에 클라이언트가 사이트를 이용하지 못하게 되는데

이를 지칭한다고 생각하면 된다

 

이러한 다운타임이 생기지 않게 하는것이 무중단 배포

우리가 Nginx를 이용하는 이유이다

 

무중단 설정은 Nginx의 reverse proxy 기능을 사용했고

Rolling 배포방식을 이용했으며 인스턴스를 1개 사용하고

포트를 8081, 8082로 두개를 이용해서 구현했다

 

2. Nginx 설정하기

1) 가장 먼저 EC2에 접속해서 보안 인바운드규칙에 80포트를 추가한다

 

2) EC2에서 Nginx를 설치한다

$ sudo apt install nginx

 

설치를 마쳤으면 시작해본다

$ sudo service nginx start

 

nginx가 실행되고 있는지 확인해본다

$ ps -ef | grep nginx

 

3) http(80), https(443) 으로 접속하면 nginx에서 서비스가 올라간

8081포트로 서비스를 전달하도록 하기위해 아래와 같이 설정한다

 

설정을 위한 파일 생성

$ sudo vim /etc/nginx/conf.d/service-url.inc

 

service-url.inc 첫줄에 아래와같이 설정한다

set $service_url http://127.0.0.1:8081;

 

이렇게 설정해두면 nginx가 아래 코드로 포트를 바라보고

나중에 switch.sh 스크립트 파일로 인해 동적으로 변경된다

 

4) nginx.conf 파일을 수정해준다

많은 블로그에서 sudo vim /etc/nginx/nginx.conf 에서 설정 파일을 수정하라는데

나같은 경우 들어오니 아래와 같이 2개의 코드가 적혀있다

include /etc/nginx/conf.d/*.conf;

include /etc/nginx/sites-enabled/*;

 

저 파일을 포함하고 있다는 뜻이고

우리가 지금 수정하려는 nginx.conf는 2번째 줄의

sites-enabled 아래에 default 파일로 존재한다

 

default파일로 접근해서 아래와같이 코드를 추가해주자

 

server {

    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;
    }
}

 

설정을 마쳤으면 아래 코드로 nginx를 재시작 해준다

$ sudo service nginx start

 

3. 프로젝트 설정하기

1) nginx 컨트롤러 만들기

@RestController
@RequiredArgsConstructor
public class NginxController {
	private final String HEALTH = "up";

	private final Environment env;

	@GetMapping("/api/nginx/profile")
	public String getProfile() {
		return Arrays.stream(env.getActiveProfiles()).findFirst().orElse("");
	}

	@GetMapping("/api/nginx/health")
	public String getHealth() {
		return HEALTH;
	}
}

 

2) 변경 할 포드 yml 2개 만들기

application-port1.yml

server:
  port: 8081

spring:
  config:
    import: classpath:application.yml

 

application-port2.yml

server:
  port: 8082

spring:
  config:
    import: classpath:application.yml

 

3) 배포 스크립트 작성하기

deploy.sh

#!/bin/bash
BUILD_JAR=$(ls /home/ubuntu/app/deploy/mountainz-*.jar)
JAR_NAME=$(basename $BUILD_JAR)
echo "> build 파일명: $JAR_NAME" >> /home/ubuntu/app/deploy/deploy.log

echo "> build 파일 복사" >> /home/ubuntu/app/deploy/deploy.log
DEPLOY_PATH=/home/ubuntu/app/deploy/
cp $BUILD_JAR $DEPLOY_PATH

RESPONSE_CODE=${curl -s -o /dev/null -w "%http_code" http://localhost/api/nginx/profile}
echo "> $RESPONSE_CODE response code"

if [ ${RESPONSE_CODE} -ge 400 ]
then
  echo "> NO RUNNING APP"
  CURRENT_PROFILE=port2
else
  CURRENT_PROFILE=$(curl -s http://localhost/api/nginx/profile)
fi

echo "> $CURRENT_PROFILE current profile"

if [ $CURRENT_PROFILE == port1 ]
then
  IDLE_PROFILE=port2
  IDLE_PORT=8082
elif [ $CURRENT_PROFILE == port2 ]
then
  IDLE_PROFILE=port1
  IDLE_PORT=8081
else
  echo "> no coincidence profile: $CURRENT_PROFILE"
  echo "> set profile: port1"
  IDLE_PROFILE=port1
  IDLE_PORT=8081
fi

IDLE_APPLICATION=$IDLE_PROFILE-$JAR_NAME
IDLE_APPLICATION_PATH=$DEPLOY_PATH$IDLE_APPLICATION

ln -Tfs $DEPLOY_PATH$JAR_NAME $IDLE_APPLICATION_PATH

echo "> 현재 실행중인 애플리케이션 pid 확인" >> /home/ubuntu/app/deploy/deploy.log

IDLE_PID=$(pgrep -f $IDLE_APPLICATION)

if [ -z $IDLE_PID ]
then
  echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다." >> /home/ubuntu/app/deploy/deploy.log
else
  echo "> kill -15 $IDLE_PID"
  kill -15 $IDLE_PID
  sleep 10
fi

echo "> $IDLE_PROFILE 배포"    >> /home/ubuntu/app/deploy/deploy.log
nohup java -jar -Duser.timezone=GMT+9 -Dspring.profiles.active=$IDLE_PROFILE $IDLE_APPLICATION_PATH >> /home/ubuntu/app/deploy/deploy.log 2>/home/ubuntu/app/deploy/deploy_err.log &

for RETRY in {1...10}
do
  RESPONSE=$(curl -s http://localhost:$IDLE_PORT/api/nginx/health)
  HEALTH_WORD_COUNT=$(echo $RESPONSE | grep 'up' | wc -l)

  if [ ${HEALTH_WORD_COUNT} -ge 1 ]
  then
    echo "> health check done"
    break
  else
    echo "> health check fail"
    echo "> $RESPONSE"
  fi

  if [ ${RETRY} -eq 10 ]
  then
    echo "> health check loop fail"
    echo "> NGINX Switching fail"
    exit 1
  fi

  echo "> health check retrying"
  sleep 10
done

echo "> Profile Switch"
sleep 10
sudo sh /home/ubuntu/app/deploy/switch.sh

 

switch.sh

echo "> 현재 구동중인 Port 확인"
CURRENT_PROFILE=$(curl -s http://localhost/api/nginx/profile)

# 쉬고 있는 set 찾기: port1이 사용중이면 port2가 쉬고 있고, 반대면 port1이 쉬고 있음
if [ $CURRENT_PROFILE == port1 ]
then
  IDLE_PORT=8082
elif [ $CURRENT_PROFILE == port2 ]
then
  IDLE_PORT=8081
else
  echo "> 일치하는 Profile이 없습니다. Profile: $CURRENT_PROFILE"
  echo "> 8081을 할당합니다."
  IDLE_PORT=8081
fi

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

PROXY_PORT=$(curl -s http://localhost/api/nginx/profile)
echo "> Nginx Current Proxy Port: $PROXY_PORT"

echo "> Nginx Reload"
sudo nginx -s reload

 

appspec.yml

version: 0.0
os: linux

files:
  - source: /
    destination: /home/ubuntu/app/deploy
    overwrite: yes

permissions:
  - object: /
    pattern: "**"
    owner: ubuntu
    group: ubuntu

hooks:
  ApplicationStart:
    - location: deploy.sh
      timeout: 60
      runas: ubuntu

 

main.yml

name: Java CI TEST with Gradle

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

permissions:
  contents: read

env:
  RESOURCE_PATH: ./src/main/resources/application.yml
  PROJECT_NAME: mountainz
  # Database
  DB_URL: ${{ secrets.DB_URL }}
  DB_USERNAME: ${{ secrets.DB_USERNAME }}
  DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
  # JWT Secret
  JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}
  # AWS S3
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
  S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}
  S3_REGION: ${{ secrets.S3_REGION }}
  AWS_REGION: ap-northeast-2
  # CODE_DEPLOY
  APPLICATION_NAME: ${{ secrets.APPLICATION_NAME }}
  DEPLOY_GROUP_NAME: ${{ secrets.DEPLOY_GROUP_NAME }}
  # SLACK WEBHOOK
  SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

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

      - name: Generate Environment Variables File for Properties
        uses: microsoft/variable-substitution@v1
        with:
          files: ${{ env.RESOURCE_PATH }}
        env:
          spring.datasource.url: ${{ env.DB_URL }}
          spring.datasource.username: ${{ env.DB_USERNAME }}
          spring.datasource.password: ${{ env.DB_PASSWORD }}
          jwt.secretKey: ${{ env.JWT_SECRET_KEY }}
          cloud.aws.credentials.access-key: ${{ env.AWS_ACCESS_KEY_ID }}
          cloud.aws.credentials.secret-key: ${{ env.AWS_SECRET_ACCESS_KEY }}
          cloud.aws.s3.bucket: ${{ env.S3_BUCKET_NAME }}
          cloud.aws.region.static: ${{ env.S3_REGION }}
          logging.slack.webhook-uri: ${{ env.SLACK_WEBHOOK_URL }}

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      # Build
      - name: Build with Gradle
        run: ./gradlew clean build

      # 전송할 파일을 담을 디렉토리 생성
      - name: Make Directory for deliver
        run: mkdir deploy

      # Jar 파일 Copy
      - name: Copy Jar
        run: cp ./build/libs/*.jar ./deploy/

      # appspec.yml Copy
      - name: Copy appspec
        run: cp ./appspec.yml ./deploy/

      # script file Copy
      - name: Copy shell
        run: cp ./scripts/* ./deploy/

      # 압축파일 형태로 전달
      - name: Make zip file
        run: zip -r -qq -j ./$PROJECT_NAME.zip ./deploy

      # S3 Bucket으로 copy
      - name: Deliver to AWS S3
        env:
          AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }}
        run: aws s3 cp --region $S3_REGION --acl private ./$PROJECT_NAME.zip s3://$S3_BUCKET_NAME/

      # Deploy
      - name: Deploy
        env:
          AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }}
        run: aws deploy create-deployment --application-name $APPLICATION_NAME --deployment-group-name $DEPLOY_GROUP_NAME --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$PROJECT_NAME.zip --region $S3_REGION