무중단 배포에서 사용한 Script 파일 내용 정리

Denia·2022년 6월 8일
0

무중단 배포에서 사용한 Script 파일들의 내용을 정리 해보려고 합니다.
(인터넷에서 참고한 자료들을 제 기준으로 다시 정리해봤습니다.)

사용한 환경
1.AWS EC2 FreeTier t2.micro Ubuntu 18.04 버전
2.Bash Shell

사용하려는 포트는 8081,8082 2종류 (profile 도 prof1 , prof2 로 2종류)

오타 및 잘못된 부분이 있을 경우 댓글을 달아주시면 정말 감사하겠습니다.


무중단 배포에서 사용한 Script 파일은 총 5개 입니다.

  1. profile.sh
  2. start.sh
  3. switch.sh
  4. health.sh
  5. stop.sh

위에서부터 하나 하나 파일들을 정리해보겠습니다.


1. profile.sh

다른 Script 파일에서 자주 사용할 function들을 정리한 Script 파일 입니다. 2가지의 function으로 이루어진 파일이며 idle profile 과 port를 찾는 function이 있습니다.

#!/bin/bash

# Idle profile 찾기
function find_idle_profile()
{
    RESPONSE_CODE=$(sudo curl -s -o /dev/null -w "%{http_code}" https://example.com/api/test/profile)

    if [ ${RESPONSE_CODE} -ge 400 ] 
    then
        CURRENT_PROFILE=prof2
    else
        CURRENT_PROFILE=$(sudo curl -s https://example.com/api/test/profile)
    fi

    if [ ${CURRENT_PROFILE} == prof1 ]
    then
      IDLE_PROFILE=prof2
    else
      IDLE_PROFILE=prof1
    fi

    echo "${IDLE_PROFILE}"
}

# Idle profile의 port 찾기
function find_idle_port()
{
    IDLE_PROFILE=$(find_idle_profile)

    if [ ${IDLE_PROFILE} == prof1 ]
    then
      echo 8081
    else
      echo 8082
    fi
}

#!/bin/bash

지금부터 셸 스크립트를 사용하겠다는 의미 이며 bash를 사용할 경우 sh 대신에 bash를 붙여줍니다.

function find_idle_profile(){
...
}

스크립트 파일에서는 function을 작성하는 것이 가능합니다.

function 안에 들어간 내용들을 하나 하나 살펴보겠습니다.

먼저 Idle profile 찾는 목적의 function find_idle_profile( ) 입니다.

RESPONSE_CODE=$(sudo curl -s -o /dev/null -w "%{http_code}" https://example.com/api/test/profile)

Bash shell에서 명령어 실행 결과를 변수에 저장할 수 있으며 실행 결과를 문자열로 받아 특정 조건을 체크할 수 도 있습니다.

$(command)로 실행 결과를 변수에 저장 합니다.

위의 코드는 RESPONSE_CODE 라는 변수에 값을 할당하는 과정입니다.

변수에 할당할 값은
sudo curl -s -o /dev/null -w "%{http_code}" https://example.com/api/test/profile
이며 하나 하나 살펴보겠습니다.

  • sudo : 현재 계정에서 root 권한을 이용하여 명령어를 실행할 때 사용
  • curl : 리눅스에서 http 메시지를 쉘상에서 요청하여 결과를 확인하는 명령어
  • -s (--silent): 정숙 모드, 진행 내역이나 메세지등을 출력하지 않는다. -o 옵션으로 remote data 도 /dev/null 로 보내면 결과물도 출력되지 않는다
  • -o : 응답을 stdout이 아닌 file로 출력한다. (※ /dev/null 로 보내면 결과물 출력 X)
  • -w "%{http_code}" : 해당 옵션을 사용하면 StatusCode 가져올 수 있다.
  • https://주소.com : http 메세지를 보낼 주소

그러므로 해당 명령어를 실행하면 https://example.com/api/test/profile 해당 주소로 http Get요청을 보내고 StatusCode를 받아옵니다. 그리고 우리는 RESPONSE_CODE 변수를 사용했으므로 받아온 StatusCode를 RESPONSE_CODE에 저장합니다.

if [ ${RESPONSE_CODE} -ge 400 ]

조건문을 사용한 코드 이고 RESPONSE_CODE 가 400이상인지 아닌지 확인하는 조건문 입니다.

  • -ge : is greater than or equal to 이상인지 아닌지 확인하는 연산자

응답 코드가 400이상이면 if문 조건이 true 가 나오고 then 으로 빠지게 됩니다.
then 으로 빠졌을때는 CURRENT_PROFILE 에는 prof2가 들어가게 됩니다.

응답 코드가 400이상이 아니면 if문 조건이 false 가 나오고 else로 빠지게 됩니다.
else 로 빠졌을때는 CURRENT_PROFILE 에 다시 명령어를 넣어서 값을 받아오도록 되어있는데 이번엔 -o /dev/null , -w 옵션이 없으므로 응답값을 받아서 CURRENT_PROFILE 에 저장하게 됩니다.
(※ 주의하실 점은 서버를 구성하실때 해당 주소값 요청의 응답값으로 현재 세팅된 profile 값을 리턴 되도록 미리 세팅을 해주셔야 합니다.)

if [ ${CURRENT_PROFILE} -eq prof1 ]

조건문을 사용한 코드 이고 CURRENT_PROFILE 가 prof1 과 동일한지 아닌지 확인하는 조건문 입니다.

  • == : 문자열이 같은지 비교하는 연산자

CURRENT_PROFILE 가 prof1 이면 if문 조건이 true 가 나오고 then 으로 빠지게 됩니다.
then 으로 빠졌을때는 IDLE_PROFILE 에는 prof2가 들어가게 됩니다.

CURRENT_PROFILE 가 prof1 아니면 if문 조건이 false 가 나오고 else로 빠지게 됩니다.
else 로 빠졌을때는 IDLE_PROFILE 에는 prof1이 들어가게 됩니다.
(※ 다음과 같이 되는 이유는 현재 무중단 배포로 사용하려고 하는 포트가 2종류 (8081,8082) 이기 때문입니다. prof1이 아니라면 무조건 prof2가 될 수 밖에 없음)

echo "${IDLE_PROFILE}"

echo를 사용해주면 echo 로 출력된 값이 함수의 return 값으로 나가게 됩니다.
(※ return 명령어는 동작이 되지 않음)

function find_idle_port()

다음으로는 Idle profile 의 port를 찾는 function find_idle_port( ) 입니다.

IDLE_PROFILE=$(find_idle_profile)

IDLE_PROFILE 변수에 find_idle_profile 함수의 return 값을 할당합니다.
방금 위에서 확인한 function의 값이 IDLE_PROFILE 로 저장됩니다.

그리고 if문을 확인해보면 IDLE_PROFILE 가 prof1 일 경우
find_idle_port 함수의 return 값으로 8081 이 나가게 됩니다.
prof2 일 경우 8082 이 나가게 됩니다.


2. start.sh

idle_port 와 profile을 찾아서 Docker를 실행시키는 Script 파일 입니다.

#!/bin/bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh

DOCKER_IMG_FULL_NAME=$(head -1 env.txt)
GITHUB_SHA=$(tail -1 env.txt)

IDLE_PORT=$(find_idle_port)

IDLE_PROFILE=$(find_idle_profile)

echo "> profile=$IDLE_PROFILE 로 실행합니다."
echo "> docker img full name = $DOCKER_IMG_FULL_NAME"
echo "> GITHUB_SHA => $GITHUB_SHA => ${GITHUB_SHA::7}"

docker run -d --rm --name "$IDLE_PROFILE" -e active=$IDLE_PROFILE -p $IDLE_PORT:$IDLE_PORT $DOCKER_IMG_FULL_NAME:${GITHUB_SHA::7}

ABSPATH=$(readlink -f $0)

  • $0: 상대 경로로 현재 파일의 위치
  • readlink -f $0: 절대경로로 해당 파일의 위치가 출력됩니다. (즉, $0 파일의 실제 경로를 알려주는 역할)

ABSDIR=$(dirname $ABSPATH)

  • dirname : 입력된 경로(또는 경로+파일)로부터 디렉토리를 추출하는 리눅스 명령어

위에서 현재 파일의 절대경로를 ABSPATH로 가져와서 dirname을 통해 스크립트 파일들이 있는 디텍토리 경로를 추출합니다.

source ${ABSDIR}/profile.sh

  • source : import와 같은 역할

현재 파일에서 profile.sh를 import 한 것

DOCKER_IMG_FULL_NAME=$(head -1 env.txt)
GITHUB_SHA=$(tail -1 env.txt)

env.txt 파일에서 첫번째 줄을 가져와서 DOCKER_IMG_FULL_NAME 변수에 할당

env.txt 파일에서 마지막 줄을 가져와서 GITHUB_SHA 변수에 할당
(※ env.txt 파일은 제가 EC2안에 Github Actions를 통해서 만들어준 파일이며 내용으로는 Docker 이미지의 이름 과 현재 푸시된 코드의 Hash값이 들어갑니다.)

IDLE_PORT=$(find_idle_port)
IDLE_PROFILE=$(find_idle_profile)

import 한 profile.sh 에서 function을 가져와서 값들을 할당함

docker run -d --rm --name "$IDLE_PROFILE" -e active=$IDLE_PROFILE -p $IDLE_PORT:$IDLE_PORT $DOCKER_IMG_FULL_NAME:${GITHUB_SHA::7}

docker를 실행시키는 명령어 입니다.

  • run : docker img를 실행
  • -d : Background 에서 실행하는 옵션
  • --rm : container를 stop하면 container가 삭제되는 옵션
  • --name : container에 이름을 붙여주는 옵션
  • -e : docker 안에 환경변수를 설정하는 옵션
  • -p : port 포워딩 옵션

아까 위에서 변수 설정한 DOCKER_IMG_FULL_NAME 와 GITHUB_SHA 의 앞에서 7번째 자리값 까지를 사용해서 해당 이름으로 된 docker img를 실행


3. switch.sh

NginX 의 reverse proxy 되는 설정값을 변경하고 reload 명령어를 실행시켜줄 Script 파일

#!/bin/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 service nginx reload"
    sudo service nginx reload
}

function switch_proxy()

NginX의 reverse_proxy 되는 값을 변경해줄 function

IDLE_PORT=$(find_idle_port)
...
echo "set $service_url http://127.0.0.1:${IDLE_PORT};" | sudo tee /etc/nginx/conf.d/service-url.inc

  • tee : 표준 입력(standard input)에서 읽어서 표준 출력(standard output) 과 파일에 쓰는 명령어

idle_port를 확인하고 해당 포트의 값으로 /etc/nginx/conf.d/service-url.inc 안의 값을 변경한다

sudo service nginx reload

세팅값을 변경했으므로 NginX를 Reload한다.


4. health.sh

start.sh 에서 실행시킨 docker img가 제대로 실행이 되었는지 확인하고 제대로 켜져있을 경우 NginX의 reverse_proxy 값을 변경시켜 무중단 배포를 구현한다.

#!/bin/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://127.0.0.1:$IDLE_PORT/api/test/profile"
sleep 10

for RETRY_COUNT in {1..10}
do
  RESPONSE=$(curl -s http://127.0.0.1:${IDLE_PORT}/api/test/profile)
  UP_COUNT=$(echo ${RESPONSE} | grep 'prof' | wc -l)

  if [ ${UP_COUNT} -ge 1 ]
  then 
      echo "> Health check 성공"
      switch_proxy
      sleep 3
      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

sleep 10

  • sleep : 옆에 적힌 sec만큼 기다린다.

for RETRY_COUNT in {1..10}

for문이며 10번 돈다. 변수는 RETRY_COUNT 이다

안에서 실행되느 내용은

RESPONSE=$(curl -s http://127.0.0.1:\${IDLE_PORT}/api/test/profile)
UP_COUNT=$(echo ${RESPONSE} | grep 'prof' | wc -l)

RESPONSE 변수에는 IDLE_PORT로 api/test/profile 요청을 보내서 받아온 응답을 확인한다.

  • grep : 특정 파일에서 지정한 문자열이나 정규표현식을 포함한 행을 출력해주는 명령어
  • wc : word count 사용자가 지정한 파일의 행, 단어, 문자수를 세는 명령어 (※ -l 옵션은 줄 의 수를 출력함)

UP_COUNT 변수는 받아온 RESPONSE 에서 grep 명령어를 통해 prof 단어가 있는 줄을 출력하고 wc는 해당 줄이 몇줄인지 확인해서 돌려준다. 즉 prof1 이나 2라는 값이 왔으면 UP_COUNT에는 1이라는 값이 들어가게 된다.

if [ ${UP_COUNT} -ge 1 ]

해당 if문은 $up_count >= 1 과 같고 ("prof" 문자열이 있는지를 검증)

then
	  echo "> Health check 성공"
      switch_proxy
      sleep 3
      break

있으면 Health Check에 성공한것이므로 switch_proxy function을 사용해서 NginX의 세팅값 변경후 Reload 실행

  else
      echo "> Health check의 응답을 알 수 없거나 혹은 실행 상태가 아닙니다."
      echo "> Health check: \${RESPONSE}"
  fi

계속해서 실패하면 현재 받아온 값을 출력하고 if문 종료

if [ ${RETRY_COUNT} -eq 10 ]

for문 변수인 RETRY_COUNT가 10번이 되었는지 체크하는 if문

  then
    echo "> Health check 실패. "
    echo "> 엔진엑스에 연결하지 않고 배포를 종료합니다."
    exit 1
  fi

10번이 되었다면 Health Check는 실패로 돌아가고 port는 변경되지 않고 health.sh는 종료

echo "> Health check 연결 실패. 재시도..."
sleep 10

1번의 for문이 돌때마다 10초를 기다렸다가 다시 재시도를 한다


5. stop.sh

start.sh에서 실행시킨 container가 있기 때문에 현재 2개의 container가 실행되고 있으므로 1개의 container는 멈춰야 한다. health.sh의 상태에 따라 멈추게 되는 container는 달라진다.

#!/bin/bash

ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh

IDLE_PROFILE=$(find_idle_profile)

CONTAINER_ID=$(docker container ls -f "name=${IDLE_PROFILE}" -q)

echo "> 정지할 컨테이너 ID = ${CONTAINER_ID}"
echo "> 정지할 Profile은 = ${IDLE_PROFILE}"

if [ -z ${CONTAINER_ID} ]
then
  echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
  echo "> docker stop ${IDLE_PROFILE}"
  sudo docker stop ${IDLE_PROFILE}
  sleep 5
fi

CONTAINER_ID=$(docker container ls -f "name=${IDLE_PROFILE}" -q)

IDLE_PROFILE의 이름을 가지고 있는 컨테이너의 ID를 알아내고 CONTAINER_ID에 할당

if [ -z ${CONTAINER_ID} ]
then
  echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
  echo "> docker stop ${IDLE_PROFILE}"
  sudo docker stop ${IDLE_PROFILE}
  sleep 5
fi
  • -z String : 해당 String의 길이가 0인지 확인

CONTAINER_ID를 받아와서 해당 ID가 없으면 아무것도 하지 않고 Script 파일 종료
존재할 경우 docker stop 명령어가 실행되고 5초간 기다린다.

profile
HW -> FW -> Web

0개의 댓글