From 9a23cde8ed2bceb7687c216d1ba9c477cf502eb9 Mon Sep 17 00:00:00 2001 From: do0ori Date: Sun, 22 Dec 2024 15:29:35 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20EC2=20=EC=84=9C=EB=B2=84=EC=9D=98?= =?UTF-8?q?=20run=5Fbackend.sh=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EC2 서버에만 존재하던 run_backend.sh 파일을 repo로 가져옴 --- .github/script/run_backend.sh | 94 +++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 .github/script/run_backend.sh diff --git a/.github/script/run_backend.sh b/.github/script/run_backend.sh new file mode 100644 index 00000000..60b2689d --- /dev/null +++ b/.github/script/run_backend.sh @@ -0,0 +1,94 @@ +#!/bin/bash + +TAG=$1 +ENV_FILE=$2 +REPO=$3 +IMAGE=$4 +ENV_PATH="/home/ubuntu/actions-runner/_work/dangdang-walk/dangdang-walk/backend/server" +FILE_PATH="$ENV_PATH/$ENV_FILE" + + +echo"TAG: " +echo $TAG + +echo "ENV_FILE: " +echo $ENV_FILE + +echo "REPO: " +echo $REPO + +echo "IMAGE: " +echo $IMAGE + +echo"FILE PATH: " +echo $FILE_PATH + +aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin "$REPO" + +IS_BLUE_RUNNING=$(docker inspect -f '{{.State.Status}}' dangdang-api-blue | grep running) +echo "Blue : " +echo "$IS_BLUE_RUNNING" + +if [ -z "$TAG" ]; then + echo "ERROR: Image tag argument is missing." + exit 1 +fi + +if [ -n "$IS_BLUE_RUNNING" ]; then + echo "Green 배포를 시작합니다." + + docker stop dangdang-api-green && docker rm dangdang-api-green + docker pull "$REPO"/"$IMAGE":"$TAG" + + docker run -d --name dangdang-api-green --restart always -p 3031:3031 --env-file "$FILE_PATH" -v logs:/app/log "$REPO"/"$IMAGE":"$TAG" + + while [ 1 = 1 ]; do + echo "Green Health check를 시작합니다." + sleep 3 + + REQUEST=$(curl http://localhost:3031) + if [ -n "$REQUEST" ]; then + echo "Green Health check 성공했습니다." + break ; + fi + done; + + echo "Nginx를 재시작합니다." + sudo nginx -s reload + + echo "Blue Container 종료합니다." + docker stop dangdang-api-blue + + echo "Green 배포를 성공적으로 종료합니다." +else + echo "Green : running" + echo "Blue 배포를 시작합니다." + + docker stop dangdang-api-blue && docker rm dangdang-api-blue + docker pull "$REPO"/"$IMAGE":"$TAG" + docker run -d --name dangdang-api-blue --restart always -p 3032:3031 --env-file "$FILE_PATH" -v logs:/app/log "$REPO"/"$IMAGE":"$TAG" + + while [ 1 = 1 ]; do + echo "Blue Health check를 시작합니다." + sleep 3 + + REQUEST=$(curl http://localhost:3032) + if [ -n "$REQUEST" ]; then + echo "Blue Health check 성공했습니다." + break ; + fi + done; + + echo "Nginx를 재시작합니다." + sudo nginx -s reload + + echo "Green Container ��료합니다." + docker stop dangdang-api-green + + echo "Blue 배포를 성공적으로 종료합니다." +fi + +sleep 3 + +echo "이전 이미지를 삭제합니다." +docker image prune -af From 5f4e14c2a4faa307caffc90dbf5b7d9962bf9835 Mon Sep 17 00:00:00 2001 From: do0ori Date: Sun, 22 Dec 2024 15:32:09 +0900 Subject: [PATCH 2/6] =?UTF-8?q?refactor:=20=EB=B0=B1=EC=97=94=EB=93=9C=20?= =?UTF-8?q?=EB=B0=B0=ED=8F=AC=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 코드 가독성 향상을 위한 함수 모듈화 - stop_and_remove_container: 컨테이너 중지 및 제거 - run_and_health_check_container: 컨테이너 실행 및 헬스체크 - deploy_container: 컨테이너 배포 프로세스 - reload_nginx: nginx 재시작 - 변수 및 상수 정리 - ROOT_PATH, CONTAINER_IMAGE 등 상수 분리 - 환경변수 이름 명확화 (FILE_PATH → ENV_FILE_PATH) - 에러 처리 개선 - 컨테이너 중지/제거 시 에러 무시 (2>/dev/null) - 디버그 정보 출력 개선 --- .github/script/run_backend.sh | 132 ++++++++++++++++------------------ 1 file changed, 60 insertions(+), 72 deletions(-) diff --git a/.github/script/run_backend.sh b/.github/script/run_backend.sh index 60b2689d..30d4bedf 100644 --- a/.github/script/run_backend.sh +++ b/.github/script/run_backend.sh @@ -1,33 +1,64 @@ #!/bin/bash +# Constants +ROOT_PATH="/home/ubuntu/actions-runner/_work/dangdang-walk/dangdang-walk/backend/server" + +# Variables TAG=$1 ENV_FILE=$2 REPO=$3 IMAGE=$4 -ENV_PATH="/home/ubuntu/actions-runner/_work/dangdang-walk/dangdang-walk/backend/server" -FILE_PATH="$ENV_PATH/$ENV_FILE" - - -echo"TAG: " -echo $TAG -echo "ENV_FILE: " -echo $ENV_FILE +CONTAINER_IMAGE="$REPO/$IMAGE:$TAG" +ENV_FILE_PATH="$ROOT_PATH/$ENV_FILE" -echo "REPO: " -echo $REPO - -echo "IMAGE: " -echo $IMAGE - -echo"FILE PATH: " -echo $FILE_PATH +# Print debug information +echo "CONTAINER IMAGE: $CONTAINER_IMAGE" +echo "ENV FILE PATH: $ENV_FILE_PATH" +# AWS ECR Login aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin "$REPO" -IS_BLUE_RUNNING=$(docker inspect -f '{{.State.Status}}' dangdang-api-blue | grep running) -echo "Blue : " -echo "$IS_BLUE_RUNNING" +# Helper Functions +stop_and_remove_container() { + local container_name=$1 + echo "Stopping and removing container: $container_name" + docker stop "$container_name" 2>/dev/null && docker rm "$container_name" 2>/dev/null +} + +run_and_health_check_container() { + local container_name=$1 + local port=$2 + echo "Running container: $container_name on port: $port" + docker run -d --name "$container_name" --restart always -p "$port":3031 --env-file "$ENV_FILE_PATH" -v logs:/app/log "$CONTAINER_IMAGE" + + echo "Starting health check on port: $port" + while true; do + sleep 3 + if curl -s http://localhost:"$port" > /dev/null; then + echo "Health check successful on port: $port" + break + fi + done +} + +deploy_container() { + local container_name=$1 + local port=$2 + log_info "Deploying $container_name on port $port..." + stop_and_remove_container "$container_name" + docker pull "$CONTAINER_IMAGE" + run_and_health_check_container "$container_name" "$port" +} + +reload_nginx() { + echo "Reloading Nginx" + sudo nginx -s reload +} + +# Main Deployment Logic +IS_BLUE_RUNNING=$(docker inspect -f '{{.State.Status}}' dangdang-api-blue 2>/dev/null | grep running) +echo "Blue container running: $IS_BLUE_RUNNING" if [ -z "$TAG" ]; then echo "ERROR: Image tag argument is missing." @@ -35,60 +66,17 @@ if [ -z "$TAG" ]; then fi if [ -n "$IS_BLUE_RUNNING" ]; then - echo "Green 배포를 시작합니다." - - docker stop dangdang-api-green && docker rm dangdang-api-green - docker pull "$REPO"/"$IMAGE":"$TAG" - - docker run -d --name dangdang-api-green --restart always -p 3031:3031 --env-file "$FILE_PATH" -v logs:/app/log "$REPO"/"$IMAGE":"$TAG" - - while [ 1 = 1 ]; do - echo "Green Health check를 시작합니다." - sleep 3 - - REQUEST=$(curl http://localhost:3031) - if [ -n "$REQUEST" ]; then - echo "Green Health check 성공했습니다." - break ; - fi - done; - - echo "Nginx를 재시작합니다." - sudo nginx -s reload - - echo "Blue Container 종료합니다." - docker stop dangdang-api-blue - - echo "Green 배포를 성공적으로 종료합니다." + echo "Switching to Green container..." + deploy_container "dangdang-api-green" 3031 + reload_nginx + stop_and_remove_container "dangdang-api-blue" else - echo "Green : running" - echo "Blue 배포를 시작합니다." - - docker stop dangdang-api-blue && docker rm dangdang-api-blue - docker pull "$REPO"/"$IMAGE":"$TAG" - docker run -d --name dangdang-api-blue --restart always -p 3032:3031 --env-file "$FILE_PATH" -v logs:/app/log "$REPO"/"$IMAGE":"$TAG" - - while [ 1 = 1 ]; do - echo "Blue Health check를 시작합니다." - sleep 3 - - REQUEST=$(curl http://localhost:3032) - if [ -n "$REQUEST" ]; then - echo "Blue Health check 성공했습니다." - break ; - fi - done; - - echo "Nginx를 재시작합니다." - sudo nginx -s reload - - echo "Green Container ��료합니다." - docker stop dangdang-api-green - - echo "Blue 배포를 성공적으로 종료합니다." + echo "Switching to Blue container..." + deploy_container "dangdang-api-blue" 3032 + reload_nginx + stop_and_remove_container "dangdang-api-green" fi -sleep 3 - -echo "이전 이미지를 삭제합니다." +# Cleanup unused Docker images +echo "Pruning unused images" docker image prune -af From b5c7fd0b4f48d25c5f15a14472138a74b50527ca Mon Sep 17 00:00:00 2001 From: do0ori Date: Sun, 22 Dec 2024 15:35:34 +0900 Subject: [PATCH 3/6] =?UTF-8?q?refactor:=20=EB=B0=B1=EC=97=94=EB=93=9C=20?= =?UTF-8?q?=EB=B0=B0=ED=8F=AC=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=ED=99=94=20=EB=B0=8F=20=EB=A1=9C=EA=B9=85=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 스크립트 구조 개선 - main 함수 도입으로 전체 실행 흐름 명확화 - 기능별 함수 모듈화 (validate, authenticate, deploy 등) - Blue/Green 배포 함수 분리 (activate_blue/green_deployment) - 로깅 시스템 개선 - 컬러 로깅 함수 추가 (debug, info, success, error) - 각 단계별 상세 로그 메시지 추가 - 에러 발생 시 명확한 피드백 제공 - 컨테이너 관리 개선 - 헬스 체크 재시도 로직 추가 (최대 10회) - 컨테이너 상태 검증 강화 - nginx 재시작 실패 시 에러 처리 --- .github/script/run_backend.sh | 139 ++++++++++++++++++++++++---------- 1 file changed, 97 insertions(+), 42 deletions(-) diff --git a/.github/script/run_backend.sh b/.github/script/run_backend.sh index 30d4bedf..a0028e5a 100644 --- a/.github/script/run_backend.sh +++ b/.github/script/run_backend.sh @@ -12,71 +12,126 @@ IMAGE=$4 CONTAINER_IMAGE="$REPO/$IMAGE:$TAG" ENV_FILE_PATH="$ROOT_PATH/$ENV_FILE" -# Print debug information -echo "CONTAINER IMAGE: $CONTAINER_IMAGE" -echo "ENV FILE PATH: $ENV_FILE_PATH" +# Logger Functions +log_debug() { echo -e "\033[1;36m[DEBUG]\033[0m $1"; } +log_info() { echo -e "\033[1;34m[INFO]\033[0m $1"; } +log_success() { echo -e "\033[1;32m[SUCCESS]\033[0m $1"; } +log_error() { echo -e "\033[1;31m[ERROR]\033[0m $1" >&2; } -# AWS ECR Login -aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin "$REPO" +# Input Validation +validate_deployment_args() { + if [ -z "$TAG" ]; then + log_error "Image tag argument is missing." + exit 1 + fi + log_info "Deployment arguments validated." +} + +# AWS ECR Authentication +authenticate_to_ecr() { + log_info "Authenticating with AWS ECR..." + aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin "$REPO" +} -# Helper Functions -stop_and_remove_container() { +# Docker Container Management +cleanup_container() { local container_name=$1 - echo "Stopping and removing container: $container_name" + log_info "Cleaning up container: $container_name" docker stop "$container_name" 2>/dev/null && docker rm "$container_name" 2>/dev/null } -run_and_health_check_container() { +deploy_new_container() { local container_name=$1 local port=$2 - echo "Running container: $container_name on port: $port" - docker run -d --name "$container_name" --restart always -p "$port":3031 --env-file "$ENV_FILE_PATH" -v logs:/app/log "$CONTAINER_IMAGE" + log_info "Deploying container: $container_name on port: $port" + docker run -d \ + --name "$container_name" \ + --restart always \ + -p "$port":3031 \ + --env-file "$ENV_FILE_PATH" \ + -v logs:/app/log \ + "$CONTAINER_IMAGE" + verify_container_health "$port" +} + +verify_container_health() { + local port=$1 + local max_retries=10 + local retries=0 - echo "Starting health check on port: $port" - while true; do + log_info "Verifying container health on port: $port" + + while [ $retries -lt $max_retries ]; do sleep 3 if curl -s http://localhost:"$port" > /dev/null; then - echo "Health check successful on port: $port" - break + log_success "Container health check passed on port: $port" + return 0 fi + retries=$((retries + 1)) + log_info "Retrying health check... ($retries/$max_retries)" done + + log_error "Health check failed after $max_retries attempts." + exit 1 } -deploy_container() { +perform_deployment() { local container_name=$1 local port=$2 - log_info "Deploying $container_name on port $port..." - stop_and_remove_container "$container_name" + log_info "Starting deployment for $container_name on port $port..." + cleanup_container "$container_name" docker pull "$CONTAINER_IMAGE" - run_and_health_check_container "$container_name" "$port" + deploy_new_container "$container_name" "$port" } reload_nginx() { - echo "Reloading Nginx" - sudo nginx -s reload + log_info "Reloading Nginx..." + if ! sudo nginx -s reload; then + log_error "Failed to reload Nginx." + exit 1 + fi + log_success "Nginx reloaded successfully." } -# Main Deployment Logic -IS_BLUE_RUNNING=$(docker inspect -f '{{.State.Status}}' dangdang-api-blue 2>/dev/null | grep running) -echo "Blue container running: $IS_BLUE_RUNNING" - -if [ -z "$TAG" ]; then - echo "ERROR: Image tag argument is missing." - exit 1 -fi - -if [ -n "$IS_BLUE_RUNNING" ]; then - echo "Switching to Green container..." - deploy_container "dangdang-api-green" 3031 +# Blue-Green Deployment Functions +activate_blue_deployment() { + perform_deployment "dangdang-api-blue" 3032 reload_nginx - stop_and_remove_container "dangdang-api-blue" -else - echo "Switching to Blue container..." - deploy_container "dangdang-api-blue" 3032 + cleanup_container "dangdang-api-green" +} + +activate_green_deployment() { + perform_deployment "dangdang-api-green" 3031 reload_nginx - stop_and_remove_container "dangdang-api-green" -fi + cleanup_container "dangdang-api-blue" +} + +# Main Execution +main() { + log_debug "Target Container Image: $CONTAINER_IMAGE" + log_debug "Environment File Path: $ENV_FILE_PATH" + + validate_deployment_args + authenticate_to_ecr + + log_info "Checking deployment status..." + BLUE_CONTAINER_STATUS=$(docker inspect -f '{{.State.Status}}' dangdang-api-blue 2>/dev/null) + GREEN_CONTAINER_STATUS=$(docker inspect -f '{{.State.Status}}' dangdang-api-green 2>/dev/null) + + if [ "$BLUE_CONTAINER_STATUS" == "running" ]; then + log_info "Active: Blue container - Switching to Green deployment..." + activate_green_deployment + elif [ "$GREEN_CONTAINER_STATUS" == "running" ]; then + log_info "Active: Green container - Switching to Blue deployment..." + activate_blue_deployment + else + log_info "No active deployment found - Initiating Blue deployment..." + activate_blue_deployment + fi + + log_info "Cleaning up unused Docker images..." + docker image prune -af + log_success "Deployment completed successfully!" +} -# Cleanup unused Docker images -echo "Pruning unused images" -docker image prune -af +main From e7acf4daa24f448762121009e980cd43afa2b56f Mon Sep 17 00:00:00 2001 From: do0ori Date: Sun, 22 Dec 2024 15:50:53 +0900 Subject: [PATCH 4/6] =?UTF-8?q?refactor:=20=EC=83=81=EC=88=98=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 상수 값 관리 개선 - readonly 키워드를 사용하여 상수 보호 - 매직 넘버 제거 (포트 번호, 재시도 횟수, 간격 등) - 내부/외부 포트 구분을 명확히 함 - 입력값 검증 강화 - 필수 환경변수 일괄 검증 로직 추가 - 배열을 사용한 체계적인 변수 검증 - 헬스 체크 로직 개선 - 재시도 횟수와 간격을 상수로 분리 - 로그 메시지 일관성 개선 --- .github/script/run_backend.sh | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/.github/script/run_backend.sh b/.github/script/run_backend.sh index a0028e5a..38fdcd36 100644 --- a/.github/script/run_backend.sh +++ b/.github/script/run_backend.sh @@ -1,7 +1,12 @@ #!/bin/bash # Constants -ROOT_PATH="/home/ubuntu/actions-runner/_work/dangdang-walk/dangdang-walk/backend/server" +readonly ROOT_PATH="/home/ubuntu/actions-runner/_work/dangdang-walk/dangdang-walk/backend/server" +readonly HEALTH_CHECK_MAX_RETRIES=10 +readonly HEALTH_CHECK_INTERVAL=3 +readonly CONTAINER_INTERNAL_PORT=3031 +readonly BLUE_EXTERNAL_PORT=3032 +readonly GREEN_EXTERNAL_PORT=3031 # Variables TAG=$1 @@ -20,10 +25,15 @@ log_error() { echo -e "\033[1;31m[ERROR]\033[0m $1" >&2; } # Input Validation validate_deployment_args() { - if [ -z "$TAG" ]; then - log_error "Image tag argument is missing." - exit 1 - fi + local required_vars=("TAG" "ENV_FILE" "REPO" "IMAGE") + + for var in "${required_vars[@]}"; do + if [ -z "${!var}" ]; then + log_error "Required variable $var is not set" + exit 1 + fi + done + log_info "Deployment arguments validated." } @@ -47,7 +57,7 @@ deploy_new_container() { docker run -d \ --name "$container_name" \ --restart always \ - -p "$port":3031 \ + -p "$port":"$CONTAINER_INTERNAL_PORT" \ --env-file "$ENV_FILE_PATH" \ -v logs:/app/log \ "$CONTAINER_IMAGE" @@ -56,22 +66,21 @@ deploy_new_container() { verify_container_health() { local port=$1 - local max_retries=10 local retries=0 log_info "Verifying container health on port: $port" - while [ $retries -lt $max_retries ]; do - sleep 3 + while [ $retries -lt $HEALTH_CHECK_MAX_RETRIES ]; do + sleep $HEALTH_CHECK_INTERVAL if curl -s http://localhost:"$port" > /dev/null; then log_success "Container health check passed on port: $port" return 0 fi retries=$((retries + 1)) - log_info "Retrying health check... ($retries/$max_retries)" + log_info "Retrying health check... ($retries/$HEALTH_CHECK_MAX_RETRIES)" done - log_error "Health check failed after $max_retries attempts." + log_error "Health check failed after $HEALTH_CHECK_MAX_RETRIES attempts." exit 1 } @@ -95,13 +104,13 @@ reload_nginx() { # Blue-Green Deployment Functions activate_blue_deployment() { - perform_deployment "dangdang-api-blue" 3032 + perform_deployment "dangdang-api-blue" $BLUE_EXTERNAL_PORT reload_nginx cleanup_container "dangdang-api-green" } activate_green_deployment() { - perform_deployment "dangdang-api-green" 3031 + perform_deployment "dangdang-api-green" $GREEN_EXTERNAL_PORT reload_nginx cleanup_container "dangdang-api-blue" } From 113d67b045ee020046f26b05beb9ea8a761f2368 Mon Sep 17 00:00:00 2001 From: do0ori Date: Sun, 22 Dec 2024 20:17:58 +0900 Subject: [PATCH 5/6] =?UTF-8?q?test:=20=EB=B0=B1=EC=97=94=EB=93=9C=20?= =?UTF-8?q?=EB=B0=B0=ED=8F=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 백엔드 배포 로직 검증을 위한 `test_run_backend.sh` 스크립트 추가: - Docker, AWS, curl, nginx, sudo와 같은 모의(mock) 바이너리 지원. - 커스터마이징 가능한 테스트 환경과 설정 지원. - 인자 검증, AWS 인증, Docker 작업, Blue-Green 배포를 포함한 다양한 테스트 케이스 제공. - 주요 기능: - 상세 출력을 위한 verbose 모드 지원. - 테스트 결과(성공 및 실패)를 추적 및 보고. - 장시간 실행되는 테스트의 타임아웃 감지 기능 포함. - 테스트 시나리오: - 유효한 조건에서의 성공적인 배포. - 인자 누락, 잘못된 환경 파일, 실패한 작업(AWS ECR 로그인, Docker pull 등)으로 인한 실패. - Blue-to-Green 및 Green-to-Blue 배포 시 유효성 검증. --- .github/script/test_run_backend.sh | 451 +++++++++++++++++++++++++++++ 1 file changed, 451 insertions(+) create mode 100644 .github/script/test_run_backend.sh diff --git a/.github/script/test_run_backend.sh b/.github/script/test_run_backend.sh new file mode 100644 index 00000000..c0bf6b40 --- /dev/null +++ b/.github/script/test_run_backend.sh @@ -0,0 +1,451 @@ +#!/bin/bash + +# 색상 및 스타일 정의 +readonly RED="\033[31m" +readonly GREEN="\033[32m" +readonly YELLOW="\033[33m" +readonly DARK_GRAY="\033[90m" +readonly BOLD="\033[1m" +readonly RESET="\033[0m" + +# 테스트 결과 추적 +TOTAL_TESTS=0 +PASSED_TESTS=0 +declare -a FAILED_TESTS +START_TIME=0 + +# 로깅 함수들 +test_debug() { echo -e "${DARK_GRAY}[DEBUG]${RESET} $1"; } + +# 테스트 컨텍스트 스택 +declare -a DESCRIBE_STACK +declare -a CONTEXT_STACK + +# 컨텍스트 관리 함수 +push_describe() { DESCRIBE_STACK+=("$1"); } +pop_describe() { unset 'DESCRIBE_STACK[${#DESCRIBE_STACK[@]}-1]'; } +push_context() { CONTEXT_STACK+=("$1"); } +pop_context() { unset 'CONTEXT_STACK[${#CONTEXT_STACK[@]}-1]'; } +get_current_describe() { echo "${DESCRIBE_STACK[${#DESCRIBE_STACK[@]}-1]}"; } +get_current_context() { echo "${CONTEXT_STACK[${#CONTEXT_STACK[@]}-1]}"; } + +# 테스트 구조 함수 +describe() { + echo -e "\n ${BOLD}$1${RESET}" + push_describe "$1" + eval "$2" + pop_describe +} + +context() { + echo -e " ${BOLD}$1${RESET}" + push_context "$1" + eval "$2" + pop_context +} + +# 플래그 기본값 설정 +VERBOSE=false + +# 인자 파싱 +while getopts "vh" opt; do + case $opt in + v) + VERBOSE=true + ;; + h) + echo "Usage: $0 [-v] [-h]" + echo " -v Verbose mode" + echo " -h Show this help" + exit 0 + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + exit 1 + ;; + esac +done + +it() { + local title=$1 + local test_fn=$2 + local current_describe=$(get_current_describe) + local current_context=$(get_current_context) + + ((TOTAL_TESTS++)) + + local start_time=$(date +%s%3N) + + local output + output=$($test_fn 2>&1) + local test_result=$? + + local end_time=$(date +%s%3N) + local duration=$((end_time - start_time)) + + if [ $duration -gt 10000 ]; then + echo -e " ${RED}✕${RESET} ${DARK_GRAY}$title (Timeout: ${duration} ms)${RESET}" + FAILED_TESTS+=("$(printf "%s > %s > %s" "$current_describe" "$current_context" "$title")") + return + fi + + if [ $test_result -eq 0 ]; then + echo -e " ${GREEN}✓${RESET} ${DARK_GRAY}$title (${duration} ms)${RESET}" + if [ "$VERBOSE" = "true" ]; then + echo -e "\n${DARK_GRAY}Output:${RESET}" + echo "$output" | sed 's/^/ /' + echo + fi + ((PASSED_TESTS++)) + else + echo -e " ${RED}✕${RESET} ${DARK_GRAY}$title (${duration} ms)${RESET}" + echo -e "\n${DARK_GRAY}Output:${RESET}" + echo "$output" | sed 's/^/ /' + echo + FAILED_TESTS+=("$(printf "%s > %s > %s" "$current_describe" "$current_context" "$title")") + fi +} + +# 테스트 환경 설정 +setup_test_env() { + # 실제 파일 시스템이 아닌 임시 경로 사용 + export ROOT_PATH="/tmp/test" + rm -rf "$ROOT_PATH" + mkdir -p "$ROOT_PATH" + + # env 파일 생성 + export TEST_ENV_PATH="$ROOT_PATH/test.env.prod" + echo "PORT=3031" > "$TEST_ENV_PATH" + + # 모든 외부 명령어를 모킹으로 대체 + export PATH="$ROOT_PATH/mock-bin:$PATH" + + # 테스트용 환경 변수 설정 + export HEALTH_CHECK_INTERVAL=0.1 + export HEALTH_CHECK_MAX_RETRIES=3 + + test_debug "Test environment setup completed at $ROOT_PATH" +} + +# Mock 바이너리 생성 +create_mock_binaries() { + mkdir -p "$ROOT_PATH/mock-bin" + + # Docker mock + cat > "$ROOT_PATH/mock-bin/docker" << 'EOF' +#!/bin/bash +case "$1" in + "inspect") + case "$4" in + "dangdang-api-blue") + echo "${MOCK_BLUE_STATUS:-}" + ;; + "dangdang-api-green") + echo "${MOCK_GREEN_STATUS:-}" + ;; + esac + ;; + "login") + echo "[MOCK] Docker login executed" + if [ "${MOCK_DOCKER_LOGIN_FAIL:-false}" = "true" ]; then + exit 1 + fi + exit 0 + ;; + "pull"|"stop"|"rm"|"run"|"image") + echo "[MOCK] Docker $1 executed" + exit "${MOCK_DOCKER_EXIT_CODE:-0}" + ;; + *) + echo "[MOCK] Unknown docker command: $1" + exit 1 + ;; +esac +EOF + + # AWS mock + cat > "$ROOT_PATH/mock-bin/aws" << 'EOF' +#!/bin/bash +if [ "$1" = "ecr" ] && [ "$2" = "get-login-password" ]; then + if [ "${MOCK_AWS_EXIT_CODE:-0}" != "0" ]; then + echo "AWS ECR authentication failed" >&2 + exit "${MOCK_AWS_EXIT_CODE}" + fi + echo "mock-password" + exit 0 +fi +EOF + + # curl mock + cat > "$ROOT_PATH/mock-bin/curl" << 'EOF' +#!/bin/bash +if [ "${MOCK_HEALTH_CHECK_FAIL:-false}" = "true" ]; then + exit 1 +fi +echo "mock-response" +exit 0 +EOF + + # nginx mock + cat > "$ROOT_PATH/mock-bin/nginx" << 'EOF' +#!/bin/bash +if [ "${MOCK_NGINX_FAIL:-false}" = "true" ]; then + exit 1 +fi +echo "[MOCK] Nginx reload" +exit 0 +EOF + + # sudo mock + cat > "$ROOT_PATH/mock-bin/sudo" << 'EOF' +#!/bin/bash +"$@" +EOF + + chmod +x "$ROOT_PATH/mock-bin/"* +} + +cleanup_test_env() { + rm -rf "$ROOT_PATH" + test_debug "Cleaned up test environment" +} + +# 테스트 케이스들 +test_basic_deployment() { + local temp_blue_status="$MOCK_BLUE_STATUS" + local temp_green_status="$MOCK_GREEN_STATUS" + local temp_docker_exit="$MOCK_DOCKER_EXIT_CODE" + local temp_health_check="$MOCK_HEALTH_CHECK_FAIL" + local temp_nginx="$MOCK_NGINX_FAIL" + + export MOCK_BLUE_STATUS="running" + export MOCK_GREEN_STATUS="" + export MOCK_DOCKER_EXIT_CODE=0 + export MOCK_HEALTH_CHECK_FAIL=false + export MOCK_NGINX_FAIL=false + + local result=0 + ./run_backend.sh "test-tag" "test.env.prod" "test-repo" "test-image" || result=1 + + export MOCK_BLUE_STATUS="$temp_blue_status" + export MOCK_GREEN_STATUS="$temp_green_status" + export MOCK_DOCKER_EXIT_CODE="$temp_docker_exit" + export MOCK_HEALTH_CHECK_FAIL="$temp_health_check" + export MOCK_NGINX_FAIL="$temp_nginx" + + return $result +} + +test_missing_arguments() { + if ./run_backend.sh; then + return 1 + fi + return 0 +} + +test_invalid_env_file() { + local temp_env_path="$TEST_ENV_PATH" + rm -f "$TEST_ENV_PATH" # 기존 env 파일 삭제 + + local result=0 + if ./run_backend.sh "test-tag" "test.env.prod" "test-repo" "test-image"; then + result=1 + fi + + # 테스트 후 env 파일 복구 + echo "SERVER_PORT=3031" > "$TEST_ENV_PATH" + + return $result +} + +test_aws_ecr_login_failure() { + local temp_aws_exit_code="$MOCK_AWS_EXIT_CODE" + + export MOCK_AWS_EXIT_CODE=1 + + local result=0 + if ./run_backend.sh "test-tag" "test.env.prod" "test-repo" "test-image"; then + result=1 + fi + + export MOCK_AWS_EXIT_CODE="$temp_aws_exit_code" + return $result +} + +test_docker_login_failure() { + local temp_docker_login="$MOCK_DOCKER_LOGIN_FAIL" + + export MOCK_DOCKER_LOGIN_FAIL=true + + local result=0 + if ./run_backend.sh "test-tag" "test.env.prod" "test-repo" "test-image"; then + result=1 + fi + + export MOCK_DOCKER_LOGIN_FAIL="$temp_docker_login" + return $result +} + +test_docker_pull_failure() { + local temp_exit_code="$MOCK_DOCKER_EXIT_CODE" + export MOCK_DOCKER_EXIT_CODE=1 + + local result=0 + if ./run_backend.sh "test-tag" "test.env.prod" "test-repo" "test-image"; then + result=1 + fi + + export MOCK_DOCKER_EXIT_CODE="$temp_exit_code" + return $result +} + +test_health_check_failure() { + local temp_health_check="$MOCK_HEALTH_CHECK_FAIL" + + export MOCK_HEALTH_CHECK_FAIL=true + + local result=0 + if ./run_backend.sh "test-tag" "test.env.prod" "test-repo" "test-image"; then + result=1 + fi + + export MOCK_HEALTH_CHECK_FAIL="$temp_health_check" + return $result +} + +test_nginx_reload_failure() { + local temp_nginx="$MOCK_NGINX_FAIL" + export MOCK_NGINX_FAIL=true + + local result=0 + if ./run_backend.sh "test-tag" "test.env.prod" "test-repo" "test-image"; then + result=1 + fi + + export MOCK_NGINX_FAIL="$temp_nginx" + return $result +} + +test_blue_to_green_deployment() { + local temp_blue_status="$MOCK_BLUE_STATUS" + local temp_green_status="$MOCK_GREEN_STATUS" + + export MOCK_BLUE_STATUS="running" + export MOCK_GREEN_STATUS="" + + local result=0 + ./run_backend.sh "test-tag" "test.env.prod" "test-repo" "test-image" || result=1 + + export MOCK_BLUE_STATUS="$temp_blue_status" + export MOCK_GREEN_STATUS="$temp_green_status" + + return $result +} + +test_green_to_blue_deployment() { + local temp_blue_status="$MOCK_BLUE_STATUS" + local temp_green_status="$MOCK_GREEN_STATUS" + + export MOCK_BLUE_STATUS="" + export MOCK_GREEN_STATUS="running" + + local result=0 + ./run_backend.sh "test-tag" "test.env.prod" "test-repo" "test-image" || result=1 + + export MOCK_BLUE_STATUS="$temp_blue_status" + export MOCK_GREEN_STATUS="$temp_green_status" + + return $result +} + +# 테스트 결과 요약 출력 +print_test_summary() { + local end_time=$(date +%s) + local duration=$((end_time - START_TIME)) + local failed_count=$((TOTAL_TESTS - PASSED_TESTS)) + + echo -e "\nTest Suites: ${BOLD}$([ $failed_count -eq 0 ] && echo "${GREEN}1 passed${RESET}" || echo "${RED}1 failed${RESET}")${RESET}, 1 total" + + if [ $failed_count -eq 0 ]; then + echo -e "Tests: ${GREEN}${TOTAL_TESTS} passed${RESET}, ${BOLD}${TOTAL_TESTS} total${RESET}" + else + echo -e "Tests: ${RED}${failed_count} failed${RESET}, ${GREEN}${PASSED_TESTS} passed${RESET}, ${BOLD}${TOTAL_TESTS} total${RESET}" + fi + + echo -e "Time: ${YELLOW}${duration}.${RANDOM:0:3}s${RESET}" + + if [ ${#FAILED_TESTS[@]} -gt 0 ]; then + echo -e "\n${RED}Failed Tests:${RESET}" + for failed_test in "${FAILED_TESTS[@]}"; do + echo -e " ${RED}✕${RESET} $failed_test" + done + fi +} + +# 메인 테스트 실행 +main() { + START_TIME=$(date +%s) + + echo -e "\n ${BOLD}Backend Deployment Tests${RESET}" + setup_test_env + create_mock_binaries + + describe "Basic Deployment" " + context 'when all configurations are valid' ' + it \"should deploy successfully\" test_basic_deployment + ' + " + + describe "Input Validation" " + context 'when required arguments are missing' ' + it \"should fail with error message\" test_missing_arguments + ' + + context 'when ENV file does not exist' ' + it \"should fail with file not found error\" test_invalid_env_file + ' + " + + describe "AWS Operations" " + context 'when AWS ECR login fails' ' + it \"should fail with authentication error\" test_aws_ecr_login_failure + ' + + context 'when Docker login fails' ' + it \"should fail with login error\" test_docker_login_failure + ' + " + + describe "Docker Operations" " + context 'when docker pull fails' ' + it \"should fail and exit with error\" test_docker_pull_failure + ' + + context 'when health check fails' ' + it \"should fail and rollback deployment\" test_health_check_failure + ' + " + + describe "Nginx Operations" " + context 'when nginx reload fails' ' + it \"should fail and exit with error\" test_nginx_reload_failure + ' + " + + describe "Blue-Green Deployment" " + context 'when blue instance is running' ' + it \"should deploy to green instance\" test_blue_to_green_deployment + ' + + context 'when green instance is running' ' + it \"should deploy to blue instance\" test_green_to_blue_deployment + ' + " + + print_test_summary + cleanup_test_env + + return $([ "$PASSED_TESTS" -eq "$TOTAL_TESTS" ]) +} + +main From 74cff2cf7da787d1b43720c571de1e988e39ec04 Mon Sep 17 00:00:00 2001 From: do0ori Date: Sun, 22 Dec 2024 20:19:03 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=95=88?= =?UTF-8?q?=EC=A0=95=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 환경 변수 지원 추가: - `ROOT_PATH`, `HEALTH_CHECK_MAX_RETRIES`, `HEALTH_CHECK_INTERVAL`을 환경 변수로 설정 가능하도록 수정. - 배포 인자 검증 로직 강화: - 환경 변수 파일 존재 여부 및 읽기 권한 확인 추가. - AWS ECR 인증 과정 개선: - AWS 로그인 실패 시 에러 메시지를 출력하고 스크립트를 종료하도록 수정. - 인증 성공 여부를 명확히 로그로 출력. - Docker 이미지 풀링 및 배포 과정 안정화: - 이미지 풀링 실패 시 에러 메시지를 출력하고 스크립트를 종료하도록 처리. - Blue/Green 배포 로직 메시지를 가독성 높게 수정. 테스트를 통해 발견된 문제를 해결하고, 스크립트의 안정성과 유연성을 강화했습니다. --- .github/script/run_backend.sh | 44 +++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/.github/script/run_backend.sh b/.github/script/run_backend.sh index 38fdcd36..f02c9f6e 100644 --- a/.github/script/run_backend.sh +++ b/.github/script/run_backend.sh @@ -1,9 +1,9 @@ #!/bin/bash # Constants -readonly ROOT_PATH="/home/ubuntu/actions-runner/_work/dangdang-walk/dangdang-walk/backend/server" -readonly HEALTH_CHECK_MAX_RETRIES=10 -readonly HEALTH_CHECK_INTERVAL=3 +readonly ROOT_PATH=${ROOT_PATH:-"/home/ubuntu/actions-runner/_work/dangdang-walk/dangdang-walk/backend/server"} +readonly HEALTH_CHECK_MAX_RETRIES=${HEALTH_CHECK_MAX_RETRIES:-10} +readonly HEALTH_CHECK_INTERVAL=${HEALTH_CHECK_INTERVAL:-3} readonly CONTAINER_INTERNAL_PORT=3031 readonly BLUE_EXTERNAL_PORT=3032 readonly GREEN_EXTERNAL_PORT=3031 @@ -34,13 +34,36 @@ validate_deployment_args() { fi done + # Check if ENV file exists and has read permissions + if [ ! -f "$ENV_FILE_PATH" ]; then + log_error "Environment file not found: $ENV_FILE_PATH" + exit 1 + fi + + if [ ! -r "$ENV_FILE_PATH" ]; then + log_error "Environment file is not readable: $ENV_FILE_PATH" + exit 1 + fi + log_info "Deployment arguments validated." } # AWS ECR Authentication authenticate_to_ecr() { log_info "Authenticating with AWS ECR..." - aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin "$REPO" + + local aws_login_password + if ! aws_login_password=$(aws ecr get-login-password --region ap-northeast-2); then + log_error "Failed to get AWS ECR login password" + exit 1 + fi + + if ! echo "$aws_login_password" | docker login --username AWS --password-stdin "$REPO"; then + log_error "Failed to authenticate with AWS ECR" + exit 1 + fi + + log_success "Successfully authenticated with AWS ECR" } # Docker Container Management @@ -87,9 +110,16 @@ verify_container_health() { perform_deployment() { local container_name=$1 local port=$2 + log_info "Starting deployment for $container_name on port $port..." + cleanup_container "$container_name" - docker pull "$CONTAINER_IMAGE" + + if ! docker pull "$CONTAINER_IMAGE"; then + log_error "Failed to pull container image: $CONTAINER_IMAGE" + exit 1 + fi + deploy_new_container "$container_name" "$port" } @@ -128,10 +158,10 @@ main() { GREEN_CONTAINER_STATUS=$(docker inspect -f '{{.State.Status}}' dangdang-api-green 2>/dev/null) if [ "$BLUE_CONTAINER_STATUS" == "running" ]; then - log_info "Active: Blue container - Switching to Green deployment..." + log_info "Blue container is running - Switching to Green container..." activate_green_deployment elif [ "$GREEN_CONTAINER_STATUS" == "running" ]; then - log_info "Active: Green container - Switching to Blue deployment..." + log_info "Green container is running - Switching to Blue container..." activate_blue_deployment else log_info "No active deployment found - Initiating Blue deployment..."