ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [EC2] Github-Action으로 EC2에 Spring-Boot 배포(Blue-Green) - 2
    공부/AWS 2025. 12. 28. 22:46

    지난번에는 AWS 설정하는 부분을 작성하였습니다.(링크)
    이번에는 Github-Action과 CodeDeploy Script 관련해서 작성하려고 합니다.

     

    혹시라도 부정확한 정보를 전달드릴 수 있습니다. 다만 틀린 거나 부정확한 정보가 있다면 댓글을 남겨주세요


    1. Github-Action Work-flow 작성

    - .github 폴더에 workflows 폴더 생성 및 deploy.yml 파일 생성

    # 원한시는 Deploy 이름을 작성하시면 됩니다.
    name: Blue-Green Deployment with CodeDeploy
    
    on:
      push:
      # 어떤 브랜치에서 진행이 될지 입력합니다.
      # push만 있는게 아닌 PR도 있습니다.
        branches:
          - main
          - develop
    
    env:
      AWS_REGION: ap-northeast-2
      # secrets 설정된 부분은 Github Repo 설정에서 Secrets and variables에 Repository secrets에 작성하시면 됩니다.
      S3_BUCKET: ${{ secrets.S3_DEPLOYMENT_BUCKET }}
      CODEDEPLOY_APP_NAME: ${{secrets.CODEDEPLOY_APP_NAME}}
    
    jobs:
      build-and-deploy:
     	# Github actions의 실행 환경을 지정합니다.
        name: Build and Deploy
        runs-on: ubuntu-latest
        environment:
        # 브랜치에 따라 배포 환경을 설정합니다.
          name: ${{ github.ref == 'refs/heads/main' && 'production' || 'development' }}
        # 저장소의 코드를 확인합니다.
        steps:
          - name: Checkout code
            uses: actions/checkout@v4
    
    	# Spring-Boot 서버의 java version과 일치하게 설정합니다.
          - name: Set up JDK 17
            uses: actions/setup-java@v4
            with:
              java-version: '17'
              distribution: 'temurin'
              cache: 'gradle'
    	# gradlew에 실행권한을 부여합니다.
          - name: Grant execute permission for gradlew
            run: chmod +x gradlew
    	# 테스트 코드가 있다면 진행하는 부분입니다.
          - name: Build with Gradle
            run: ./gradlew clean build -x test
          
          # 브랜치에 따라 CodeDeploy 배포 그룹 설정합니다.
          - name: Set deployment group
            id: set-deployment-group
            run: |
              if [ "${{ github.ref }}" == "refs/heads/main" ]; then
                echo "CODEDEPLOY_DEPLOYMENT_GROUP=${{ secrets.DEPLOYMENT_GROUP_PROD }}" >> $GITHUB_ENV
              else
                echo "CODEDEPLOY_DEPLOYMENT_GROUP=${{ secrets.DEPLOYMENT_GROUP_DEV }}" >> $GITHUB_ENV
              fi
    
          # - name: Run tests
          #   run: ./gradlew test
    
    	# AWS 발급받은 키로 로그인 인증하는 부분입니다.(키 발급은 보안 자격 증명에서 발급받아서 사용할 수 있습니다.)
          - name: Configure AWS credentials
            uses: aws-actions/configure-aws-credentials@v4
            with:
              aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
              aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
              aws-region: ${{ env.AWS_REGION }}
    
          - name: Login to Amazon ECR (Optional)
            if: false  # ECR 사용시 true로 변경
            id: login-ecr
            uses: aws-actions/amazon-ecr-login@v2
    
    	# 배포 버전 태그와 환경 변수를 생성합니다.
          - name: Generate deployment tag
            run: |
              IMAGE_TAG=$(date +%Y%m%d-%H%M%S)-${GITHUB_SHA::7}
              echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
              echo "DEPLOYMENT_ID=$(date +%Y%m%d-%H%M%S)" >> $GITHUB_ENV
    
              # 환경 변수 결정 (GitHub Actions environment에 맞춤)
              if [ "${{ github.ref }}" == "refs/heads/main" ]; then
                DEPLOY_ENV="prod"
              elif [ "${{ github.ref }}" == "refs/heads/develop" ]; then
                DEPLOY_ENV="dev"
              else
                DEPLOY_ENV="dev"
              fi
              echo "DEPLOY_ENV=$DEPLOY_ENV" >> $GITHUB_ENV
    
    	# 배포 패키지를 생성하고 압축합니다.
          - name: Create deployment package
            run: |
              # 배포에 필요한 파일들 복사
              mkdir -p deployment
              # build/libs 디렉토리 구조를 유지하여 복사
              mkdir -p deployment/build/libs
              cp build/libs/*.jar deployment/build/libs/
              cp Dockerfile.deploy deployment/Dockerfile.deploy
              cp docker-compose.blue-green.${{ env.DEPLOY_ENV }}.yml deployment/
              cp appspec.yml deployment/
              cp -r scripts deployment/
              cp -r conf deployment/
    
              # 스크립트 파일에 실행 권한 부여
              find deployment/scripts -name "*.sh" -type f -exec chmod +x {} \;
    
              # 환경변수를 .env 파일로 저장
              echo "IMAGE_TAG=${{ env.IMAGE_TAG }}" > deployment/.env.deploy
              echo "DEPLOY_ENV=${{ env.DEPLOY_ENV }}" >> deployment/.env.deploy
    
              # zip으로 압축
              cd deployment
              zip -r ../deployment-${{ env.DEPLOYMENT_ID }}.zip .
              cd ..
    
    	# 배포 패키지를 S3에 업로드 합니다.
          - name: Upload to S3
            run: |
              aws s3 cp deployment-${{ env.DEPLOYMENT_ID }}.zip \
                s3://${{ env.S3_BUCKET }}/deployment-${{ env.DEPLOYMENT_ID }}.zip
    
    	# CodeDeploy 배포를 생성하고 실행합니다.
          - name: Create CodeDeploy deployment
            id: deploy
            run: |
              DEPLOYMENT_ID=$(aws deploy create-deployment \
                --application-name ${{ env.CODEDEPLOY_APP_NAME }} \
                --deployment-group-name ${{ env.CODEDEPLOY_DEPLOYMENT_GROUP }} \
                --s3-location bucket=${{ env.S3_BUCKET }},key=deployment-${{ env.DEPLOYMENT_ID }}.zip,bundleType=zip \
                --description "Deployment from GitHub Actions - ${{ github.sha }}" \
                --query 'deploymentId' \
                --output text)
    
              echo "DEPLOYMENT_ID=$DEPLOYMENT_ID" >> $GITHUB_ENV
              echo "deployment_id=$DEPLOYMENT_ID" >> $GITHUB_OUTPUT
    
    	# 배포가 완료될 때까지 대기합니다.
          - name: Wait for deployment to complete
            run: |
              echo "Waiting for deployment ${{ env.DEPLOYMENT_ID }} to complete..."
              aws deploy wait deployment-successful --deployment-id ${{ env.DEPLOYMENT_ID }}
    
    	# 배포 상태를 조회하여 출력합니다.
          - name: Get deployment status
            if: always()
            run: |
              aws deploy get-deployment --deployment-id ${{ env.DEPLOYMENT_ID }}

     

    Repository secrets

    - Repository secrets입니다.

    - secrets으로 가져오는 값은 Repository secrets에 등록이 필요합니다.

     

    2. CodeDeploy Script 작성

    폴더구조

    - 저는 이미지와 같은 폴더 구조로 구성하였습니다.

    - script의 진행 순서는 AWS 공식문서에 잘 작성 돼 있습니다.(링크)

    2-1. before_install.sh 생성 및 코드 입력

    #!/bin/bash
    
    # ============================================
    # CodeDeploy BeforeInstall Hook
    # 배포 전 준비 작업
    # ============================================
    
    set -e
    
    echo "========================================="
    echo "BeforeInstall: 배포 전 준비 작업 시작"
    echo "========================================="
    
    # 배포 디렉토리 백업 및 정리
    if [ -d /home/ubuntu/test ]; then
        echo "기존 배포 디렉토리 백업 중..."
        BACKUP_DIR="/home/ubuntu/backup/test-backup-$(date +%Y%m%d-%H%M%S)"
        sudo cp -r /home/ubuntu/test "$BACKUP_DIR"
        echo "백업 완료: $BACKUP_DIR"
    
        # 백업 개수 제한 (최대 5개 유지)
        echo "백업 개수 확인 및 정리 중..."
        BACKUP_COUNT=$(find /home/ubuntu/backup -maxdepth 1 -type d -name "test-backup-*" | wc -l)
        echo "현재 백업 개수: $BACKUP_COUNT"
        
        # 제거하지 않으면 SSD 용량 문제가 발생할 수 있습니다.
        if [ "$BACKUP_COUNT" -gt 5 ]; then
            echo "백업 개수가 5개를 초과합니다. 오래된 백업 삭제 중..."
            # 가장 오래된 백업부터 삭제 (이름의 날짜/시간 순서로 정렬)
            find /home/ubuntu/backup -maxdepth 1 -type d -name "test-backup-*" | sort | head -n -5 | while read -r old_backup; do
                echo "오래된 백업 삭제: $old_backup"
                sudo rm -rf "$old_backup"
            done
            echo "백업 정리 완료"
        fi
        
        # 기존 디렉토리 삭제 (CodeDeploy가 새 파일을 배포할 수 있도록 정리)
        echo "기존 배포 디렉토리 삭제 중..."
        sudo rm -rf /home/ubuntu/test
        echo "기존 디렉토리 삭제 완료"
    fi
    
    # 배포 디렉토리 생성
    sudo mkdir -p /home/ubuntu/test
    # 권한 설정
    sudo chown -R ubuntu:ubuntu /home/ubuntu/test
    
    # Docker 이미지 정리 (오래된 이미지 제거)
    # 오래된 이미지를 제거하지 않으면 디스크 용량 부족 문제가 발생할 수 있습니다.
    echo "오래된 Docker 이미지 정리 중..."
    docker image prune -f --filter "until=72h" || true
    
    echo "BeforeInstall 완료"

     

    2-2. After_install.sh 생성 및 코드 입력

    #!/bin/bash
    
    # ============================================
    # CodeDeploy AfterInstall Hook
    # 파일 설치 후 환경 설정
    # ============================================
    
    set -e
    
    echo "========================================="
    echo "AfterInstall: 환경 설정 시작"
    echo "========================================="
    
    cd /home/ubuntu/test
    
    # GitHub Actions에서 전달한 배포 환경 변수 로드 (.env.deploy 파일)
    # 이 파일에는 IMAGE_TAG, DEPLOY_ENV 등이 포함되어 있습니다
    if [ -f .env.deploy ]; then
        export $(cat .env.deploy | xargs)
        echo "배포 환경 변수 로드 완료"
        echo "  - IMAGE_TAG: ${IMAGE_TAG}"
        echo "  - DEPLOY_ENV: ${DEPLOY_ENV}"
    fi
    
    # 환경 결정 (dev 또는 prod)
    # DEPLOY_ENV가 없으면 기본값으로 'dev' 사용
    DEPLOY_ENV=${DEPLOY_ENV:-dev}
    PARAMETER_NAME="/test/${DEPLOY_ENV}/env"
    
    echo "환경: ${DEPLOY_ENV}"
    echo "Parameter Store 경로: ${PARAMETER_NAME}"
    
    # 환경 변수 파일을 저장할 디렉토리 생성
    mkdir -p env
    
    # Parameter Store에서 환경 변수 가져와서 .env 파일 생성
    echo "Parameter Store에서 환경 변수 가져오는 중..."
    
    # .env 파일 생성(예: env/.env.dev 또는 env/.env.prod)
    ENV_FILE="env/.env.${DEPLOY_ENV}"
    
    # 파일 초기화(기존 내용 삭제)
    > "$ENV_FILE"
    
    # AWS Systems Manager Parameter Store에서 환경 변수 전체 가져오기
    # --with-decryption: SecureString 타입지정시 파라미터 복호화 필요합니다.
    echo "파라미터 가져오는 중: ${PARAMETER_NAME}"
    ENV_CONTENT=$(aws ssm get-parameter \
      --name "${PARAMETER_NAME}" \
      --with-decryption \
      --query 'Parameter.Value' \
      --output text \
      --region ap-northeast-2 2>&1)
    
    AWS_EXIT_CODE=$?
    
    # 에러 체크
    if [ $AWS_EXIT_CODE -ne 0 ]; then
        echo "ERROR: Parameter Store에서 환경 변수를 가져올 수 없습니다: ${PARAMETER_NAME}"
        echo "Exit code: $AWS_EXIT_CODE"
        echo "에러 메시지: ${ENV_CONTENT}"
        exit 1
    fi
    
    # Parameter 값이 비어있는지 확인합니다.
    if [ -z "$ENV_CONTENT" ] || [ "$ENV_CONTENT" == "None" ]; then
        echo "ERROR: Parameter Store 값이 비어있습니다: ${PARAMETER_NAME}"
        echo "받은 값: '${ENV_CONTENT}'"
        exit 1
    fi
    
    echo "환경 변수 파싱 중..."
    
    # 임시 파일에 저장합니다. (while loop에서 읽기 위함)
    TEMP_ENV=$(mktemp)
    echo "$ENV_CONTENT" > "$TEMP_ENV"
    
    # 환경 변수 파싱 및 .env 파일에 저장
    # 환경 변수를 한 줄씩 읽어서 처리
    while IFS= read -r line || [ -n "$line" ]; do
        # 주석 라인 제거 (#으로 시작하는 라인)
        if [[ "$line" =~ ^[[:space:]]*# ]]; then
            continue
        fi
        
        # 빈 라인 제거
        line_trimmed=$(echo "$line" | xargs)
        if [ -z "$line_trimmed" ]; then
            continue
        fi
        
        # KEY=VALUE 형태인지 확인
        if [[ "$line" =~ ^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
            KEY="${BASH_REMATCH[1]}"
            VALUE="${BASH_REMATCH[2]}"
            
            # 앞뒤 공백 제거
            VALUE=$(echo "$VALUE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
            
            # 따옴표 제거 (있는 경우)
            VALUE=$(echo "$VALUE" | sed "s/^['\"]//;s/['\"]$//")
            
            echo "${KEY}=${VALUE}" >> "$ENV_FILE"
            echo "  ✓ ${KEY} 설정 완료"
        else
    	    # KEY=VALUE 형태가 아닌 라인은 무시
            echo "  ⚠ 무시된 라인: $line"
        fi
    done < "$TEMP_ENV"
    
    # 임시 파일 삭제
    rm -f "$TEMP_ENV"
    
    # GitHub Actions에서 전달받은 IMAGE_TAG를 .env 파일에 추가
    # docker-compose.yml에서 이 값을 사용하여 올바른 이미지 버전을 실행
    if [ -n "$IMAGE_TAG" ]; then
        echo "IMAGE_TAG=${IMAGE_TAG}" >> "$ENV_FILE"
        echo "  ✓ IMAGE_TAG 설정 완료: ${IMAGE_TAG}"
    fi
    
    echo "환경 변수 파일 생성 완료: ${ENV_FILE}"
    echo "AfterInstall 완료"

     

    2-3. application_start.sh 생성 및 코드 입력

    #!/bin/bash
    
    # ============================================
    # CodeDeploy ApplicationStart Hook
    # Blue-Green 배포 실행
    # ============================================
    # 에러 발생시 즉시 종료
    set -e 
    
    echo "========================================="
    echo "ApplicationStart: Blue-Green 배포 시작"
    echo "========================================="
    
    cd /home/ubuntu/test
    
    # 환경 변수 로드
    if [ -f .env.deploy ]; then
        export $(cat .env.deploy | xargs)
    fi
    
    # DEPLOY_ENV 확인 (기본값: dev)
    DEPLOY_ENV=${DEPLOY_ENV:-dev}
    echo "배포 환경: ${DEPLOY_ENV}"
    
    # SSM에서 가져온 환경 변수 파일 로드 (ALB 환경 변수 포함)
    # ENV_FILE에는 ALB ARN, ALB_TARGET_GROUP_BLUE_ARN, ALB_TARGET_GROUP_GREEN_ARN 등이 작성된 상태여야 합니다.
    ENV_FILE="env/.env.${DEPLOY_ENV}"
    if [ -f "$ENV_FILE" ]; then
        echo "환경 변수 파일 로드 중: ${ENV_FILE}"
        # 주석과 빈 라인 제외하고 환경 변수 로드
        export $(cat "$ENV_FILE" | grep -v '^#' | grep -v '^$' | xargs)
        echo "환경 변수 로드 완료"
    else
        echo "ERROR: 환경 변수 파일이 없습니다: ${ENV_FILE}"
        echo "after_install.sh가 정상적으로 실행되었는지 확인하세요."
        exit 1
    fi
    
    # 환경 변수 확인
    if [ -z "$ALB_TARGET_GROUP_BLUE_ARN" ]; then
        echo "ERROR: ALB_TARGET_GROUP_BLUE_ARN 환경 변수가 설정되지 않았습니다."
        echo "Parameter Store에 다음 형식으로 추가하세요:"
        echo "  ALB_TARGET_GROUP_BLUE_ARN=arn:aws:elasticloadbalancing:..."
        exit 1
    fi
    
    if [ -z "$ALB_TARGET_GROUP_GREEN_ARN" ]; then
        echo "ERROR: ALB_TARGET_GROUP_GREEN_ARN 환경 변수가 설정되지 않았습니다."
    	echo "Parameter Store에 다음 형식으로 추가하세요:"
        echo "  ALB_TARGET_GROUP_GREEN_ARN=arn:aws:elasticloadbalancing:..."
        exit 1
    fi
    
    # ALB 리스너 또는 리스너 규칙 ARN 확인(둘 중 하나는 반드시 필요합니다.)
    # 저의 경우 하나의 ALB에 LISTENER_RULE을 사용하고 있어서 ALB_LISTENER_RULE_ARN도 parameter에 작성하였습니다.
    if [ -z "$ALB_LISTENER_RULE_ARN" ] && [ -z "$ALB_LISTENER_ARN" ]; then
        echo "설정 방법:"
        echo "  - 리스너 규칙 사용 시: ALB_LISTENER_RULE_ARN=arn:aws:elasticloadbalancing:..."
        echo "  - 리스너 직접 사용 시: ALB_LISTENER_ARN=arn:aws:elasticloadbalancing:..."
        exit 1
    fi
    
    # 디버깅: ALB 환경 변수 확인
    echo "========================================="
    echo "배포 설정 정보"
    echo "========================================="
    echo "배포 환경: ${DEPLOY_ENV}"
    echo "이미지 태그: ${IMAGE_TAG}"
    echo ""
    echo "ALB 설정:"
    echo "  Blue Target Group: ${ALB_TARGET_GROUP_BLUE_ARN}"
    echo "  Green Target Group: ${ALB_TARGET_GROUP_GREEN_ARN}"
    
    if [ -n "$ALB_LISTENER_RULE_ARN" ]; then
        echo "ALB_LISTENER_RULE_ARN: ${ALB_LISTENER_RULE_ARN}"
    else
        echo "ALB_LISTENER_ARN: ${ALB_LISTENER_ARN}"
    fi
    
    # Blue-Green 배포 스크립트 실행 및 권한 부여
    chmod +x scripts/deploy.sh
    ./scripts/deploy.sh
    
    echo "ApplicationStart 완료"

     

    - application_start.sh까지가 Deploy를 위한 준비과정이었습니다.

    - application_start.sh에서 deploy.sh을 실행하여 ec2 인스턴스에서 deploy가 진행되도록 합니다.

     

    2-4. deploy.sh 생성 및 코드 입력

    #!/bin/bash
    
    # ============================================
    # Blue-Green 배포 스크립트
    # ALB 타겟 그룹 자동 전환
    # ============================================
    
    set -e # 에러 발생시 즉시 종료
    
    # 색상 정의
    RED='\033[0;31m'
    GREEN='\033[0;32m'
    BLUE='\033[0;34m'
    YELLOW='\033[1;33m'
    NC='\033[0m' # No Color
    
    # 로그 함수
    log_info() {
        echo -e "${BLUE}[INFO]${NC} $1"
    }
    
    log_success() {
        echo -e "${GREEN}[SUCCESS]${NC} $1"
    }
    
    log_error() {
        echo -e "${RED}[ERROR]${NC} $1"
    }
    
    log_warning() {
        echo -e "${YELLOW}[WARNING]${NC} $1"
    }
    
    # 의존성 확인 함수 추가
    check_dependencies() {
        log_info "필수 도구 확인 중..."
        
        # jq 설치 확인 (JSON 파싱에 필요)
        if ! command -v jq &> /dev/null; then
            log_error "jq가 설치되지 않았습니다."
            log_error "설치 방법: sudo apt-get install -y jq"
            exit 1
        fi
    }
    
    # 환경 변수 확인
    check_env_vars() {
        log_info "환경 변수 확인 중..."
    
        if [ -z "$ALB_TARGET_GROUP_BLUE_ARN" ]; then
            log_error "ALB_TARGET_GROUP_BLUE_ARN 환경 변수가 설정되지 않았습니다."
            exit 1
        fi
    
        if [ -z "$ALB_TARGET_GROUP_GREEN_ARN" ]; then
            log_error "ALB_TARGET_GROUP_GREEN_ARN 환경 변수가 설정되지 않았습니다."
            exit 1
        fi
    
         # 리스너 규칙 ARN 필수
        if [ -z "$ALB_LISTENER_RULE_ARN" ]; then
            log_error "ALB_LISTENER_RULE_ARN 환경 변수가 설정되지 않았습니다."
            log_error "특정 리스너 규칙에 대해 Blue-Green 배포를 진행합니다."
            exit 1
        fi
    
        # ========== 추가: 환경별 Docker Compose 파일 결정 ==========
        # DEPLOY_ENV 확인 (기본값: dev)
        DEPLOY_ENV=${DEPLOY_ENV:-dev}
        log_info "배포 환경: ${DEPLOY_ENV}"
    
        # 환경에 따른 Docker Compose 파일 설정
        if [ "$DEPLOY_ENV" = "prod" ]; then
            DOCKER_COMPOSE_FILE="docker-compose.blue-green.prod.yml"
        else
            DOCKER_COMPOSE_FILE="docker-compose.blue-green.dev.yml"
        fi
    
        log_info "Docker Compose 파일: ${DOCKER_COMPOSE_FILE}"
    
        # Docker Compose 파일 존재 확인
        if [ ! -f "$DOCKER_COMPOSE_FILE" ]; then
            log_error "Docker Compose 파일이 존재하지 않습니다: ${DOCKER_COMPOSE_FILE}"
            exit 1
        fi
    
        log_success "환경 변수 확인 완료"
    }
    
    # 현재 활성 환경 감지
    get_active_environment() {
        log_info "현재 활성 환경 감지 중..."
    
        # 전체 규칙 데이터 가져오기
        RULE_JSON=$(aws elbv2 describe-rules \
            --rule-arns "$ALB_LISTENER_RULE_ARN" \
            --output json)
        
        # Weight가 1인 타겟 그룹 찾기
    	# Blue-Green 배포에서는 한 쪽이 Weight=1, 다른 쪽이 Weight=0
        CURRENT_TARGET_GROUP=$(echo "$RULE_JSON" | \
            grep -B 1 '"Weight": 1' | \
            grep '"TargetGroupArn"' | \
            sed 's/.*"TargetGroupArn": "\([^"]*\)".*/\1/')
        
        # 값 확인
        log_info "현재 규칙의 타겟 그룹 ARN: '${CURRENT_TARGET_GROUP}'"
        log_info "Blue 타겟 그룹 ARN: '${ALB_TARGET_GROUP_BLUE_ARN}'"
        log_info "Green 타겟 그룹 ARN: '${ALB_TARGET_GROUP_GREEN_ARN}'"
    
        # ARN 비교 (앞뒤 공백 제거)
        CURRENT_TARGET_GROUP=$(echo "$CURRENT_TARGET_GROUP" | xargs)
        ALB_TARGET_GROUP_BLUE_ARN=$(echo "$ALB_TARGET_GROUP_BLUE_ARN" | xargs)
        ALB_TARGET_GROUP_GREEN_ARN=$(echo "$ALB_TARGET_GROUP_GREEN_ARN" | xargs)
    
        if [ "$CURRENT_TARGET_GROUP" = "$ALB_TARGET_GROUP_BLUE_ARN" ]; then
            ACTIVE_ENV="blue"
            INACTIVE_ENV="green"
            ACTIVE_PORT=8080
            INACTIVE_PORT=8081
        elif [ "$CURRENT_TARGET_GROUP" = "$ALB_TARGET_GROUP_GREEN_ARN" ]; then
            ACTIVE_ENV="green"
            INACTIVE_ENV="blue"
            ACTIVE_PORT=8081
            INACTIVE_PORT=8080
        else
            log_error "현재 타겟 그룹이 Blue 또는 Green과 일치하지 않습니다."
            log_error "현재 타겟 그룹: '${CURRENT_TARGET_GROUP}'"
            log_error "Blue ARN: '${ALB_TARGET_GROUP_BLUE_ARN}'"
            log_error "Green ARN: '${ALB_TARGET_GROUP_GREEN_ARN}'"
            exit 1
        fi
    
        log_success "활성 환경: $ACTIVE_ENV (포트: $ACTIVE_PORT)"
        log_info "배포 대상 환경: $INACTIVE_ENV (포트: $INACTIVE_PORT)"
    }
    
    # Docker 이미지 빌드
    build_image() {
        log_info "Docker 이미지 빌드 중..."
    
        IMAGE_TAG="${IMAGE_TAG:-$(date +%Y%m%d-%H%M%S)}"
        export IMAGE_TAG
    
        docker compose -f $DOCKER_COMPOSE_FILE build test-$INACTIVE_ENV
    
        log_success "이미지 빌드 완료: test:$IMAGE_TAG"
    }
    
    # 비활성 환경에 새 버전 배포
    deploy_to_inactive() {
        log_info "$INACTIVE_ENV 환경에 배포 중..."
    
        # 기존 컨테이너 중지 및 제거
        docker compose -f $DOCKER_COMPOSE_FILE stop test-$INACTIVE_ENV || true
        docker compose -f $DOCKER_COMPOSE_FILE rm -f test-$INACTIVE_ENV || true
    
        # 새 컨테이너 시작
        docker compose -f $DOCKER_COMPOSE_FILE up -d test-$INACTIVE_ENV
    
        log_success "$INACTIVE_ENV 환경 시작 완료"
    }
    
    # 헬스체크
    health_check() {
        log_info "$INACTIVE_ENV 환경 헬스체크 중..."
    
        MAX_RETRY=30
        RETRY_COUNT=0
        HEALTH_URL="http://localhost:$INACTIVE_PORT/actuator/health"
    
        while [ $RETRY_COUNT -lt $MAX_RETRY ]; do
            if curl -f -s "$HEALTH_URL" > /dev/null 2>&1; then
                log_success "헬스체크 성공"
                return 0
            fi
    
            RETRY_COUNT=$((RETRY_COUNT + 1))
            log_warning "헬스체크 대기 중... ($RETRY_COUNT/$MAX_RETRY)"
            sleep 10
        done
    
        log_error "헬스체크 실패: $INACTIVE_ENV 환경이 정상적으로 시작되지 않았습니다."
        exit 1
    }
    
    # ALB 타겟 그룹 등록 확인
    check_target_health() {
        log_info "ALB 타겟 헬스 상태 확인 중..."
    
    	# 배포할 환경의 타겟 그룹 ARN 결정
        if [ "$INACTIVE_ENV" = "blue" ]; then
            TARGET_GROUP_ARN="$ALB_TARGET_GROUP_BLUE_ARN"
        else
            TARGET_GROUP_ARN="$ALB_TARGET_GROUP_GREEN_ARN"
        fi
    
        # EC2 인스턴스 ID 가져오기(IMDSv2 우선, v1 폴백)
        # 토큰 발급(IMDSv2)
        TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
            -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" \
            -s --connect-timeout 2)
    
        # 사용하여 인스턴스 ID 가져오기
        if [ -n "$TOKEN" ]; then
            # IMDSv2 사용
            INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" \
                -s http://169.254.169.254/latest/meta-data/instance-id)
            log_info "IMDSv2를 사용하여 인스턴스 ID 가져옴"
        else
            # IMDSv1 fallback
            INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id \
                --connect-timeout 2)
            log_info "IMDSv1을 사용하여 인스턴스 ID 가져옴"
        fi
    
    
        # 디버깅: 변수 값 확인
        log_info "인스턴스 ID: $INSTANCE_ID"
        log_info "타겟 그룹 ARN: $TARGET_GROUP_ARN"
        log_info "포트: $INACTIVE_PORT"
    
        # 변수 검증
        if [ -z "$INSTANCE_ID" ] || [ "$INSTANCE_ID" = "null" ]; then
            log_error "인스턴스 ID를 가져올 수 없습니다."
            log_error "IMDS가 활성화되어 있는지 확인하세요."
            exit 1
        fi
        
        if [ -z "$INACTIVE_PORT" ]; then
            log_error "포트 번호가 설정되지 않았습니다."
            exit 1
        fi
    
          # 대기 시간 증가: 30번 × 15초 = 최대 7.5분
          # 대기시간을 최대 7.5분으로 한 이유는 서버가 실행하고 대상그룹이 상태를 확인하는데 시간이 어느정도 소요되기 때문입니다.
          # 짧은 시간으로 설정하면 unhealthy로 인해 배포가 실패합니다.
          MAX_RETRY=30
          RETRY_COUNT=0
          SLEEP_INTERVAL=15
    
        log_info "타겟 헬스 체크 시작 (최대 대기: $((MAX_RETRY * SLEEP_INTERVAL))초)"
    
        while [ $RETRY_COUNT -lt $MAX_RETRY ]; do
            HEALTH_INFO=$(aws elbv2 describe-target-health \
                --target-group-arn "$TARGET_GROUP_ARN" \
                --targets Id="$INSTANCE_ID",Port="$INACTIVE_PORT" \
                --query 'TargetHealthDescriptions[0].{State:TargetHealth.State,Reason:TargetHealth.Reason,Description:TargetHealth.Description}' \
                --output json 2>/dev/null)
    
            TARGET_HEALTH=$(echo "$HEALTH_INFO" | jq -r '.State // "unknown"')
            HEALTH_REASON=$(echo "$HEALTH_INFO" | jq -r '.Reason // ""')
            HEALTH_DESC=$(echo "$HEALTH_INFO" | jq -r '.Description // ""')
    
    
            if [ "$TARGET_HEALTH" = "healthy" ]; then
                log_success "타겟 헬스 상태: healthy"
                return 0
            fi
    
            RETRY_COUNT=$((RETRY_COUNT + 1))
             # 상세 정보 출력
            if [ -n "$HEALTH_REASON" ]; then
                log_warning "타겟 헬스 대기 중... 상태: $TARGET_HEALTH, 사유: $HEALTH_REASON ($RETRY_COUNT/$MAX_RETRY)"
            else
                log_warning "타겟 헬스 대기 중... 현재 상태: $TARGET_HEALTH ($RETRY_COUNT/$MAX_RETRY)"
            fi
    
            sleep $SLEEP_INTERVAL
        done
    
    
        log_error "타겟이 healthy 상태가 되지 않았습니다."
        log_error "마지막 상태: $TARGET_HEALTH"
        if [ -n "$HEALTH_REASON" ]; then
            log_error "사유: $HEALTH_REASON - $HEALTH_DESC"
        fi
        exit 1
    }
    
    # ALB 리스너 규칙 변경 (트래픽 전환)
    switch_traffic() {
        log_info "트래픽을 $INACTIVE_ENV 환경으로 전환 중..."
    
        if [ "$INACTIVE_ENV" = "blue" ]; then
            NEW_TARGET_GROUP_ARN="$ALB_TARGET_GROUP_BLUE_ARN"
            OLD_TARGET_GROUP_ARN="$ALB_TARGET_GROUP_GREEN_ARN"
        else
            NEW_TARGET_GROUP_ARN="$ALB_TARGET_GROUP_GREEN_ARN"
            OLD_TARGET_GROUP_ARN="$ALB_TARGET_GROUP_BLUE_ARN"
        fi
    
        # Weight로 타겟 그룹 변경
        # 새 타겟 그룹: Weight=1, 기존 타겟 그룹: Weight=0
        aws elbv2 modify-rule \
            --rule-arn "$ALB_LISTENER_RULE_ARN" \
            --actions Type=forward,ForwardConfig="{TargetGroups=[{TargetGroupArn=${OLD_TARGET_GROUP_ARN},Weight=0},{TargetGroupArn=${NEW_TARGET_GROUP_ARN},Weight=1}],TargetGroupStickinessConfig={Enabled=false}}"
    
        log_success "트래픽 전환 완료: $ACTIVE_ENV -> $INACTIVE_ENV"
    }
    
    # 이전 환경 정리 (선택사항)
    cleanup_old_environment() {
        log_info "이전 환경 정리 여부 확인..."
    
        # 30초 대기 후 이전 환경 중지 (롤백 가능 시간 확보)
        log_info "30초 후 이전 환경($ACTIVE_ENV)을 중지합니다..."
        sleep 30
    
        log_info "$ACTIVE_ENV 환경 중지 중..."
        docker compose -f $DOCKER_COMPOSE_FILE stop test-$ACTIVE_ENV
    
        log_success "이전 환경 정리 완료"
    }
    
    # 롤백 함수
    rollback() {
        log_error "배포 실패! 롤백을 시작합니다..."
    
        # 트래픽을 다시 이전 환경으로 전환
        if [ "$ACTIVE_ENV" = "blue" ]; then
            ROLLBACK_TARGET_GROUP_ARN="$ALB_TARGET_GROUP_BLUE_ARN"
            OTHER_TARGET_GROUP_ARN="$ALB_TARGET_GROUP_GREEN_ARN"
        else
            ROLLBACK_TARGET_GROUP_ARN="$ALB_TARGET_GROUP_GREEN_ARN"
            OTHER_TARGET_GROUP_ARN="$ALB_TARGET_GROUP_BLUE_ARN"
        fi
    
        # Weight를 배포 인전 환경으로 되돌립니다.
        # 새 타겟 그룹: Weight=0, 기존 타겟 그룹: Weight=1
        aws elbv2 modify-rule \
            --rule-arn "$ALB_LISTENER_RULE_ARN" \
            --actions Type=forward,ForwardConfig="{TargetGroups=[{TargetGroupArn=${OTHER_TARGET_GROUP_ARN},Weight=0},{TargetGroupArn=${ROLLBACK_TARGET_GROUP_ARN},Weight=1}],TargetGroupStickinessConfig={Enabled=false}}"
    
        # 실패한 컨테이너 중지
        docker compose -f $DOCKER_COMPOSE_FILE stop test-$INACTIVE_ENV
    
        log_success "롤백 완료"
        exit 1
    
    }
    
    # 메인 배포 프로세스
    main() {
        log_info "========================================="
        log_info "Blue-Green 배포 시작"
        log_info "========================================="
    
        # 오류 발생 시 롤백
        trap rollback ERR
    
        # 사전 검증
    	check_dependencies
        check_env_vars
        
        # 환경 분석
        get_active_environment
    
    	# 새 버전 빌드 및 배포
    	build_image
        deploy_to_inactive
    	
        # 헬스체크
    	health_check
        check_target_health
    
    	# 트래픽 전환
        switch_traffic
        
        # 정리
        cleanup_old_environment
    
        log_success "========================================="
        log_success "배포 완료!"
        log_success "활성 환경: $INACTIVE_ENV (포트: $INACTIVE_PORT)"
        log_success "========================================="
    }
    
    # 스크립트 실행
    main "$@"

     

    - deploy.sh이 실행이 되면서 EC2 내부에서 배포가 진행이 됩니다.

    - 각 함수마다 어떤 내용이 있는지 주석으로 설명을 작성하였습니다.

    - check target health에서 7.5분까지 하지 않으셔도 되지만 health check가 실패한다면 시간을 너무 짧게 가져가셔 그럴 수 도 있으니 확인해보시면 될 거 같습니다.

     

    3. docker-compose 파일 작성

    services:
      # Blue 환경
      test-blue:
        build:
          context: .
          dockerfile: Dockerfile.deploy
          args:
            PROFILE: dev
        image: test:${IMAGE_TAG:-latest}
        container_name: test-blue
        env_file:
          - env/.env.dev
        environment:
          SPRING_PROFILES_ACTIVE: dev
          DB_HOST: ${DB_HOST}
          DB_PORT: ${DB_PORT}
          DB_NAME: ${DB_NAME}
          DB_SCHEMA: ${DB_SCHEMA}
          DB_USERNAME: ${DB_USER}
          DB_PASSWORD: ${DB_PASSWORD}
          SERVER_PORT: 8080
          TZ: Asia/Seoul
          DEPLOYMENT_ENV: blue
          JAVA_OPTS: >-
            -Xms512m -Xmx1024m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/test/heapdump.hprof -Duser.timezone=Asia/Seoul -Dfile.encoding=UTF-8
        ports:
          - "8080:8080"
        restart: always
        logging:
          driver: "json-file"
          options:
            max-size: "100m"
            max-file: "10"
        healthcheck:
          test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/actuator/health" ]
          interval: 30s
          timeout: 10s
          retries: 5
          start_period: 90s
        deploy:
          resources:
            limits:
              cpus: '1.0'
              memory: 1536M
            reservations:
              cpus: '0.5'
              memory: 1024M
          restart_policy:
            condition: on-failure
            delay: 5s
            max_attempts: 3
            window: 120s
    
      # Green 환경
      test-green:
        build:
          context: .
          dockerfile: Dockerfile.deploy
          args:
            PROFILE: dev
        image: test:${IMAGE_TAG:-latest}
        container_name: test-green
        env_file:
          - env/.env.dev
        environment:
          SPRING_PROFILES_ACTIVE: dev
          DB_HOST: ${DB_HOST}
          DB_PORT: ${DB_PORT}
          DB_NAME: ${DB_NAME}
          DB_SCHEMA: ${DB_SCHEMA}
          DB_USERNAME: ${DB_USER}
          DB_PASSWORD: ${DB_PASSWORD}
          SERVER_PORT: 8080
          TZ: Asia/Seoul
          DEPLOYMENT_ENV: green
          JAVA_OPTS: >-
            -Xms512m -Xmx1024m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/test/heapdump.hprof -Duser.timezone=Asia/Seoul -Dfile.encoding=UTF-8
        ports:
          - "8081:8080"
        restart: always
        logging:
          driver: "json-file"
          options:
            max-size: "100m"
            max-file: "10"
        healthcheck:
          test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/actuator/health" ]
          interval: 30s
          timeout: 10s
          retries: 5
          start_period: 90s
        deploy:
          resources:
            limits:
              cpus: '1.0'
              memory: 1536M
            reservations:
              cpus: '0.5'
              memory: 1024M
          restart_policy:
            condition: on-failure
            delay: 5s
            max_attempts: 3
            window: 120s
    ....

     

    - docker compose 파일은 blue, green 서비스를 생성하는 방식으로 하였습니다.

    - script에서도 실행을 할 때, blue가 active 상태인 경우 green을 실행하도록 구성하였습니다.

     

    - 정상적으로 배포가 완료되었다면 CodeDeploy에서 확인하실 수 있습니다.

     

    추가적으로 github-action에서 마지막 step 이후에 배포가 성공/실패/취소 여부에 따라서 슬랙에 알림이 오도록 구현하였습니다.

    이 부분은 CI/CD와는 연결된 부분이 아니라서 제거하였습니다.

     

     


     

    CI/CD를 할 때 여러 번 실패를 경험하고 조금씩 수정하면서 진행하다 보니 생각보다 걸리긴 했지만, 저도 여러번 실패학 수정하고 시도하였습니다. 그래도 성공했을 때 행복했던 느낌을 같이 가져가셨으면 합니다.

     

     

    Ref.

    https://develop-706.tistory.com/40

     

    [CI/CD] Github Action 을 통한 Spring boot 배포 자동화 구축하기 !

    📌 개요배포 경험이 있는 사람들은 공감하실 것 입니다.수동으로 배포하는 과정이 얼마나 힘들고 귀찮은 과정인지를..!  저번에는 Github 과 Jenkins 를 사용해서 배포 자동화를 구축했었는데요. 

    develop-706.tistory.com

     

    https://velog.io/@chae_ag/series%EB%B0%B0%ED%8F%ACSpring-Boot-GitHub-Actions-AWS-CodeDeploy%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-CICD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0

     

    Spring Boot + GitHub Actions + AWS CodeDeploy를 사용하여 CI/CD 구축하기

    Spring Boot + GitHub Actions + AWS CodeDeploy를 사용하여 CI/CD 구축하기

    velog.io

     

     

    댓글

Designed by Tistory.