fix(be): Fix WebSocket chat connection and reduce production logs (#i… #49
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: deploy.yml | |
| env: | |
| IMAGE_REPOSITORY: team11 # GHCR 이미지 리포지토리명(소유자 포함 X) | |
| CONTAINER_1_NAME: team11_1 # 슬롯1(고정 이름) | |
| CONTAINER_2_NAME: team11_2 # 슬롯2(고정 이름) | |
| CONTAINER_PORT: 8080 # 컨테이너 내부 포트(스프링부트) | |
| EC2_INSTANCE_TAG_NAME: team11-terra-ec2-1 # 배포 대상 EC2 Name 태그 | |
| DOCKER_NETWORK: common # 도커 네트워크 | |
| BACKEND_DIR: . # Dockerfile 위치 | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} # 커밋이 짧은 시간안에 몰려도 최신 커밋에 대해서만 액션이 수행되도록 | |
| cancel-in-progress: false # 기존에 시작된 작업은 새 커밋이 들어왔다고 해서 바로 끄지 않고 끝날때까지 대기 | |
| on: | |
| push: | |
| paths: | |
| - ".github/workflows/**" | |
| - ".env" | |
| - "src/**" | |
| - "build.gradle.kts" | |
| - "settings.gradle.kts" | |
| - "Dockerfile" | |
| branches: | |
| - main | |
| permissions: | |
| contents: write # 태그/릴리즈 | |
| packages: write # GHCR 푸시 | |
| # 기본 셸 | |
| defaults: | |
| run: | |
| shell: bash | |
| jobs: | |
| makeTagAndRelease: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| tag_name: ${{ steps.create_tag.outputs.new_tag }} # 이후 잡에서 사용할 태그명 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # 버전 태그 자동 생성 (vX.Y.Z) | |
| - name: Create Tag | |
| id: create_tag | |
| uses: mathieudutour/[email protected] | |
| with: | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| # 릴리즈 생성 | |
| - name: Create Release | |
| id: create_release | |
| uses: actions/create-release@v1 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| tag_name: ${{ steps.create_tag.outputs.new_tag }} | |
| release_name: Release ${{ steps.create_tag.outputs.new_tag }} | |
| body: ${{ steps.create_tag.outputs.changelog }} | |
| draft: false | |
| prerelease: false | |
| # --------------------------------------------------------- | |
| # 2) 도커 이미지 빌드/푸시 (캐시 최대 활용) | |
| # --------------------------------------------------------- | |
| buildImageAndPush: | |
| name: 도커 이미지 빌드와 푸시 | |
| needs: makeTagAndRelease | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # 빌드 컨텍스트에 .env 생성 (비어있어도 실패하지 않게) | |
| - name: .env 파일 생성 | |
| env: | |
| DOT_ENV: ${{ secrets.DOT_ENV }} | |
| run: | | |
| # .env가 없으면 빌드 캐시가 매번 깨질 수 있으므로 항상 생성 | |
| mkdir -p "${{ env.BACKEND_DIR }}" | |
| printf "%s" "${DOT_ENV}" > "${{ env.BACKEND_DIR }}/.env" | |
| - name: Docker Buildx 설치 | |
| uses: docker/setup-buildx-action@v3 | |
| # GHCR 로그인 | |
| - name: 레지스트리 로그인 | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # 저장소 소유자명을 소문자로 (GHCR 경로 표준화) | |
| - name: set lower case owner name | |
| run: | | |
| echo "OWNER_LC=${OWNER,,}" >> "${GITHUB_ENV}" | |
| env: | |
| OWNER: "${{ github.repository_owner }}" | |
| # 캐시를 최대한 활용하여 빌드 → 버전태그 및 latest 동시 푸시 | |
| - name: 빌드 앤 푸시 | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: ${{ env.BACKEND_DIR }} | |
| push: true | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| tags: | | |
| ghcr.io/${{ env.OWNER_LC }}/${{ env.IMAGE_REPOSITORY }}:${{ needs.makeTagAndRelease.outputs.tag_name }} | |
| ghcr.io/${{ env.OWNER_LC }}/${{ env.IMAGE_REPOSITORY }}:latest | |
| # --------------------------------------------------------- | |
| # 3) Blue/Green 무중단 배포 (EC2 + NPM 스위치) | |
| # --------------------------------------------------------- | |
| deploy: | |
| name: Blue/Green 무중단 배포 | |
| runs-on: ubuntu-latest | |
| needs: [ makeTagAndRelease, buildImageAndPush ] | |
| steps: | |
| # AWS 자격 구성 | |
| - uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| aws-region: ${{ secrets.AWS_REGION }} | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| # Name 태그로 EC2 인스턴스 조회 (없으면 실패) | |
| - name: 인스턴스 ID 가져오기 | |
| id: get_instance_id | |
| run: | | |
| INSTANCE_ID=$(aws ec2 describe-instances \ | |
| --filters "Name=tag:Name,Values=${{ env.EC2_INSTANCE_TAG_NAME }}" "Name=instance-state-name,Values=running" \ | |
| --query "Reservations[].Instances[].InstanceId" --output text) | |
| [[ -n "${INSTANCE_ID}" && "${INSTANCE_ID}" != "None" ]] || { echo "No running instance found"; exit 1; } | |
| echo "INSTANCE_ID=${INSTANCE_ID}" >> "${GITHUB_ENV}" | |
| # 원격(SSM)으로 Blue/Green 스위치 수행 | |
| - name: AWS SSM Send-Command | |
| uses: peterkimzz/aws-ssm-send-command@master | |
| with: | |
| aws-region: ${{ secrets.AWS_REGION }} | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| instance-ids: ${{ env.INSTANCE_ID }} | |
| working-directory: / | |
| comment: Deploy | |
| command: | | |
| set -Eeuo pipefail | |
| # --------------------------------------------------------- | |
| # 0) 실행 로그(라인 타임스탬프 부착) | |
| # --------------------------------------------------------- | |
| LOG="/tmp/ssm-$(date +%Y%m%d_%H%M%S).log" | |
| exec > >(awk '{ fflush(); print strftime("[%Y-%m-%d %H:%M:%S]"), $0 }' | tee -a "$LOG") | |
| exec 2> >(awk '{ fflush(); print strftime("[%Y-%m-%d %H:%M:%S]"), $0 }' | tee -a "$LOG" >&2) | |
| # --------------------------------------------------------- | |
| # 1) 변수 정의 | |
| # - 슬롯 이름은 고정(두 개의 컨테이너 이름) | |
| # - blue/green은 "역할"이며 매 배포마다 바뀜 | |
| # --------------------------------------------------------- | |
| source /etc/environment || true # 시스템 전역 변수(+ 비밀) 주입 시도 | |
| OWNER_LC="${{ github.repository_owner }}" | |
| OWNER_LC="${OWNER_LC,,}" # 소문자 표준화 | |
| IMAGE_TAG="${{ needs.makeTagAndRelease.outputs.tag_name }}" | |
| IMAGE_REPOSITORY="${{ env.IMAGE_REPOSITORY }}" | |
| IMAGE="ghcr.io/${OWNER_LC}/${IMAGE_REPOSITORY}:${IMAGE_TAG}" | |
| SLOT1="${{ env.CONTAINER_1_NAME }}" | |
| SLOT2="${{ env.CONTAINER_2_NAME }}" | |
| PORT_IN="${{ env.CONTAINER_PORT }}" | |
| NET="${{ env.DOCKER_NETWORK }}" | |
| echo "🔹 Use image: ${IMAGE}" | |
| docker pull "${IMAGE}" | |
| # --------------------------------------------------------- | |
| # 2) Nginx Proxy Manager(NPM) API 토큰 발급 | |
| # - /etc/environment 등에 설정된 PASSWORD_1, APP_1_DOMAIN 사용 가정 | |
| # --------------------------------------------------------- | |
| TOKEN=$(curl -s -X POST http://127.0.0.1:81/api/tokens \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"identity\": \"[email protected]\", \"secret\": \"${PASSWORD_1:-}\"}" | jq -r '.token') | |
| # 토큰/도메인 검증(없으면 실패) | |
| [[ -n "${TOKEN}" && "${TOKEN}" != "null" ]] || { echo "NPM token issue failed"; exit 1; } | |
| [[ -n "${APP_1_DOMAIN:-}" ]] || { echo "APP_1_DOMAIN is empty"; exit 1; } | |
| # 대상 프록시 호스트 ID 조회(도메인 매칭) | |
| PROXY_ID=$(curl -s -X GET "http://127.0.0.1:81/api/nginx/proxy-hosts" \ | |
| -H "Authorization: Bearer ${TOKEN}" \ | |
| | jq ".[] | select(.domain_names[]==\"${APP_1_DOMAIN}\") | .id") | |
| [[ -n "${PROXY_ID}" && "${PROXY_ID}" != "null" ]] || { echo "Proxy host not found for ${APP_1_DOMAIN}"; exit 1; } | |
| # 현재 프록시가 바라보는 업스트림(컨테이너명) 조회 | |
| CURRENT_HOST=$(curl -s -X GET "http://127.0.0.1:81/api/nginx/proxy-hosts/${PROXY_ID}" \ | |
| -H "Authorization: Bearer ${TOKEN}" \ | |
| | jq -r '.forward_host') | |
| echo "🔎 CURRENT_HOST: ${CURRENT_HOST:-none}" | |
| # --------------------------------------------------------- | |
| # 3) 역할(blue/green) 판정 | |
| # - blue : 현재 운영 중(CURRENT_HOST) | |
| # - green: 다음 운영(교체 대상) | |
| # --------------------------------------------------------- | |
| if [[ "${CURRENT_HOST:-}" == "${SLOT1}" ]]; then | |
| BLUE="${SLOT1}" | |
| GREEN="${SLOT2}" | |
| elif [[ "${CURRENT_HOST:-}" == "${SLOT2}" ]]; then | |
| BLUE="${SLOT2}" | |
| GREEN="${SLOT1}" | |
| else | |
| BLUE="none" # 초기 배포 | |
| GREEN="${SLOT1}" | |
| fi | |
| echo "🎨 role -> blue(now): ${BLUE}, green(next): ${GREEN}" | |
| # --------------------------------------------------------- | |
| # 4) green 역할 컨테이너 재기동 | |
| # - 같은 네트워크 상에서 NPM이 컨테이너명:PORT로 프록시하므로 -p 불필요 | |
| # --------------------------------------------------------- | |
| docker rm -f "${GREEN}" >/dev/null 2>&1 || true | |
| echo "🚀 run new container → ${GREEN}" | |
| docker run -d --name "${GREEN}" \ | |
| --restart unless-stopped \ | |
| --network "${NET}" \ | |
| -e TZ=Asia/Seoul \ | |
| -e SPRING_PROFILES_ACTIVE=prod \ | |
| -e SPRING__DATASOURCE__URL___DB_NAME="${APP_1_DB_NAME}" \ | |
| -e SPRING_DATASOURCE_USERNAME=team11 \ | |
| -e SPRING_DATASOURCE_PASSWORD="${PASSWORD_1}" \ | |
| -e REDIS_HOST=redis_1 \ | |
| -e REDIS_PORT=6379 \ | |
| -e REDIS_PASSWORD="${PASSWORD_1}" \ | |
| -e RABBITMQ_HOST=rabbitmq_1 \ | |
| -e RABBITMQ_PORT=5672 \ | |
| -e RABBITMQ_USERNAME=admin \ | |
| -e RABBITMQ_PASSWORD="${PASSWORD_1}" \ | |
| -e RABBITMQ_STOMP_PORT=61613 \ | |
| "${IMAGE}" | |
| # --------------------------------------------------------- | |
| # 5) 헬스체크 (/actuator/health 200 OK까지 대기) | |
| # --------------------------------------------------------- | |
| echo "⏱ health-check: ${GREEN}" | |
| TIMEOUT=120 | |
| INTERVAL=3 | |
| ELAPSED=0 | |
| sleep 8 # 초기 부팅 여유 | |
| while (( ELAPSED < TIMEOUT )); do | |
| CODE=$(docker exec "${GREEN}" curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:${PORT_IN}/actuator/health" || echo 000) | |
| [[ "${CODE}" == "200" ]] && { echo "✅ ${GREEN} healthy"; break; } | |
| sleep "${INTERVAL}" | |
| ELAPSED=$((ELAPSED + INTERVAL)) | |
| done | |
| [[ "${CODE:-000}" == "200" ]] || { echo "❌ ${GREEN} health failed"; docker logs --tail=200 "${GREEN}" || true; docker rm -f "${GREEN}" || true; exit 1; } | |
| # --------------------------------------------------------- | |
| # 6) 업스트림 전환 (forward_host/forward_port만 업데이트) | |
| # --------------------------------------------------------- | |
| NEW_CFG=$(jq -n --arg host "${GREEN}" --argjson port ${PORT_IN} '{forward_host:$host, forward_port:$port}') | |
| curl -s -X PUT "http://127.0.0.1:81/api/nginx/proxy-hosts/${PROXY_ID}" \ | |
| -H "Authorization: Bearer ${TOKEN}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "${NEW_CFG}" >/dev/null | |
| echo "🔁 switch upstream → ${GREEN}:${PORT_IN}" | |
| # --------------------------------------------------------- | |
| # 7) 이전 blue 종료(최초 배포면 생략) | |
| # --------------------------------------------------------- | |
| if [[ "${BLUE}" != "none" ]]; then | |
| docker stop "${BLUE}" >/dev/null 2>&1 || true | |
| docker rm "${BLUE}" >/dev/null 2>&1 || true | |
| echo "🧹 removed old blue: ${BLUE}" | |
| fi | |
| # --------------------------------------------------------- | |
| # 8) 이미지 정리(현재 태그/ latest 제외) - 결과 없음도 오류 아님 | |
| # - pipefail과 grep의 종료코드(1)를 고려해 블록 전체에 || true 적용 | |
| # --------------------------------------------------------- | |
| { | |
| docker images --format '{{.Repository}}:{{.Tag}}' \ | |
| | grep -F "ghcr.io/${OWNER_LC}/${IMAGE_REPOSITORY}:" \ | |
| | grep -v -F ":${IMAGE_TAG}" \ | |
| | grep -v -F ":latest" \ | |
| | xargs -r docker rmi | |
| } || true | |
| echo "🏁 Blue/Green switch complete. now blue = ${GREEN}" |