Gyunpang

6. 여러대의 인스턴스에 무중단 배포하기

시롱시롱 2024. 4. 6. 14:13

마지막 CI/CD 작업이다.

하나의 이미지를 여러 인스턴스에 그리고 인스턴스 내 여러 컨테이너를 통해 실행하는 것이 목적이다.

Oracle 인스턴스를 통해 백엔드 배포하기

오라클 인스턴스 역시 리눅스 기반이기에 동일하게

  1. 인바운드 포트 열고 (지금은 테스트 용으로 전부 개방했지만 ec2에 있는 게이트웨이 인스턴스와 내 로컬 pc ip 만 허용시킬 것이다)
  2. 해당 포트로 서버 배포시키고
  3. 테스트해보기

큰 무리 없이 정상적으로 서버가 확인 되었다면 이제 다음으로 넘어가자

nginx를 통해 포트 없이 배포시킨 서버로 요청을 넘겨보자

docker compose를 통해 scale 값을 주고 여러 컨테이너를 띄우려면 해당 컨테이너가 받을 포트를 하나만 딱 정할 수 없다

즉, 컨테이너를 8080:8080 이런식으로 명시한 상태로 동일 이미지로 2개 띄워버리면 두 컨테이너가 충돌하기에 다른 방법이 필요하다.

8080 ~ 8088 이런식으로 포트 범위를 주는 방법도 있지만 특정 갯수를 내가 설정해야 하기에 유연하지 못하다고 생각한다.

그래서, docker compose에 nginx를 80:80으로 배포시키고 80으로 오는걸 proxy_pass로 be 서버들에게 보내는 것을 목적으로 한다.

그렇기에 우선 포트 없이 배포시킨 컨테이너를 Nginx를 통해 접근해보자.

events {
  worker_connections 1000;
}

http {
  upstream be-blue {
    server be-blue:8080;
  }

  server {
    listen 80;
    location / {
      proxy_pass <http://be-blue/>;
     }
  }
}

이런식으로 간단하게 nginx 설정 파일을 생성하고 저장해준다.

version: '3'

services:
  be-blue:
    image: sok5188/gyunpang-be:latest
    environment:
      CONTAINER_COLOR: "blue"

  nginx:
    image: nginx:latest
    volumes:
      - ./nginx/config/nginx.conf:/etc/nginx/nginx.conf
    restart: always
    ports:
      - "80:80"

docker compose 파일도 위처럼 설정해준다.

volume에 아까 작성했던 nginx 파일을 잡아주고 컨테이너 포트도 80:80으로 잡아서 80 포트로 오는 접근 모두를 8080으로 패싱하게끔 설정하자.

그리고 포트번호 없이 접속하게 되면

정상적으로 응답하는 모습을 볼 수 있다.

scale 옵션으로 여러 컨테이너를 띄워보기

sudo docker compose up -d --scale be-blue=3

위 명령을 통해 백엔드 서버를 여러개 올려본 후

log를 확인해보면

이런식으로 서버가 3대 올라가게 되고

 

api 호출 후 로그를 다시 보면 1,2 컨테이너가 응답을 받고 처리한 로그를 확인할 수 있다.

근데.. 3번 컨테이너는 정상적으로 올라가긴했지만 막상 api 요청을 처리하지는 않았다.

추측하기론, 라운드로빈 방식으로 api 요청을 돌린다고는 했으니 분명 1→2→3→1 이렇게 순차적으로 요청이가야 하는데.. api가 단순히 string만 반환하기에 3번째 서버까지 쓰지 않으려 하는 것 같기도 한데..

우선 추후에 서버가 응답을 보내고 비동기 이벤트로 쓰레드를 재우는 api를 만들어서 테스트를 다시 해봐야 할 것 같다.

여튼..여러 컨테이너가 띄워졌으니 이제 배포 자동화만 설정하면 끝이다.

Blue Green 컨테이너를 통한 무중단 배포하기

이제 진행해볼 단계에선

  1. 들어온 요청을 Blue Green 컨테이너로 동적으로 변환하기
  2. docker compose를 통해 nginx 컨테이너만 reload 시키기
  3. docker compose 를 통해 blue, green을 스케일을 적용한 상태로 개별로 올리고 내리기
  4. 위의 내용들을 바탕으로 github workflow를 작성하고 실행 스크립트 파일을 작성하기

크게 위 단계로 진행해보려 한다.

 

1.들어온 요청을 Blue Green 컨테이너로 동적으로 변환하기

gateway 처럼 포트가 정해진 컨테이너가 아니기에 조금은 다른 방식이 필요하다고 생각한다.

더보기
  • 실패시도
    • 로컬 변수 설정 → upstream에서 변수 사용
      • http 블록에서 사용 불가
    • map 모듈과 환경 변수설정을 통해 변수에 따라 upstream 동적 변경
      • docker compose 설정 파일에 environment로 값 추가 → unknown variable 에러 발생 및 해결 시도 실패
      • 설정 파일을 template으로 만들고 envsubt 실행??(gpt친구가 말해준거..) → invalid number of arguments 오류 → 이것저것 해봤지만 역시 실패
    • 프록시 패스하는 부분을 변수로 설정하고 upstream을 blue,green 모두 잡기
      • blue 와 green을 동시에 실행시키는 상황은 deploy 시점 잠깐 뿐이기에 green으로 업스트림을 보낼 수 없음 → 실패

생각보다 단순하게 설정할 수 있다.

gateway 시 했던 것 처럼 배포 시점에 복사될 nginx.conf 파일을 생성하고 해당 파일에 덮어씌울 nginx_blue.conf , nginx_green.conf 를 생성하는 것이다.

즉, 기존에 작성했던 파일 그대로를 green 버전으로 생성만 하면 된다.

 

nginx_green.comf

events {
  worker_connections 1000;
}

http {
  upstream be-green {
    server be-green:8080;
  }

  server {
    listen 80;
    location / {
      proxy_pass <http://be-green/>;
     }
  }
}

docker-compose.yml

version: '3'

services:
  be-blue:
    image: sok5188/gyunpang-be:latest
    environment:
      CONTAINER_COLOR: "blue"

  be-green:
    image: sok5188/gyunpang-be:latest
    environment:
      CONTAINER_COLOR: "green"

  nginx:
    image: nginx:latest
    volumes:
      - ./nginx/config/nginx.conf:/etc/nginx/nginx.conf
    restart: always
    ports:
      - "80:80"

이런식으로 nginx 컨테이너를 띄우는 시점에 nginx.conf 파일을 복사해가도록 설정하고 blue, green 상태에 맞춰 nginx.conf 를 바꿔 주면 된다.

 

2. docker compose를 통해 nginx 컨테이너만 reload 시키기

 

기존에는 인스턴스 자체에서 nginx를 실행시켰기에 인스턴스 터미널에서 reload를 시켰다.

docker compose로 구성된 nginx 컨테이너를 Reload를 시키려면 어떻게 해야 할 까 ?

또, 그렇게 reload해도 docker compose 설정 파일을 바탕으로 다시 volume을 잡아갈까 ?

우선 현재는 green이 라우팅 되고 있으니 이걸 blue로 변경해보자

sudo cp nginx_blue.conf nginx.conf

이제 우리가 nginx 컨테이너에 전달하는 nginx 설정 파일은 변경했으니 nginx 컨테이너를 reload해보면

sudo docker compose exec nginx(container name) service nginx reload

원하던 대로 변경된 것을 볼 수 있다.

 

아무래도 볼륨에 연결된 파일을 변경하면 컨테이너 내부 볼륨의 파일도 바로 변경되는 것 같아 보인다.

확인해보자

 

다시 green으로 nginx.conf를 덮어씌우고

sudo docker compose exec -it nginx /bin/bash

로 nginx bash로 들어가 nginx.conf를 확인해보면

생각했던대로 바로 변경된 것을 확인할 수 있다.

 

3.docker compose 를 통해 blue, green을 스케일을 적용한 상태로 개별로 올리고 내리기

 

간단하다 개별 컨테이너를 올리듯이 up해주고 —scale로 옵션만 넣어주면 된다.

sudo docker compose up -d be-green --scale be-green=3

sudo docker compose down be-green

이런 식으로 올리고 내리면 된다.

 

4.위의 내용들을 바탕으로 github workflow를 작성하고 실행 스크립트 파일을 작성하기

 

이제 필요한 모든 기능에 대해서 동작을 확인했으니 이걸 바탕으로 워크플로우와 스크립트 파일을 작성해보자

deploy.sh

#!/bin/bash

IS_GREEN=$(sudo docker ps | grep green) # 현재 실행중인 App이 blue인지 확인합니다.

if [ -z "$IS_GREEN" ]; then # blue라면

  echo "### BLUE => GREEN ###"

  echo "1. get green image"
  sudo docker compose pull be-green

  echo "2. green container up"
  sudo docker compose up -d be-green --scale be-green=3

  for cnt in {1..10}
  do
    echo "3. green health check..."
    echo "서버 응답 확인중(${cnt}/10)";

    REQUEST=$(curl <http://localhost/health>)
      if [ -n "$REQUEST" ]
        then # 서비스 가능하면 health check 중지
          echo "health check success"
          break ;
        else
          sleep 10

      fi
  done;

  if [ $cnt -eq 10 ]
  then
  	echo "서버가 정상적으로 구동되지 않았습니다."
  	exit 1
  fi

  echo "4. reload nginx"
  sudo cp /home/ubuntu/nginx/config/nginx_green.conf /home/ubuntu/nginx/config/nginx.conf
  sudo docker compose exec nginx service nginx reload

  echo "5. blue container down"
  sudo docker compose down be-blue
else
  echo "### GREEN => BLUE ###"

  echo "1. get blue image"
  sudo docker compose pull be-blue

  echo "2. blue container up"
  sudo docker compose up -d be-blue --scale be-blue=3

  for cnt in {1..10}
  do
    echo "3. blue health check..."
    echo "서버 응답 확인중(${cnt}/10)";

    REQUEST=$(curl <http://localhost/health>) # blue로 request

    if [ -n "$REQUEST" ]
      then # 서비스 가능하면 health check 중지
        echo "health check success"
        break ;
      else
        sleep 10

    fi
  done;

  if [ $cnt -eq 10 ]
  then
  	echo "서버가 정상적으로 구동되지 않았습니다."
  	exit 1
  fi

  echo "4. reload nginx"
  sudo cp /home/ubuntu/nginx/config/nginx_blue.conf /home/ubuntu/nginx/config/nginx.conf
  sudo docker compose exec nginx service nginx reload

  echo "5. green container down"
  sudo docker compose down be-green
fi

/github/workflows/main.yml

name: CI/CD using github actions & docker

# event trigger
on:
  push:
    branches: [ "master" ]

permissions:
  contents: read

jobs:
  CI-CD:
    runs-on: ubuntu-latest
    steps:

      # JDK setting
      - uses: actions/checkout@v3
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

      # gradle caching - 빌드 시간 향상
      - name: Gradle Caching
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      # gradle build
      - name: Build with Gradle
        run: ./gradlew build -x test

      # send docker-compose.yml to be1
      - name: Send docker-compose.yml to BE1
        uses: appleboy/scp-action@master
        with:
          key: ${{ secrets.BE1_PRIVATE_KEY }}
          host: ${{ secrets.BE1_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          port: 22
          source: "./docker-compose.yml"
          target: "/home/ubuntu/"

      # send docker-compose.yml to be2
      - name: Send docker-compose.yml to BE2
        uses: appleboy/scp-action@master
        with:
          key: ${{ secrets.BE2_PRIVATE_KEY }}
          host: ${{ secrets.BE2_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          port: 22
          source: "./docker-compose.yml"
          target: "/home/ubuntu/"

      # send deploy.sh be1
      - name: Send deploy.sh to BE1
        uses: appleboy/scp-action@master
        with:
          key: ${{ secrets.BE1_PRIVATE_KEY }}
          host: ${{ secrets.BE1_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          port: 22
          source: "./deploy.sh"
          target: "/home/ubuntu/"

      # send deploy.sh be2
      - name: Send deploy.sh to BE2
        uses: appleboy/scp-action@master
        with:
          key: ${{ secrets.BE2_PRIVATE_KEY }}
          host: ${{ secrets.BE2_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          port: 22
          source: "./deploy.sh"
          target: "/home/ubuntu/"

      # docker build & push to hub
      - name: Docker build & push to hub
        if: contains(github.ref, 'master')
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_TOKEN }}
          docker build -f Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/gyunpang-be .
          docker push ${{ secrets.DOCKER_USERNAME }}/gyunpang-be

      # deploy to be1
      - name: run script at BE1
        uses: appleboy/ssh-action@master
        id: run-script-be1
        if: contains(github.ref, 'master')
        with:
          key: ${{ secrets.BE1_PRIVATE_KEY }}
          host: ${{ secrets.BE1_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          port: 22
          script: |
            sudo docker ps
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/gyunpang-be
            chmod 777 ./deploy.sh
            ./deploy.sh
            sudo docker image prune -f 

      ## deploy to be2
      - name: run script at BE2
        uses: appleboy/ssh-action@master
        id: run-script-be2
        if: contains(github.ref, 'master')
        with:
          key: ${{ secrets.BE2_PRIVATE_KEY }}
          host: ${{ secrets.BE2_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          port: 22
          script: |
            sudo docker ps
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/gyunpang-be
            chmod 777 ./deploy.sh
            ./deploy.sh
            sudo docker image prune -f 

 

be1 서버에도 동일한 nginx 설정을 해준 뒤 위와같이 workflow를 작성하면 동일 이미지를 be1,be2에 모두 배포할 수 있게 된다 !

 

끝 !


참고

https://jojaeng2.tistory.com/86

https://medium.com/@vinodkrane/microservices-scaling-and-load-balancing-using-docker-compose-78bf8dc04da9