Github Action, Nginx, Docker 무중단 배포하기(블루/그린)

2023. 3. 30. 17:27AWS

어제 새벽에 kkini 프로젝트에 blue/green 방식으로 무중단 배포를 완료했습니다. 

Pull Request

1. 무중단 배포를 도입한 이유

이번 주 부터 kkini 프로젝트를 리팩토링해서, 빠른 시일내에 운영을 해보려고 합니다. 
앞으로 변경이나 배포가 잦아질 것 같아서 downtime을 없애는 방향으로 개선하고자 무중단 배포를 도입했습니다.

기존에는 새로운 버전을 배포하기 위해서는 
1) 실행중인 서버를 종료한다.
2) 새로운 서비스를 실행한다. 
와 같은 과정을 거쳤으나 이 방식은
1번에서 서버가 종료되는 시점부터 2번 서버가 온전히 켜지는 시점까지 downtime이 발생하게 됩니다.
무중단 배포를 도입하면 이 downtime을 해결할 수 있습니다.

 

2. 왜 Nginx와 Blue/Green?

이미 Nginx 서버를 사용하고 있어서 따로 환경을 구축 할 필요가 없었습니다. 
또, Nginx는 러닝커브가 낮고 저렴해서 Nginx를 이용하기로 하였습니다.

배포 전략에는 Blue/Green 이외에 롤링과 카나리 전략이 존재합니다. 
롤링 전략은 WAS 서버가 최소 두 대 이상이 필요합니다.
현재 AWS를 지원받아서 운영하는 상태라 서버 증설은 어렵다고 판단했습니다.

카나리 전략의 장점인 A/B 테스트는 현재 서비스에서 불필요하다고 판단되어 
신속하고 빠른 롤백이 가능한 블루/그린 방식을 채택했습니다.

동작하는 방식은 다음과 같습니다.

1) 실행중인 서버는 8080 포트로 SpringBoot 컨테이너와 연결되어 있습니다.

출처 https://jay-ji.tistory.com/m/99

2) CI가 완료된 새로운 버전의 컨테이너를 8081 포트로 띄웁니다.

출처 https://jay-ji.tistory.com/m/99

3) nginx를 변경한 뒤 reload하여 8081포트를 가리키도록 합니다.

출처 https://jay-ji.tistory.com/m/99

4) 구버전이 된 8080포트의 컨테이너를 제거합니다.

출처 https://jay-ji.tistory.com/m/99

 

3. docker-compose.yml 작성

저는 Dockerhub에 kiseo/kkini라는 이름으로 저장된 Image를 내려받는 방식으로 진행했습니다.
Github Action 과정에서 SpringBoot Jar를 Dockerfile을 이용해 빌드한 후, Dockerhub에 push하도록 설정했습니다.  

아래와 같이 docker-compose.yml blue, green을 추가합니다.

version: '3'

  green:
    container_name: green
    image: # docker hub의 image 이름
    ports:
      - "8080:8080"  # green은 8080 포트를 열어줍니다.
    environment:
      MY_SERVER: ${MY_SERVER}
      AWS_ACCESS_KEY: ${AWS_ACCESS_KEY}
      AWS_SECRET_KEY: ${AWS_SECRET_KEY}
      SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL}
      SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME}
      SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD}
      JWT_SECRET_KEY: ${JWT_SECRET_KEY}
      KAKAO_CLIENT_ID: ${KAKAO_CLIENT_ID}
      KAKAO_CLIENT_SECRET: ${KAKAO_CLIENT_SECRET}
      KAKAO_REDIRECT_URI: ${KAKAO_REDIRECT_URI}
      GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
      GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
      GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI}
      REDIS_HOST: ${REDIS_HOST}
      REDIS_PORT: ${REDIS_PORT}
      ACTIVE_PROFILE: ${ACTIVE_PROFILE}
      SLACK_WEBHOOK: ${SLACK_WEBHOOK}

  blue:
    container_name: blue
    image: # docker hub의 image 이름
    ports:
      - "8081:8080"  #blue는 8081 포트를 열어줍니다.
    environment:
      MY_SERVER: ${MY_SERVER}
      AWS_ACCESS_KEY: ${AWS_ACCESS_KEY}
      AWS_SECRET_KEY: ${AWS_SECRET_KEY}
      SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL}
      SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME}
      SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD}
      JWT_SECRET_KEY: ${JWT_SECRET_KEY}
      KAKAO_CLIENT_ID: ${KAKAO_CLIENT_ID}
      KAKAO_CLIENT_SECRET: ${KAKAO_CLIENT_SECRET}
      KAKAO_REDIRECT_URI: ${KAKAO_REDIRECT_URI}
      GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
      GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
      GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI}
      REDIS_HOST: ${REDIS_HOST}
      REDIS_PORT: ${REDIS_PORT}
      ACTIVE_PROFILE: ${ACTIVE_PROFILE}
      SLACK_WEBHOOK: ${SLACK_WEBHOOK}

 

이제 blue/green을 번갈아 실행시킬 수 있는 스크립트를 작성하겠습니다.

 

4. Nginx.conf 추가

Nginx가 blue 컨테이너를 사용할 때는 8081 포트로, green 컨테이너를 사용할 때는 8080포트로 연결을 맺어줘야 합니다.
/etc/nginx/ 하위에 nginx.blue.conf와 nginx.green.conf로 설정파일을 두 개 생성했습니다.
다음 단계에서 blue 컨테이너를 사용할 때는 nginx.blue.conf를, 
green 컨테이너를 사용할 때는 nginx.green.conf를 사용하도록 설정하겠습니다. 

# nginx.green.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

    events {
        worker_connections  1024;
        }

        http {

        include       mime.types;

        # 443 포트로 접근시 ssl을 적용한 뒤 3000포트로 요청을 전달해주도록 하는 설정.
        server {
        
        server_name dev.kkini.site;

        location / {
		
        # GREEN - 8080 포트로 연결합니다.
        # BLUE 설정파일은 이부분의 포트만 8081로 변경해주면 됩니다.
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        
        }

        listen 443 ssl; # managed by Certbot

        ssl_certificate /etc/letsencrypt/live/dev.kkini.site/fullchain.pem; # managed by Cert>
        ssl_certificate_key /etc/letsencrypt/live/dev.kkini.site/privkey.pem; # managed by Ce>
        include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
        }
        
        # 80 포트로 접근시 443 포트로 리다이렉트 시켜주는 설정
        server {
        
        return 301 https://$host$request_uri;

        listen 80;
        server_name dev.kkini.site;
        return 404; # managed by Certbot
        }

}

nginx.blue.conf 파일은 위와 동일하나, 8081 포트로 연결하는 부분만 다릅니다.

proxy_pass http://127.0.0.1:8081;

 

5. deploy.sh 작성

스크립트의 내용은 다음 5단계로 요약할 수 있습니다.

1) 실행중인 컨테이너를 확인합니다.

2) 실행중인 컨테이너가 blue라면 green으로 이미지를 내려받고 컨테이너를 실행시킵니다.

3) green은 8080포트로 떠있으니 8080 포트로 curl 명령을 통해 health check를 진행합니다.

4) 사용 가능하다면 nginx의 설정을 변경하고 reload합니다.
nginx.conf의 내용을 앞서 미리 추가한 nginx.green.conf, nginx.blue.conf 두개의 설정 파일 중 적절한 것으로 덮어 쓰도록 하였습니다.
blue 컨테이너를 띄우는 중이라면 nginx.blue.conf를,
green 컨테이너를 띄우는 중이라면 nginx.green.conf를 덮어쓰게 한 뒤 reload합니다.

5) blue 컨테이너를 정지시킵니다.

#!/bin/bash

IS_GREEN=$(docker ps | grep green) # 현재 실행중인 App이 blue인지 확인합니다.
DEFAULT_CONF=" /etc/nginx/nginx.conf"

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

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

  echo "1. get green image"
  docker-compose pull green # green으로 이미지를 내려받습니다.

  echo "2. green container up"
  docker-compose up -d green # green 컨테이너 실행

  while [ 1 = 1 ]; do
  echo "3. green health check..."
  sleep 3

  REQUEST=$(curl http://127.0.0.1:8080) # green으로 request
    if [ -n "$REQUEST" ]; then # 서비스 가능하면 health check 중지
            echo "health check success"
            break ;
            fi
  done;

  echo "4. reload nginx"
  sudo cp /etc/nginx/nginx.green.conf /etc/nginx/nginx.conf
  sudo nginx -s rel

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

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

  echo "2. blue container up"
  docker-compose up -d blue

  while [ 1 = 1 ]; do
    echo "3. blue health check..."
    sleep 3
    REQUEST=$(curl http://127.0.0.1:8081) # blue로 request

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

  echo "4. reload nginx" 
  sudo cp /etc/nginx/nginx.blue.conf /etc/nginx/nginx.conf
  sudo nginx -s reload

  echo "5. green container down"
  docker-compose stop green
fi

 

6. Github Action 변경

위에서 생성한 deploy.sh 파일을 EC2에 전달해야 합니다.
EC2에 직접 deploy.sh를 생성하셨다면 아래의 과정이 필요없습니다.

      # deploy.sh 파일 서버로 전달하기(복사 후 붙여넣기)
      - name: Send deploy.sh
        uses: appleboy/scp-action@master
        with:
          username: ubuntu
          host: ${{ secrets.EC2_HOST }}
          key: ${{ secrets.EC2_PRIVATE_KEY }}
          port: 22
          source: "./deploy.sh"
          target: "/home/ubuntu/"

 

EC2에 deploy.sh를 전달했다면 이제 배포과정에서 deploy.sh를 실행시키도록 설정합니다.

      # 도커 허브에서 jar파일 및 pull후에 컴포즈 up
      - name: Deploy to Dev
        uses: appleboy/ssh-action@master
        with:
          username: ubuntu
          host: ${{ secrets.EC2_HOST }}
          key: ${{ secrets.EC2_PRIVATE_KEY }}
          script: |
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}
            chmod 777 ./deploy.sh
            ./deploy.sh
            docker image prune -f

실행 결과

현재 green으로 container가 실행중입니다.

./deploy.sh 명령으로 deploy.sh 파일을 실행할 경우 아래와 같이 정상적으로 교체되는 것을 확인할 수 있습니다.

 

 

참고자료

https://jay-ji.tistory.com/m/99

https://wkddntjr1123.github.io/project/devrank4/

'AWS' 카테고리의 다른 글

Let's Encrypt 인증서와 Nginx로 https 설정하기  (0) 2023.04.03