본문 바로가기

백엔드

[ToHero 백엔드 개발일지] Blue-Green 무중단 배포 with Docker

개요

큐시즘에서 만난 좋은 인연 중 한 명인 기획자 형이 같이 서비스 하나 개발해보자고 좋은 제안을 주었다. 근 1년 간 준비했던 우테코 7기에 떨어지고 계획했던 일정들이 날라가면서 공허함을 느끼고 있던 찰나에 다시 열정을 불태워 볼 수 있는 좋은 기회였다. 형 주변에 잘하시는 개발자 분들이 많았던터라 내가 제안을 받았음이 의아하면서도 굉장히 감사했다. 뭔가 위로를 받는 기분이라 카톡 화면을 캡쳐해두었다. (대헌이형, 건우형, 굿머닝.. 큐시즘을 통해 알게된 좋은 인연이 진짜 많은 것 같다)

채용공고도 자주 보내주고 근로 장학생도 추천해주고.. 여러모로 고마운 형이다.

 

제안을 받고 한다고 결정했을 때에는 설렘 반, 걱정 반이었다. 나에게 제안을 준 형의 기대를 저버리고 싶지 않았기도 하고 같이 하시는 기획, 디자인, 프론트 분들이 잘하시는 분들이라 내가 민폐를 끼치진 않을까 싶었다. 하지만 그 점이 오히려 원동력이 되어서 더 열심히 해야지 하는 마음이 들었다. 

 

 

이 포스팅 시리즈는 내가 이 프로젝트를 하면서 백엔드 개발자로서 했던 고민들, 트러블 슈팅들을 정리하고자 하는 포스팅이다. 릴리즈가 목표인 서비스인만큼 아이디어에 대한 구체적인 이야기는 못할 것 같다. 그리고 기술이나 개념을 설명하는 것이 이 포스팅의 주목적이 아님을 미리 알린다.

 

백엔드 개발 환경

아직 개발 초기 환경 세팅 중일 당시, PM 형이 무중단 배포를 제안했다. 서비스가 릴리즈 된 이후에도 유저의 동향이나 KPI 달성 정도를 바탕으로 새로운 버전으로의 마이그레이션을 진행해야 했고, 서버 재배포 과정에서 사용자가 겪을 불편함을 감소하고자 무중단 배포를 제안한 것이다. 기술 도입의 이유가 정확하고 합리적이어서 나도 동의했다.

 

다만 우려한 점은 2주 조금 안되는 시간이 개발 기간으로 주어졌는데 내가 무중단 배포가 처음이라는 것이다..!!  그래도 우리 서비스에 필수적인 기능이고 한 번 구축해놓으면 내 스스로도 편리할 것임을 알고 있었기에 한 번 공부해보자는 생각으로 진행했다. 

 

 

선택한 무중단 배포 방식

무중단 배포에는 익히 잘 알려진 대로 Rolling, Canary, Blue-Green  총 3가지가 존재한다. (각 방법에 대한 설명이 목적인 포스팅이 아님으로 설명은 생략한다) 나는 그중 Blue-Green 방식을 선택했다.

 

우선 우리가 사용할 서버는 인스턴스 한 개면 충분할 것 같다는 판단을 했다. KPI로 설정한 목표는 작성된 메시지 기준 1000개이고, 서비스 특성 상 특정 시간대에 트래픽이 몰리거나 한 번에 많은 트래픽이 발생하는 서버가 아니기 때문에 인스턴스 한 개로 가정했다.

 

카나리 방식을 굳이 사용할 필요가 없다고 판단했고, 인스턴스 하나로 충분한 서비스인데 굳이 여러 개의 인스턴스를 nginx에 연결해두는 것은 불필요하다고 판단했기에 Rolling 방식도 제외, 결국 Blue-Green 방식을 사용하기로 결정했다. 그리고 RollBack이 쉽다는 장점도 Blue-Green 을 선택하는데 한 몫 했다.

 

진행 과정 with 기술 도입 이유

일단 프리티어 EC2를 사용해서 배포해보기로 했다. 전체적인 구조는 다음과 같다. 

 

Why Docker?

그냥 nginx를 설치하고 blue/green 각 버전의 jar 파일을 실행하는 방식을 사용해도 되지만 Docker를 쓴 이유는 버 마이그레이션을 위함이다. 

일단 비즈니스 구글계정을 생성해 AWS 계정을 새로 만들었기에 프리티어 제공이 1년은 맞지만 서비스가 1년 이상 지속될 수도 있는 것이고 언제까지고 프리티어를 쓸 수는 없다고 판단해서 언제든 환경을 옮길 수 있도록 만들고 싶었다. 그리고 현재 내가 사용가능한 로컬 PC가 학교에 한 대 있는데 그걸로 서버를 돌려보고 싶었다. 

일단 EC2에 배포해서 환경을 미리 세팅해두고 여유가 될 때 로컬 PC로 마이그레이션을 진행하고 싶었고 이에 디바이스간 환경에 구애받지 않는 Docker를 사용하기로 결정해서 다음과 같은 구조가 나오게 되었다. 

 

CD 환경도 구성했는데 툴은 GitHub Action을 사용했다. 사용해본 적 있는 툴이라 익숙하기도 했고, Jenkins를 써본 적 없는 내가 개발 기간이 빠듯한 와중에 공부하면서 써보는 것은 무리라고 판단해서 GitHub Action을 선택했다. 

 

무중단 CD 를 위한 파일 구성

내가 설정한 파일의 구성은 다음과 같다. 

CD.yml에서 job과 step 흐름

job - build

레포지토리 특정 시점 불러오기 —> JDK setup —> Gradle 캐싱 —> gradle 파일 권한 부여 —> gradle 빌드 —> docker image 빌드 후 DockerHub에 push

  build:
    runs-on: ubuntu-latest

    steps:

      # 레포지토리의 특정 브랜치, 커밋을 가져오는 설정
      # Github Action 라이브러리인 actions/checkout@v3 액션을 사용
      - uses: actions/checkout@v3

      # JDK 셋업
      # Github Action 라이브러리인 actions/setup-java@v3 액션을 사용
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

      # Gradle 빌드에 필요한 데이터를 캐싱하여 빌드 속도를 향상시키는 설정
      # Github Action 라이브러리인 actions/cache 액션을 사용
      # Gradle 캐시에 의존성, 래퍼 등을 빌드할 때 저장해두었다가 나중에 재빌드할 때 재사용해서 속도를 향상
      - name: Cache Gradle packages
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
          restore-keys: ${{ runner.os }}-gradle-

      # gradle 파일에 접근할 권한을 부여
      - name: Run chmod to make gradlew executable
        run: chmod +x ./gradlew

      # build 작업 수행
      - name: Build with Gradle Wrapper
        run: ./gradlew build

      # docker image를 build 하고 docker hub에 push
      - name: Docker Build and Push
        run: |
          sudo docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          sudo docker build -f ./.deploy/Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPOSITORY }} .
          sudo docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPOSITORY }}

 

 

job - deploy

DockerHub에 로그인 —> EC2에 접속해서 스크립트 실행 (env 파일 생성 및 echo, Blue-Green 배포를 위한 deploy.sh 파일 실행)

 

deploy job에서 EC2에 접속하고 나서 Blue-Green을 위한 container 실행과 종료, 그리고 nginx 설정 변경하는 스크립트는 너무 길어서 deploy.sh 라는 파일로 분리 시켰다. 

  deploy:
    needs: build
    runs-on: ubuntu-latest

    steps:
      # DockerHub에 로그인
      # GitHub Action 라이브러리 사용
      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      # .env 파일에 환경 변수 값들 집어넣기 (Github Secret 에 저장한 정보를 .env 파일로 echo)
      # 그리고 Blue-Green 배포 스크립트인 deploy.sh 실행
      - name: Run a New Version of the the application on EC2
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ubuntu
          key: ${{ secrets.EC2_KEY }}
          script: |
            cd ~/Backend/.deploy
            rm -rf ./.env
            touch ./.env
            echo "DOCKER_USERNAME=${{ secrets.DOCKER_USERNAME }}" >> ./.env
            echo "DOCKER_REPOSITORY=${{ secrets.DOCKER_REPOSITORY }}" >> ./.env
            echo "DATASOURCE_URL_LOCAL=${{ secrets.DATASOURCE_URL_LOCAL }}" >> ./.env
            echo "DATASOURCE_USERNAME=${{ secrets.DATASOURCE_USERNAME }}" >> ./.env
            echo "DATASOURCE_PASSWORD=${{ secrets.DATASOURCE_PASSWORD }}" >> ./.env
            chmod +x deploy.sh
            source deploy.sh

 

 

 

deploy.sh 파일의 내용은 다음과 같다. 이 파일은 ubuntu 환경의 terminal에서 직접 실행되는 쉘 스크립트이다. 

흐름은 다음과 같다. 

 

현재 실행 중인 container 중 blue가 있는지 판단 —> 참,거짓 유무에 따라 실행/종료할 container name과 port 변수 값 할당 —> 실행할 버전의 docker-compose.yml 파일을 실행 —> nginx 설정 파일 sed -i 명령어로 변경 (포트 포워딩) 후 reload —> 구 버전의 container 종료 후 remove

#!/bin/bash

set -e

ERR_MSG=''

trap 'echo "Error occured: $ERR_MSG. Exiting deploy script."; exit 1' ERR

if sudo docker ps --filter "name=neighbors-blue" --quiet | grep -E .; then
  RUN_TARGET="neighbors-green"
  STOP_TARGET="neighbors-blue"
  WAS_RUN_PORT=8081
  WAS_STOP_PORT=8080
else
  RUN_TARGET="neighbors-blue"
  STOP_TARGET="neighbors-green"
  WAS_RUN_PORT=8080
  WAS_STOP_PORT=8081
fi

echo "The $STOP_TARGET version is currently running on the server. Starting the $RUN_TARGET version."

DOCKER_COMPOSE_FILE="$RUN_TARGET-deploy.yml"
sudo docker-compose -f "$DOCKER_COMPOSE_FILE" pull || { ERR_MSG='Failed to pull docker image'; exit 1; }
sudo docker-compose -f "$DOCKER_COMPOSE_FILE" up -d || { ERR_MSG='Failed to start docker image'; exit 1; }
sleep 50

NGINX_ID=$(sudo docker ps --filter "name=nginx" --quiet)
NGINX_CONFIG="/etc/nginx/conf.d/default.conf"

sudo docker exec $NGINX_ID /bin/bash -c "sed -i 's/$STOP_TARGET:8080/$RUN_TARGET:8080/' $NGINX_CONFIG"
sudo docker exec $NGINX_ID /bin/bash -c "nginx -s reload" || { ERR_MSG='Failed to reload nginx'; exit 1; }

echo "Terminating the $STOP_TARGET applications."

STOP_CONTAINER_ID=$(sudo docker ps --filter "name=$STOP_TARGET" --quiet)
if [ -n "$STOP_CONTAINER_ID" ]; then
  sudo docker stop $STOP_CONTAINER_ID
  sudo docker rm $STOP_CONTAINER_ID
  sudo docker image prune -af
fi

rm .env

echo "Deployment success."
exit 0

 

 

nginx 설정 파일

nginx의 설정 파일은 다음과 같다. deploy.sh 에서 sed -i  구문이 설정파일 가장 상단의 upstream app{ server blue:8080} 을 찾아서 blue:8080이면 green:8080으로, 반대라면 역으로 바꾸는 로직이다. 이렇게 nginx 포트 포워딩을 한다. 

 

 

의문점

여기서 내가 들었던 한가지 의문점은 nginx reload 하는 과정에서 사용자의 요청이 오면 무중단 배포가 안되는 것 아닌가? 였다. 

 

아래 블로그 글에 따르면 nginx의 reload 시간은 0.1 초 이내라고 한다. 개인 포스팅이라 완전신뢰는 어렵지만 일단 믿기로 하고 사용하면서 불편함을 겪을 정도로 딜레이가 길면 다른 방법을 생각해보기로 했다. 

https://smpark1020.tistory.com/239

 

[Nginx] 24시간 365일 중단 없는 서비스 만들기 1 - 무중단 배포 소개

배포하는 동안 애플리케이션이 종료된다는 문제가 있습니다. 긴 기간은 아니지만, 새로운 Jar가 실행되기 전까진 기존 Jar를 종료시켜 놓기 때문에 서비스가 종료됩니다. 반면 24시간 서비스하는

smpark1020.tistory.com

 

 

EC2 및 GitHub Secret 환경 세팅 

EC2에 Docker, docker-compose, Git, Vim 설치를 마치고, SSH 키를 발급해서 내 깃헙 계정에 등록도 해두었다. 

CD.yml 파일에서 가져다 쓸  환경 변수도 GitHub Secret에 등록해두었다. 

 

 

트러블 슈팅

docker에 green 띄워져 있는데 자꾸 blue가 띄워져 있다고 인식
if sudo docker ps --filter "name=neighbors-blue" | grep -E .; then
  RUN_TARGET="neighbors-green"
  STOP_TARGET="neighbors-blue"
  WAS_RUN_PORT=8081
  WAS_STOP_PORT=8080
else
  RUN_TARGET="neighbors-blue"
  STOP_TARGET="neighbors-green"
  WAS_RUN_PORT=8080
  WAS_STOP_PORT=8081
fi

 

해당 스크립트에서 조건문의 condition이 문제가 있었다. sudo docker ps --filter "name=neighbors-blue" 까지 실행하면 container는 출력되지 않지만 컨테이너가 없다고 해서 아무 것도 출력되지 않는 것이 아니라 목차(?) 같은게 출력되서 grep -E . 에서 true가 자꾸 나온 것이었다. 다른 옵션을 주어서 해결했다.

 

if sudo docker ps --filter "name=neighbors-blue" --quiet | grep -E .; then

 

container끼리 이름 지정하려면 같은 네트워크 써야함.

nginx 에서 blue:8080, green:8081 지정하는데도 자꾸 그런 container 못 찾는다고 해서 왜그런가 했더니 같은 네트워크를 공유하지 않아서 라고 한다.

보통 docker-compose에서 여러 개의 docker를 한 번에 실행할 때 network라는 키워드를 통해 하나로 지정해주곤 하는데 나는 Nginx는 ec2에서 계속 돌아가고, blue, green 만 계속 없앴다가 다시 생성했다가 하는 방식이기 때문에 docker-compose에서 한 번에 지정하는 것이 안됐다.


nginx는 cli에서 app-network라는 것을 create해서 connect 해주었고, blue, green은 docker-compose 파일에서 external: true 옵션으로 외부 네트워크를 지정해주었다.

blue는 test api 잘 되는데 green 은 안되는 현상

docker의 expose port 도 그렇고 nginx 에서 포트 포워딩 하는 방식의 이해가 부족해서 벌어진 헤프닝이었다.


docker-compose 파일에서 expose 로 지정하는 포트는 외부에서의 접근은 막고 같은 네트워크를 공유하는 container끼리만 접근가능하게 하겠다는 것이고, port: 8080:8080 이런 거는 앞의 포트는 호스트 PC의 포트고, 뒤의 포트는 container의 포트이다. 서로 mapping 해주는 것이다.


그리고 nginx 에서 포워딩 하는게 살짝 헷갈리는데 일단 나는 다음과 같은 설정 파일을 썼다.

여기에서 가장 상단에 blue:8080, green:8081을 사용했는데 이러면 안됐다. 만약 내가 nginx 를 그냥 local에서 바로 깔아서 돌렸으면 외부에서 호스트 PC의 포트에 접근하는거라 저게 맞는데, 나는 nginx도 container로 돌리고 nginx, blue, green 모두 같은 network를 공유한다. 이때 green conatianer는 8081 포트를 안쓰고 8080을 쓰니까 nginx 에서도 green:8080으로 해줘야 하는 것이다.

 

 

결론

GitHub Action 실행화면
EC2 docker ps

 

잘 된다! 처음 해보는 거고 수많은 시행 착오를 겪었지만 결국 잘 되는 것을 보니 안심이 된다.

나중에 로컬 PC 환경으로 배포를 옮길 예정이다. 그때 또 포스팅 하려고 한다.

 

레퍼런스

Blue-Green을 위한 스크립트

docker-compose yml 파일과 GitHub Action yml 파일 구성 (순서대로 흐름이 있음)

[docker] Docker Compose로 멀티 컨테이너 어플리케이션 만들기 (yaml/volumes)

 

[docker] Docker Compose로 멀티 컨테이너 어플리케이션 만들기 (yaml/volumes)

이번엔 도커 컴포즈를 이용하여 멀티 컨테이너 어플리케이션을 만들 것이다. 멀티 컨테이너를 구축하면 서비스별로 컨테이너를 만들기 때문에 각 컨테이너가 한 가지 일만 수행하고, 서로의 리

kimyu0218.tistory.com

[docker] Docker Compose로 멀티 컨테이너 어플리케이션 만들기 (yaml/volumes)

 

[docker] Docker Compose로 멀티 컨테이너 어플리케이션 만들기 (yaml/volumes)

이번엔 도커 컴포즈를 이용하여 멀티 컨테이너 어플리케이션을 만들 것이다. 멀티 컨테이너를 구축하면 서비스별로 컨테이너를 만들기 때문에 각 컨테이너가 한 가지 일만 수행하고, 서로의 리

kimyu0218.tistory.com

[docker] Docker Compose로 멀티 컨테이너 어플리케이션 만들기 (yaml/volumes)

 

[docker] Docker Compose로 멀티 컨테이너 어플리케이션 만들기 (yaml/volumes)

이번엔 도커 컴포즈를 이용하여 멀티 컨테이너 어플리케이션을 만들 것이다. 멀티 컨테이너를 구축하면 서비스별로 컨테이너를 만들기 때문에 각 컨테이너가 한 가지 일만 수행하고, 서로의 리

kimyu0218.tistory.com

 

나중에 로컬 PC로 옮길 때 레퍼런스

Github Action 으로 물리서버에 자동배포하기

 

Github Action 으로 물리서버에 자동배포하기

이번에 토이프로젝트중 하나로 사내 서비스용 챗봇서비스를 만들기 시작했다. 그런데 스케일 아웃을 고려할 필요 없는 작은 규모의 서비스이기 때문에 가장 간단한 배포 방법을 찾아보기로 했

velog.io