Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ logs
**/application-prod.yml
**/application-dev.yml
**/application-local.yml
**/application-*.yml
**/application-*.yml

!build/libs
!build/libs/*.jar
281 changes: 281 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
name: Backend Deploy (CD)

on:
push:
branches:
- develop
- main
paths:
- 'src/**'
- 'build.gradle.kts'
- 'docker/**'
- 'cd.yml'

workflow_dispatch:
inputs:
environment:
description: 'Deploy environment'
required: true
type: choice
options:
- dev
- prod

Comment on lines +3 to +23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

cd.yml 경로 필터가 잘못되었습니다.

Line 12의 경로 필터 'cd.yml'는 저장소 루트의 파일을 의미하지만, 실제 워크플로우 파일은 .github/workflows/cd.yml에 위치합니다. 이로 인해 워크플로우 파일 자체를 수정해도 CD가 트리거되지 않습니다.

🔧 경로 수정 제안
     paths:
       - 'src/**'
       - 'build.gradle.kts'
       - 'docker/**'
-      - 'cd.yml'
+      - '.github/workflows/cd.yml'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
on:
push:
branches:
- develop
- main
paths:
- 'src/**'
- 'build.gradle.kts'
- 'docker/**'
- 'cd.yml'
workflow_dispatch:
inputs:
environment:
description: 'Deploy environment'
required: true
type: choice
options:
- dev
- prod
on:
push:
branches:
- develop
- main
paths:
- 'src/**'
- 'build.gradle.kts'
- 'docker/**'
- '.github/workflows/cd.yml'
workflow_dispatch:
inputs:
environment:
description: 'Deploy environment'
required: true
type: choice
options:
- dev
- prod
🤖 Prompt for AI Agents
In @.github/workflows/cd.yml around lines 3 - 23, The workflow's push paths
include 'cd.yml' which references a root-level file and won't match the actual
workflow file; update the paths list in the on.push.paths block to use the
actual workflow location '.github/workflows/cd.yml' so changes to the workflow
file itself trigger the CD pipeline (edit the on.push.paths entry that currently
contains 'cd.yml').

jobs:
# 1. 공통 빌드/테스트 워크플로우 호출 (ci.yml 재사용)
ci-and-build:
uses: ./.github/workflows/ci.yml
with:
environment: ${{ inputs.environment }}

# 2. Deploy Job
deploy:
needs: ci-and-build
if: needs.ci-and-build.outputs.environment != 'test'
runs-on: self-hosted
# 빌드 단계에서 결정된 환경 사용
environment: ${{ needs.ci-and-build.outputs.environment }}

env:
ENVIRONMENT: ${{ needs.ci-and-build.outputs.environment }}
REPO_OWNER: ${{ needs.ci-and-build.outputs.repo_owner }}
IMAGE_TAG: ${{ needs.ci-and-build.outputs.image_tag }}

steps:
- name: 🔍 환경변수 검증
run: |
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔍 필수 환경 변수 검증 시작..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
MISSING_VARS=()
# Docker Hub 관련
echo "📦 Docker Hub 자격증명 확인..."
[[ -z "${{ secrets.DOCKERHUB_USERNAME }}" ]] && MISSING_VARS+=("DOCKERHUB_USERNAME")
[[ -z "${{ secrets.DOCKERHUB_TOKEN }}" ]] && MISSING_VARS+=("DOCKERHUB_TOKEN")
[[ -z "${{ secrets.DOCKERHUB_REPOSITORY_NAME }}" ]] && MISSING_VARS+=("DOCKERHUB_REPOSITORY_NAME")
# 서버 SSH 관련
echo "🔐 SSH 접속 정보 확인..."
[[ -z "${{ secrets.SERVER_APP_DIRECTORY }}" ]] && MISSING_VARS+=("SERVER_APP_DIRECTORY")
[[ -z "${{ secrets.SERVER_SSH_HOST }}" ]] && MISSING_VARS+=("SERVER_SSH_HOST")
[[ -z "${{ secrets.SERVER_SSH_USERNAME }}" ]] && MISSING_VARS+=("SERVER_SSH_USERNAME")
[[ -z "${{ secrets.SERVER_SSH_PRIVATE_KEY }}" ]] && MISSING_VARS+=("SERVER_SSH_PRIVATE_KEY")
[[ -z "${{ secrets.SERVER_SSH_PORT }}" ]] && MISSING_VARS+=("SERVER_SSH_PORT")
# 애플리케이션 설정 관련
echo "⚙️ 애플리케이션 설정 확인..."
[[ -z "${{ secrets.DOCKER_COMPOSE_ENV }}" ]] && MISSING_VARS+=("DOCKER_COMPOSE_ENV")
[[ -z "${{ secrets.APPLICATION_SECRET }}" ]] && MISSING_VARS+=("APPLICATION_SECRET")
[[ -z "${{ secrets.APPLICATION_PROFILE_SECRET }}" ]] && MISSING_VARS+=("APPLICATION_PROFILE_SECRET")
# Environment 변수
echo "🌍 환경 변수 확인..."
[[ -z "${{ env.ENVIRONMENT }}" ]] && MISSING_VARS+=("ENVIRONMENT")
[[ -z "${{ env.IMAGE_TAG }}" ]] && MISSING_VARS+=("IMAGE_TAG")
# 검증 결과 확인
if [ ${#MISSING_VARS[@]} -gt 0 ]; then
echo ""
echo "❌ 다음 환경 변수들이 설정되지 않았습니다:"
printf ' • %s\n' "${MISSING_VARS[@]}"
echo ""
exit 1
fi
echo ""
echo "✅ 모든 필수 환경 변수가 설정되었습니다."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
- name: 📥 코드베이스 체크아웃
uses: actions/checkout@v4

- name: 📋 배포 정보 출력
run: |
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📋 배포 정보"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🌍 Environment: ${{ env.ENVIRONMENT }}"
echo "📦 Image Tag: ${{ env.IMAGE_TAG }}"
echo "📁 App Directory: ${{ secrets.SERVER_APP_DIRECTORY }}"
echo "🖥️ Server: ${{ secrets.SERVER_SSH_HOST }}"
echo "👤 User: ${{ secrets.SERVER_SSH_USERNAME }}"
echo "🔌 Port: ${{ secrets.SERVER_SSH_PORT }}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
- name: 📤 Docker Compose 파일 전송
uses: appleboy/scp-action@v1
with:
host: ${{ secrets.SERVER_SSH_HOST }}
username: ${{ secrets.SERVER_SSH_USERNAME }}
key: ${{ secrets.SERVER_SSH_PRIVATE_KEY }}
port: ${{ secrets.SERVER_SSH_PORT }}
source: "docker/app/docker-compose.yml"
target: "${{ secrets.SERVER_APP_DIRECTORY }}/"
strip_components: 2
overwrite: true

- name: 🚀 SSH 접속 및 배포 실행
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_SSH_HOST }}
username: ${{ secrets.SERVER_SSH_USERNAME }}
key: ${{ secrets.SERVER_SSH_PRIVATE_KEY }}
port: ${{ secrets.SERVER_SSH_PORT }}
script: |
echo "=============================="
echo "🚀 배포 시작: ${{ env.ENVIRONMENT }} 환경"
echo "=============================="
# Docker 경로 확인
echo "[1] Docker 설치 확인"
if which docker > /dev/null 2>&1; then
echo "✅ Docker 인식됨: $(which docker)"
else
echo "⚠️ PATH에 /usr/local/bin 추가"
export PATH="$PATH:/usr/local/bin"
if which docker > /dev/null 2>&1; then
echo "✅ Docker 인식됨: $(which docker)"
else
echo "❌ Docker를 찾을 수 없습니다"
exit 1
fi
fi
# Docker Hub 로그인
echo ""
echo "[2] Docker Hub 로그인"
echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
if [ $? -eq 0 ]; then
echo "✅ Docker Hub 로그인 성공"
else
echo "❌ Docker Hub 로그인 실패"
exit 1
fi
# App Directory가 존재하는지 확인
echo ""
echo "[3] 애플리케이션 디렉토리 확인: ${{ secrets.SERVER_APP_DIRECTORY }}"
if [ ! -d "${{ secrets.SERVER_APP_DIRECTORY }}" ]; then
echo "❌ 애플리케이션 디렉토리가 존재하지 않습니다. 환경변수를 확인해주세요."
exit 1
else
echo "✅ 애플리케이션 디렉토리가 존재합니다. 다음 단계로 진행합니다."
fi
# Docker Compose 파일용 env 로딩
echo ""
echo "📝 Docker Compose 용 .env 파일 생성 중..."
echo "${{ secrets.DOCKER_COMPOSE_ENV }}" > ${{ secrets.SERVER_APP_DIRECTORY }}/.env
echo "✅ Docker Compose 용 .env 파일 생성 완료"
# SpringBoot Config 파일들 로딩
echo ""
echo "📝 Spring Boot 설정 파일 생성 중..."
mkdir -p ${{ secrets.SERVER_APP_DIRECTORY }}/config
echo "${{ secrets.APPLICATION_SECRET }}" > ${{ secrets.SERVER_APP_DIRECTORY }}/config/application-secret.yml
echo "${{ secrets.APPLICATION_PROFILE_SECRET }}" > ${{ secrets.SERVER_APP_DIRECTORY }}/config/application-${ENVIRONMENT}.yml
Comment on lines +174 to +179
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

설정 파일 권한 제한 권장

민감한 설정 파일(application-secret.yml, application-${ENVIRONMENT}.yml)을 생성할 때 파일 권한을 제한하는 것이 좋습니다. 현재는 기본 umask에 따라 권한이 설정됩니다.

🔒 파일 권한 제한 제안
             # SpringBoot Config 파일들 로딩
             echo ""
             echo "📝 Spring Boot 설정 파일 생성 중..."
             mkdir -p ${{ secrets.SERVER_APP_DIRECTORY }}/config
-            echo "${{ secrets.APPLICATION_SECRET }}" > ${{ secrets.SERVER_APP_DIRECTORY }}/config/application-secret.yml
-            echo "${{ secrets.APPLICATION_PROFILE_SECRET }}" > ${{ secrets.SERVER_APP_DIRECTORY }}/config/application-${ENVIRONMENT}.yml
+            umask 077
+            echo "${{ secrets.APPLICATION_SECRET }}" > ${{ secrets.SERVER_APP_DIRECTORY }}/config/application-secret.yml
+            echo "${{ secrets.APPLICATION_PROFILE_SECRET }}" > ${{ secrets.SERVER_APP_DIRECTORY }}/config/application-${ENVIRONMENT}.yml
+            chmod 600 ${{ secrets.SERVER_APP_DIRECTORY }}/config/application-secret.yml
+            chmod 600 ${{ secrets.SERVER_APP_DIRECTORY }}/config/application-${ENVIRONMENT}.yml
🤖 Prompt for AI Agents
In @.github/workflows/cd.yml around lines 174 - 179, Limit permissions on the
generated Spring Boot secret files by creating them with a restrictive umask or
by explicitly setting their mode to owner-read/write only; specifically, when
writing application-secret.yml and application-${ENVIRONMENT}.yml (the echo ...
> lines), either set umask 077 before creation or run a chmod 600 on those two
filenames immediately after they're written so only the file owner can
read/write the secrets.

# 기존 컨테이너 중지 및 제거
echo ""
echo "서비스 디렉토리로 이동합니다, 이전 위치: $(pwd)"
cd ${{ secrets.SERVER_APP_DIRECTORY }}
echo "서비스 디렉토리로 이동을 완료했습니다. 현재 위치: $(pwd)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📥 Docker 이미지 Pull"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
docker compose pull
echo "🛑 기존 컨테이너 중지 중..."
docker compose down
echo "🚀 새 컨테이너 시작 중..."
docker compose up -d --remove-orphans
if [ $? -ne 0 ]; then
echo "❌ 컨테이너 시작 실패"
echo ""
echo "📋 컨테이너 로그:"
docker compose logs --tail=50
exit 1
fi
echo "✅ 컨테이너가 성공적으로 시작되었습니다"
echo ""
# 컨테이너 상태 확인
echo "📊 컨테이너 상태:"
docker compose ps
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🎉 배포 완료!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
Comment on lines +187 to +216
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

오래된 Docker 이미지 정리 고려

배포 시 이전 이미지가 누적되어 디스크 공간이 부족해질 수 있습니다. 배포 성공 후 사용하지 않는 이미지를 정리하는 단계를 추가하는 것을 고려해 주세요.

🧹 이미지 정리 추가 제안

docker compose up -d --remove-orphans 이후에 다음을 추가:

# 사용하지 않는 이미지 정리 (dangling images)
echo "🧹 미사용 Docker 이미지 정리 중..."
docker image prune -f
🤖 Prompt for AI Agents
In @.github/workflows/cd.yml around lines 187 - 216, Add an image-cleanup step
after the successful `docker compose up -d --remove-orphans` run: after
verifying the compose up exit status and success messages, invoke `docker image
prune -f` (or `docker system prune -f` if you want to clean unused
volumes/networks as well) to remove dangling/unreferenced images and reclaim
disk space; ensure this runs only on successful deploys and that its output is
logged so any reclaiming errors are visible.

Comment on lines +190 to +217
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

배포 후 헬스 체크 및 롤백 메커니즘이 누락되었습니다.

컨테이너가 시작된 후(Line 196) 애플리케이션이 실제로 정상 작동하는지 확인하는 헬스 체크가 없습니다. 이로 인해:

  1. 컨테이너는 실행 중이지만 앱이 크래시하거나 응답하지 않는 경우를 감지하지 못함
  2. 배포 실패 시 이전 버전으로 롤백하는 메커니즘이 없음
🏥 헬스 체크 및 롤백 로직 추가 제안
             echo "🚀 새 컨테이너 시작 중..."
             docker compose up -d --remove-orphans
             
             if [ $? -ne 0 ]; then
               echo "❌ 컨테이너 시작 실패"
               echo ""
               echo "📋 컨테이너 로그:"
               docker compose logs --tail=50
               exit 1
             fi
             
             echo "✅ 컨테이너가 성공적으로 시작되었습니다"
             echo ""
+            
+            # 헬스 체크 (애플리케이션이 실제로 응답하는지 확인)
+            echo "🏥 애플리케이션 헬스 체크 중..."
+            max_attempts=30
+            attempt=0
+            while [ $attempt -lt $max_attempts ]; do
+              if docker compose exec -T app wget --spider --tries=1 --timeout=2 http://localhost:8080/actuator/health 2>/dev/null; then
+                echo "✅ 애플리케이션이 정상적으로 응답합니다"
+                break
+              fi
+              attempt=$((attempt + 1))
+              echo "⏳ 헬스 체크 대기 중... ($attempt/$max_attempts)"
+              sleep 2
+            done
+            
+            if [ $attempt -eq $max_attempts ]; then
+              echo "❌ 애플리케이션 헬스 체크 실패"
+              echo ""
+              echo "📋 최근 컨테이너 로그:"
+              docker compose logs --tail=100
+              echo ""
+              echo "🔄 이전 버전으로 롤백 시도..."
+              docker compose down
+              # 이전 이미지로 롤백하는 로직 추가 가능
+              exit 1
+            fi
             
             # 컨테이너 상태 확인
             echo "📊 컨테이너 상태:"

Note: Spring Boot Actuator의 /actuator/health 엔드포인트를 사용한다고 가정했습니다. 실제 헬스 체크 엔드포인트에 맞게 조정하세요.

- name: 📊 Deployment Summary
if: always()
run: |
echo "### 🚀 Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| 항목 | 값 |" >> $GITHUB_STEP_SUMMARY
echo "|------|-----|" >> $GITHUB_STEP_SUMMARY
echo "| 🌍 Environment | \`${{ env.ENVIRONMENT }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| 🌿 Branch | \`${{ github.ref_name }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| 📝 Commit | \`${{ github.sha }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| 📦 Image | \`${{ secrets.DOCKERHUB_REPOSITORY_NAME }}:${{ env.IMAGE_TAG }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| 🖥️ Server | \`${{ secrets.SERVER_SSH_HOST }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| 📁 Directory | \`${{ secrets.SERVER_APP_DIRECTORY }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| ✅ Status | \`${{ job.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
fi
- name: ❌ 배포 실패 알림
if: failure()
run: |
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "❌ 배포 실패"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "환경: ${{ env.ENVIRONMENT }}"
echo "이미지: ${{ secrets.DOCKERHUB_REPOSITORY_NAME }}:${{ env.IMAGE_TAG }}"
echo "서버: ${{ secrets.SERVER_SSH_HOST }}"
echo ""
echo "::error::Deployment to ${{ env.ENVIRONMENT }} failed!"
echo "::error::Please check the deployment logs for details."
# 3. Test 환경 스킵 알림
skip-test-deployment:
needs: ci-and-build
if: needs.ci-and-build.outputs.environment == 'test'
runs-on: ubuntu-latest
steps:
- name: ℹ️ Test 환경 배포 스킵
run: |
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "ℹ️ Test 환경 배포 스킵"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Test 환경은 CI에서만 빌드하고 배포하지 않습니다."
echo ""
echo "✅ CI 빌드 완료"
echo "📦 이미지: ${{ secrets.DOCKERHUB_REPOSITORY_NAME }}:${{ needs.ci-and-build.outputs.image_tag }}"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
- name: 📊 Summary
run: |
echo "### ℹ️ Test Environment - Deployment Skipped" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Test 환경은 CI에서만 빌드하고 실제 배포는 수행하지 않습니다." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- ✅ **CI Build**: Completed" >> $GITHUB_STEP_SUMMARY
echo "- 📦 **Image**: \`${{ secrets.DOCKERHUB_REPOSITORY_NAME }}:${{ needs.ci-and-build.outputs.image_tag }}\`" >> $GITHUB_STEP_SUMMARY
echo "- 🚫 **Deployment**: Skipped" >> $GITHUB_STEP_SUMMARY
Loading
Loading