-
[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
Spring Boot + GitHub Actions + AWS CodeDeploy를 사용하여 CI/CD 구축하기
Spring Boot + GitHub Actions + AWS CodeDeploy를 사용하여 CI/CD 구축하기
velog.io
'공부 > AWS' 카테고리의 다른 글
[EC2] Github-Action으로 EC2에 Spring-Boot 배포(Blue-Green) - 1 (0) 2025.12.25 [AWS]No space left on device @ fptr_finalize_flush - /opt/codedeploy-agent/deployment-root/ongoing-deployment/ (0) 2025.12.22 ACM(외부에서 발급받은 도메인) - Route53 연결 (0) 2025.12.19 SAM 을 이용한 AWS Lambda 배포 (0) 2025.11.30 AWS SSM - Private Subnet EC2 (0) 2025.11.08