티스토리 뷰

지난 블로그에서 Github Actions, Nginx, Docker를 이용해서 무중단 배포에 대해 다룬 적이 있습니다

[CI/CD] github actions, nginx, docker이용해서 blue-green 무중단 배포하기

1. Blue_Green 배포란? 8080 포트로 연결된 컨테이너 8081포트로 다른 다른 버전의 컨테이너 띄우기 nginx.conf 수정후 reload (업스트립 8081 수정) 8080 컨테이너 제거 새로 배포할 때마다 새로운 컨테이너들

minsu20.tistory.com

 
현재 시스템의 단점은 주요 구성 파일들이 GitHub에서 관리되지 않고 EC2 서버에 직접 생성되어 관리된다는 것입니다. 이러한 접근 방식은 버전 관리 및 추적 면에서 비효율적일 수 있습니다. 또한, 파일 전송에 SCP를 사용하는 대신 Docker Hub를 활용할 예정입니다.
 

개선 제안

  1. 버전 관리의 통합:Dockerfile, entry.sh 등 모든 중요한 구성 파일을 GitHub 저장소에 포함시켜 버전 관리를 통합합니다. 이렇게 하면 변경 사항 추적, 협업, 백업이 용이해집니다.
  2. Docker Hub 활용: 빌드된 Docker 이미지를 Docker Hub에 푸시하여, 배포 시 Docker Hub에서 이미지를 가져와서 사용합니다. 이 방법은 파일 전송 과정을 단순화하고, 배포 프로세스의 일관성을 높일 수 있습니다.

 

이 프로세스를 따르는데, 여기서 nginx를 이용해 무중단 배포를 추가한 것입니다.


❓ 블루-그린 배포란
블루-그린 배포는 두 개의 별도 환경(블루와 그린)을 사용하여 새로운 버전의 애플리케이션을 서비스 중단 없이 배포하는 방법입니다. 이 전략은 안정성을 높이고, 배포 중 발생할 수 있는 다운타임을 최소화합니다.

 

1. GitHub Actions를 이용한 CI/CD 파이프라인 구축 - actions.yml 작성

GitHub Actions를 활용하여 코드 변경이 발생할 때마다 자동으로 애플리케이션을 빌드하고 Docker 이미지로 패키징하는 워크플로우를 설정합니다.

 

name: Backend CI/CD

on:
  push:
    branches: [ "backend_main" ]

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      working-directory: ./backend
      APPLICATION: ${{ secrets.APPLICATION }}

    steps:
    # 소스 코드 체크아웃
    - uses: actions/checkout@v2

    # JDK 17 설정
    - name: Set up JDK 17
      uses: actions/setup-java@v2
      with:
        java-version: '17'
        distribution: 'adopt'

    # Gradle 패키지 캐시
    - name: Cache Gradle packages
      uses: actions/cache@v2
      with:
        path: |
             ~/.gradle/caches
             ~/.gradle/wrapper
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
        restore-keys: |
              ${{ runner.os }}-gradle-

   # 설정 파일 생성
    - run: |
          mkdir ./backend/src/main/resources
          cd ./backend/src/main/resources
          touch ./application.yml 
          echo "${{env.APPLICATION}}" > ./application.yml
  
    # 설정 파일을 작업공간에 저장
    - uses: actions/upload-artifact@v2
      with:
        name: application.yml
        path: ./backend/src/main/resources/application.yml

    # gradlew 권한 설정
    - name: Grant execute permission for gradlew
      run: chmod +x gradlew
      working-directory: ${{ env.working-directory }}

    # Gradle로 빌드
    - name: Build with Gradle
      run: ./gradlew build
      working-directory: ${{ env.working-directory }}

    # Gradle 캐시 정리
    - name: Cleanup Gradle Cache
      if: ${{ always() }}
      run: |
          rm -f ~/.gradle/caches/modules-2/modules-2.lock
          rm -f ~/.gradle/caches/modules-2/gc.properties

    # Docker 이미지 빌드 및 푸시
    - name: Docker build
      run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker build --no-cache -t ${{ secrets.DOCKER_USERNAME }}/moing_admin:green .
          docker build --no-cache -t ${{ secrets.DOCKER_USERNAME }}/moing_admin:blue .
          docker push ${{ secrets.DOCKER_USERNAME }}/moing_admin:green
          docker push ${{ secrets.DOCKER_USERNAME }}/moing_admin:blue

    # EC2로 deploy.sh 전송
    - name: Deploy deploy.sh to EC2
      uses: appleboy/scp-action@master
      with:
        host: ${{ secrets.EC2_SERVER_HOST }}
        username: ec2-user
        key: ${{ secrets.PRIVATE_KEY }}
        source: "./deploy.sh"
        target: "/home/ec2-user/"

    # 배포 스크립트 실행
    - name: Deploy on EC2
      uses: appleboy/ssh-action@master
      with:
          host: ${{ secrets.EC2_SERVER_HOST }}
          username: ec2-user
          key: ${{ secrets.PRIVATE_KEY }}
          envs: GITHUB_SHA
          script: |
            chmod +x /home/ec2-user/deploy.sh
            /home/ec2-user/deploy.sh

 

  1. 트리거 설정 (on):
    • push 이벤트에 반응하여, backend_main 브랜치에 대한 푸시가 있을 때마다 워크플로우가 실행됩니다.
  2. 작업 설정 (jobs):
    • ubuntu-latest 환경에서 실행됩니다.
    • 환경 변수 설정: 작업 디렉토리, 애플리케이션 파일(application.yml)을 설정합니다.
  3. 단계별 실행 (steps):
    • 코드 체크아웃: 현재 저장소에서 소스 코드를 가져옵니다.
    • JDK 설정: JDK 17을 설치합니다. Java 애플리케이션 빌드를 위해 필요합니다.
    • Gradle 캐시: Gradle 빌드 시간을 단축하기 위해 캐시를 설정합니다.
    • 구성 파일 생성 및 저장: application.yml 파일을 생성하고, 비밀 환경 변수로부터 값을 가져와 파일에 씁니다.
    • gradlew 권한 설정: 빌드 스크립트인 gradlew에 실행 권한을 부여합니다.
    • Gradle 빌드: 애플리케이션을 Gradle로 빌드합니다.
    • Gradle 캐시 정리: 캐시를 정리하여 저장 공간을 확보합니다.
    • Docker 이미지 빌드 및 푸시: Docker 이미지를 빌드하고 Docker Hub에 푸시합니다. 이는 blue-green 배포 전략을 위한 두 버전의 이미지를 준비하는 단계입니다.
    • EC2 서버에 배포 스크립트 전송 및 실행: deploy.sh 스크립트를 EC2 서버에 전송하고 실행하여, 빌드된 이미지를 EC2 서버에 배포합니다.

 

2. Docker를 이용한 컨테이너 관리 - deploy.sh 작성 

블루와 그린 환경에 해당하는 Docker 컨테이너를 관리하는 deploy.sh 스크립트를 작성합니다. 이 스크립트는 새로운 버전의 컨테이너를 실행하고, 이전 버전의 컨테이너를 종료합니다.

 

DOCKER_APP_NAME=meetup
DOCKER_USERNAME=modagbul_bo

# 최신 이미지 가져오기
docker pull ${DOCKER_USERNAME}/moing_bo:blue
docker pull ${DOCKER_USERNAME}/moing_bo:green

# 현재 실행 중인 컨테이너를 확인 (blue 또는 green)
EXIST_BLUE=$(docker ps --filter name=${DOCKER_APP_NAME}-blue --filter status=running -q)
EXIST_GREEN=$(docker ps --filter name=${DOCKER_APP_NAME}-green --filter status=running -q)

# 둘 다 실행 중이지 않을 경우 blue 실행
if [ -z "$EXIST_BLUE" ] && [ -z "$EXIST_GREEN" ]; then
    echo "No containers running. Starting blue up"
    
    # 만약 컨테이너가 중지된 상태로 존재하면 삭제한다.
    if [ "$(docker ps -a --filter name=${DOCKER_APP_NAME}-blue -q)" ]; then
        docker rm ${DOCKER_APP_NAME}-blue
    fi
    
    docker run -d --name ${DOCKER_APP_NAME}-blue -p 8081:8080 ${DOCKER_USERNAME}/moing_bo:blue
    BEFORE_COMPOSE_COLOR="green"
    AFTER_COMPOSE_COLOR="blue"
elif [ -z "$EXIST_BLUE" ]; then
    echo "blue up"
    
    # 만약 컨테이너가 중지된 상태로 존재하면 삭제한다.
    if [ "$(docker ps -a --filter name=${DOCKER_APP_NAME}-blue -q)" ]; then
        docker rm ${DOCKER_APP_NAME}-blue
    fi

    docker run -d --name ${DOCKER_APP_NAME}-blue -p 8081:8080 ${DOCKER_USERNAME}/moing_bo:blue
    BEFORE_COMPOSE_COLOR="green"
    AFTER_COMPOSE_COLOR="blue"
else
    echo "green up"

    # 만약 컨테이너가 중지된 상태로 존재하면 삭제한다.
    if [ "$(docker ps -a --filter name=${DOCKER_APP_NAME}-green -q)" ]; then
        docker rm ${DOCKER_APP_NAME}-green
    fi

    docker run -d --name ${DOCKER_APP_NAME}-green -p 8082:8080 ${DOCKER_USERNAME}/moing_bo:green
    BEFORE_COMPOSE_COLOR="blue"
    AFTER_COMPOSE_COLOR="green"
fi


sleep 40

# 새로운 컨테이너가 제대로 실행되었는지 확인
EXIST_AFTER=$(docker ps --filter name=${DOCKER_APP_NAME}-${AFTER_COMPOSE_COLOR} --filter status=running -q)
if [ -n "$EXIST_AFTER" ]; then
    # nginx.config를 컨테이너에 맞게 변경해주고 reload 한다
    cp ./nginx.${AFTER_COMPOSE_COLOR}.conf /etc/nginx/nginx.conf
    sudo nginx -s reload

    # 이전 컨테이너 종료
    docker stop ${DOCKER_APP_NAME}-${BEFORE_COMPOSE_COLOR}
    docker rm ${DOCKER_APP_NAME}-${BEFORE_COMPOSE_COLOR}
    echo "$BEFORE_COMPOSE_COLOR down"
else
    # Docker logs
    LOGS=$(docker logs ${DOCKER_APP_NAME}-${AFTER_COMPOSE_COLOR} 2>&1)
    echo "Error: ${DOCKER_APP_NAME}-${AFTER_COMPOSE_COLOR} failed to start."
    echo "$LOGS"
fi

 

  1. 환경 변수 설정:
    • DOCKER_APP_NAME: 애플리케이션의 이름입니다.
    • DOCKER_USERNAME: Docker Hub의 사용자 이름입니다.
  2. 최신 이미지 가져오기:
    • Docker Hub에서 blue와 green 태그가 붙은 최신 이미지를 가져옵니다.
  3. 현재 실행 중인 컨테이너 확인:
    • 실행 중인 blue와 green 컨테이너를 확인합니다.
  4. 컨테이너 실행 로직:
    • 만약 blue와 green 컨테이너가 모두 실행되지 않고 있다면, blue 컨테이너를 시작합니다.
    • 만약 blue 컨테이너만 중지된 상태라면, blue 컨테이너를 시작합니다.
    • 만약 green 컨테이너만 실행 중이라면, green 컨테이너를 시작합니다.
    • 이미 중지된 상태의 컨테이너가 있다면 삭제하고 새로 시작합니다.
  5. 포트 설정:
    • Blue 컨테이너는 8081 포트에, Green 컨테이너는 8082 포트에 연결합니다.
  6. 컨테이너 실행 후 40초 대기:
    • 새로운 컨테이너가 정상적으로 시작되었는지 확인하기 전에 40초 동안 대기합니다.
  7. Nginx 설정 변경 및 리로드:
    • 새로 실행된 컨테이너(AFTER_COMPOSE_COLOR)에 맞게 Nginx 설정 파일을 변경하고, Nginx를 리로드합니다.
  8. 이전 컨테이너 종료:
    • 새 컨테이너가 정상적으로 시작된 경우, 이전 컨테이너(BEFORE_COMPOSE_COLOR)를 중지하고 제거합니다.
  9. 오류 확인 및 로그 출력:
    • 새 컨테이너가 정상적으로 시작되지 않았을 경우, 오류 메시지와 해당 컨테이너의 로그를 출력합니다.

 

3. Nginx를 활용한 무중단 배포

 Nginx는 블루-그린 환경 간의 트래픽 전환을 관리합니다. 각 환경에 대한 별도의 Nginx 설정 파일(nginx.blue.conf 및nginx.green.conf)을 작성하여, 애플리케이션의 트래픽을 해당 환경으로 리디렉션합니다.
  • 블루 환경: 트래픽을 블루 컨테이너로 리디렉션합니다.
  • 그린 환경: 트래픽을 그린 컨테이너로 리디렉션합니다.

EC2 서버에 nginx 설치

AWS EC2(Amazon Linux 2)에 Nginx 설치하기 ( AWS / EC2 / Linux / Nginx )

안녕하세요. 그린주입니다 ๑'ٮ'๑ 경험이 많이 부족하지만 최선을 다해 적어보겠습니다! 개요 이번 글에서는 AWS EC2(Amazon Linux 2)에 Nginx 설치 방법을 공유하고자 합니다. 목차 Nginx 설치 EC2 인바

green-joo.tistory.com

EC2 서버에 nginx.blue.conf 작성

# user www-data;
# worker_processes auto;
# pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    upstream backend {
        server 내 IP :8081; # blue

    }

     access_log /var/log/nginx/access.log;

    server {
	     listen 80;

        location / {
                  proxy_pass http://localhost:8081;
                  proxy_set_header Host $host;
                  proxy_set_header X-Real-IP $remote_addr;
                  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                  proxy_set_header X-Forwarded-Proto $scheme;
        }

    }
}

EC2 서버에 nginx.green.conf 작성

# user www-data;
# worker_processes auto;
# pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    upstream backend {
        server 내 IP:8082; # green

    }

     access_log /var/log/nginx/access.log;

    server {
         listen 80;

        location / {
                  proxy_pass http://localhost:8082;
                  proxy_set_header Host $host;
                  proxy_set_header X-Real-IP $remote_addr;
                  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                  proxy_set_header X-Forwarded-Proto $scheme;
        }

    }
}

❗ !!! 내 IP에는 퍼블릭 IP를 입력해주어야 합니다!!!

4. Dockerfile 작성

FROM openjdk:17-jdk
ARG JAR_FILE=./build/libs/dashboardback-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]