diff --git a/.github/workflows/backend-dev-ci-cd.yml b/.github/workflows/backend-dev-ci-cd.yml index 255ca93d..6a7dc9a4 100644 --- a/.github/workflows/backend-dev-ci-cd.yml +++ b/.github/workflows/backend-dev-ci-cd.yml @@ -7,12 +7,12 @@ on: - "backend/**" - ".github/workflows/backend-dev-ci-cd.yml" - "Dockerfile" - # pull_request: - # branches: [ "develop" ] - # paths: - # - "backend/**" - # - ".github/workflows/backend-dev-ci-cd.yml" - # - "Dockerfile" +# pull_request: +# branches: [ "develop" ] +# paths: +# - "backend/**" +# - ".github/workflows/backend-dev-ci-cd.yml" +# - "Dockerfile" jobs: @@ -57,16 +57,24 @@ jobs: docker tag ${{ secrets.BE_DOCKER_IMAGE_NAME_DEV }} ${{ secrets.BE_DOCKERHUB_USERNAME }}/${{ secrets.BE_DOCKER_IMAGE_NAME_DEV }}:${GITHUB_SHA::7} docker push ${{ secrets.BE_DOCKERHUB_USERNAME }}/${{ secrets.BE_DOCKER_IMAGE_NAME_DEV }}:${GITHUB_SHA::7} - deploy: + deploy-new-container: needs: build-and-test runs-on: [ self-hosted, dev ] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Deploy new container + run: | + bash launch_next_container.sh ${GITHUB_SHA::7} dev ${{ secrets.BE_DOCKERHUB_USERNAME }} ${{ secrets.BE_DOCKER_IMAGE_NAME_DEV }} + working-directory: backend/deploy + + switch-new-container: + needs: deploy-new-container + runs-on: [ self-hosted, dev ] steps: - - name: Pull Image And Restart Container + - name: Switch from old to new container run: | - docker login -u ${{ secrets.BE_DOCKERHUB_USERNAME }} -p ${{ secrets.BE_DOCKERHUB_PASSWORD }} - docker stop ${{ secrets.BE_DOCKER_CONTAINER_NAME }} | true - docker rm ${{ secrets.BE_DOCKER_CONTAINER_NAME }} | true - docker image prune -a -f - docker pull ${{ secrets.BE_DOCKERHUB_USERNAME }}/${{ secrets.BE_DOCKER_IMAGE_NAME_DEV }}:${GITHUB_SHA::7} - docker run --name ${{ secrets.BE_DOCKER_CONTAINER_NAME }} -d -p 80:8080 -v /logs:/logs -e SPRING_PROFILES_ACTIVE=dev ${{ secrets.BE_DOCKERHUB_USERNAME }}/${{ secrets.BE_DOCKER_IMAGE_NAME_DEV }}:${GITHUB_SHA::7} + bash switch_blue_green_container.sh + working-directory: backend/deploy diff --git a/backend/deploy/README.md b/backend/deploy/README.md new file mode 100644 index 00000000..45f23b62 --- /dev/null +++ b/backend/deploy/README.md @@ -0,0 +1,33 @@ +# blue-green 스크립트 + +무중단 배포를 하기 위한 blue green 스크립트 입니다. + +## 초기 설정 + +blue green을 처음 구축할 때 사용됩니다. + +스크립트 실행 시 nginx 컨테이너와 총대마켓 컨테이너가 각각 80, 8080 포트로 구동되고, nginx는 총대마켓 컨테이너로 proxy_pass가 설정되도록 구축이됩니다. +외부의 요청을 받기 위해 nginx는 외부 80포트가 오픈됩니다. + +### 스크립트 실행 방법 + +``` bash +./setup.sh <최근 배포 성공한 commit 해시> <적용 프로파일> <컨테이너 계정 명> <컨테이너 이미지 명> +``` + +## 이후 전략 + +CI에서 배포가 끝나면 배포 단계에서 `launch_next_container.sh`을 실행합니다. +실행 시 최신 빌드 성공한 commit이 반영 된 컨테이너가 실행됩니다. +컨테이너가 올라가면, `sleep second` 동안 대기 후 성공적으로 컨테이너가 실행됬는지 확인합니다. + +이후 성공적으로 컨테이너가 구동 완료 되면 `switch_blue_green_container.sh`를 실행시켜 nginx가 새로운 컨테이너로 proxy_pass하도록 설정을 변경합니다. + +### 스크립트 실행 과정 + +``` bash +./launch_next_container.sh <적용 프로파일> <컨테이너 계정 명> <컨테이너 이미지 명> + +# blue -> green , green -> blue 전환 +./switch_blue_green_container.sh +``` diff --git a/backend/deploy/default.conf b/backend/deploy/default.conf new file mode 100644 index 00000000..6dfeddc8 --- /dev/null +++ b/backend/deploy/default.conf @@ -0,0 +1,14 @@ +upstream active_server { + server chongdae_backend:8080; +} + +server { + listen 80; + location / { + proxy_pass http://active_server; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} + diff --git a/backend/deploy/launch_next_container.sh b/backend/deploy/launch_next_container.sh new file mode 100755 index 00000000..287d0232 --- /dev/null +++ b/backend/deploy/launch_next_container.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +GITHUB_SHA=$1 +PROFILE_ACTIVE=$2 +DOCKERHUB_USER_NAME=$3 +DOCKER_IMAGE_NAME=$4 + +INITIAL_BLUE_GREEN_NETWORK_NAME="blue_green_network" +BLUE_CONTAINER="chongdae_backend_blue" +GREEN_CONTAINER="chongdae_backend_green" +NGINX_CONTAINER="nginx" + +# parameter check +if [ -z "$GITHUB_SHA" ] || [ -z "$PROFILE_ACTIVE" ] || [ -z "$DOCKERHUB_USER_NAME" ] || [ -z "$DOCKER_IMAGE_NAME" ]; then + echo "사용법: $0 " + exit 1 +fi + +get_active_container() { + local active_container_name=$(docker exec ${NGINX_CONTAINER} cat /etc/nginx/conf.d/default.conf | \ + grep -P '^\s*server\s+\S+:' | \ + grep -oP '(?<=server\s)[^:;]+' | \ + sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + + echo "$active_container_name" +} + +get_next_container() { + local active_container=$1 + if [[ "$active_container" == "$GREEN_CONTAINER" ]]; then + echo "$BLUE_CONTAINER" + return 0 + fi + + if [[ "$active_container" == "$BLUE_CONTAINER" ]]; then + echo "$GREEN_CONTAINER" + return 0 + fi + + return 1 +} + +health_check() { + local next_container=$1 + local max_tries=$2 + local sleep_seconds=$3 + + for ((i=1; i<=max_tries; i++)); do + echo "[INFO] Attempt $i: Checking health of $next_container" + local status=$(docker exec nginx curl -H "Host: localhost" -o /dev/null -s -w "%{http_code}\n" http://$next_container:8080/health-check) + + if [[ "$status" -eq "200" ]]; then + echo "[INFO] Health check successful." + return 0 + fi + + echo "[WARNING] Health check failed. Attempt $i of $max_tries." + sleep $sleep_seconds + done + + echo "[ERROR] All $max_tries health check attempts failed for $next_container. Exiting..." + return 1 +} + +ACTIVE_CONTAINER=$(get_active_container) +NEXT_CONTAINER=$(get_next_container $ACTIVE_CONTAINER) + +echo "[+] launch $NEXT_CONTAINER" + +docker rm -f ${NEXT_CONTAINER} >/dev/null 2>&1 + +docker run -d \ + --network ${INITIAL_BLUE_GREEN_NETWORK_NAME} \ + --name ${NEXT_CONTAINER} \ + -v /logs:/logs \ + -e SPRING_PROFILES_ACTIVE=${PROFILE_ACTIVE} \ + ${DOCKERHUB_USER_NAME}/${DOCKER_IMAGE_NAME}:${GITHUB_SHA:0:7} + +MAX_TRIES=5 +SLEEP_SECONDS=10 +health_check "$NEXT_CONTAINER" "$MAX_TRIES" "$SLEEP_SECONDS" +if [ $? -ne 0 ]; then + exit 1 +fi diff --git a/backend/deploy/setup.sh b/backend/deploy/setup.sh new file mode 100755 index 00000000..e9812c79 --- /dev/null +++ b/backend/deploy/setup.sh @@ -0,0 +1,62 @@ +#!/bin/bash +GITHUB_SHA=$1 +PROFILE_ACTIVE=$2 +DOCKERHUB_USER_NAME=$3 +DOCKER_IMAGE_NAME=$4 + +# parameter check +if [ -z "$GITHUB_SHA" ] || [ -z "$PROFILE_ACTIVE" ] || [ -z "$DOCKERHUB_USER_NAME" ] || [ -z "$DOCKER_IMAGE_NAME" ]; then + echo "사용법: $0 " + exit 1 +fi + +# intialize blue & gren +INITIAL_INSTANCE_NAME="chongdae_backend_green" +INITIAL_BLUE_GREEN_NETWORK_NAME="blue_green_network" +NGINX_DEFAULT_CONF_PATH="default.conf" +NGINX_NEW_CONF_PATH="new-default.conf" + +# 1. ADD DOCKER NETWORK +if ! docker network ls | grep -q ${INITIAL_BLUE_GREEN_NETWORK_NAME}; then + echo "[+] Create Docker Netowrk ${INITIAL_BLUE_GREEN_NETWORK_NAME}" + docker network create --driver bridge "${INITIAL_BLUE_GREEN_NETWORK_NAME}" +fi + +# 2. RUN INITIAL_CONTAINER +echo "[+] stop and remove intial container" +docker rm -f ${INITIAL_INSTANCE_NAME} >/dev/null 2>&1 + +echo "[+] add chongdae_backend container" +docker run -d \ + --network ${INITIAL_BLUE_GREEN_NETWORK_NAME} \ + --name ${INITIAL_INSTANCE_NAME} \ + -v /logs:/logs \ + -e SPRING_PROFILES_ACTIVE=${PROFILE_ACTIVE} \ + ${DOCKERHUB_USER_NAME}/${DOCKER_IMAGE_NAME}:${GITHUB_SHA:0:7} + +if [ $? -ne 0 ]; then + exit 1 +fi + +# 3. CHANGE INITIAL UP STREAM SERVER +cp ${NGINX_DEFAULT_CONF_PATH} ${NGINX_NEW_CONF_PATH} + +if grep -q "server chongdae_backend:" "${NGINX_NEW_CONF_PATH}"; then + sed -i "s/server chongdae_backend:/server ${INITIAL_INSTANCE_NAME}:/" "${NGINX_NEW_CONF_PATH}" +fi + +# 4. RUN DOCKER NGINX +echo "[+] RUN nginx server" +docker run -d \ + --network ${INITIAL_BLUE_GREEN_NETWORK_NAME} \ + --name nginx \ + -p 80:80 \ + nginx:latest + +# 5. setup proxy in nginx container +docker cp ${NGINX_NEW_CONF_PATH} nginx:/etc/nginx/conf.d/${NGINX_NEW_CONF_PATH} +docker exec nginx mv /etc/nginx/conf.d/${NGINX_NEW_CONF_PATH} /etc/nginx/conf.d/default.conf +docker exec nginx nginx -s reload + +# 6. clean up +rm -rf ${NGINX_NEW_CONF_PATH} diff --git a/backend/deploy/switch_blue_green_container.sh b/backend/deploy/switch_blue_green_container.sh new file mode 100755 index 00000000..2ba24b0a --- /dev/null +++ b/backend/deploy/switch_blue_green_container.sh @@ -0,0 +1,83 @@ +#!/bin/bash +NGINX_CONTAINER_NAME="nginx" +BLUE_CONTAINER="chongdae_backend_blue" +GREEN_CONTAINER="chongdae_backend_green" +NGINX_DEFAULT_CONF="default.conf" +NGINX_CHANGED_DEFAULT_CONF="new-default.conf" + +get_active_container() { + local active_container_name=$(docker exec nginx cat /etc/nginx/conf.d/default.conf | \ + grep -P '^\s*server\s+\S+:' | \ + grep -oP '(?<=server\s)[^:;]+' | \ + sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + + echo "$active_container_name" +} + +get_next_container() { + local active_container=$1 + if [[ "$active_container" == "$GREEN_CONTAINER" ]]; then + echo "$BLUE_CONTAINER" + return 0 + fi + + if [[ "$active_container" == "$BLUE_CONTAINER" ]]; then + echo "$GREEN_CONTAINER" + return 0 + fi + + return 1 +} + +change_blue_green_container() { + local next_container=$1 + cp ${NGINX_DEFAULT_CONF} ${NGINX_CHANGED_DEFAULT_CONF} + + if grep -q "server chongdae_backend:" "${NGINX_CHANGED_DEFAULT_CONF}"; then + sed -i "s/server chongdae_backend:/server ${next_container}:/" "${NGINX_CHANGED_DEFAULT_CONF}" + fi + echo "[+] change container $next_container" + docker cp ./${NGINX_CHANGED_DEFAULT_CONF} ${NGINX_CONTAINER_NAME}:/etc/nginx/conf.d/${NGINX_CHANGED_DEFAULT_CONF} + docker exec ${NGINX_CONTAINER_NAME} cp /etc/nginx/conf.d/${NGINX_CHANGED_DEFAULT_CONF} /etc/nginx/conf.d/default.conf + docker exec ${NGINX_CONTAINER_NAME} rm -rf /etc/nginx/conf.d/${NGINX_CHANGED_DEFAULT_CONF} + docker exec ${NGINX_CONTAINER_NAME} nginx -s reload + + # clean up + rm -rf ${NGINX_CHANGED_DEFAULT_CONF} +} + + +health_check() { + local status=$(curl -o /dev/null -s -w "%{http_code}\n" http://localhost/health-check) + if [[ "$status" -eq "200" ]]; then + return 0 + fi + return 1 +} + +remove_container() { + local removed_container_name=$1 + + docker rm -f $removed_container_name + if [ $? -eq 0 ]; then + echo "[-] stop $removed_container_name" + return 0 + fi + return 1 +} + +ACTIVE_CONTAINER=$(get_active_container) +NEXT_CONTAINER=$(get_next_container $ACTIVE_CONTAINER) + +if [ $? -eq 0 ]; then + change_blue_green_container $NEXT_CONTAINER +fi + +health_check + +if [ $? -eq 0 ]; then + remove_container $ACTIVE_CONTAINER +fi + +echo "[-] clean docker image" +docker image prune -a -f