DevOps

[CICD] Docker + Github Action + Nginx + Spring Boot를 이용한 blue/green 방식 무중단 배포

ShinySinee 2024. 9. 4. 12:10

오늘은 Blue green 배포에 대해 써보겠다.

1) blue/green 방식에 대한 이해

blue/green 배포 방식은 트래픽을 한번에 구버전에서 신버전으로 옮기는 방식으로 blue와 green을 나란히 구성해 두 상태로 배포 시점에 트래픽을 blue에서 green으로 일제히 전환시킨다.

현재 blue 컨테이너 8081포트를 바라보고 있지만

green 컨테이너가 활성화 되는 동안에도 요청은 blue 컨테이너로 reverse proxy 되기에 서비스는 중단되지 않는다. 

 

green 컨테이너 가 활성화 되면, blue 컨테이너로 보내던 요청을 green으로 향하도록 바꾸고 nginx를 reload시켜준다. 

 

따라서 nginx는 green 컨테이너 를 바라보고 reverse proxy 시켜준다.

 

이를 통해 서버가 로드되는 시간을 nginx가 reload 되는 시간 만큼으로 줄일 수 있다.
-> 실제 실행시켜보니 reload 되는 약 30초 동안만 api 서버가 502 gateway 에러로 사용 불가능하고, reload 끝난 후 바로 사용 가능하다!!!

2) 포트포워딩 포트 연결 

80으로 접속하면 → 8081에서 → 8080으로

2-1) nginx 설치

#업데이트
$ sudo apt update

# nginx 설치

$ sudo apt install nginx

# Nginx 실행 확인

$ sudo systemctl start nginx

$ sudo systemctl status nginx

2-2) /etc/nginx/sites-available/nginx.conf 수정

없으면 만들기! 원래 default 있었는데 삭제해주고 만들었다.

server {
        listen 80 default_server;
        listen [::]:80 default_server;
        root /var/www/html;

        server_name _;

        location / {
            try_files $uri $uri/ =404;
        }
}

 

sites-enabled에 symlink 만들기

cd /etc/nginx/sites-enabled
sudo ln -s /etc/nginx/sites-available/nginx.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo service nginx restart

3) DockerFile

FROM openjdk:17-jdk-alpine

ARG JAR_FILE=build/libs/BlueGreen-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar
##
ENTRYPOINT [ "java", "-jar", "-Dspring.profiles.active=prod", "/app.jar" ]

4) Docker-compose.yml

 

# docker-compose.blue.yml

version: '3'
services:
  blue:
    image: DOCKER REPO
    ports:
      - 8081:8080
# docker-compose.green.yml

version: '3'
services:
  green:
    image: DOCKER REPO
    ports:
      - 8082:8080
# nginx.blue.conf

events {
    worker_connections 1024;
}

http {
    upstream backend {
        server {public 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;
        }

    }
}
# nginx.green.conf

events {
    worker_connections 1024;
}

http {
    upstream backend {
        server {public 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;
        }

    }
}

5) deploy.sh

docker-compose ps를 통해 blue가 실행중인지 확인. 
실행중이 아니라면
	blue up, 
	before-compose-color=green, after-compose-color=blue
실행중이라면
	green up
	before-compose-color=blue, after-compose-color=green
새로운 컨테이너 띄운 후에 서버(aws-linux2) 의 nginx.conf 파일 수정

sudo nginx reload

이전 컨테이너 종료
# deploy.sh

DOCKER_APP_NAME=meetup
# Blue 를 기준으로 현재 떠있는 컨테이너를 체크한다.

EXIST_BLUE=$(docker-compose -p ${DOCKER_APP_NAME}-blue-1 -f docker-compose.blue.yml ps | grep Up)

# 컨테이너 스위칭
if [ -z "$EXIST_BLUE" ]; then
    echo "blue up"
    docker-compose -p ${DOCKER_APP_NAME}-blue-1 -f docker-compose.blue.yml up -d
    BEFORE_COMPOSE_COLOR="green"
    AFTER_COMPOSE_COLOR="blue"
else
    echo "green up"
    docker-compose -p ${DOCKER_APP_NAME}-green-1 -f docker-compose.green.yml up -d
    BEFORE_COMPOSE_COLOR="blue"
    AFTER_COMPOSE_COLOR="green"
fi

sleep 10

# 새로운 컨테이너가 제대로 떴는지 확인
EXIST_AFTER=$(docker-compose -p ${DOCKER_APP_NAME}-${AFTER_COMPOSE_COLOR}-1 -f docker-compose.${AFTER_COMPOSE_COLOR}.yml ps | grep Up)
if [ -n "$EXIST_AFTER" ]; then

  # nginx.config를 컨테이너에 맞게 변경해주고 reload 한다
  sudo cp ./nginx.${AFTER_COMPOSE_COLOR}.conf /etc/nginx/nginx.conf
	sudo  nginx -s reload

  # 이전 컨테이너 종료
  docker-compose -p ${DOCKER_APP_NAME}-${BEFORE_COMPOSE_COLOR}-1 -f docker-compose.${BEFORE_COMPOSE_COLOR}.yml down
  echo "$BEFORE_COMPOSE_COLOR down"
fi

 

위의 로직이 담긴 쉘 스크립트 명령어 파일을 github actions workflow yml에서 실행.

blue/green으로 따로 빌드업을 하고 blue/green 방식에 따라 바꾸는 sh 작성

ec2 서버의 nginx를 이용해 프록시를 진행하려한다.

그렇기 위해서는

docker가 8081,8082 포트로 연결해주고,

ec2의 nginx가 실행중인 8081,8082 포트를 80으로 연결해주는것.

[ docker가 8081,8082 포트로 연결해주고, ] 이 과정은 docker-compose에서 진행하고,

[ nginx가 실행중인 8081,8082 포트를 80으로 연결해주는것. ] 은 /etc/nginx/nginx.conf 파일 변경을 통해 이루어 진다고 보면 된다.

이 일련의 과정은 모두 deploy.sh에서 수행된다.

6) Github Actions 설정

# github repository Actions 페이지에 나타낼 이름
name: CI/CD

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

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-22.04
    steps:
      ## jdk setting
      - uses: actions/checkout@v3
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin' # https://github.com/actions/setup-java
        
      ## 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-
      - name: Grant execute permission for gradlew
        run: 
          chmod +x gradlew

      ## create application-prod.yml
      - name: create application.yml
        if: contains(github.ref, 'main')
        run: |
          cd ./src/main
          mkdir -p resources
          cd ./resources
          touch ./application.yml
          ls *
          echo "${{ secrets.PROPERTIES_PROD }}" > ./application-prod.yml
        shell: bash

      - name: Build With Gradle
        if: contains(github.ref, 'main')
        run: ./gradlew build -x test

      - name: Login to Docker Hub
        run: |
          echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin

      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: .
          file: ./Dockerfile-prod
          push: true
          tags: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}
            
      ## deploy to production
      - name: Deploy to prod
        uses: appleboy/ssh-action@v0.1.6
        id: deploy-prod
        if: contains(github.ref, 'main')
        with:
          host: ${{ secrets.EC2_HOST_PROD }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_PRIVATE_KEY }}
          envs: GITHUB_SHA
          script: |
              sudo docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}
              chmod 777 ./deploy.sh
              ./deploy.sh
              docker image prune -f

deploy가 실행되게 설정해준다.

 

잘 안보이지만 ./deploy.sh 명령어를 치면 green이 올라가고 blue이 removing 되는 걸 볼 수 있다!!!


이슈!!!

자세히 보면 Permission denied라 쓰여있다. 이는 deploy.sh cp부분에 sudo를 안써줬기 떄문이다.!!