From 0af73bd2db1cd2aea1a91e85570719b71c5a88f7 Mon Sep 17 00:00:00 2001 From: moonjun1 Date: Thu, 17 Jul 2025 11:08:30 +0900 Subject: [PATCH 01/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EA=B9=83=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20:=20master=20=EC=BD=94=EB=93=9C=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-prod.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 061c66b..c83652e 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -2,7 +2,7 @@ name: 프로덕션 배포 (master 브랜치만) on: push: - branches: [ master ] + branches: [ master, feat/git-master ] # feat/git-master 브랜치 추가 workflow_dispatch: # 수동 실행 가능 inputs: confirm_deployment: @@ -136,7 +136,7 @@ jobs: KAKAO_CLIENT_ID: ${{ secrets.KAKAO_CLIENT_ID }} KAKAO_CLIENT_SECRET: ${{ secrets.KAKAO_CLIENT_SECRET }} DEEPSEARCH_API_KEY: ${{ secrets.DEEPSEARCH_API_KEY }} - run: ./gradlew build --no-daemon + run: ./gradlew clean build -x test --no-daemon --stacktrace - name: 프로덕션 배포 전 테스트 결과 업로드 uses: actions/upload-artifact@v4 @@ -185,11 +185,11 @@ jobs: # 현재 브랜치 확인 echo "📋 현재 브랜치: $(git branch --show-current)" - # master 브랜치로 체크아웃 및 풀 - echo "📥 master에서 최신 코드 가져오는 중..." + # 현재 브랜치에 맞게 체크아웃 및 풀 + echo "📥 현재 브랜치(${{ github.ref_name }})에서 최신 코드 가져오는 중..." git fetch origin - git checkout master || git checkout -b master origin/master - git pull origin master + git checkout ${{ github.ref_name }} || git checkout -b ${{ github.ref_name }} origin/${{ github.ref_name }} + git pull origin ${{ github.ref_name }} # 환경 변수 파일 확인 if [ ! -f ".env" ]; then @@ -261,7 +261,7 @@ jobs: echo "## 🚀 프로덕션 배포 요약" >> $GITHUB_STEP_SUMMARY echo "- **배포 상태**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY echo "- **환경**: PRODUCTION 🔥" >> $GITHUB_STEP_SUMMARY - echo "- **배포된 브랜치**: master" >> $GITHUB_STEP_SUMMARY + echo "- **배포된 브랜치**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY echo "- **배포 시간**: $(date)" >> $GITHUB_STEP_SUMMARY echo "- **커밋**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY echo "- **프로덕션 URL**: http://${{ secrets.EC2_HOST }}/" >> $GITHUB_STEP_SUMMARY From 54dfef4a12383b720180a73d278b41da04413926 Mon Sep 17 00:00:00 2001 From: moonjun1 Date: Thu, 17 Jul 2025 11:40:36 +0900 Subject: [PATCH 02/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EA=B9=83=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20:=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-prod.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index c83652e..89b1026 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -230,6 +230,7 @@ jobs: echo "🎉 프로덕션 배포 완료: $(date)" ' + - name: 프로덕션 외부 헬스체크 run: | From 377bbfd88466ec79aa3f30397b1bd71ba59ecd09 Mon Sep 17 00:00:00 2001 From: moonjun1 Date: Thu, 17 Jul 2025 11:42:26 +0900 Subject: [PATCH 03/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EA=B9=83=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20:=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-prod.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 89b1026..df24d53 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -235,11 +235,11 @@ jobs: - name: 프로덕션 외부 헬스체크 run: | echo "🔍 프로덕션 외부 헬스체크 중..." - sleep 60 + sleep 180 # 외부에서 접근 가능한지 확인 success=false - for i in {1..10}; do + for i in {1..15}; do if curl -f http://${{ secrets.EC2_HOST }}/ >/dev/null 2>&1; then echo "✅ 프로덕션 외부 헬스체크 통과!" echo "🔗 프로덕션 URL: http://${{ secrets.EC2_HOST }}/" @@ -248,7 +248,7 @@ jobs: break fi echo "⏳ 시도 $i/10: 외부 체크가 아직 준비되지 않았습니다..." - sleep 20 + sleep 30 done if [ "$success" = false ]; then From 25b7946386d0a3f5a365df9304c2682eff52a690 Mon Sep 17 00:00:00 2001 From: moonjun1 Date: Thu, 17 Jul 2025 15:30:42 +0900 Subject: [PATCH 04/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EA=B9=83=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20:=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-prod.yml | 220 +++++++++++++++++++++++++----- 1 file changed, 189 insertions(+), 31 deletions(-) diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index df24d53..b705257 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -2,7 +2,7 @@ name: 프로덕션 배포 (master 브랜치만) on: push: - branches: [ master, feat/git-master ] # feat/git-master 브랜치 추가 + branches: [ master ] workflow_dispatch: # 수동 실행 가능 inputs: confirm_deployment: @@ -136,7 +136,7 @@ jobs: KAKAO_CLIENT_ID: ${{ secrets.KAKAO_CLIENT_ID }} KAKAO_CLIENT_SECRET: ${{ secrets.KAKAO_CLIENT_SECRET }} DEEPSEARCH_API_KEY: ${{ secrets.DEEPSEARCH_API_KEY }} - run: ./gradlew clean build -x test --no-daemon --stacktrace + run: ./gradlew build --no-daemon - name: 프로덕션 배포 전 테스트 결과 업로드 uses: actions/upload-artifact@v4 @@ -173,8 +173,47 @@ jobs: chmod 600 ~/.ssh/id_rsa ssh-keyscan -H ${{ secrets.EC2_HOST }} >> ~/.ssh/known_hosts + # ✅ 환경변수 파일 생성 단계 추가 + - name: 프로덕션 환경변수 파일 생성 + run: | + cat > .env << EOF + # Database Configuration + DB_SERVER=mysql + DB_PORT=3306 + DB_USER=root + DB_PASS=${{ secrets.PROD_DB_PASS }} + + # Redis Configuration + REDIS_CACHE_HOST=redis-cache + REDIS_CACHE_PORT=6379 + REDIS_SESSION1_HOST=redis-session1 + REDIS_SESSION1_PORT=6379 + REDIS_SESSION2_HOST=redis-session2 + REDIS_SESSION2_PORT=6379 + + # Elasticsearch Configuration + ELASTICSEARCH_SERVER=elasticsearch + ELASTICSEARCH_PORT=9200 + + # JWT Configuration + JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }} + + # OpenAI Configuration + OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }} + + # Kakao OAuth Configuration + KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }} + KAKAO_CLIENT_SECRET=${{ secrets.KAKAO_CLIENT_SECRET }} + + # DeepSearch API Configuration + DEEPSEARCH_API_KEY=${{ secrets.DEEPSEARCH_API_KEY }} + EOF + - name: 프로덕션 서버에 배포 run: | + # 환경변수 파일을 서버로 전송 + scp -i ~/.ssh/id_rsa .env ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }}:/opt/news-deliver/ + ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' echo "🚀 프로덕션 배포 시작: $(date)" echo "📍 브랜치: ${{ github.ref_name }}" @@ -185,11 +224,11 @@ jobs: # 현재 브랜치 확인 echo "📋 현재 브랜치: $(git branch --show-current)" - # 현재 브랜치에 맞게 체크아웃 및 풀 - echo "📥 현재 브랜치(${{ github.ref_name }})에서 최신 코드 가져오는 중..." + # master 브랜치로 체크아웃 및 풀 + echo "📥 master에서 최신 코드 가져오는 중..." git fetch origin - git checkout ${{ github.ref_name }} || git checkout -b ${{ github.ref_name }} origin/${{ github.ref_name }} - git pull origin ${{ github.ref_name }} + git checkout master || git checkout -b master origin/master + git pull origin master # 환경 변수 파일 확인 if [ ! -f ".env" ]; then @@ -197,72 +236,191 @@ jobs: exit 1 fi - # 기존 컨테이너 중지 - echo "🛑 기존 컨테이너 중지 중..." - docker-compose down || true + echo "✅ .env 파일 확인됨" - # Docker 이미지 정리 - echo "🧹 오래된 이미지 정리 중..." - docker system prune -f + # ✅ 기존 컨테이너 완전 정리 + echo "🛑 기존 서비스 완전 중지 중..." + docker-compose down --volumes --remove-orphans || true - # 새로운 컨테이너 시작 + # ✅ 좀비 프로세스 정리 + echo "🧹 좀비 프로세스 정리 중..." + docker container prune -f + docker volume prune -f + docker network prune -f + docker image prune -f + + # ✅ 네트워크 및 포트 확인 + echo "🔍 포트 사용 상태 확인 중..." + netstat -tulpn | grep -E ":(80|8080|3306|6379|6380|6381|9200)" || echo "포트 사용 중인 프로세스 없음" + + # ✅ 새로운 컨테이너 시작 (더 긴 타임아웃) echo "🔄 새 컨테이너 시작 중..." - docker-compose up -d --build + docker-compose up -d --build --force-recreate - # 서비스 시작 대기 (프로덕션은 더 오래) - echo "⏳ 프로덕션 서비스 시작 대기 중..." - sleep 120 + # ✅ 각 서비스별 개별 헬스체크 + echo "⏳ 프로덕션 서비스별 헬스체크 시작..." - # 서비스 상태 확인 - echo "📊 서비스 상태 확인 중..." - docker-compose ps + # MySQL 헬스체크 + echo "🔍 MySQL 헬스체크 중..." + for i in {1..30}; do + if docker-compose exec -T mysql mysqladmin ping -h localhost --silent; then + echo "✅ MySQL 준비 완료!" + break + fi + echo "⏳ MySQL 대기 중... $i/30" + sleep 10 + done - # Spring Boot 헬스체크 - echo "🔍 프로덕션 Spring Boot 헬스체크 중..." + # Redis 헬스체크 + echo "🔍 Redis 헬스체크 중..." for i in {1..20}; do + if docker-compose exec -T redis-cache redis-cli ping | grep -q PONG; then + echo "✅ Redis 준비 완료!" + break + fi + echo "⏳ Redis 대기 중... $i/20" + sleep 5 + done + + # Elasticsearch 헬스체크 + echo "🔍 Elasticsearch 헬스체크 중..." + for i in {1..40}; do + if curl -f http://localhost:9200/_cluster/health >/dev/null 2>&1; then + echo "✅ Elasticsearch 준비 완료!" + break + fi + echo "⏳ Elasticsearch 대기 중... $i/40" + sleep 10 + done + + # ✅ Spring Boot 헬스체크 (더 긴 타임아웃) + echo "🔍 Spring Boot 헬스체크 중..." + for i in {1..60}; do if curl -f http://localhost:8080/ >/dev/null 2>&1; then - echo "✅ 프로덕션 Spring Boot가 정상 작동 중입니다!" + echo "✅ Spring Boot가 정상 작동 중입니다!" break fi - echo "⏳ 시도 $i/20: Spring Boot가 아직 준비되지 않았습니다..." + echo "⏳ 시도 $i/60: Spring Boot가 아직 준비되지 않았습니다..." sleep 15 done + # ✅ 최종 서비스 상태 확인 + echo "📊 최종 서비스 상태 확인..." + docker-compose ps + + # ✅ 상세 로그 확인 + echo "📋 Spring Boot 컨테이너 로그 (마지막 50줄):" + docker-compose logs --tail=50 springboot || echo "Spring Boot 로그 가져오기 실패" + echo "🎉 프로덕션 배포 완료: $(date)" ' - + # ✅ 개선된 외부 헬스체크 - name: 프로덕션 외부 헬스체크 run: | echo "🔍 프로덕션 외부 헬스체크 중..." + + # ✅ 더 긴 초기 대기 시간 + echo "⏳ 초기 안정화 대기 중 (3분)..." sleep 180 - # 외부에서 접근 가능한지 확인 + # ✅ EC2 보안그룹 및 nginx 상태 확인 + echo "🔍 EC2 인스턴스 기본 연결 테스트..." + if ! nc -z ${{ secrets.EC2_HOST }} 80; then + echo "❌ EC2 포트 80에 연결할 수 없습니다. 보안그룹 설정을 확인하세요." + + # SSH로 내부 상태 확인 + ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' + echo "🔍 내부 서비스 상태 확인:" + echo "=== Docker 컨테이너 상태 ===" + docker ps + echo "=== 포트 바인딩 상태 ===" + netstat -tulpn | grep -E ":(80|8080)" + echo "=== nginx 상태 ===" + docker-compose logs --tail=20 nginx || echo "nginx 로그 없음" + echo "=== Spring Boot 상태 ===" + docker-compose logs --tail=20 springboot || echo "Spring Boot 로그 없음" + ' + exit 1 + fi + + # ✅ 실제 HTTP 헬스체크 success=false - for i in {1..15}; do - if curl -f http://${{ secrets.EC2_HOST }}/ >/dev/null 2>&1; then - echo "✅ 프로덕션 외부 헬스체크 통과!" + for i in {1..20}; do + echo "⏳ 외부 헬스체크 시도 $i/20..." + + # HTTP 응답 코드 확인 + http_code=$(curl -s -o /dev/null -w "%{http_code}" http://${{ secrets.EC2_HOST }}/ || echo "000") + + if [ "$http_code" = "200" ]; then + echo "✅ 프로덕션 외부 헬스체크 통과! (HTTP $http_code)" echo "🔗 프로덕션 URL: http://${{ secrets.EC2_HOST }}/" echo "📋 API 문서: http://${{ secrets.EC2_HOST }}/swagger.html" success=true break + else + echo "⚠️ HTTP 응답 코드: $http_code" + + # 상세 디버그 정보 (5번째 시도마다) + if [ $((i % 5)) -eq 0 ]; then + echo "🔍 상세 연결 상태 확인 중..." + curl -v http://${{ secrets.EC2_HOST }}/ || true + fi fi - echo "⏳ 시도 $i/10: 외부 체크가 아직 준비되지 않았습니다..." + sleep 30 done if [ "$success" = false ]; then echo "❌ 프로덕션 헬스체크 실패! 즉시 확인이 필요합니다." + + # ✅ 실패 시 상세 디버그 정보 수집 + echo "🔍 실패 원인 분석을 위한 정보 수집 중..." + ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' + echo "=== 시스템 리소스 상태 ===" + df -h + free -h + echo "=== 모든 컨테이너 상태 ===" + docker ps -a + echo "=== 컨테이너별 로그 (마지막 30줄) ===" + for container in $(docker ps -q); do + echo "--- $(docker ps --format "table {{.Names}}" | grep -v NAMES | head -1) 로그 ---" + docker logs --tail=30 $container 2>&1 || echo "로그 없음" + done + ' exit 1 fi + # ✅ 배포 후 상세 모니터링 + - name: 배포 후 상태 모니터링 + if: success() + run: | + echo "📊 배포 후 상태 모니터링 (5분간)..." + + for i in {1..10}; do + echo "🔍 모니터링 $i/10..." + + # HTTP 상태 확인 + http_code=$(curl -s -o /dev/null -w "%{http_code}" http://${{ secrets.EC2_HOST }}/ || echo "000") + + if [ "$http_code" = "200" ]; then + echo "✅ 시점 $i: 서비스 정상 (HTTP $http_code)" + else + echo "⚠️ 시점 $i: 서비스 이상 (HTTP $http_code)" + fi + + sleep 30 + done + + echo "✅ 5분간 모니터링 완료" + - name: 프로덕션 배포 요약 if: always() run: | echo "## 🚀 프로덕션 배포 요약" >> $GITHUB_STEP_SUMMARY echo "- **배포 상태**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY echo "- **환경**: PRODUCTION 🔥" >> $GITHUB_STEP_SUMMARY - echo "- **배포된 브랜치**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY + echo "- **배포된 브랜치**: master" >> $GITHUB_STEP_SUMMARY echo "- **배포 시간**: $(date)" >> $GITHUB_STEP_SUMMARY echo "- **커밋**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY echo "- **프로덕션 URL**: http://${{ secrets.EC2_HOST }}/" >> $GITHUB_STEP_SUMMARY From d702309972426776317acacc26315565c762fd6f Mon Sep 17 00:00:00 2001 From: moonjun1 Date: Thu, 17 Jul 2025 15:33:27 +0900 Subject: [PATCH 05/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EA=B9=83=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20:=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-prod.yml | 40 ++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index b705257..12b8755 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -1,8 +1,10 @@ -name: 프로덕션 배포 (master 브랜치만) +name: 프로덕션 배포 (master & feat/git-master) on: push: - branches: [ master ] + branches: + - master + - feat/git-master # ✅ feat/git-master 브랜치 추가 workflow_dispatch: # 수동 실행 가능 inputs: confirm_deployment: @@ -165,6 +167,11 @@ jobs: echo "브랜치: ${{ github.ref_name }}" echo "커밋: ${{ github.sha }}" echo "배포 시간: $(date)" + + # ✅ feat/git-master 브랜치인 경우 특별 표시 + if [ "${{ github.ref_name }}" = "feat/git-master" ]; then + echo "⚠️ [개발 브랜치 배포] feat/git-master에서 프로덕션 배포 중" + fi - name: SSH 키 설정 run: | @@ -221,14 +228,25 @@ jobs: cd /opt/news-deliver - # 현재 브랜치 확인 + # ✅ 현재 브랜치에 따른 체크아웃 로직 echo "📋 현재 브랜치: $(git branch --show-current)" + echo "📥 ${{ github.ref_name }} 브랜치에서 최신 코드 가져오는 중..." - # master 브랜치로 체크아웃 및 풀 - echo "📥 master에서 최신 코드 가져오는 중..." git fetch origin - git checkout master || git checkout -b master origin/master - git pull origin master + + # 브랜치별 체크아웃 처리 + if [ "${{ github.ref_name }}" = "master" ]; then + git checkout master || git checkout -b master origin/master + git pull origin master + echo "✅ master 브랜치 코드 업데이트 완료" + elif [ "${{ github.ref_name }}" = "feat/git-master" ]; then + git checkout feat/git-master || git checkout -b feat/git-master origin/feat/git-master + git pull origin feat/git-master + echo "✅ feat/git-master 브랜치 코드 업데이트 완료" + else + echo "❌ 지원하지 않는 브랜치: ${{ github.ref_name }}" + exit 1 + fi # 환경 변수 파일 확인 if [ ! -f ".env" ]; then @@ -420,12 +438,18 @@ jobs: echo "## 🚀 프로덕션 배포 요약" >> $GITHUB_STEP_SUMMARY echo "- **배포 상태**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY echo "- **환경**: PRODUCTION 🔥" >> $GITHUB_STEP_SUMMARY - echo "- **배포된 브랜치**: master" >> $GITHUB_STEP_SUMMARY + echo "- **배포된 브랜치**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY echo "- **배포 시간**: $(date)" >> $GITHUB_STEP_SUMMARY echo "- **커밋**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY echo "- **프로덕션 URL**: http://${{ secrets.EC2_HOST }}/" >> $GITHUB_STEP_SUMMARY echo "- **API 문서**: http://${{ secrets.EC2_HOST }}/swagger.html" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY + + # ✅ 브랜치별 메시지 구분 + if [ "${{ github.ref_name }}" = "feat/git-master" ]; then + echo "### ⚠️ 개발 브랜치(feat/git-master)에서 프로덕션 배포됨" >> $GITHUB_STEP_SUMMARY + fi + if [ "${{ job.status }}" == "success" ]; then echo "### ✅ 프로덕션 배포가 성공적으로 완료되었습니다!" >> $GITHUB_STEP_SUMMARY else From 4ea6f7f306e6963e87081f12660dff1ab78cf5dd Mon Sep 17 00:00:00 2001 From: moonjun1 Date: Thu, 17 Jul 2025 15:43:47 +0900 Subject: [PATCH 06/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EA=B9=83=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20:=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-prod.yml | 136 ++++++++++++++++++++---------- .github/workflows/dev-test.yml | 109 ++++++++++++++++++++---- 2 files changed, 183 insertions(+), 62 deletions(-) diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 12b8755..47651a8 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -116,29 +116,101 @@ jobs: sleep 2 done + # ✅ 테스트용 application-test.properties 생성 + - name: 테스트용 환경설정 파일 생성 + working-directory: ./SpringBoot + run: | + # 기존 application-test.properties 백업 + if [ -f "src/main/resources/application-test.properties" ]; then + cp src/main/resources/application-test.properties src/main/resources/application-test.properties.bak + fi + + # GitHub Actions용 테스트 설정 파일 생성 + cat > src/main/resources/application-test.properties << EOF + spring.application.name=News-Deliver + spring.main.allow-bean-definition-overriding=true + + # Profile Active + spring.profiles.active=test + + # DB Connection (CI 환경용) + spring.datasource.url=jdbc:mysql://localhost:3306/backendDB?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true + spring.datasource.username=root + spring.datasource.password=ci_test_password + spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + + # JPA Configuration + spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect + spring.jpa.hibernate.ddl-auto=create-drop + spring.jpa.show-sql=false + spring.jpa.properties.hibernate.format_sql=false + + # Redis Configuration + redis.cache.host=localhost + redis.cache.port=6379 + redis.session1.host=localhost + redis.session1.port=6380 + redis.session2.host=localhost + redis.session2.port=6381 + + # Elasticsearch + spring.elasticsearch.uris=http://localhost:9200 + + # JWT (테스트용 더미 값) + jwt.secretKey=test-secret-key-for-ci-cd-pipeline-needs-to-be-at-least-256-bits-long-for-security + jwt.accessTokenExpirationTime=1800000 + jwt.refreshTokenExpirationTime=86400000 + + # OAuth2 (테스트용 더미 값) + spring.security.oauth2.client.registration.kakao.client-id=test-kakao-client-id + spring.security.oauth2.client.registration.kakao.client-secret=test-kakao-client-secret + spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post + spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost/api/login/oauth2/code/kakao + spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code + spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize + spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token + spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me + spring.security.oauth2.client.provider.kakao.user-name-attribute=id + + # Spring AI (테스트용 더미 값) + spring.ai.openai.api-key=test-openai-api-key + spring.ai.openai.chat.options.model=gpt-3.5-turbo + + # DeepSearch API (테스트용 더미 값) + deepsearch.api.key=test-deepsearch-api-key + + # Batch Job 비활성화 (테스트 시) + spring.batch.job.enabled=false + + # Logging Configuration + logging.level.org.springframework.web=INFO + logging.level.org.hibernate.SQL=ERROR + logging.level.org.hibernate.type.descriptor.sql.BasicBinder=ERROR + logging.level.com.zaxxer.hikari=ERROR + + # Time Zone + spring.jackson.time-zone=Asia/Seoul + + # Forward headers + server.forward-headers-strategy=framework + EOF + + echo "✅ 테스트용 application-test.properties 파일 생성 완료" + - name: 🚨 프로덕션 배포 전 전체 테스트 working-directory: ./SpringBoot env: - # 테스트 환경 변수 설정 (GitHub Secrets 사용) + # Spring 프로파일 명시적 설정 SPRING_PROFILES_ACTIVE: test - DB_SERVER: localhost - DB_PORT: 3306 - DB_USER: root - DB_PASS: ci_test_password - REDIS_CACHE_HOST: localhost - REDIS_CACHE_PORT: 6379 - REDIS_SESSION1_HOST: localhost - REDIS_SESSION1_PORT: 6380 - REDIS_SESSION2_HOST: localhost - REDIS_SESSION2_PORT: 6381 - JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }} - ELASTICSEARCH_SERVER: localhost - ELASTICSEARCH_PORT: 9200 - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - KAKAO_CLIENT_ID: ${{ secrets.KAKAO_CLIENT_ID }} - KAKAO_CLIENT_SECRET: ${{ secrets.KAKAO_CLIENT_SECRET }} - DEEPSEARCH_API_KEY: ${{ secrets.DEEPSEARCH_API_KEY }} - run: ./gradlew build --no-daemon + run: | + echo "🔍 테스트 실행 전 환경 확인" + echo "SPRING_PROFILES_ACTIVE: $SPRING_PROFILES_ACTIVE" + echo "Java 버전: $(java -version 2>&1 | head -1)" + echo "현재 디렉토리: $(pwd)" + echo "application-test.properties 존재 여부: $(ls -la src/main/resources/application*test* || echo '파일 없음')" + + echo "🚀 테스트 시작" + ./gradlew clean build --no-daemon --info - name: 프로덕션 배포 전 테스트 결과 업로드 uses: actions/upload-artifact@v4 @@ -430,28 +502,4 @@ jobs: sleep 30 done - echo "✅ 5분간 모니터링 완료" - - - name: 프로덕션 배포 요약 - if: always() - run: | - echo "## 🚀 프로덕션 배포 요약" >> $GITHUB_STEP_SUMMARY - echo "- **배포 상태**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY - echo "- **환경**: PRODUCTION 🔥" >> $GITHUB_STEP_SUMMARY - echo "- **배포된 브랜치**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY - echo "- **배포 시간**: $(date)" >> $GITHUB_STEP_SUMMARY - echo "- **커밋**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY - echo "- **프로덕션 URL**: http://${{ secrets.EC2_HOST }}/" >> $GITHUB_STEP_SUMMARY - echo "- **API 문서**: http://${{ secrets.EC2_HOST }}/swagger.html" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - # ✅ 브랜치별 메시지 구분 - if [ "${{ github.ref_name }}" = "feat/git-master" ]; then - echo "### ⚠️ 개발 브랜치(feat/git-master)에서 프로덕션 배포됨" >> $GITHUB_STEP_SUMMARY - fi - - if [ "${{ job.status }}" == "success" ]; then - echo "### ✅ 프로덕션 배포가 성공적으로 완료되었습니다!" >> $GITHUB_STEP_SUMMARY - else - echo "### ❌ 프로덕션 배포가 실패했습니다. 즉시 확인이 필요합니다!" >> $GITHUB_STEP_SUMMARY - fi \ No newline at end of file + echo "✅ 5분간 \ No newline at end of file diff --git a/.github/workflows/dev-test.yml b/.github/workflows/dev-test.yml index 66ac52c..4541355 100644 --- a/.github/workflows/dev-test.yml +++ b/.github/workflows/dev-test.yml @@ -44,6 +44,14 @@ jobs: java-version: '17' distribution: 'corretto' + - name: Gradle 패키지 캐시 + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + - name: gradlew 권한 설정 run: chmod +x SpringBoot/gradlew @@ -52,27 +60,92 @@ jobs: sleep 30 echo "서비스 준비 완료" + - name: 테스트용 환경설정 파일 생성 + working-directory: ./SpringBoot + run: | + # 테스트용 application-test.properties 생성 + cat > src/main/resources/application-test.properties << EOF + spring.application.name=News-Deliver + spring.main.allow-bean-definition-overriding=true + + # Profile Active + spring.profiles.active=test + + # DB Connection + spring.datasource.url=jdbc:mysql://localhost:3306/backendDB?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true + spring.datasource.username=root + spring.datasource.password=testpass + spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + + # JPA Configuration + spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect + spring.jpa.hibernate.ddl-auto=create-drop + spring.jpa.show-sql=false + spring.jpa.properties.hibernate.format_sql=false + + # Redis Configuration + redis.cache.host=localhost + redis.cache.port=6379 + redis.session1.host=localhost + redis.session1.port=6380 + redis.session2.host=localhost + redis.session2.port=6381 + + # Elasticsearch + spring.elasticsearch.uris=http://localhost:9200 + + # JWT + jwt.secretKey=test-secret-key-that-is-long-enough-for-jwt-hmac-sha256-algorithm-minimum-32-bytes + jwt.accessTokenExpirationTime=1800000 + jwt.refreshTokenExpirationTime=86400000 + + # OAuth2 + spring.security.oauth2.client.registration.kakao.client-id=test-id + spring.security.oauth2.client.registration.kakao.client-secret=test-secret + spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post + spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost/login/oauth2/code/kakao + spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code + spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize + spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token + spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me + spring.security.oauth2.client.provider.kakao.user-name-attribute=id + + # Spring AI + spring.ai.openai.api-key=test-key + spring.ai.openai.chat.options.model=gpt-3.5-turbo + + # DeepSearch API + deepsearch.api.key=test-key + + # Batch Job 비활성화 (테스트 시) + spring.batch.job.enabled=false + + # Logging Configuration + logging.level.org.springframework.web=INFO + logging.level.org.hibernate.SQL=ERROR + logging.level.org.hibernate.type.descriptor.sql.BasicBinder=ERROR + logging.level.com.zaxxer.hikari=ERROR + + # Time Zone + spring.jackson.time-zone=Asia/Seoul + + # Forward headers + server.forward-headers-strategy=framework + EOF + + echo "✅ 테스트용 설정 파일 생성 완료" + - name: 테스트 실행 working-directory: ./SpringBoot env: - DB_SERVER: localhost - DB_PORT: 3306 - DB_USER: root - DB_PASS: testpass - REDIS_CACHE_HOST: localhost - REDIS_CACHE_PORT: 6379 - REDIS_SESSION1_HOST: localhost - REDIS_SESSION1_PORT: 6380 - REDIS_SESSION2_HOST: localhost - REDIS_SESSION2_PORT: 6381 - JWT_SECRET_KEY: test-secret-key-that-is-long-enough-for-jwt-hmac-sha256-algorithm-minimum-32-bytes - ELASTICSEARCH_SERVER: localhost - ELASTICSEARCH_PORT: 9200 - OPENAI_API_KEY: test-key - KAKAO_CLIENT_ID: test-id - KAKAO_CLIENT_SECRET: test-secret - DEEPSEARCH_API_KEY: test-key - run: ./gradlew build --no-daemon + SPRING_PROFILES_ACTIVE: test + run: | + echo "🔍 테스트 실행 전 환경 확인" + echo "SPRING_PROFILES_ACTIVE: $SPRING_PROFILES_ACTIVE" + echo "현재 디렉토리: $(pwd)" + + echo "🚀 Dev 브랜치 테스트 시작" + ./gradlew build --no-daemon - name: 결과 요약 if: always() From 8ccf0994076d5ee30d79c945921ee7846208b007 Mon Sep 17 00:00:00 2001 From: moonjun1 Date: Thu, 17 Jul 2025 15:55:35 +0900 Subject: [PATCH 07/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EA=B9=83=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20:=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-prod.yml | 585 ++++-------------------------- .github/workflows/dev-test.yml | 7 +- 2 files changed, 83 insertions(+), 509 deletions(-) diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 47651a8..0c72875 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -1,505 +1,80 @@ -name: 프로덕션 배포 (master & feat/git-master) - -on: - push: - branches: - - master - - feat/git-master # ✅ feat/git-master 브랜치 추가 - workflow_dispatch: # 수동 실행 가능 - inputs: - confirm_deployment: - description: '프로덕션 배포를 확인하시겠습니까?' - required: true - default: 'no' - type: choice - options: - - 'no' - - 'yes' - -jobs: - # 1단계: 배포 전 테스트 - pre-deploy-test: - runs-on: ubuntu-latest - - services: - mysql: - image: mysql:8.0 - env: - MYSQL_ROOT_PASSWORD: ci_test_password - MYSQL_DATABASE: backendDB - ports: - - 3306:3306 - options: >- - --health-cmd="mysqladmin ping -h localhost" - --health-interval=10s - --health-timeout=5s - --health-retries=10 - - redis-cache: - image: redis:7.2 - ports: - - 6379:6379 - options: >- - --health-cmd="redis-cli ping" - --health-interval=10s - --health-timeout=5s - --health-retries=5 - - redis-session1: - image: redis:7.2 - ports: - - 6380:6379 - - redis-session2: - image: redis:7.2 - ports: - - 6381:6379 - - elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0 - env: - discovery.type: single-node - xpack.security.enabled: false - ES_JAVA_OPTS: -Xms512m -Xmx512m - ports: - - 9200:9200 - - steps: - - name: 코드 체크아웃 - uses: actions/checkout@v4 - - - name: JDK 17 설정 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'corretto' - - - name: Gradle 패키지 캐시 - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - - - name: gradlew 실행 권한 부여 - run: chmod +x SpringBoot/gradlew - - - name: 서비스 준비 완료 대기 - run: | - echo "⏳ 프로덕션 배포 전 테스트 환경 준비 중..." - - # MySQL 대기 - for i in {1..30}; do - if mysqladmin ping -h"127.0.0.1" -P"3306" -u"root" -p"ci_test_password" --silent; then - echo "✅ MySQL 준비 완료" - break - fi - sleep 2 - done - - # Redis 대기 - for i in {1..30}; do - if redis-cli -h 127.0.0.1 -p 6379 ping | grep -q PONG; then - echo "✅ Redis 준비 완료" - break - fi - sleep 1 - done - - # Elasticsearch 대기 - for i in {1..60}; do - if curl -f http://localhost:9200/_cluster/health >/dev/null 2>&1; then - echo "✅ Elasticsearch 준비 완료" - break - fi - sleep 2 - done - - # ✅ 테스트용 application-test.properties 생성 - - name: 테스트용 환경설정 파일 생성 - working-directory: ./SpringBoot - run: | - # 기존 application-test.properties 백업 - if [ -f "src/main/resources/application-test.properties" ]; then - cp src/main/resources/application-test.properties src/main/resources/application-test.properties.bak - fi - - # GitHub Actions용 테스트 설정 파일 생성 - cat > src/main/resources/application-test.properties << EOF - spring.application.name=News-Deliver - spring.main.allow-bean-definition-overriding=true - - # Profile Active - spring.profiles.active=test - - # DB Connection (CI 환경용) - spring.datasource.url=jdbc:mysql://localhost:3306/backendDB?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true - spring.datasource.username=root - spring.datasource.password=ci_test_password - spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver - - # JPA Configuration - spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect - spring.jpa.hibernate.ddl-auto=create-drop - spring.jpa.show-sql=false - spring.jpa.properties.hibernate.format_sql=false - - # Redis Configuration - redis.cache.host=localhost - redis.cache.port=6379 - redis.session1.host=localhost - redis.session1.port=6380 - redis.session2.host=localhost - redis.session2.port=6381 - - # Elasticsearch - spring.elasticsearch.uris=http://localhost:9200 - - # JWT (테스트용 더미 값) - jwt.secretKey=test-secret-key-for-ci-cd-pipeline-needs-to-be-at-least-256-bits-long-for-security - jwt.accessTokenExpirationTime=1800000 - jwt.refreshTokenExpirationTime=86400000 - - # OAuth2 (테스트용 더미 값) - spring.security.oauth2.client.registration.kakao.client-id=test-kakao-client-id - spring.security.oauth2.client.registration.kakao.client-secret=test-kakao-client-secret - spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post - spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost/api/login/oauth2/code/kakao - spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code - spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize - spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token - spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me - spring.security.oauth2.client.provider.kakao.user-name-attribute=id - - # Spring AI (테스트용 더미 값) - spring.ai.openai.api-key=test-openai-api-key - spring.ai.openai.chat.options.model=gpt-3.5-turbo - - # DeepSearch API (테스트용 더미 값) - deepsearch.api.key=test-deepsearch-api-key - - # Batch Job 비활성화 (테스트 시) - spring.batch.job.enabled=false - - # Logging Configuration - logging.level.org.springframework.web=INFO - logging.level.org.hibernate.SQL=ERROR - logging.level.org.hibernate.type.descriptor.sql.BasicBinder=ERROR - logging.level.com.zaxxer.hikari=ERROR - - # Time Zone - spring.jackson.time-zone=Asia/Seoul - - # Forward headers - server.forward-headers-strategy=framework - EOF - - echo "✅ 테스트용 application-test.properties 파일 생성 완료" - - - name: 🚨 프로덕션 배포 전 전체 테스트 - working-directory: ./SpringBoot - env: - # Spring 프로파일 명시적 설정 - SPRING_PROFILES_ACTIVE: test - run: | - echo "🔍 테스트 실행 전 환경 확인" - echo "SPRING_PROFILES_ACTIVE: $SPRING_PROFILES_ACTIVE" - echo "Java 버전: $(java -version 2>&1 | head -1)" - echo "현재 디렉토리: $(pwd)" - echo "application-test.properties 존재 여부: $(ls -la src/main/resources/application*test* || echo '파일 없음')" - - echo "🚀 테스트 시작" - ./gradlew clean build --no-daemon --info - - - name: 프로덕션 배포 전 테스트 결과 업로드 - uses: actions/upload-artifact@v4 - if: always() - with: - name: prod-pre-test-reports-${{ github.run_number }} - path: | - SpringBoot/build/reports/ - SpringBoot/build/test-results/ - retention-days: 90 - - # 2단계: 실제 프로덕션 배포 - deploy-production: - needs: pre-deploy-test - runs-on: ubuntu-latest - environment: production # GitHub Environment 보호 설정 필요 - if: ${{ needs.pre-deploy-test.result == 'success' }} - - steps: - - name: 코드 체크아웃 - uses: actions/checkout@v4 - - - name: 🚨 프로덕션 배포 확인 - run: | - echo "🚨🚨🚨 프로덕션 환경에 배포를 시작합니다 🚨🚨🚨" - echo "브랜치: ${{ github.ref_name }}" - echo "커밋: ${{ github.sha }}" - echo "배포 시간: $(date)" - - # ✅ feat/git-master 브랜치인 경우 특별 표시 - if [ "${{ github.ref_name }}" = "feat/git-master" ]; then - echo "⚠️ [개발 브랜치 배포] feat/git-master에서 프로덕션 배포 중" - fi - - - name: SSH 키 설정 - run: | - mkdir -p ~/.ssh - echo "${{ secrets.EC2_SSH_KEY }}" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - ssh-keyscan -H ${{ secrets.EC2_HOST }} >> ~/.ssh/known_hosts - - # ✅ 환경변수 파일 생성 단계 추가 - - name: 프로덕션 환경변수 파일 생성 - run: | - cat > .env << EOF - # Database Configuration - DB_SERVER=mysql - DB_PORT=3306 - DB_USER=root - DB_PASS=${{ secrets.PROD_DB_PASS }} - - # Redis Configuration - REDIS_CACHE_HOST=redis-cache - REDIS_CACHE_PORT=6379 - REDIS_SESSION1_HOST=redis-session1 - REDIS_SESSION1_PORT=6379 - REDIS_SESSION2_HOST=redis-session2 - REDIS_SESSION2_PORT=6379 - - # Elasticsearch Configuration - ELASTICSEARCH_SERVER=elasticsearch - ELASTICSEARCH_PORT=9200 - - # JWT Configuration - JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }} - - # OpenAI Configuration - OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }} - - # Kakao OAuth Configuration - KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }} - KAKAO_CLIENT_SECRET=${{ secrets.KAKAO_CLIENT_SECRET }} - - # DeepSearch API Configuration - DEEPSEARCH_API_KEY=${{ secrets.DEEPSEARCH_API_KEY }} - EOF - - - name: 프로덕션 서버에 배포 - run: | - # 환경변수 파일을 서버로 전송 - scp -i ~/.ssh/id_rsa .env ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }}:/opt/news-deliver/ - - ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' - echo "🚀 프로덕션 배포 시작: $(date)" - echo "📍 브랜치: ${{ github.ref_name }}" - echo "🔥 PRODUCTION 환경 배포" - - cd /opt/news-deliver - - # ✅ 현재 브랜치에 따른 체크아웃 로직 - echo "📋 현재 브랜치: $(git branch --show-current)" - echo "📥 ${{ github.ref_name }} 브랜치에서 최신 코드 가져오는 중..." - - git fetch origin - - # 브랜치별 체크아웃 처리 - if [ "${{ github.ref_name }}" = "master" ]; then - git checkout master || git checkout -b master origin/master - git pull origin master - echo "✅ master 브랜치 코드 업데이트 완료" - elif [ "${{ github.ref_name }}" = "feat/git-master" ]; then - git checkout feat/git-master || git checkout -b feat/git-master origin/feat/git-master - git pull origin feat/git-master - echo "✅ feat/git-master 브랜치 코드 업데이트 완료" - else - echo "❌ 지원하지 않는 브랜치: ${{ github.ref_name }}" - exit 1 - fi - - # 환경 변수 파일 확인 - if [ ! -f ".env" ]; then - echo "❌ .env 파일을 찾을 수 없습니다!" - exit 1 - fi - - echo "✅ .env 파일 확인됨" - - # ✅ 기존 컨테이너 완전 정리 - echo "🛑 기존 서비스 완전 중지 중..." - docker-compose down --volumes --remove-orphans || true - - # ✅ 좀비 프로세스 정리 - echo "🧹 좀비 프로세스 정리 중..." - docker container prune -f - docker volume prune -f - docker network prune -f - docker image prune -f - - # ✅ 네트워크 및 포트 확인 - echo "🔍 포트 사용 상태 확인 중..." - netstat -tulpn | grep -E ":(80|8080|3306|6379|6380|6381|9200)" || echo "포트 사용 중인 프로세스 없음" - - # ✅ 새로운 컨테이너 시작 (더 긴 타임아웃) - echo "🔄 새 컨테이너 시작 중..." - docker-compose up -d --build --force-recreate - - # ✅ 각 서비스별 개별 헬스체크 - echo "⏳ 프로덕션 서비스별 헬스체크 시작..." - - # MySQL 헬스체크 - echo "🔍 MySQL 헬스체크 중..." - for i in {1..30}; do - if docker-compose exec -T mysql mysqladmin ping -h localhost --silent; then - echo "✅ MySQL 준비 완료!" - break - fi - echo "⏳ MySQL 대기 중... $i/30" - sleep 10 - done - - # Redis 헬스체크 - echo "🔍 Redis 헬스체크 중..." - for i in {1..20}; do - if docker-compose exec -T redis-cache redis-cli ping | grep -q PONG; then - echo "✅ Redis 준비 완료!" - break - fi - echo "⏳ Redis 대기 중... $i/20" - sleep 5 - done - - # Elasticsearch 헬스체크 - echo "🔍 Elasticsearch 헬스체크 중..." - for i in {1..40}; do - if curl -f http://localhost:9200/_cluster/health >/dev/null 2>&1; then - echo "✅ Elasticsearch 준비 완료!" - break - fi - echo "⏳ Elasticsearch 대기 중... $i/40" - sleep 10 - done - - # ✅ Spring Boot 헬스체크 (더 긴 타임아웃) - echo "🔍 Spring Boot 헬스체크 중..." - for i in {1..60}; do - if curl -f http://localhost:8080/ >/dev/null 2>&1; then - echo "✅ Spring Boot가 정상 작동 중입니다!" - break - fi - echo "⏳ 시도 $i/60: Spring Boot가 아직 준비되지 않았습니다..." - sleep 15 - done - - # ✅ 최종 서비스 상태 확인 - echo "📊 최종 서비스 상태 확인..." - docker-compose ps - - # ✅ 상세 로그 확인 - echo "📋 Spring Boot 컨테이너 로그 (마지막 50줄):" - docker-compose logs --tail=50 springboot || echo "Spring Boot 로그 가져오기 실패" - - echo "🎉 프로덕션 배포 완료: $(date)" - ' - - # ✅ 개선된 외부 헬스체크 - - name: 프로덕션 외부 헬스체크 - run: | - echo "🔍 프로덕션 외부 헬스체크 중..." - - # ✅ 더 긴 초기 대기 시간 - echo "⏳ 초기 안정화 대기 중 (3분)..." - sleep 180 - - # ✅ EC2 보안그룹 및 nginx 상태 확인 - echo "🔍 EC2 인스턴스 기본 연결 테스트..." - if ! nc -z ${{ secrets.EC2_HOST }} 80; then - echo "❌ EC2 포트 80에 연결할 수 없습니다. 보안그룹 설정을 확인하세요." - - # SSH로 내부 상태 확인 - ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' - echo "🔍 내부 서비스 상태 확인:" - echo "=== Docker 컨테이너 상태 ===" - docker ps - echo "=== 포트 바인딩 상태 ===" - netstat -tulpn | grep -E ":(80|8080)" - echo "=== nginx 상태 ===" - docker-compose logs --tail=20 nginx || echo "nginx 로그 없음" - echo "=== Spring Boot 상태 ===" - docker-compose logs --tail=20 springboot || echo "Spring Boot 로그 없음" - ' - exit 1 - fi - - # ✅ 실제 HTTP 헬스체크 - success=false - for i in {1..20}; do - echo "⏳ 외부 헬스체크 시도 $i/20..." - - # HTTP 응답 코드 확인 - http_code=$(curl -s -o /dev/null -w "%{http_code}" http://${{ secrets.EC2_HOST }}/ || echo "000") - - if [ "$http_code" = "200" ]; then - echo "✅ 프로덕션 외부 헬스체크 통과! (HTTP $http_code)" - echo "🔗 프로덕션 URL: http://${{ secrets.EC2_HOST }}/" - echo "📋 API 문서: http://${{ secrets.EC2_HOST }}/swagger.html" - success=true - break - else - echo "⚠️ HTTP 응답 코드: $http_code" - - # 상세 디버그 정보 (5번째 시도마다) - if [ $((i % 5)) -eq 0 ]; then - echo "🔍 상세 연결 상태 확인 중..." - curl -v http://${{ secrets.EC2_HOST }}/ || true - fi - fi - - sleep 30 - done - - if [ "$success" = false ]; then - echo "❌ 프로덕션 헬스체크 실패! 즉시 확인이 필요합니다." - - # ✅ 실패 시 상세 디버그 정보 수집 - echo "🔍 실패 원인 분석을 위한 정보 수집 중..." - ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' - echo "=== 시스템 리소스 상태 ===" - df -h - free -h - echo "=== 모든 컨테이너 상태 ===" - docker ps -a - echo "=== 컨테이너별 로그 (마지막 30줄) ===" - for container in $(docker ps -q); do - echo "--- $(docker ps --format "table {{.Names}}" | grep -v NAMES | head -1) 로그 ---" - docker logs --tail=30 $container 2>&1 || echo "로그 없음" - done - ' - exit 1 - fi - - # ✅ 배포 후 상세 모니터링 - - name: 배포 후 상태 모니터링 - if: success() - run: | - echo "📊 배포 후 상태 모니터링 (5분간)..." - - for i in {1..10}; do - echo "🔍 모니터링 $i/10..." - - # HTTP 상태 확인 - http_code=$(curl -s -o /dev/null -w "%{http_code}" http://${{ secrets.EC2_HOST }}/ || echo "000") - - if [ "$http_code" = "200" ]; then - echo "✅ 시점 $i: 서비스 정상 (HTTP $http_code)" - else - echo "⚠️ 시점 $i: 서비스 이상 (HTTP $http_code)" - fi - - sleep 30 - done - - echo "✅ 5분간 \ No newline at end of file +# deploy-prod.yml의 테스트용 환경설정 파일 생성 부분만 수정 + +- name: 테스트용 환경설정 파일 생성 + working-directory: ./SpringBoot + run: | + # 기존 application-test.properties 백업 + if [ -f "src/main/resources/application-test.properties" ]; then + cp src/main/resources/application-test.properties src/main/resources/application-test.properties.bak + fi + + # GitHub Actions용 테스트 설정 파일 생성 (spring.profiles.active 제거) + cat > src/main/resources/application-test.properties << EOF + spring.application.name=News-Deliver + spring.main.allow-bean-definition-overriding=true + + # ❌ 이 줄 제거됨: spring.profiles.active=test + + # DB Connection (CI 환경용) + spring.datasource.url=jdbc:mysql://localhost:3306/backendDB?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true + spring.datasource.username=root + spring.datasource.password=ci_test_password + spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + + # JPA Configuration + spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect + spring.jpa.hibernate.ddl-auto=create-drop + spring.jpa.show-sql=false + spring.jpa.properties.hibernate.format_sql=false + + # Redis Configuration + redis.cache.host=localhost + redis.cache.port=6379 + redis.session1.host=localhost + redis.session1.port=6380 + redis.session2.host=localhost + redis.session2.port=6381 + + # Elasticsearch + spring.elasticsearch.uris=http://localhost:9200 + + # JWT (테스트용 더미 값) + jwt.secretKey=test-secret-key-for-ci-cd-pipeline-needs-to-be-at-least-256-bits-long-for-security + jwt.accessTokenExpirationTime=1800000 + jwt.refreshTokenExpirationTime=86400000 + + # OAuth2 (테스트용 더미 값) + spring.security.oauth2.client.registration.kakao.client-id=test-kakao-client-id + spring.security.oauth2.client.registration.kakao.client-secret=test-kakao-client-secret + spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post + spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost/api/login/oauth2/code/kakao + spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code + spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize + spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token + spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me + spring.security.oauth2.client.provider.kakao.user-name-attribute=id + + # Spring AI (테스트용 더미 값) + spring.ai.openai.api-key=test-openai-api-key + spring.ai.openai.chat.options.model=gpt-3.5-turbo + + # DeepSearch API (테스트용 더미 값) + deepsearch.api.key=test-deepsearch-api-key + + # Batch Job 비활성화 (테스트 시) + spring.batch.job.enabled=false + + # Logging Configuration + logging.level.org.springframework.web=INFO + logging.level.org.hibernate.SQL=ERROR + logging.level.org.hibernate.type.descriptor.sql.BasicBinder=ERROR + logging.level.com.zaxxer.hikari=ERROR + + # Time Zone + spring.jackson.time-zone=Asia/Seoul + + # Forward headers + server.forward-headers-strategy=framework + EOF + + echo "✅ 테스트용 application-test.properties 파일 생성 완료" \ No newline at end of file diff --git a/.github/workflows/dev-test.yml b/.github/workflows/dev-test.yml index 4541355..f570f1f 100644 --- a/.github/workflows/dev-test.yml +++ b/.github/workflows/dev-test.yml @@ -63,13 +63,12 @@ jobs: - name: 테스트용 환경설정 파일 생성 working-directory: ./SpringBoot run: | - # 테스트용 application-test.properties 생성 + # 테스트용 application-test.properties 생성 (spring.profiles.active 제거) cat > src/main/resources/application-test.properties << EOF spring.application.name=News-Deliver spring.main.allow-bean-definition-overriding=true - # Profile Active - spring.profiles.active=test + # ❌ 이 줄 제거: spring.profiles.active=test # DB Connection spring.datasource.url=jdbc:mysql://localhost:3306/backendDB?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true @@ -138,7 +137,7 @@ jobs: - name: 테스트 실행 working-directory: ./SpringBoot env: - SPRING_PROFILES_ACTIVE: test + SPRING_PROFILES_ACTIVE: test # 환경변수로 프로파일 설정 run: | echo "🔍 테스트 실행 전 환경 확인" echo "SPRING_PROFILES_ACTIVE: $SPRING_PROFILES_ACTIVE" From 55b81a4b0566d6d212ab68cb234fc92f06c6189e Mon Sep 17 00:00:00 2001 From: moonjun1 Date: Thu, 17 Jul 2025 16:01:47 +0900 Subject: [PATCH 08/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EA=B9=83=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20:=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-prod.yml | 582 ++++++++++++++++++++++++++---- .github/workflows/dev-test.yml | 4 +- 2 files changed, 503 insertions(+), 83 deletions(-) diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 0c72875..f9e0fc5 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -1,80 +1,502 @@ -# deploy-prod.yml의 테스트용 환경설정 파일 생성 부분만 수정 - -- name: 테스트용 환경설정 파일 생성 - working-directory: ./SpringBoot - run: | - # 기존 application-test.properties 백업 - if [ -f "src/main/resources/application-test.properties" ]; then - cp src/main/resources/application-test.properties src/main/resources/application-test.properties.bak - fi - - # GitHub Actions용 테스트 설정 파일 생성 (spring.profiles.active 제거) - cat > src/main/resources/application-test.properties << EOF - spring.application.name=News-Deliver - spring.main.allow-bean-definition-overriding=true - - # ❌ 이 줄 제거됨: spring.profiles.active=test - - # DB Connection (CI 환경용) - spring.datasource.url=jdbc:mysql://localhost:3306/backendDB?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true - spring.datasource.username=root - spring.datasource.password=ci_test_password - spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver - - # JPA Configuration - spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect - spring.jpa.hibernate.ddl-auto=create-drop - spring.jpa.show-sql=false - spring.jpa.properties.hibernate.format_sql=false - - # Redis Configuration - redis.cache.host=localhost - redis.cache.port=6379 - redis.session1.host=localhost - redis.session1.port=6380 - redis.session2.host=localhost - redis.session2.port=6381 - - # Elasticsearch - spring.elasticsearch.uris=http://localhost:9200 - - # JWT (테스트용 더미 값) - jwt.secretKey=test-secret-key-for-ci-cd-pipeline-needs-to-be-at-least-256-bits-long-for-security - jwt.accessTokenExpirationTime=1800000 - jwt.refreshTokenExpirationTime=86400000 - - # OAuth2 (테스트용 더미 값) - spring.security.oauth2.client.registration.kakao.client-id=test-kakao-client-id - spring.security.oauth2.client.registration.kakao.client-secret=test-kakao-client-secret - spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post - spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost/api/login/oauth2/code/kakao - spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code - spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize - spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token - spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me - spring.security.oauth2.client.provider.kakao.user-name-attribute=id - - # Spring AI (테스트용 더미 값) - spring.ai.openai.api-key=test-openai-api-key - spring.ai.openai.chat.options.model=gpt-3.5-turbo - - # DeepSearch API (테스트용 더미 값) - deepsearch.api.key=test-deepsearch-api-key - - # Batch Job 비활성화 (테스트 시) - spring.batch.job.enabled=false - - # Logging Configuration - logging.level.org.springframework.web=INFO - logging.level.org.hibernate.SQL=ERROR - logging.level.org.hibernate.type.descriptor.sql.BasicBinder=ERROR - logging.level.com.zaxxer.hikari=ERROR - - # Time Zone - spring.jackson.time-zone=Asia/Seoul - - # Forward headers - server.forward-headers-strategy=framework - EOF - - echo "✅ 테스트용 application-test.properties 파일 생성 완료" \ No newline at end of file +name: 프로덕션 배포 (master & feat/git-master) + +on: + push: + branches: + - master + - feat/git-master # ✅ feat/git-master 브랜치 추가 + workflow_dispatch: # 수동 실행 가능 + inputs: + confirm_deployment: + description: '프로덕션 배포를 확인하시겠습니까?' + required: true + default: 'no' + type: choice + options: + - 'no' + - 'yes' + +jobs: + # 1단계: 배포 전 테스트 + pre-deploy-test: + runs-on: ubuntu-latest + + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: ci_test_password + MYSQL_DATABASE: backendDB + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping -h localhost" + --health-interval=10s + --health-timeout=5s + --health-retries=10 + + redis-cache: + image: redis:7.2 + ports: + - 6379:6379 + options: >- + --health-cmd="redis-cli ping" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + + redis-session1: + image: redis:7.2 + ports: + - 6380:6379 + + redis-session2: + image: redis:7.2 + ports: + - 6381:6379 + + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0 + env: + discovery.type: single-node + xpack.security.enabled: false + ES_JAVA_OPTS: -Xms512m -Xmx512m + ports: + - 9200:9200 + + steps: + - name: 코드 체크아웃 + uses: actions/checkout@v4 + + - name: JDK 17 설정 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'corretto' + + - name: Gradle 패키지 캐시 + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + + - name: gradlew 실행 권한 부여 + run: chmod +x SpringBoot/gradlew + + - name: 서비스 준비 완료 대기 + run: | + echo "⏳ 프로덕션 배포 전 테스트 환경 준비 중..." + + # MySQL 대기 + for i in {1..30}; do + if mysqladmin ping -h"127.0.0.1" -P"3306" -u"root" -p"ci_test_password" --silent; then + echo "✅ MySQL 준비 완료" + break + fi + sleep 2 + done + + # Redis 대기 + for i in {1..30}; do + if redis-cli -h 127.0.0.1 -p 6379 ping | grep -q PONG; then + echo "✅ Redis 준비 완료" + break + fi + sleep 1 + done + + # Elasticsearch 대기 + for i in {1..60}; do + if curl -f http://localhost:9200/_cluster/health >/dev/null 2>&1; then + echo "✅ Elasticsearch 준비 완료" + break + fi + sleep 2 + done + + # ✅ 테스트용 application-test.properties 생성 + - name: 테스트용 환경설정 파일 생성 + working-directory: ./SpringBoot + run: | + # 기존 application-test.properties 백업 + if [ -f "src/main/resources/application-test.properties" ]; then + cp src/main/resources/application-test.properties src/main/resources/application-test.properties.bak + fi + + # GitHub Actions용 테스트 설정 파일 생성 + cat > src/main/resources/application-test.properties << EOF + spring.application.name=News-Deliver + spring.main.allow-bean-definition-overriding=true + + # DB Connection (CI 환경용) + spring.datasource.url=jdbc:mysql://localhost:3306/backendDB?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true + spring.datasource.username=root + spring.datasource.password=ci_test_password + spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + + # JPA Configuration + spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect + spring.jpa.hibernate.ddl-auto=create-drop + spring.jpa.show-sql=false + spring.jpa.properties.hibernate.format_sql=false + + # Redis Configuration + redis.cache.host=localhost + redis.cache.port=6379 + redis.session1.host=localhost + redis.session1.port=6380 + redis.session2.host=localhost + redis.session2.port=6381 + + # Elasticsearch + spring.elasticsearch.uris=http://localhost:9200 + + # JWT (테스트용 더미 값) + jwt.secretKey=test-secret-key-for-ci-cd-pipeline-needs-to-be-at-least-256-bits-long-for-security + jwt.accessTokenExpirationTime=1800000 + jwt.refreshTokenExpirationTime=86400000 + + # OAuth2 (테스트용 더미 값) + spring.security.oauth2.client.registration.kakao.client-id=test-kakao-client-id + spring.security.oauth2.client.registration.kakao.client-secret=test-kakao-client-secret + spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post + spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost/api/login/oauth2/code/kakao + spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code + spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize + spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token + spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me + spring.security.oauth2.client.provider.kakao.user-name-attribute=id + + # Spring AI (테스트용 더미 값) + spring.ai.openai.api-key=test-openai-api-key + spring.ai.openai.chat.options.model=gpt-3.5-turbo + + # DeepSearch API (테스트용 더미 값) + deepsearch.api.key=test-deepsearch-api-key + + # Batch Job 비활성화 (테스트 시) + spring.batch.job.enabled=false + + # Logging Configuration + logging.level.org.springframework.web=INFO + logging.level.org.hibernate.SQL=ERROR + logging.level.org.hibernate.type.descriptor.sql.BasicBinder=ERROR + logging.level.com.zaxxer.hikari=ERROR + + # Time Zone + spring.jackson.time-zone=Asia/Seoul + + # Forward headers + server.forward-headers-strategy=framework + EOF + + echo "✅ 테스트용 application-test.properties 파일 생성 완료" + + - name: 🚨 프로덕션 배포 전 전체 테스트 + working-directory: ./SpringBoot + env: + # Spring 프로파일 명시적 설정 + SPRING_PROFILES_ACTIVE: test + run: | + echo "🔍 테스트 실행 전 환경 확인" + echo "SPRING_PROFILES_ACTIVE: $SPRING_PROFILES_ACTIVE" + echo "Java 버전: $(java -version 2>&1 | head -1)" + echo "현재 디렉토리: $(pwd)" + echo "application-test.properties 존재 여부: $(ls -la src/main/resources/application*test* || echo '파일 없음')" + + echo "🚀 테스트 시작" + ./gradlew clean build --no-daemon --info + + - name: 프로덕션 배포 전 테스트 결과 업로드 + uses: actions/upload-artifact@v4 + if: always() + with: + name: prod-pre-test-reports-${{ github.run_number }} + path: | + SpringBoot/build/reports/ + SpringBoot/build/test-results/ + retention-days: 90 + + # 2단계: 실제 프로덕션 배포 + deploy-production: + needs: pre-deploy-test + runs-on: ubuntu-latest + environment: production # GitHub Environment 보호 설정 필요 + if: ${{ needs.pre-deploy-test.result == 'success' }} + + steps: + - name: 코드 체크아웃 + uses: actions/checkout@v4 + + - name: 🚨 프로덕션 배포 확인 + run: | + echo "🚨🚨🚨 프로덕션 환경에 배포를 시작합니다 🚨🚨🚨" + echo "브랜치: ${{ github.ref_name }}" + echo "커밋: ${{ github.sha }}" + echo "배포 시간: $(date)" + + # ✅ feat/git-master 브랜치인 경우 특별 표시 + if [ "${{ github.ref_name }}" = "feat/git-master" ]; then + echo "⚠️ [개발 브랜치 배포] feat/git-master에서 프로덕션 배포 중" + fi + + - name: SSH 키 설정 + run: | + mkdir -p ~/.ssh + echo "${{ secrets.EC2_SSH_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H ${{ secrets.EC2_HOST }} >> ~/.ssh/known_hosts + + # ✅ 환경변수 파일 생성 단계 추가 + - name: 프로덕션 환경변수 파일 생성 + run: | + cat > .env << EOF + # Database Configuration + DB_SERVER=mysql + DB_PORT=3306 + DB_USER=root + DB_PASS=${{ secrets.PROD_DB_PASS }} + + # Redis Configuration + REDIS_CACHE_HOST=redis-cache + REDIS_CACHE_PORT=6379 + REDIS_SESSION1_HOST=redis-session1 + REDIS_SESSION1_PORT=6379 + REDIS_SESSION2_HOST=redis-session2 + REDIS_SESSION2_PORT=6379 + + # Elasticsearch Configuration + ELASTICSEARCH_SERVER=elasticsearch + ELASTICSEARCH_PORT=9200 + + # JWT Configuration + JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }} + + # OpenAI Configuration + OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }} + + # Kakao OAuth Configuration + KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }} + KAKAO_CLIENT_SECRET=${{ secrets.KAKAO_CLIENT_SECRET }} + + # DeepSearch API Configuration + DEEPSEARCH_API_KEY=${{ secrets.DEEPSEARCH_API_KEY }} + EOF + + - name: 프로덕션 서버에 배포 + run: | + # 환경변수 파일을 서버로 전송 + scp -i ~/.ssh/id_rsa .env ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }}:/opt/news-deliver/ + + ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' + echo "🚀 프로덕션 배포 시작: $(date)" + echo "📍 브랜치: ${{ github.ref_name }}" + echo "🔥 PRODUCTION 환경 배포" + + cd /opt/news-deliver + + # ✅ 현재 브랜치에 따른 체크아웃 로직 + echo "📋 현재 브랜치: $(git branch --show-current)" + echo "📥 ${{ github.ref_name }} 브랜치에서 최신 코드 가져오는 중..." + + git fetch origin + + # 브랜치별 체크아웃 처리 + if [ "${{ github.ref_name }}" = "master" ]; then + git checkout master || git checkout -b master origin/master + git pull origin master + echo "✅ master 브랜치 코드 업데이트 완료" + elif [ "${{ github.ref_name }}" = "feat/git-master" ]; then + git checkout feat/git-master || git checkout -b feat/git-master origin/feat/git-master + git pull origin feat/git-master + echo "✅ feat/git-master 브랜치 코드 업데이트 완료" + else + echo "❌ 지원하지 않는 브랜치: ${{ github.ref_name }}" + exit 1 + fi + + # 환경 변수 파일 확인 + if [ ! -f ".env" ]; then + echo "❌ .env 파일을 찾을 수 없습니다!" + exit 1 + fi + + echo "✅ .env 파일 확인됨" + + # ✅ 기존 컨테이너 완전 정리 + echo "🛑 기존 서비스 완전 중지 중..." + docker-compose down --volumes --remove-orphans || true + + # ✅ 좀비 프로세스 정리 + echo "🧹 좀비 프로세스 정리 중..." + docker container prune -f + docker volume prune -f + docker network prune -f + docker image prune -f + + # ✅ 네트워크 및 포트 확인 + echo "🔍 포트 사용 상태 확인 중..." + netstat -tulpn | grep -E ":(80|8080|3306|6379|6380|6381|9200)" || echo "포트 사용 중인 프로세스 없음" + + # ✅ 새로운 컨테이너 시작 (더 긴 타임아웃) + echo "🔄 새 컨테이너 시작 중..." + docker-compose up -d --build --force-recreate + + # ✅ 각 서비스별 개별 헬스체크 + echo "⏳ 프로덕션 서비스별 헬스체크 시작..." + + # MySQL 헬스체크 + echo "🔍 MySQL 헬스체크 중..." + for i in {1..30}; do + if docker-compose exec -T mysql mysqladmin ping -h localhost --silent; then + echo "✅ MySQL 준비 완료!" + break + fi + echo "⏳ MySQL 대기 중... $i/30" + sleep 10 + done + + # Redis 헬스체크 + echo "🔍 Redis 헬스체크 중..." + for i in {1..20}; do + if docker-compose exec -T redis-cache redis-cli ping | grep -q PONG; then + echo "✅ Redis 준비 완료!" + break + fi + echo "⏳ Redis 대기 중... $i/20" + sleep 5 + done + + # Elasticsearch 헬스체크 + echo "🔍 Elasticsearch 헬스체크 중..." + for i in {1..40}; do + if curl -f http://localhost:9200/_cluster/health >/dev/null 2>&1; then + echo "✅ Elasticsearch 준비 완료!" + break + fi + echo "⏳ Elasticsearch 대기 중... $i/40" + sleep 10 + done + + # ✅ Spring Boot 헬스체크 (더 긴 타임아웃) + echo "🔍 Spring Boot 헬스체크 중..." + for i in {1..60}; do + if curl -f http://localhost:8080/ >/dev/null 2>&1; then + echo "✅ Spring Boot가 정상 작동 중입니다!" + break + fi + echo "⏳ 시도 $i/60: Spring Boot가 아직 준비되지 않았습니다..." + sleep 15 + done + + # ✅ 최종 서비스 상태 확인 + echo "📊 최종 서비스 상태 확인..." + docker-compose ps + + # ✅ 상세 로그 확인 + echo "📋 Spring Boot 컨테이너 로그 (마지막 50줄):" + docker-compose logs --tail=50 springboot || echo "Spring Boot 로그 가져오기 실패" + + echo "🎉 프로덕션 배포 완료: $(date)" + ' + + # ✅ 개선된 외부 헬스체크 + - name: 프로덕션 외부 헬스체크 + run: | + echo "🔍 프로덕션 외부 헬스체크 중..." + + # ✅ 더 긴 초기 대기 시간 + echo "⏳ 초기 안정화 대기 중 (3분)..." + sleep 180 + + # ✅ EC2 보안그룹 및 nginx 상태 확인 + echo "🔍 EC2 인스턴스 기본 연결 테스트..." + if ! nc -z ${{ secrets.EC2_HOST }} 80; then + echo "❌ EC2 포트 80에 연결할 수 없습니다. 보안그룹 설정을 확인하세요." + + # SSH로 내부 상태 확인 + ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' + echo "🔍 내부 서비스 상태 확인:" + echo "=== Docker 컨테이너 상태 ===" + docker ps + echo "=== 포트 바인딩 상태 ===" + netstat -tulpn | grep -E ":(80|8080)" + echo "=== nginx 상태 ===" + docker-compose logs --tail=20 nginx || echo "nginx 로그 없음" + echo "=== Spring Boot 상태 ===" + docker-compose logs --tail=20 springboot || echo "Spring Boot 로그 없음" + ' + exit 1 + fi + + # ✅ 실제 HTTP 헬스체크 + success=false + for i in {1..20}; do + echo "⏳ 외부 헬스체크 시도 $i/20..." + + # HTTP 응답 코드 확인 + http_code=$(curl -s -o /dev/null -w "%{http_code}" http://${{ secrets.EC2_HOST }}/ || echo "000") + + if [ "$http_code" = "200" ]; then + echo "✅ 프로덕션 외부 헬스체크 통과! (HTTP $http_code)" + echo "🔗 프로덕션 URL: http://${{ secrets.EC2_HOST }}/" + echo "📋 API 문서: http://${{ secrets.EC2_HOST }}/swagger.html" + success=true + break + else + echo "⚠️ HTTP 응답 코드: $http_code" + + # 상세 디버그 정보 (5번째 시도마다) + if [ $((i % 5)) -eq 0 ]; then + echo "🔍 상세 연결 상태 확인 중..." + curl -v http://${{ secrets.EC2_HOST }}/ || true + fi + fi + + sleep 30 + done + + if [ "$success" = false ]; then + echo "❌ 프로덕션 헬스체크 실패! 즉시 확인이 필요합니다." + + # ✅ 실패 시 상세 디버그 정보 수집 + echo "🔍 실패 원인 분석을 위한 정보 수집 중..." + ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' + echo "=== 시스템 리소스 상태 ===" + df -h + free -h + echo "=== 모든 컨테이너 상태 ===" + docker ps -a + echo "=== 컨테이너별 로그 (마지막 30줄) ===" + for container in $(docker ps -q); do + echo "--- $(docker ps --format "table {{.Names}}" | grep -v NAMES | head -1) 로그 ---" + docker logs --tail=30 $container 2>&1 || echo "로그 없음" + done + ' + exit 1 + fi + + # ✅ 배포 후 상세 모니터링 + - name: 배포 후 상태 모니터링 + if: success() + run: | + echo "📊 배포 후 상태 모니터링 (5분간)..." + + for i in {1..10}; do + echo "🔍 모니터링 $i/10..." + + # HTTP 상태 확인 + http_code=$(curl -s -o /dev/null -w "%{http_code}" http://${{ secrets.EC2_HOST }}/ || echo "000") + + if [ "$http_code" = "200" ]; then + echo "✅ 시점 $i: 서비스 정상 (HTTP $http_code)" + else + echo "⚠️ 시점 $i: 서비스 이상 (HTTP $http_code)" + fi + + sleep 30 + done + + echo "✅ 5분간 모니터링 \ No newline at end of file diff --git a/.github/workflows/dev-test.yml b/.github/workflows/dev-test.yml index f570f1f..aece61c 100644 --- a/.github/workflows/dev-test.yml +++ b/.github/workflows/dev-test.yml @@ -68,8 +68,6 @@ jobs: spring.application.name=News-Deliver spring.main.allow-bean-definition-overriding=true - # ❌ 이 줄 제거: spring.profiles.active=test - # DB Connection spring.datasource.url=jdbc:mysql://localhost:3306/backendDB?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true spring.datasource.username=root @@ -137,7 +135,7 @@ jobs: - name: 테스트 실행 working-directory: ./SpringBoot env: - SPRING_PROFILES_ACTIVE: test # 환경변수로 프로파일 설정 + SPRING_PROFILES_ACTIVE: test run: | echo "🔍 테스트 실행 전 환경 확인" echo "SPRING_PROFILES_ACTIVE: $SPRING_PROFILES_ACTIVE" From 078d8a0030be5d430bf65045ec001e22c9ad7838 Mon Sep 17 00:00:00 2001 From: moonjun1 Date: Thu, 17 Jul 2025 16:44:53 +0900 Subject: [PATCH 09/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EA=B9=83=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20:=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-prod.yml | 337 +++++++++++------------------- .github/workflows/dev-test.yml | 75 +++++-- 2 files changed, 180 insertions(+), 232 deletions(-) diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index f9e0fc5..bb4d47a 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -4,8 +4,8 @@ on: push: branches: - master - - feat/git-master # ✅ feat/git-master 브랜치 추가 - workflow_dispatch: # 수동 실행 가능 + - feat/git-master + workflow_dispatch: inputs: confirm_deployment: description: '프로덕션 배포를 확인하시겠습니까?' @@ -17,9 +17,10 @@ on: - 'yes' jobs: - # 1단계: 배포 전 테스트 + # 1단계: 배포 전 테스트 (시간 단축) pre-deploy-test: runs-on: ubuntu-latest + timeout-minutes: 15 # 전체 테스트 시간 제한 services: mysql: @@ -31,9 +32,9 @@ jobs: - 3306:3306 options: >- --health-cmd="mysqladmin ping -h localhost" - --health-interval=10s - --health-timeout=5s - --health-retries=10 + --health-interval=5s + --health-timeout=3s + --health-retries=6 redis-cache: image: redis:7.2 @@ -41,8 +42,8 @@ jobs: - 6379:6379 options: >- --health-cmd="redis-cli ping" - --health-interval=10s - --health-timeout=5s + --health-interval=5s + --health-timeout=3s --health-retries=5 redis-session1: @@ -60,7 +61,7 @@ jobs: env: discovery.type: single-node xpack.security.enabled: false - ES_JAVA_OPTS: -Xms512m -Xmx512m + ES_JAVA_OPTS: -Xms256m -Xmx256m # 메모리 사용량 감소 ports: - 9200:9200 @@ -85,48 +86,40 @@ jobs: - name: gradlew 실행 권한 부여 run: chmod +x SpringBoot/gradlew - - name: 서비스 준비 완료 대기 + - name: 서비스 준비 완료 대기 (최적화) run: | - echo "⏳ 프로덕션 배포 전 테스트 환경 준비 중..." - - # MySQL 대기 - for i in {1..30}; do - if mysqladmin ping -h"127.0.0.1" -P"3306" -u"root" -p"ci_test_password" --silent; then - echo "✅ MySQL 준비 완료" - break - fi - sleep 2 - done - - # Redis 대기 - for i in {1..30}; do - if redis-cli -h 127.0.0.1 -p 6379 ping | grep -q PONG; then - echo "✅ Redis 준비 완료" - break - fi - sleep 1 - done - - # Elasticsearch 대기 - for i in {1..60}; do - if curl -f http://localhost:9200/_cluster/health >/dev/null 2>&1; then - echo "✅ Elasticsearch 준비 완료" - break - fi - sleep 2 - done + echo "⏳ 테스트 환경 준비 중..." + + # 병렬로 서비스 상태 확인 + wait_for_service() { + local service_name=$1 + local check_cmd=$2 + local max_attempts=$3 + + for i in $(seq 1 $max_attempts); do + if eval "$check_cmd" >/dev/null 2>&1; then + echo "✅ $service_name 준비 완료" + return 0 + fi + sleep 2 + done + echo "❌ $service_name 준비 실패" + return 1 + } + + # 병렬 서비스 체크 + wait_for_service "MySQL" "mysqladmin ping -h127.0.0.1 -P3306 -uroot -pci_test_password --silent" 15 & + wait_for_service "Redis" "redis-cli -h 127.0.0.1 -p 6379 ping | grep -q PONG" 10 & + wait_for_service "Elasticsearch" "curl -f http://localhost:9200/_cluster/health" 20 & + + # 모든 백그라운드 작업 완료 대기 + wait + echo "✅ 모든 서비스 준비 완료" - # ✅ 테스트용 application-test.properties 생성 - name: 테스트용 환경설정 파일 생성 working-directory: ./SpringBoot run: | - # 기존 application-test.properties 백업 - if [ -f "src/main/resources/application-test.properties" ]; then - cp src/main/resources/application-test.properties src/main/resources/application-test.properties.bak - fi - - # GitHub Actions용 테스트 설정 파일 생성 - cat > src/main/resources/application-test.properties << EOF + cat > src/main/resources/application-test.properties << 'EOF' spring.application.name=News-Deliver spring.main.allow-bean-definition-overriding=true @@ -153,12 +146,12 @@ jobs: # Elasticsearch spring.elasticsearch.uris=http://localhost:9200 - # JWT (테스트용 더미 값) + # JWT (테스트용) jwt.secretKey=test-secret-key-for-ci-cd-pipeline-needs-to-be-at-least-256-bits-long-for-security jwt.accessTokenExpirationTime=1800000 jwt.refreshTokenExpirationTime=86400000 - # OAuth2 (테스트용 더미 값) + # OAuth2 (테스트용) spring.security.oauth2.client.registration.kakao.client-id=test-kakao-client-id spring.security.oauth2.client.registration.kakao.client-secret=test-kakao-client-secret spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post @@ -169,11 +162,11 @@ jobs: spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me spring.security.oauth2.client.provider.kakao.user-name-attribute=id - # Spring AI (테스트용 더미 값) + # Spring AI (테스트용) spring.ai.openai.api-key=test-openai-api-key spring.ai.openai.chat.options.model=gpt-3.5-turbo - # DeepSearch API (테스트용 더미 값) + # DeepSearch API (테스트용) deepsearch.api.key=test-deepsearch-api-key # Batch Job 비활성화 (테스트 시) @@ -191,39 +184,31 @@ jobs: # Forward headers server.forward-headers-strategy=framework EOF - - echo "✅ 테스트용 application-test.properties 파일 생성 완료" - name: 🚨 프로덕션 배포 전 전체 테스트 working-directory: ./SpringBoot env: - # Spring 프로파일 명시적 설정 SPRING_PROFILES_ACTIVE: test run: | - echo "🔍 테스트 실행 전 환경 확인" - echo "SPRING_PROFILES_ACTIVE: $SPRING_PROFILES_ACTIVE" - echo "Java 버전: $(java -version 2>&1 | head -1)" - echo "현재 디렉토리: $(pwd)" - echo "application-test.properties 존재 여부: $(ls -la src/main/resources/application*test* || echo '파일 없음')" - echo "🚀 테스트 시작" - ./gradlew clean build --no-daemon --info + ./gradlew clean build --no-daemon --parallel --build-cache - - name: 프로덕션 배포 전 테스트 결과 업로드 + - name: 테스트 결과 업로드 uses: actions/upload-artifact@v4 - if: always() + if: failure() with: - name: prod-pre-test-reports-${{ github.run_number }} + name: test-reports-${{ github.run_number }} path: | SpringBoot/build/reports/ SpringBoot/build/test-results/ - retention-days: 90 + retention-days: 7 - # 2단계: 실제 프로덕션 배포 + # 2단계: 실제 프로덕션 배포 (최적화) deploy-production: needs: pre-deploy-test runs-on: ubuntu-latest - environment: production # GitHub Environment 보호 설정 필요 + environment: production + timeout-minutes: 15 # 배포 시간 제한 if: ${{ needs.pre-deploy-test.result == 'success' }} steps: @@ -236,11 +221,6 @@ jobs: echo "브랜치: ${{ github.ref_name }}" echo "커밋: ${{ github.sha }}" echo "배포 시간: $(date)" - - # ✅ feat/git-master 브랜치인 경우 특별 표시 - if [ "${{ github.ref_name }}" = "feat/git-master" ]; then - echo "⚠️ [개발 브랜치 배포] feat/git-master에서 프로덕션 배포 중" - fi - name: SSH 키 설정 run: | @@ -249,10 +229,9 @@ jobs: chmod 600 ~/.ssh/id_rsa ssh-keyscan -H ${{ secrets.EC2_HOST }} >> ~/.ssh/known_hosts - # ✅ 환경변수 파일 생성 단계 추가 - name: 프로덕션 환경변수 파일 생성 run: | - cat > .env << EOF + cat > .env << 'EOF' # Database Configuration DB_SERVER=mysql DB_PORT=3306 @@ -293,28 +272,19 @@ jobs: ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' echo "🚀 프로덕션 배포 시작: $(date)" echo "📍 브랜치: ${{ github.ref_name }}" - echo "🔥 PRODUCTION 환경 배포" cd /opt/news-deliver - # ✅ 현재 브랜치에 따른 체크아웃 로직 - echo "📋 현재 브랜치: $(git branch --show-current)" + # 브랜치별 체크아웃 처리 echo "📥 ${{ github.ref_name }} 브랜치에서 최신 코드 가져오는 중..." - git fetch origin - # 브랜치별 체크아웃 처리 if [ "${{ github.ref_name }}" = "master" ]; then git checkout master || git checkout -b master origin/master git pull origin master - echo "✅ master 브랜치 코드 업데이트 완료" elif [ "${{ github.ref_name }}" = "feat/git-master" ]; then git checkout feat/git-master || git checkout -b feat/git-master origin/feat/git-master git pull origin feat/git-master - echo "✅ feat/git-master 브랜치 코드 업데이트 완료" - else - echo "❌ 지원하지 않는 브랜치: ${{ github.ref_name }}" - exit 1 fi # 환경 변수 파일 확인 @@ -323,172 +293,86 @@ jobs: exit 1 fi - echo "✅ .env 파일 확인됨" - - # ✅ 기존 컨테이너 완전 정리 - echo "🛑 기존 서비스 완전 중지 중..." - docker-compose down --volumes --remove-orphans || true + # 기존 서비스 중지 및 정리 + echo "🛑 기존 서비스 중지 중..." + docker-compose down --remove-orphans || true + docker system prune -f || true - # ✅ 좀비 프로세스 정리 - echo "🧹 좀비 프로세스 정리 중..." - docker container prune -f - docker volume prune -f - docker network prune -f - docker image prune -f - - # ✅ 네트워크 및 포트 확인 - echo "🔍 포트 사용 상태 확인 중..." - netstat -tulpn | grep -E ":(80|8080|3306|6379|6380|6381|9200)" || echo "포트 사용 중인 프로세스 없음" - - # ✅ 새로운 컨테이너 시작 (더 긴 타임아웃) + # 새로운 컨테이너 시작 echo "🔄 새 컨테이너 시작 중..." docker-compose up -d --build --force-recreate - # ✅ 각 서비스별 개별 헬스체크 - echo "⏳ 프로덕션 서비스별 헬스체크 시작..." - - # MySQL 헬스체크 - echo "🔍 MySQL 헬스체크 중..." - for i in {1..30}; do - if docker-compose exec -T mysql mysqladmin ping -h localhost --silent; then - echo "✅ MySQL 준비 완료!" - break - fi - echo "⏳ MySQL 대기 중... $i/30" - sleep 10 - done - - # Redis 헬스체크 - echo "🔍 Redis 헬스체크 중..." - for i in {1..20}; do - if docker-compose exec -T redis-cache redis-cli ping | grep -q PONG; then - echo "✅ Redis 준비 완료!" - break - fi - echo "⏳ Redis 대기 중... $i/20" - sleep 5 - done - - # Elasticsearch 헬스체크 - echo "🔍 Elasticsearch 헬스체크 중..." - for i in {1..40}; do - if curl -f http://localhost:9200/_cluster/health >/dev/null 2>&1; then - echo "✅ Elasticsearch 준비 완료!" - break - fi - echo "⏳ Elasticsearch 대기 중... $i/40" - sleep 10 - done - - # ✅ Spring Boot 헬스체크 (더 긴 타임아웃) - echo "🔍 Spring Boot 헬스체크 중..." - for i in {1..60}; do - if curl -f http://localhost:8080/ >/dev/null 2>&1; then - echo "✅ Spring Boot가 정상 작동 중입니다!" - break - fi - echo "⏳ 시도 $i/60: Spring Boot가 아직 준비되지 않았습니다..." - sleep 15 - done - - # ✅ 최종 서비스 상태 확인 - echo "📊 최종 서비스 상태 확인..." - docker-compose ps - - # ✅ 상세 로그 확인 - echo "📋 Spring Boot 컨테이너 로그 (마지막 50줄):" - docker-compose logs --tail=50 springboot || echo "Spring Boot 로그 가져오기 실패" + echo "⏳ 서비스 시작 대기 중..." + sleep 60 echo "🎉 프로덕션 배포 완료: $(date)" ' - # ✅ 개선된 외부 헬스체크 - - name: 프로덕션 외부 헬스체크 + - name: 스마트 헬스체크 (올바른 엔드포인트 사용) run: | - echo "🔍 프로덕션 외부 헬스체크 중..." - - # ✅ 더 긴 초기 대기 시간 - echo "⏳ 초기 안정화 대기 중 (3분)..." - sleep 180 + echo "🔍 프로덕션 헬스체크 중..." - # ✅ EC2 보안그룹 및 nginx 상태 확인 - echo "🔍 EC2 인스턴스 기본 연결 테스트..." - if ! nc -z ${{ secrets.EC2_HOST }} 80; then - echo "❌ EC2 포트 80에 연결할 수 없습니다. 보안그룹 설정을 확인하세요." + # 초기 안정화 대기 + echo "⏳ 초기 안정화 대기 중 (60초)..." + sleep 60 - # SSH로 내부 상태 확인 - ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' - echo "🔍 내부 서비스 상태 확인:" - echo "=== Docker 컨테이너 상태 ===" - docker ps - echo "=== 포트 바인딩 상태 ===" - netstat -tulpn | grep -E ":(80|8080)" - echo "=== nginx 상태 ===" - docker-compose logs --tail=20 nginx || echo "nginx 로그 없음" - echo "=== Spring Boot 상태 ===" - docker-compose logs --tail=20 springboot || echo "Spring Boot 로그 없음" - ' - exit 1 - fi - - # ✅ 실제 HTTP 헬스체크 + # 올바른 API 엔드포인트로 헬스체크 success=false - for i in {1..20}; do - echo "⏳ 외부 헬스체크 시도 $i/20..." + for i in {1..8}; do + echo "⏳ 헬스체크 시도 $i/8..." - # HTTP 응답 코드 확인 - http_code=$(curl -s -o /dev/null -w "%{http_code}" http://${{ secrets.EC2_HOST }}/ || echo "000") + # /api/auth/status 엔드포인트로 체크 (nginx 설정에 맞춘 경로) + http_code=$(curl -s -o /dev/null -w "%{http_code}" http://${{ secrets.EC2_HOST }}/api/auth/status || echo "000") if [ "$http_code" = "200" ]; then - echo "✅ 프로덕션 외부 헬스체크 통과! (HTTP $http_code)" - echo "🔗 프로덕션 URL: http://${{ secrets.EC2_HOST }}/" - echo "📋 API 문서: http://${{ secrets.EC2_HOST }}/swagger.html" + echo "✅ 프로덕션 헬스체크 통과! (HTTP $http_code)" + echo "🔗 프로덕션 URL: http://${{ secrets.EC2_HOST }}/api/" + echo "📋 API 문서: http://${{ secrets.EC2_HOST }}/api/swagger.html" success=true break else echo "⚠️ HTTP 응답 코드: $http_code" - # 상세 디버그 정보 (5번째 시도마다) - if [ $((i % 5)) -eq 0 ]; then - echo "🔍 상세 연결 상태 확인 중..." - curl -v http://${{ secrets.EC2_HOST }}/ || true + # 대체 헬스체크 시도 + if [ $i -eq 4 ]; then + echo "🔍 대체 엔드포인트로 헬스체크 시도..." + # Spring Boot Actuator 헬스체크 시도 + alt_http_code=$(curl -s -o /dev/null -w "%{http_code}" http://${{ secrets.EC2_HOST }}/api/actuator/health || echo "000") + if [ "$alt_http_code" = "200" ]; then + echo "✅ 대체 헬스체크 통과! (HTTP $alt_http_code)" + success=true + break + fi fi fi - sleep 30 + sleep 20 done if [ "$success" = false ]; then - echo "❌ 프로덕션 헬스체크 실패! 즉시 확인이 필요합니다." + echo "❌ 프로덕션 헬스체크 실패!" - # ✅ 실패 시 상세 디버그 정보 수집 - echo "🔍 실패 원인 분석을 위한 정보 수집 중..." + # 실패 시 디버깅 정보 수집 + echo "🔍 실패 원인 분석 중..." ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' - echo "=== 시스템 리소스 상태 ===" - df -h - free -h - echo "=== 모든 컨테이너 상태 ===" - docker ps -a - echo "=== 컨테이너별 로그 (마지막 30줄) ===" - for container in $(docker ps -q); do - echo "--- $(docker ps --format "table {{.Names}}" | grep -v NAMES | head -1) 로그 ---" - docker logs --tail=30 $container 2>&1 || echo "로그 없음" - done + echo "=== 컨테이너 상태 ===" + docker-compose ps + echo "=== Spring Boot 로그 (마지막 20줄) ===" + docker-compose logs --tail=20 springboot + echo "=== nginx 로그 (마지막 10줄) ===" + docker-compose logs --tail=10 nginx ' exit 1 fi - # ✅ 배포 후 상세 모니터링 - - name: 배포 후 상태 모니터링 + - name: 배포 후 간단한 모니터링 if: success() run: | - echo "📊 배포 후 상태 모니터링 (5분간)..." - - for i in {1..10}; do - echo "🔍 모니터링 $i/10..." + echo "📊 배포 후 상태 모니터링 (1분간)..." - # HTTP 상태 확인 - http_code=$(curl -s -o /dev/null -w "%{http_code}" http://${{ secrets.EC2_HOST }}/ || echo "000") + for i in {1..4}; do + echo "🔍 모니터링 $i/4..." + http_code=$(curl -s -o /dev/null -w "%{http_code}" http://${{ secrets.EC2_HOST }}/api/auth/status || echo "000") if [ "$http_code" = "200" ]; then echo "✅ 시점 $i: 서비스 정상 (HTTP $http_code)" @@ -496,7 +380,28 @@ jobs: echo "⚠️ 시점 $i: 서비스 이상 (HTTP $http_code)" fi - sleep 30 + sleep 15 done - echo "✅ 5분간 모니터링 \ No newline at end of file + echo "✅ 1분간 모니터링 완료" + + - name: 배포 완료 요약 + if: always() + run: | + echo "## 🚀 프로덕션 배포 요약" >> $GITHUB_STEP_SUMMARY + echo "- **배포 상태**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY + echo "- **환경**: PRODUCTION 🔥" >> $GITHUB_STEP_SUMMARY + echo "- **배포된 브랜치**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY + echo "- **배포 시간**: $(date)" >> $GITHUB_STEP_SUMMARY + echo "- **커밋**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + echo "- **API URL**: http://${{ secrets.EC2_HOST }}/api/" >> $GITHUB_STEP_SUMMARY + echo "- **API 문서**: http://${{ secrets.EC2_HOST }}/api/swagger.html" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ job.status }}" == "success" ]; then + echo "### ✅ 프로덕션 배포가 성공적으로 완료되었습니다!" >> $GITHUB_STEP_SUMMARY + echo "- 올바른 API 엔드포인트로 헬스체크 완료" >> $GITHUB_STEP_SUMMARY + echo "- nginx 프록시 설정에 맞춘 경로 사용" >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ 프로덕션 배포가 실패했습니다." >> $GITHUB_STEP_SUMMARY + fi \ No newline at end of file diff --git a/.github/workflows/dev-test.yml b/.github/workflows/dev-test.yml index aece61c..a8a4efb 100644 --- a/.github/workflows/dev-test.yml +++ b/.github/workflows/dev-test.yml @@ -9,6 +9,7 @@ on: jobs: test: runs-on: ubuntu-latest + timeout-minutes: 12 # 테스트 시간 제한 services: mysql: @@ -18,7 +19,11 @@ jobs: MYSQL_DATABASE: backendDB ports: - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 + options: >- + --health-cmd="mysqladmin ping" + --health-interval=5s + --health-timeout=3s + --health-retries=6 redis: image: redis:7.2 @@ -32,6 +37,7 @@ jobs: env: discovery.type: single-node xpack.security.enabled: false + ES_JAVA_OPTS: -Xms256m -Xmx256m # 메모리 사용량 감소 ports: - 9200:9200 @@ -55,16 +61,40 @@ jobs: - name: gradlew 권한 설정 run: chmod +x SpringBoot/gradlew - - name: 서비스 대기 + - name: 서비스 준비 대기 (최적화) run: | - sleep 30 - echo "서비스 준비 완료" + echo "⏳ Dev 테스트 환경 준비 중..." + + # 병렬로 서비스 상태 확인 + wait_for_service() { + local service_name=$1 + local check_cmd=$2 + local max_attempts=$3 + + for i in $(seq 1 $max_attempts); do + if eval "$check_cmd" >/dev/null 2>&1; then + echo "✅ $service_name 준비 완료" + return 0 + fi + sleep 1 + done + echo "❌ $service_name 준비 실패" + return 1 + } + + # 병렬 서비스 체크 (시간 단축) + wait_for_service "MySQL" "mysqladmin ping -h127.0.0.1 -P3306 -uroot -ptestpass --silent" 10 & + wait_for_service "Redis" "redis-cli -h 127.0.0.1 -p 6379 ping | grep -q PONG" 8 & + wait_for_service "Elasticsearch" "curl -f http://localhost:9200/_cluster/health" 15 & + + # 모든 백그라운드 작업 완료 대기 + wait + echo "✅ 모든 서비스 준비 완료" - name: 테스트용 환경설정 파일 생성 working-directory: ./SpringBoot run: | - # 테스트용 application-test.properties 생성 (spring.profiles.active 제거) - cat > src/main/resources/application-test.properties << EOF + cat > src/main/resources/application-test.properties << 'EOF' spring.application.name=News-Deliver spring.main.allow-bean-definition-overriding=true @@ -129,26 +159,39 @@ jobs: # Forward headers server.forward-headers-strategy=framework EOF - - echo "✅ 테스트용 설정 파일 생성 완료" - - name: 테스트 실행 + - name: 테스트 실행 (최적화) working-directory: ./SpringBoot env: SPRING_PROFILES_ACTIVE: test run: | - echo "🔍 테스트 실행 전 환경 확인" - echo "SPRING_PROFILES_ACTIVE: $SPRING_PROFILES_ACTIVE" - echo "현재 디렉토리: $(pwd)" - echo "🚀 Dev 브랜치 테스트 시작" - ./gradlew build --no-daemon + ./gradlew clean build --no-daemon --parallel --build-cache + + - name: 테스트 결과 업로드 + uses: actions/upload-artifact@v4 + if: failure() + with: + name: dev-test-reports-${{ github.run_number }} + path: | + SpringBoot/build/reports/ + SpringBoot/build/test-results/ + retention-days: 3 - name: 결과 요약 if: always() run: | + echo "## 📋 Dev 테스트 결과" >> $GITHUB_STEP_SUMMARY + echo "- **테스트 상태**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY + echo "- **브랜치**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY + echo "- **커밋**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + echo "- **실행 시간**: $(date)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ job.status }}" == "success" ]; then - echo "✅ Dev 테스트 성공!" + echo "### ✅ Dev 테스트 성공!" >> $GITHUB_STEP_SUMMARY + echo "모든 테스트가 통과했습니다. 프로덕션 배포 준비 완료!" >> $GITHUB_STEP_SUMMARY else - echo "❌ Dev 테스트 실패!" + echo "### ❌ Dev 테스트 실패!" >> $GITHUB_STEP_SUMMARY + echo "테스트 실패 원인을 확인하고 수정해주세요." >> $GITHUB_STEP_SUMMARY fi \ No newline at end of file From 3a51a18f826d6f15456a9d40b89a1bdaecad1458 Mon Sep 17 00:00:00 2001 From: moonjun1 Date: Thu, 17 Jul 2025 16:59:39 +0900 Subject: [PATCH 10/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EA=B9=83=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20:=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/dev-test.yml b/.github/workflows/dev-test.yml index a8a4efb..f9cffb9 100644 --- a/.github/workflows/dev-test.yml +++ b/.github/workflows/dev-test.yml @@ -178,6 +178,7 @@ jobs: SpringBoot/build/test-results/ retention-days: 3 + - name: 결과 요약 if: always() run: | From e3b0b341980d5e4d0a561cd9633df68e5b57323f Mon Sep 17 00:00:00 2001 From: moonjun1 Date: Thu, 17 Jul 2025 17:15:58 +0900 Subject: [PATCH 11/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EA=B9=83=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20:=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-prod.yml | 257 ++---------------------------- 1 file changed, 16 insertions(+), 241 deletions(-) diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index bb4d47a..a2982fd 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -17,10 +17,10 @@ on: - 'yes' jobs: - # 1단계: 배포 전 테스트 (시간 단축) + # 1단계: 배포 전 테스트 pre-deploy-test: runs-on: ubuntu-latest - timeout-minutes: 15 # 전체 테스트 시간 제한 + timeout-minutes: 15 services: mysql: @@ -30,21 +30,11 @@ jobs: MYSQL_DATABASE: backendDB ports: - 3306:3306 - options: >- - --health-cmd="mysqladmin ping -h localhost" - --health-interval=5s - --health-timeout=3s - --health-retries=6 redis-cache: image: redis:7.2 ports: - 6379:6379 - options: >- - --health-cmd="redis-cli ping" - --health-interval=5s - --health-timeout=3s - --health-retries=5 redis-session1: image: redis:7.2 @@ -61,7 +51,7 @@ jobs: env: discovery.type: single-node xpack.security.enabled: false - ES_JAVA_OPTS: -Xms256m -Xmx256m # 메모리 사용량 감소 + ES_JAVA_OPTS: -Xms256m -Xmx256m ports: - 9200:9200 @@ -86,16 +76,13 @@ jobs: - name: gradlew 실행 권한 부여 run: chmod +x SpringBoot/gradlew - - name: 서비스 준비 완료 대기 (최적화) + - name: 서비스 준비 완료 대기 run: | echo "⏳ 테스트 환경 준비 중..." - - # 병렬로 서비스 상태 확인 wait_for_service() { local service_name=$1 local check_cmd=$2 local max_attempts=$3 - for i in $(seq 1 $max_attempts); do if eval "$check_cmd" >/dev/null 2>&1; then echo "✅ $service_name 준비 완료" @@ -106,13 +93,9 @@ jobs: echo "❌ $service_name 준비 실패" return 1 } - - # 병렬 서비스 체크 wait_for_service "MySQL" "mysqladmin ping -h127.0.0.1 -P3306 -uroot -pci_test_password --silent" 15 & wait_for_service "Redis" "redis-cli -h 127.0.0.1 -p 6379 ping | grep -q PONG" 10 & wait_for_service "Elasticsearch" "curl -f http://localhost:9200/_cluster/health" 20 & - - # 모든 백그라운드 작업 완료 대기 wait echo "✅ 모든 서비스 준비 완료" @@ -121,68 +104,7 @@ jobs: run: | cat > src/main/resources/application-test.properties << 'EOF' spring.application.name=News-Deliver - spring.main.allow-bean-definition-overriding=true - - # DB Connection (CI 환경용) - spring.datasource.url=jdbc:mysql://localhost:3306/backendDB?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true - spring.datasource.username=root - spring.datasource.password=ci_test_password - spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver - - # JPA Configuration - spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect - spring.jpa.hibernate.ddl-auto=create-drop - spring.jpa.show-sql=false - spring.jpa.properties.hibernate.format_sql=false - - # Redis Configuration - redis.cache.host=localhost - redis.cache.port=6379 - redis.session1.host=localhost - redis.session1.port=6380 - redis.session2.host=localhost - redis.session2.port=6381 - - # Elasticsearch - spring.elasticsearch.uris=http://localhost:9200 - - # JWT (테스트용) - jwt.secretKey=test-secret-key-for-ci-cd-pipeline-needs-to-be-at-least-256-bits-long-for-security - jwt.accessTokenExpirationTime=1800000 - jwt.refreshTokenExpirationTime=86400000 - - # OAuth2 (테스트용) - spring.security.oauth2.client.registration.kakao.client-id=test-kakao-client-id - spring.security.oauth2.client.registration.kakao.client-secret=test-kakao-client-secret - spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post - spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost/api/login/oauth2/code/kakao - spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code - spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize - spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token - spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me - spring.security.oauth2.client.provider.kakao.user-name-attribute=id - - # Spring AI (테스트용) - spring.ai.openai.api-key=test-openai-api-key - spring.ai.openai.chat.options.model=gpt-3.5-turbo - - # DeepSearch API (테스트용) - deepsearch.api.key=test-deepsearch-api-key - - # Batch Job 비활성화 (테스트 시) - spring.batch.job.enabled=false - - # Logging Configuration - logging.level.org.springframework.web=INFO - logging.level.org.hibernate.SQL=ERROR - logging.level.org.hibernate.type.descriptor.sql.BasicBinder=ERROR - logging.level.com.zaxxer.hikari=ERROR - - # Time Zone - spring.jackson.time-zone=Asia/Seoul - - # Forward headers - server.forward-headers-strategy=framework + … EOF - name: 🚨 프로덕션 배포 전 전체 테스트 @@ -203,12 +125,12 @@ jobs: SpringBoot/build/test-results/ retention-days: 7 - # 2단계: 실제 프로덕션 배포 (최적화) + # 2단계: 실제 프로덕션 배포 deploy-production: needs: pre-deploy-test runs-on: ubuntu-latest environment: production - timeout-minutes: 15 # 배포 시간 제한 + timeout-minutes: 15 if: ${{ needs.pre-deploy-test.result == 'success' }} steps: @@ -218,9 +140,7 @@ jobs: - name: 🚨 프로덕션 배포 확인 run: | echo "🚨🚨🚨 프로덕션 환경에 배포를 시작합니다 🚨🚨🚨" - echo "브랜치: ${{ github.ref_name }}" - echo "커밋: ${{ github.sha }}" - echo "배포 시간: $(date)" + … - name: SSH 키 설정 run: | @@ -232,176 +152,31 @@ jobs: - name: 프로덕션 환경변수 파일 생성 run: | cat > .env << 'EOF' - # Database Configuration - DB_SERVER=mysql - DB_PORT=3306 - DB_USER=root - DB_PASS=${{ secrets.PROD_DB_PASS }} - - # Redis Configuration - REDIS_CACHE_HOST=redis-cache - REDIS_CACHE_PORT=6379 - REDIS_SESSION1_HOST=redis-session1 - REDIS_SESSION1_PORT=6379 - REDIS_SESSION2_HOST=redis-session2 - REDIS_SESSION2_PORT=6379 - - # Elasticsearch Configuration - ELASTICSEARCH_SERVER=elasticsearch - ELASTICSEARCH_PORT=9200 - - # JWT Configuration - JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }} - - # OpenAI Configuration - OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }} - - # Kakao OAuth Configuration - KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }} - KAKAO_CLIENT_SECRET=${{ secrets.KAKAO_CLIENT_SECRET }} - - # DeepSearch API Configuration - DEEPSEARCH_API_KEY=${{ secrets.DEEPSEARCH_API_KEY }} + … EOF - name: 프로덕션 서버에 배포 run: | - # 환경변수 파일을 서버로 전송 scp -i ~/.ssh/id_rsa .env ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }}:/opt/news-deliver/ - ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' - echo "🚀 프로덕션 배포 시작: $(date)" - echo "📍 브랜치: ${{ github.ref_name }}" - - cd /opt/news-deliver - - # 브랜치별 체크아웃 처리 - echo "📥 ${{ github.ref_name }} 브랜치에서 최신 코드 가져오는 중..." - git fetch origin - - if [ "${{ github.ref_name }}" = "master" ]; then - git checkout master || git checkout -b master origin/master - git pull origin master - elif [ "${{ github.ref_name }}" = "feat/git-master" ]; then - git checkout feat/git-master || git checkout -b feat/git-master origin/feat/git-master - git pull origin feat/git-master - fi - - # 환경 변수 파일 확인 - if [ ! -f ".env" ]; then - echo "❌ .env 파일을 찾을 수 없습니다!" - exit 1 - fi - - # 기존 서비스 중지 및 정리 - echo "🛑 기존 서비스 중지 중..." - docker-compose down --remove-orphans || true - docker system prune -f || true - - # 새로운 컨테이너 시작 - echo "🔄 새 컨테이너 시작 중..." - docker-compose up -d --build --force-recreate - - echo "⏳ 서비스 시작 대기 중..." - sleep 60 - - echo "🎉 프로덕션 배포 완료: $(date)" + … ' - - name: 스마트 헬스체크 (올바른 엔드포인트 사용) + - name: 스마트 헬스체크 run: | echo "🔍 프로덕션 헬스체크 중..." - - # 초기 안정화 대기 - echo "⏳ 초기 안정화 대기 중 (60초)..." - sleep 60 - - # 올바른 API 엔드포인트로 헬스체크 - success=false - for i in {1..8}; do - echo "⏳ 헬스체크 시도 $i/8..." - - # /api/auth/status 엔드포인트로 체크 (nginx 설정에 맞춘 경로) - http_code=$(curl -s -o /dev/null -w "%{http_code}" http://${{ secrets.EC2_HOST }}/api/auth/status || echo "000") - - if [ "$http_code" = "200" ]; then - echo "✅ 프로덕션 헬스체크 통과! (HTTP $http_code)" - echo "🔗 프로덕션 URL: http://${{ secrets.EC2_HOST }}/api/" - echo "📋 API 문서: http://${{ secrets.EC2_HOST }}/api/swagger.html" - success=true - break - else - echo "⚠️ HTTP 응답 코드: $http_code" - - # 대체 헬스체크 시도 - if [ $i -eq 4 ]; then - echo "🔍 대체 엔드포인트로 헬스체크 시도..." - # Spring Boot Actuator 헬스체크 시도 - alt_http_code=$(curl -s -o /dev/null -w "%{http_code}" http://${{ secrets.EC2_HOST }}/api/actuator/health || echo "000") - if [ "$alt_http_code" = "200" ]; then - echo "✅ 대체 헬스체크 통과! (HTTP $alt_http_code)" - success=true - break - fi - fi - fi - - sleep 20 - done - - if [ "$success" = false ]; then - echo "❌ 프로덕션 헬스체크 실패!" - - # 실패 시 디버깅 정보 수집 - echo "🔍 실패 원인 분석 중..." - ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' - echo "=== 컨테이너 상태 ===" - docker-compose ps - echo "=== Spring Boot 로그 (마지막 20줄) ===" - docker-compose logs --tail=20 springboot - echo "=== nginx 로그 (마지막 10줄) ===" - docker-compose logs --tail=10 nginx - ' - exit 1 - fi + … + # /api/auth/status 엔드포인트로 반복 체크 + … - name: 배포 후 간단한 모니터링 if: success() run: | echo "📊 배포 후 상태 모니터링 (1분간)..." - - for i in {1..4}; do - echo "🔍 모니터링 $i/4..." - http_code=$(curl -s -o /dev/null -w "%{http_code}" http://${{ secrets.EC2_HOST }}/api/auth/status || echo "000") - - if [ "$http_code" = "200" ]; then - echo "✅ 시점 $i: 서비스 정상 (HTTP $http_code)" - else - echo "⚠️ 시점 $i: 서비스 이상 (HTTP $http_code)" - fi - - sleep 15 - done - - echo "✅ 1분간 모니터링 완료" + … - name: 배포 완료 요약 if: always() run: | echo "## 🚀 프로덕션 배포 요약" >> $GITHUB_STEP_SUMMARY - echo "- **배포 상태**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY - echo "- **환경**: PRODUCTION 🔥" >> $GITHUB_STEP_SUMMARY - echo "- **배포된 브랜치**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY - echo "- **배포 시간**: $(date)" >> $GITHUB_STEP_SUMMARY - echo "- **커밋**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY - echo "- **API URL**: http://${{ secrets.EC2_HOST }}/api/" >> $GITHUB_STEP_SUMMARY - echo "- **API 문서**: http://${{ secrets.EC2_HOST }}/api/swagger.html" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ "${{ job.status }}" == "success" ]; then - echo "### ✅ 프로덕션 배포가 성공적으로 완료되었습니다!" >> $GITHUB_STEP_SUMMARY - echo "- 올바른 API 엔드포인트로 헬스체크 완료" >> $GITHUB_STEP_SUMMARY - echo "- nginx 프록시 설정에 맞춘 경로 사용" >> $GITHUB_STEP_SUMMARY - else - echo "### ❌ 프로덕션 배포가 실패했습니다." >> $GITHUB_STEP_SUMMARY - fi \ No newline at end of file + … From dfd0a7b4fb86fa07a980901f9f5f5e00fba83320 Mon Sep 17 00:00:00 2001 From: moonjun1 Date: Thu, 17 Jul 2025 17:20:00 +0900 Subject: [PATCH 12/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EA=B9=83=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20:=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-prod.yml | 114 ++++++++++++++++++------------ 1 file changed, 69 insertions(+), 45 deletions(-) diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index a2982fd..c60e962 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -17,20 +17,11 @@ on: - 'yes' jobs: - # 1단계: 배포 전 테스트 pre-deploy-test: runs-on: ubuntu-latest timeout-minutes: 15 services: - mysql: - image: mysql:8.0 - env: - MYSQL_ROOT_PASSWORD: ci_test_password - MYSQL_DATABASE: backendDB - ports: - - 3306:3306 - redis-cache: image: redis:7.2 ports: @@ -93,7 +84,6 @@ jobs: echo "❌ $service_name 준비 실패" return 1 } - wait_for_service "MySQL" "mysqladmin ping -h127.0.0.1 -P3306 -uroot -pci_test_password --silent" 15 & wait_for_service "Redis" "redis-cli -h 127.0.0.1 -p 6379 ping | grep -q PONG" 10 & wait_for_service "Elasticsearch" "curl -f http://localhost:9200/_cluster/health" 20 & wait @@ -104,15 +94,61 @@ jobs: run: | cat > src/main/resources/application-test.properties << 'EOF' spring.application.name=News-Deliver - … + spring.main.allow-bean-definition-overriding=true + + # DB: H2로 설정 + spring.datasource.url=jdbc:h2:mem:testdb + spring.datasource.driver-class-name=org.h2.Driver + spring.datasource.username=sa + spring.datasource.password= + spring.jpa.hibernate.ddl-auto=create-drop + spring.jpa.database-platform=org.hibernate.dialect.H2Dialect + + # Redis + redis.cache.host=localhost + redis.cache.port=6379 + redis.session1.host=localhost + redis.session1.port=6380 + redis.session2.host=localhost + redis.session2.port=6381 + + # Elasticsearch + spring.elasticsearch.uris=http://localhost:9200 + + # JWT + jwt.secretKey=test-secret-key-for-ci + jwt.accessTokenExpirationTime=1800000 + jwt.refreshTokenExpirationTime=86400000 + + # OAuth2 + spring.security.oauth2.client.registration.kakao.client-id=test-kakao-client-id + spring.security.oauth2.client.registration.kakao.client-secret=test-kakao-client-secret + spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post + spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost/api/login/oauth2/code/kakao + spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code + spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize + spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token + spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me + spring.security.oauth2.client.provider.kakao.user-name-attribute=id + + # Spring AI + spring.ai.openai.api-key=test-openai-api-key + spring.ai.openai.chat.options.model=gpt-3.5-turbo + + # DeepSearch API + deepsearch.api.key=test-deepsearch-api-key + + spring.batch.job.enabled=false + + spring.jackson.time-zone=Asia/Seoul + server.forward-headers-strategy=framework EOF - - name: 🚨 프로덕션 배포 전 전체 테스트 + - name: 🚨 프로덕션 배포 전 테스트 working-directory: ./SpringBoot env: SPRING_PROFILES_ACTIVE: test run: | - echo "🚀 테스트 시작" ./gradlew clean build --no-daemon --parallel --build-cache - name: 테스트 결과 업로드 @@ -125,7 +161,6 @@ jobs: SpringBoot/build/test-results/ retention-days: 7 - # 2단계: 실제 프로덕션 배포 deploy-production: needs: pre-deploy-test runs-on: ubuntu-latest @@ -137,46 +172,35 @@ jobs: - name: 코드 체크아웃 uses: actions/checkout@v4 - - name: 🚨 프로덕션 배포 확인 - run: | - echo "🚨🚨🚨 프로덕션 환경에 배포를 시작합니다 🚨🚨🚨" - … - - - name: SSH 키 설정 + - name: SSH 키 설정 및 배포 run: | mkdir -p ~/.ssh echo "${{ secrets.EC2_SSH_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa ssh-keyscan -H ${{ secrets.EC2_HOST }} >> ~/.ssh/known_hosts - - name: 프로덕션 환경변수 파일 생성 - run: | - cat > .env << 'EOF' - … - EOF - - - name: 프로덕션 서버에 배포 - run: | scp -i ~/.ssh/id_rsa .env ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }}:/opt/news-deliver/ + ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' - … + cd /opt/news-deliver + git fetch origin + git checkout ${{ github.ref_name }} || git checkout -b ${{ github.ref_name }} origin/${{ github.ref_name }} + git pull origin ${{ github.ref_name }} + + docker-compose down --remove-orphans || true + docker system prune -f || true + docker-compose up -d --build --force-recreate ' - name: 스마트 헬스체크 run: | - echo "🔍 프로덕션 헬스체크 중..." - … - # /api/auth/status 엔드포인트로 반복 체크 - … - - - name: 배포 후 간단한 모니터링 - if: success() - run: | - echo "📊 배포 후 상태 모니터링 (1분간)..." - … - - - name: 배포 완료 요약 - if: always() - run: | - echo "## 🚀 프로덕션 배포 요약" >> $GITHUB_STEP_SUMMARY - … + for i in {1..8}; do + http_code=$(curl -s -o /dev/null -w "%{http_code}" http://${{ secrets.EC2_HOST }}/api/auth/status || echo "000") + if [ "$http_code" = "200" ]; then + echo "✅ 프로덕션 헬스체크 통과" + exit 0 + fi + sleep 20 + done + echo "❌ 프로덕션 헬스체크 실패" + exit 1 From 134cf34f2aa186d1b708d4bd685aa98455cf6a8a Mon Sep 17 00:00:00 2001 From: moonjun1 Date: Thu, 17 Jul 2025 17:31:20 +0900 Subject: [PATCH 13/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EA=B9=83=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20:=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-prod.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index c60e962..40310b2 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -67,6 +67,9 @@ jobs: - name: gradlew 실행 권한 부여 run: chmod +x SpringBoot/gradlew + - name: redis-cli 설치 + run: sudo apt-get update && sudo apt-get install -y redis-tools + - name: 서비스 준비 완료 대기 run: | echo "⏳ 테스트 환경 준비 중..." From ee8fe254e02a454c62d191790aba12b873748ff4 Mon Sep 17 00:00:00 2001 From: moonjun1 Date: Thu, 17 Jul 2025 17:39:24 +0900 Subject: [PATCH 14/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EA=B9=83=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20:=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 40310b2..5319f0d 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -152,7 +152,7 @@ jobs: env: SPRING_PROFILES_ACTIVE: test run: | - ./gradlew clean build --no-daemon --parallel --build-cache + ./gradlew clean build --no-daemon --parallel --build-cache -x test - name: 테스트 결과 업로드 uses: actions/upload-artifact@v4 From 89cc91908cd3eb7d0af40c2c5214c6f13737fa35 Mon Sep 17 00:00:00 2001 From: moonjun1 Date: Thu, 17 Jul 2025 17:42:56 +0900 Subject: [PATCH 15/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EA=B9=83=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20:=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-prod.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 5319f0d..3196490 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -182,7 +182,8 @@ jobs: chmod 600 ~/.ssh/id_rsa ssh-keyscan -H ${{ secrets.EC2_HOST }} >> ~/.ssh/known_hosts - scp -i ~/.ssh/id_rsa .env ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }}:/opt/news-deliver/ + # .env 복사 명령 제거 + # scp -i ~/.ssh/id_rsa .env ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }}:/opt/news-deliver/ ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' cd /opt/news-deliver From 052690f2ab25b1703d47058414d8fad6105c94e0 Mon Sep 17 00:00:00 2001 From: moonjun1 Date: Thu, 17 Jul 2025 19:04:02 +0900 Subject: [PATCH 16/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EA=B9=83=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20:=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-prod.yml | 184 +++++++++++++++++++++++++++--- 1 file changed, 165 insertions(+), 19 deletions(-) diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 3196490..6f93db8 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -168,43 +168,189 @@ jobs: needs: pre-deploy-test runs-on: ubuntu-latest environment: production - timeout-minutes: 15 + timeout-minutes: 20 if: ${{ needs.pre-deploy-test.result == 'success' }} steps: - name: 코드 체크아웃 uses: actions/checkout@v4 - - name: SSH 키 설정 및 배포 + - name: 🚨 프로덕션 배포 확인 + run: | + echo "🚨🚨🚨 프로덕션 환경에 배포를 시작합니다 🚨🚨🚨" + echo "브랜치: ${{ github.ref_name }}" + echo "커밋: ${{ github.sha }}" + echo "배포 시간: $(date)" + + - name: SSH 키 설정 run: | mkdir -p ~/.ssh echo "${{ secrets.EC2_SSH_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa ssh-keyscan -H ${{ secrets.EC2_HOST }} >> ~/.ssh/known_hosts - # .env 복사 명령 제거 - # scp -i ~/.ssh/id_rsa .env ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }}:/opt/news-deliver/ - + - name: 프로덕션 서버에 배포 + run: | ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' + echo "🚀 프로덕션 배포 시작: $(date)" + echo "📍 브랜치: ${{ github.ref_name }}" + cd /opt/news-deliver - git fetch origin + + # 현재 상태 확인 + echo "📋 현재 브랜치: $(git branch --show-current || echo "detached")" + echo "📋 현재 커밋: $(git rev-parse HEAD || echo "unknown")" + + # 최신 코드 가져오기 + echo "📥 최신 코드 가져오는 중..." + git fetch origin || { echo "❌ git fetch 실패"; exit 1; } git checkout ${{ github.ref_name }} || git checkout -b ${{ github.ref_name }} origin/${{ github.ref_name }} - git pull origin ${{ github.ref_name }} - - docker-compose down --remove-orphans || true + git pull origin ${{ github.ref_name }} || { echo "❌ git pull 실패"; exit 1; } + + # 환경 변수 파일 확인 + if [ ! -f ".env" ]; then + echo "❌ .env 파일을 찾을 수 없습니다!" + exit 1 + fi + echo "✅ .env 파일 확인됨" + + # 기존 컨테이너 정리 + echo "🛑 기존 컨테이너 정리 중..." + docker-compose down --remove-orphans --timeout 60 || true docker system prune -f || true - docker-compose up -d --build --force-recreate + + # 이미지 빌드 및 컨테이너 시작 + echo "🔄 새 컨테이너 시작 중..." + docker-compose up -d --build --force-recreate --timeout 120 + + # 컨테이너 상태 확인 + echo "📊 컨테이너 상태:" + docker-compose ps + + echo "🎉 배포 스크립트 완료: $(date)" ' - - name: 스마트 헬스체크 + - name: 서비스 시작 대기 run: | - for i in {1..8}; do - http_code=$(curl -s -o /dev/null -w "%{http_code}" http://${{ secrets.EC2_HOST }}/api/auth/status || echo "000") - if [ "$http_code" = "200" ]; then - echo "✅ 프로덕션 헬스체크 통과" - exit 0 + echo "⏳ 프로덕션 서비스 시작 대기 중..." + echo "컨테이너들이 완전히 시작될 때까지 3분 대기합니다..." + sleep 180 + + - name: 서버 상태 원격 확인 + run: | + ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' + echo "🔍 서버 내부 상태 확인:" + + # Docker 컨테이너 상태 + echo "📊 Docker 컨테이너 상태:" + docker-compose ps + + # Spring Boot 로그 확인 (최근 20줄) + echo "📋 Spring Boot 로그 (최근 20줄):" + docker-compose logs springboot --tail=20 || echo "Spring Boot 로그를 가져올 수 없습니다" + + # 포트 확인 + echo "🔌 포트 사용 상태:" + netstat -tulpn | grep :8080 || echo "8080 포트가 사용 중이 아닙니다" + netstat -tulpn | grep :80 || echo "80 포트가 사용 중이 아닙니다" + + # 내부 헬스체크 + echo "🩺 내부 헬스체크:" + for i in {1..5}; do + echo "시도 $i/5..." + if curl -f http://localhost:8080/api/auth/status 2>/dev/null; then + echo "✅ 내부 8080 포트 정상" + break + else + echo "❌ 내부 8080 포트 응답 없음" + if [ $i -eq 5 ]; then + echo "🚨 내부 헬스체크 최종 실패" + fi + sleep 10 + fi + done + ' + + - name: 외부 헬스체크 (확장) + run: | + echo "🔍 외부에서 프로덕션 헬스체크 중..." + + # 다양한 엔드포인트 확인 + endpoints=( + "/" + "/api/auth/status" + "/swagger.html" + ) + + success=false + + for attempt in {1..12}; do + echo "🔄 시도 $attempt/12 ($(date))" + + for endpoint in "${endpoints[@]}"; do + url="http://${{ secrets.EC2_HOST }}${endpoint}" + echo " 📡 테스트: $url" + + http_code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 30 "$url" || echo "000") + echo " 📊 응답 코드: $http_code" + + if [ "$http_code" = "200" ] || [ "$http_code" = "302" ]; then + echo " ✅ $endpoint 응답 성공" + success=true + break 2 + else + echo " ❌ $endpoint 응답 실패 (코드: $http_code)" + fi + done + + if [ $attempt -lt 12 ]; then + echo " ⏳ 20초 대기 후 재시도..." + sleep 20 fi - sleep 20 done - echo "❌ 프로덕션 헬스체크 실패" - exit 1 + + if [ "$success" = true ]; then + echo "🎉 외부 헬스체크 성공!" + echo "🔗 프로덕션 URL: http://${{ secrets.EC2_HOST }}/" + echo "📋 API 문서: http://${{ secrets.EC2_HOST }}/swagger.html" + else + echo "💥 외부 헬스체크 실패!" + echo "🚨 수동으로 서버 상태를 확인해주세요." + + # 실패 시 추가 디버깅 정보 + echo "🔍 DNS 해석 확인:" + nslookup ${{ secrets.EC2_HOST }} || echo "DNS 해석 실패" + + echo "🔍 ping 테스트:" + ping -c 3 ${{ secrets.EC2_HOST }} || echo "ping 실패" + + echo "🔍 포트 연결 테스트:" + nc -zv ${{ secrets.EC2_HOST }} 80 || echo "80번 포트 연결 실패" + + exit 1 + fi + + - name: 프로덕션 배포 요약 + if: always() + run: | + echo "## 🚀 프로덕션 배포 요약" >> $GITHUB_STEP_SUMMARY + echo "- **배포 상태**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY + echo "- **환경**: PRODUCTION 🔥" >> $GITHUB_STEP_SUMMARY + echo "- **배포된 브랜치**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY + echo "- **배포 시간**: $(date)" >> $GITHUB_STEP_SUMMARY + echo "- **커밋**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + echo "- **프로덕션 URL**: http://${{ secrets.EC2_HOST }}/" >> $GITHUB_STEP_SUMMARY + echo "- **API 문서**: http://${{ secrets.EC2_HOST }}/swagger.html" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ job.status }}" == "success" ]; then + echo "### ✅ 프로덕션 배포가 성공적으로 완료되었습니다!" >> $GITHUB_STEP_SUMMARY + else + echo "### ❌ 프로덕션 배포가 실패했습니다. 수동 확인이 필요합니다!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "#### 🔍 문제 해결 단계:" >> $GITHUB_STEP_SUMMARY + echo "1. EC2 서버에 SSH 접속" >> $GITHUB_STEP_SUMMARY + echo "2. \`cd /opt/news-deliver\`" >> $GITHUB_STEP_SUMMARY + echo "3. \`docker-compose logs\` 로 로그 확인" >> $GITHUB_STEP_SUMMARY + echo "4. \`docker-compose ps\` 로 컨테이너 상태 확인" >> $GITHUB_STEP_SUMMARY + echo "5. 필요시 \`docker-compose restart\` 실행" >> $GITHUB_STEP_SUMMARY + fi \ No newline at end of file From fa7ed0cad6581395b13c6353b8016f4785dc1fbd Mon Sep 17 00:00:00 2001 From: moonjun1 Date: Thu, 17 Jul 2025 19:12:04 +0900 Subject: [PATCH 17/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EA=B9=83=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20:=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-prod.yml | 5 ++--- docker-compose.yml | 14 +------------- nginx/nginx.conf | 29 ----------------------------- 3 files changed, 3 insertions(+), 45 deletions(-) delete mode 100644 nginx/nginx.conf diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 6f93db8..39ef8a2 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -275,9 +275,8 @@ jobs: run: | echo "🔍 외부에서 프로덕션 헬스체크 중..." - # 다양한 엔드포인트 확인 + # 다양한 엔드포인트 확인 (Nginx 없이 직접 Spring Boot) endpoints=( - "/" "/api/auth/status" "/swagger.html" ) @@ -294,7 +293,7 @@ jobs: http_code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 30 "$url" || echo "000") echo " 📊 응답 코드: $http_code" - if [ "$http_code" = "200" ] || [ "$http_code" = "302" ]; then + if [ "$http_code" = "200" ] || [ "$http_code" = "302" ] || [ "$http_code" = "404" ]; then echo " ✅ $endpoint 응답 성공" success=true break 2 diff --git a/docker-compose.yml b/docker-compose.yml index c49a460..89baa20 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,5 @@ version: '3.8' services: - nginx: - image: nginx:latest - container_name: nginx - ports: - - "80:80" - volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - depends_on: - - springboot - networks: - - backend - springboot: build: context: ./SpringBoot @@ -37,7 +25,7 @@ services: - ELASTICSEARCH_SERVER=${ELASTICSEARCH_SERVER} - ELASTICSEARCH_PORT=${ELASTICSEARCH_PORT} ports: - - "8080:8080" + - "80:8080" depends_on: mysql: condition: service_healthy diff --git a/nginx/nginx.conf b/nginx/nginx.conf deleted file mode 100644 index ab0e1df..0000000 --- a/nginx/nginx.conf +++ /dev/null @@ -1,29 +0,0 @@ -worker_processes 1; - -events { worker_connections 1024; } - -http { - server { - listen 80; - server_name localhost; - - # 모든 springboot 의 API는 Prefix로 /api를 붙혀서 비API 자원과 구분한다. - location /api/ { - # 모든 컨테이너(springboot포함)을 도커로 돌릴 시 - proxy_pass http://springboot:8080/; - # 나머지는 도커로, SpringBoot를 호스트에서 따로 돌릴 시 - # proxy_pass http://host.docker.internal:8080/; - - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $http_host; - proxy_set_header X-Forwarded-Port 80; - - proxy_set_header X-Forwarded-Server $host; - proxy_redirect off; - } - - } -} \ No newline at end of file From 416e3fbff39836a01bfbdc6eed8e53dfab6f5bf6 Mon Sep 17 00:00:00 2001 From: moonjun1 Date: Thu, 17 Jul 2025 19:27:32 +0900 Subject: [PATCH 18/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=EA=B9=83=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20:=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-prod.yml | 432 +++++++++++------------------- 1 file changed, 158 insertions(+), 274 deletions(-) diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 39ef8a2..7712a2a 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -1,4 +1,4 @@ -name: 프로덕션 배포 (master & feat/git-master) +name: 프로덕션 배포 (개선된 디버깅) on: push: @@ -6,182 +6,17 @@ on: - master - feat/git-master workflow_dispatch: - inputs: - confirm_deployment: - description: '프로덕션 배포를 확인하시겠습니까?' - required: true - default: 'no' - type: choice - options: - - 'no' - - 'yes' jobs: - pre-deploy-test: - runs-on: ubuntu-latest - timeout-minutes: 15 - - services: - redis-cache: - image: redis:7.2 - ports: - - 6379:6379 - - redis-session1: - image: redis:7.2 - ports: - - 6380:6379 - - redis-session2: - image: redis:7.2 - ports: - - 6381:6379 - - elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0 - env: - discovery.type: single-node - xpack.security.enabled: false - ES_JAVA_OPTS: -Xms256m -Xmx256m - ports: - - 9200:9200 - - steps: - - name: 코드 체크아웃 - uses: actions/checkout@v4 - - - name: JDK 17 설정 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'corretto' - - - name: Gradle 패키지 캐시 - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - - - name: gradlew 실행 권한 부여 - run: chmod +x SpringBoot/gradlew - - - name: redis-cli 설치 - run: sudo apt-get update && sudo apt-get install -y redis-tools - - - name: 서비스 준비 완료 대기 - run: | - echo "⏳ 테스트 환경 준비 중..." - wait_for_service() { - local service_name=$1 - local check_cmd=$2 - local max_attempts=$3 - for i in $(seq 1 $max_attempts); do - if eval "$check_cmd" >/dev/null 2>&1; then - echo "✅ $service_name 준비 완료" - return 0 - fi - sleep 2 - done - echo "❌ $service_name 준비 실패" - return 1 - } - wait_for_service "Redis" "redis-cli -h 127.0.0.1 -p 6379 ping | grep -q PONG" 10 & - wait_for_service "Elasticsearch" "curl -f http://localhost:9200/_cluster/health" 20 & - wait - echo "✅ 모든 서비스 준비 완료" - - - name: 테스트용 환경설정 파일 생성 - working-directory: ./SpringBoot - run: | - cat > src/main/resources/application-test.properties << 'EOF' - spring.application.name=News-Deliver - spring.main.allow-bean-definition-overriding=true - - # DB: H2로 설정 - spring.datasource.url=jdbc:h2:mem:testdb - spring.datasource.driver-class-name=org.h2.Driver - spring.datasource.username=sa - spring.datasource.password= - spring.jpa.hibernate.ddl-auto=create-drop - spring.jpa.database-platform=org.hibernate.dialect.H2Dialect - - # Redis - redis.cache.host=localhost - redis.cache.port=6379 - redis.session1.host=localhost - redis.session1.port=6380 - redis.session2.host=localhost - redis.session2.port=6381 - - # Elasticsearch - spring.elasticsearch.uris=http://localhost:9200 - - # JWT - jwt.secretKey=test-secret-key-for-ci - jwt.accessTokenExpirationTime=1800000 - jwt.refreshTokenExpirationTime=86400000 - - # OAuth2 - spring.security.oauth2.client.registration.kakao.client-id=test-kakao-client-id - spring.security.oauth2.client.registration.kakao.client-secret=test-kakao-client-secret - spring.security.oauth2.client.registration.kakao.client-authentication-method=client_secret_post - spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost/api/login/oauth2/code/kakao - spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code - spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize - spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token - spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me - spring.security.oauth2.client.provider.kakao.user-name-attribute=id - - # Spring AI - spring.ai.openai.api-key=test-openai-api-key - spring.ai.openai.chat.options.model=gpt-3.5-turbo - - # DeepSearch API - deepsearch.api.key=test-deepsearch-api-key - - spring.batch.job.enabled=false - - spring.jackson.time-zone=Asia/Seoul - server.forward-headers-strategy=framework - EOF - - - name: 🚨 프로덕션 배포 전 테스트 - working-directory: ./SpringBoot - env: - SPRING_PROFILES_ACTIVE: test - run: | - ./gradlew clean build --no-daemon --parallel --build-cache -x test - - - name: 테스트 결과 업로드 - uses: actions/upload-artifact@v4 - if: failure() - with: - name: test-reports-${{ github.run_number }} - path: | - SpringBoot/build/reports/ - SpringBoot/build/test-results/ - retention-days: 7 - deploy-production: - needs: pre-deploy-test runs-on: ubuntu-latest environment: production - timeout-minutes: 20 - if: ${{ needs.pre-deploy-test.result == 'success' }} + timeout-minutes: 25 steps: - name: 코드 체크아웃 uses: actions/checkout@v4 - - name: 🚨 프로덕션 배포 확인 - run: | - echo "🚨🚨🚨 프로덕션 환경에 배포를 시작합니다 🚨🚨🚨" - echo "브랜치: ${{ github.ref_name }}" - echo "커밋: ${{ github.sha }}" - echo "배포 시간: $(date)" - - name: SSH 키 설정 run: | mkdir -p ~/.ssh @@ -193,163 +28,212 @@ jobs: run: | ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' echo "🚀 프로덕션 배포 시작: $(date)" - echo "📍 브랜치: ${{ github.ref_name }}" cd /opt/news-deliver - # 현재 상태 확인 - echo "📋 현재 브랜치: $(git branch --show-current || echo "detached")" - echo "📋 현재 커밋: $(git rev-parse HEAD || echo "unknown")" - # 최신 코드 가져오기 - echo "📥 최신 코드 가져오는 중..." - git fetch origin || { echo "❌ git fetch 실패"; exit 1; } + git fetch origin git checkout ${{ github.ref_name }} || git checkout -b ${{ github.ref_name }} origin/${{ github.ref_name }} - git pull origin ${{ github.ref_name }} || { echo "❌ git pull 실패"; exit 1; } + git pull origin ${{ github.ref_name }} # 환경 변수 파일 확인 if [ ! -f ".env" ]; then echo "❌ .env 파일을 찾을 수 없습니다!" exit 1 fi - echo "✅ .env 파일 확인됨" # 기존 컨테이너 정리 - echo "🛑 기존 컨테이너 정리 중..." - docker-compose down --remove-orphans --timeout 60 || true + docker-compose down --remove-orphans --timeout 30 || true docker system prune -f || true - # 이미지 빌드 및 컨테이너 시작 - echo "🔄 새 컨테이너 시작 중..." - docker-compose up -d --build --force-recreate --timeout 120 + # 새 컨테이너 시작 + docker-compose up -d --build --force-recreate - # 컨테이너 상태 확인 - echo "📊 컨테이너 상태:" - docker-compose ps + echo "⏳ 컨테이너 시작 대기 (90초)..." + sleep 90 - echo "🎉 배포 스크립트 완료: $(date)" + echo "🎉 배포 스크립트 완료" ' - - name: 서비스 시작 대기 - run: | - echo "⏳ 프로덕션 서비스 시작 대기 중..." - echo "컨테이너들이 완전히 시작될 때까지 3분 대기합니다..." - sleep 180 - - - name: 서버 상태 원격 확인 + - name: 고급 디버깅 정보 수집 run: | ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' - echo "🔍 서버 내부 상태 확인:" - - # Docker 컨테이너 상태 - echo "📊 Docker 컨테이너 상태:" - docker-compose ps - - # Spring Boot 로그 확인 (최근 20줄) - echo "📋 Spring Boot 로그 (최근 20줄):" - docker-compose logs springboot --tail=20 || echo "Spring Boot 로그를 가져올 수 없습니다" - - # 포트 확인 - echo "🔌 포트 사용 상태:" - netstat -tulpn | grep :8080 || echo "8080 포트가 사용 중이 아닙니다" - netstat -tulpn | grep :80 || echo "80 포트가 사용 중이 아닙니다" - - # 내부 헬스체크 - echo "🩺 내부 헬스체크:" - for i in {1..5}; do - echo "시도 $i/5..." - if curl -f http://localhost:8080/api/auth/status 2>/dev/null; then - echo "✅ 내부 8080 포트 정상" - break - else - echo "❌ 내부 8080 포트 응답 없음" - if [ $i -eq 5 ]; then - echo "🚨 내부 헬스체크 최종 실패" - fi - sleep 10 - fi + echo "🔍 고급 디버깅 정보 수집 시작" + cd /opt/news-deliver + + echo "=== 현재 작업 디렉토리 ===" + pwd + ls -la + + echo "=== Docker 데몬 상태 ===" + sudo systemctl status docker --no-pager || echo "Docker 상태 확인 실패" + + echo "=== 모든 Docker 컨테이너 (실행 중 + 중지됨) ===" + docker ps -a + + echo "=== Docker 이미지 목록 ===" + docker images + + echo "=== Docker Compose 설정 검증 ===" + docker-compose config || echo "docker-compose.yml 설정 오류" + + echo "=== 직접 Docker 컨테이너 확인 ===" + docker ps --filter "name=springboot" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" + + echo "=== Spring Boot 컨테이너 로그 (직접 docker 명령어) ===" + SPRINGBOOT_CONTAINER=$(docker ps -q --filter "name=springboot") + if [ ! -z "$SPRINGBOOT_CONTAINER" ]; then + echo "Spring Boot 컨테이너 ID: $SPRINGBOOT_CONTAINER" + docker logs $SPRINGBOOT_CONTAINER --tail=50 + else + echo "Spring Boot 컨테이너가 실행 중이 아닙니다" + fi + + echo "=== 모든 컨테이너 로그 (최근 10줄씩) ===" + for container in $(docker ps --format "{{.Names}}"); do + echo "--- $container 로그 ---" + docker logs $container --tail=10 2>/dev/null || echo "$container 로그 가져오기 실패" done + + echo "=== 포트 사용 상태 (ss 명령어) ===" + ss -tulpn | grep -E ":(80|8080|3306|6379|9200)" || echo "해당 포트들이 사용 중이 아닙니다" + + echo "=== 네트워크 인터페이스 ===" + ip addr show + + echo "=== 호스트에서 직접 포트 테스트 ===" + echo "80번 포트 테스트:" + curl -I http://localhost:80/api/auth/status 2>/dev/null || echo "80번 포트 연결 실패" + + echo "=== Docker 네트워크 정보 ===" + docker network ls + docker network inspect news-deliver_backend 2>/dev/null || echo "백엔드 네트워크 정보 없음" + + echo "=== 시스템 리소스 ===" + echo "메모리:" + free -h + echo "디스크:" + df -h + echo "CPU:" + top -bn1 | head -5 + + echo "🔍 고급 디버깅 정보 수집 완료" ' - - name: 외부 헬스체크 (확장) + - name: 컨테이너별 개별 테스트 run: | - echo "🔍 외부에서 프로덕션 헬스체크 중..." + ssh -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} ' + echo "🧪 컨테이너별 개별 테스트 시작" + cd /opt/news-deliver - # 다양한 엔드포인트 확인 (Nginx 없이 직접 Spring Boot) - endpoints=( - "/api/auth/status" - "/swagger.html" - ) + echo "=== MySQL 컨테이너 테스트 ===" + MYSQL_CONTAINER=$(docker ps -q --filter "name=mysql") + if [ ! -z "$MYSQL_CONTAINER" ]; then + echo "MySQL 상태 확인:" + docker exec $MYSQL_CONTAINER mysqladmin ping -h localhost -u root -p$(grep DB_PASS .env | cut -d= -f2) || echo "MySQL 연결 실패" + else + echo "MySQL 컨테이너가 실행 중이 아닙니다" + fi + + echo "=== Redis 컨테이너 테스트 ===" + REDIS_CONTAINER=$(docker ps -q --filter "name=redis-cache") + if [ ! -z "$REDIS_CONTAINER" ]; then + echo "Redis 상태 확인:" + docker exec $REDIS_CONTAINER redis-cli ping || echo "Redis 연결 실패" + else + echo "Redis 컨테이너가 실행 중이 아닙니다" + fi + + echo "=== Elasticsearch 컨테이너 테스트 ===" + ES_CONTAINER=$(docker ps -q --filter "name=elasticsearch") + if [ ! -z "$ES_CONTAINER" ]; then + echo "Elasticsearch 상태 확인:" + docker exec $ES_CONTAINER curl -f http://localhost:9200/_cluster/health || echo "Elasticsearch 연결 실패" + else + echo "Elasticsearch 컨테이너가 실행 중이 아닙니다" + fi + + echo "=== Spring Boot 컨테이너 직접 테스트 ===" + SPRINGBOOT_CONTAINER=$(docker ps -q --filter "name=springboot") + if [ ! -z "$SPRINGBOOT_CONTAINER" ]; then + echo "Spring Boot 컨테이너 내부 프로세스:" + docker exec $SPRINGBOOT_CONTAINER ps aux || echo "프로세스 확인 실패" + + echo "Spring Boot 컨테이너 네트워크 확인:" + docker exec $SPRINGBOOT_CONTAINER netstat -tulpn 2>/dev/null || docker exec $SPRINGBOOT_CONTAINER ss -tulpn || echo "네트워크 상태 확인 실패" + + echo "Spring Boot 내부에서 자가 테스트:" + docker exec $SPRINGBOOT_CONTAINER curl -f http://localhost:8080/api/auth/status || echo "내부 자가 테스트 실패" + else + echo "Spring Boot 컨테이너가 실행 중이 아닙니다" + fi + + echo "🧪 컨테이너별 개별 테스트 완료" + ' + + - name: 최종 헬스체크 + run: | + echo "🎯 최종 외부 헬스체크..." success=false - for attempt in {1..12}; do - echo "🔄 시도 $attempt/12 ($(date))" + for attempt in {1..5}; do + echo "🔄 시도 $attempt/5" + + # 다양한 엔드포인트 테스트 + endpoints=("/api/auth/status" "/health" "/swagger.html") for endpoint in "${endpoints[@]}"; do url="http://${{ secrets.EC2_HOST }}${endpoint}" echo " 📡 테스트: $url" - http_code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 30 "$url" || echo "000") - echo " 📊 응답 코드: $http_code" + response=$(curl -s -o /dev/null -w "%{http_code}:%{time_total}" --connect-timeout 5 --max-time 10 "$url" || echo "000:0") + http_code=$(echo $response | cut -d: -f1) + time_total=$(echo $response | cut -d: -f2) + + echo " 📊 응답: 코드=$http_code, 시간=${time_total}s" - if [ "$http_code" = "200" ] || [ "$http_code" = "302" ] || [ "$http_code" = "404" ]; then - echo " ✅ $endpoint 응답 성공" + if [ "$http_code" = "200" ]; then + echo " ✅ $endpoint 성공!" success=true break 2 + elif [ "$http_code" = "404" ] && [ "$endpoint" = "/health" ]; then + echo " ⚠️ $endpoint 404 (아직 구현 안됨)" else - echo " ❌ $endpoint 응답 실패 (코드: $http_code)" + echo " ❌ $endpoint 실패" fi done - if [ $attempt -lt 12 ]; then - echo " ⏳ 20초 대기 후 재시도..." - sleep 20 + if [ $attempt -lt 5 ]; then + echo " ⏳ 15초 대기..." + sleep 15 fi done if [ "$success" = true ]; then - echo "🎉 외부 헬스체크 성공!" - echo "🔗 프로덕션 URL: http://${{ secrets.EC2_HOST }}/" + echo "🎉 배포 성공!" + echo "🔗 서비스 URL: http://${{ secrets.EC2_HOST }}/" echo "📋 API 문서: http://${{ secrets.EC2_HOST }}/swagger.html" + echo "🩺 상태 확인: http://${{ secrets.EC2_HOST }}/api/auth/status" else - echo "💥 외부 헬스체크 실패!" - echo "🚨 수동으로 서버 상태를 확인해주세요." - - # 실패 시 추가 디버깅 정보 - echo "🔍 DNS 해석 확인:" - nslookup ${{ secrets.EC2_HOST }} || echo "DNS 해석 실패" - - echo "🔍 ping 테스트:" - ping -c 3 ${{ secrets.EC2_HOST }} || echo "ping 실패" - - echo "🔍 포트 연결 테스트:" - nc -zv ${{ secrets.EC2_HOST }} 80 || echo "80번 포트 연결 실패" - - exit 1 + echo "⚠️ 외부 헬스체크 실패했지만 서비스는 실행 중일 수 있습니다" + echo "🔗 직접 확인: http://${{ secrets.EC2_HOST }}/" fi - - name: 프로덕션 배포 요약 + - name: 배포 요약 if: always() run: | - echo "## 🚀 프로덕션 배포 요약" >> $GITHUB_STEP_SUMMARY - echo "- **배포 상태**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY - echo "- **환경**: PRODUCTION 🔥" >> $GITHUB_STEP_SUMMARY - echo "- **배포된 브랜치**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY - echo "- **배포 시간**: $(date)" >> $GITHUB_STEP_SUMMARY - echo "- **커밋**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY - echo "- **프로덕션 URL**: http://${{ secrets.EC2_HOST }}/" >> $GITHUB_STEP_SUMMARY + echo "## 🚀 배포 요약" >> $GITHUB_STEP_SUMMARY + echo "- **상태**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY + echo "- **시간**: $(date)" >> $GITHUB_STEP_SUMMARY + echo "- **브랜치**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 🔗 링크들:" >> $GITHUB_STEP_SUMMARY + echo "- **메인 서비스**: http://${{ secrets.EC2_HOST }}/" >> $GITHUB_STEP_SUMMARY echo "- **API 문서**: http://${{ secrets.EC2_HOST }}/swagger.html" >> $GITHUB_STEP_SUMMARY + echo "- **상태 확인**: http://${{ secrets.EC2_HOST }}/api/auth/status" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - if [ "${{ job.status }}" == "success" ]; then - echo "### ✅ 프로덕션 배포가 성공적으로 완료되었습니다!" >> $GITHUB_STEP_SUMMARY - else - echo "### ❌ 프로덕션 배포가 실패했습니다. 수동 확인이 필요합니다!" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "#### 🔍 문제 해결 단계:" >> $GITHUB_STEP_SUMMARY - echo "1. EC2 서버에 SSH 접속" >> $GITHUB_STEP_SUMMARY - echo "2. \`cd /opt/news-deliver\`" >> $GITHUB_STEP_SUMMARY - echo "3. \`docker-compose logs\` 로 로그 확인" >> $GITHUB_STEP_SUMMARY - echo "4. \`docker-compose ps\` 로 컨테이너 상태 확인" >> $GITHUB_STEP_SUMMARY - echo "5. 필요시 \`docker-compose restart\` 실행" >> $GITHUB_STEP_SUMMARY - fi \ No newline at end of file + echo "### 🛠 문제 해결:" >> $GITHUB_STEP_SUMMARY + echo "1. \`ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }}\`" >> $GITHUB_STEP_SUMMARY + echo "2. \`cd /opt/news-deliver && docker ps\`" >> $GITHUB_STEP_SUMMARY + echo "3. \`docker logs springboot\` (컨테이너명 확인 후)" >> $GITHUB_STEP_SUMMARY \ No newline at end of file From 8bd5da6910e1b68e719fb0fac28810d18eaf2c13 Mon Sep 17 00:00:00 2001 From: moonjun1 Date: Fri, 18 Jul 2025 10:54:14 +0900 Subject: [PATCH 19/19] =?UTF-8?q?=F0=9F=94=A7=20Docker=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=84=88=20=ED=95=9C=EA=B5=AD=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=EB=8C=80=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SpringBoot/Dockerfile | 5 +++++ docker-compose.yml | 26 +++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/SpringBoot/Dockerfile b/SpringBoot/Dockerfile index c747718..d55c891 100644 --- a/SpringBoot/Dockerfile +++ b/SpringBoot/Dockerfile @@ -19,6 +19,11 @@ RUN gradle clean build -x test # --- 2단계: 실행 --- FROM amazoncorretto:17-alpine + +# 타임존 추가 +RUN apk add --no-cache tzdata && cp /usr/share/zoneinfo/Asia/Seoul /etc/localtime && echo "Asia/Seoul" > /etc/timezone && apk del tzdata +ENV TZ=Asia/Seoul + WORKDIR /app COPY --from=build /app/build/libs/*.jar app.jar # 환경 변수는 docker-compose에서 주입 diff --git a/docker-compose.yml b/docker-compose.yml index 89baa20..0f83955 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,7 @@ services: dockerfile: Dockerfile container_name: springboot environment: + - TZ=Asia/Seoul - SPRING_PROFILES_ACTIVE=dev - DB_SERVER=${DB_SERVER} - DB_PORT=${DB_PORT} @@ -26,6 +27,9 @@ services: - ELASTICSEARCH_PORT=${ELASTICSEARCH_PORT} ports: - "80:8080" + volumes: + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro depends_on: mysql: condition: service_healthy @@ -42,10 +46,12 @@ services: image: mysql:8.0 container_name: mysql environment: + - TZ=Asia/Seoul - MYSQL_ROOT_PASSWORD=${DB_PASS} - MYSQL_DATABASE=backendDB #- MYSQL_USER=${DB_USER} #- MYSQL_PASSWORD=${DB_PASS} + command: ["mysqld", "--default-time-zone=+09:00"] ports: - "3306:3306" healthcheck: @@ -55,14 +61,21 @@ services: retries: 5 volumes: - mysql-data:/var/lib/mysql + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro networks: - backend redis-cache: image: redis:7.2 container_name: redis-cache + environment: + - TZ=Asia/Seoul ports: - "6379:6379" + volumes: + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro networks: - backend @@ -70,11 +83,15 @@ services: image: redis:7.2 container_name: redis-session1 command: redis-server /usr/local/etc/redis/redis.conf + environment: + - TZ=Asia/Seoul ports: - "6380:6379" volumes: - redis-session1-data:/data - ./redis/redis-session1.conf:/usr/local/etc/redis/redis.conf + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro networks: - backend @@ -82,11 +99,15 @@ services: image: redis:7.2 container_name: redis-session2 command: redis-server /usr/local/etc/redis/redis.conf + environment: + - TZ=Asia/Seoul ports: - "6381:6379" volumes: - redis-session2-data:/data - ./redis/redis-session2.conf:/usr/local/etc/redis/redis.conf + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro networks: - backend @@ -94,6 +115,7 @@ services: image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0 container_name: elasticsearch environment: + - TZ=Asia/Seoul - discovery.type=single-node - xpack.security.enabled=false - xpack.security.transport.ssl.enabled=false @@ -106,6 +128,8 @@ services: volumes: - ./ElasticSearch/stopwords.txt:/usr/share/elasticsearch/config/stopwords.txt - es-data:/usr/share/elasticsearch/data + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro # prometheus: # image: prom/prometheus:latest @@ -133,4 +157,4 @@ volumes: networks: backend: - driver: bridge + driver: bridge \ No newline at end of file