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
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,46 @@ on:

jobs:
# -------------------------------------------------------------------
# 1) CI: 컴파일·테스트·패키징만
# 1) CI
# -------------------------------------------------------------------
build:
runs-on: ubuntu-latest
steps:
# 코드 저장소에서 최신 커밋 가져오기
# 1) 저장소 코드 가져오기
- name: Checkout code
uses: actions/checkout@v3

# 2) Java 21 설치
- name: Set up Java 21
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'

# Gradle을 통해 클린 빌드, 테스트, 패키징을수행
# 3) Gradle 관련 파일들 캐싱
- name: Cache Gradle packages
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
gradle-${{ runner.os }}-

# 4) Gradle로 빌드
- name: Run Gradle Build
run: ./gradlew clean build -x test --no-daemon
run: ./gradlew build -x test --no-daemon --build-cache

# -------------------------------------------------------------------
# 2) CD: EC2에 배포 (ci 성공 후 항상 실행)
# 2) CD: EC2에 배포
# -------------------------------------------------------------------
deploy:
needs: build
runs-on: ubuntu-latest

steps:
# 1) 코드 체크아웃
# 1) 저장소 코드 가져오기
- name: Checkout code
uses: actions/checkout@v3
with:
Expand Down Expand Up @@ -121,22 +133,22 @@ jobs:
# depend_on에 의해 springboot가 가장 마지막 실행
for i in $(seq 1 $MAX_RETRIES); do
HEALTH_STATUS=$(ssh ec2 "docker inspect --format='{{.State.Health.Status}}' tokkit-springboot" 2>/dev/null || echo "none")

if [ "$HEALTH_STATUS" = "healthy" ]; then
echo "✅ tokkit-springboot 컨테이너 헬스체크 성공"
break
else
echo "⏳ 헬스체크 대기 중... 시도 $i/$MAX_RETRIES (현재 상태: $HEALTH_STATUS)"
sleep $RETRY_INTERVAL
fi

if [ "$i" -eq "$MAX_RETRIES" ]; then
echo "❌ tokkit-springboot 컨테이너가 헬스체크를 통과하지 못했습니다."
exit 1
fi
done


# 모든 컨테이너 상태 점검
COMPOSE_FILES="-f docker-compose.dev.yml -f docker-compose.prod.yml"
CONTAINERS=$(ssh ec2 "cd ${{ secrets.APP_DIR }} && docker-compose $COMPOSE_FILES ps -q")
Expand Down Expand Up @@ -186,4 +198,4 @@ jobs:
- name: Cleanup SSH
if: always()
run: rm -rf ~/.ssh
shell: bash
shell: bash
227 changes: 227 additions & 0 deletions .github/workflows/docker_hub_deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
name: Tokkit CI/CD Pipeline

on:
push:
branches:
- develop
- main
workflow_dispatch:

jobs:
# -------------------------------------------------------------------
# 1) CI - 프로젝트 빌드 및 Docker Hub로 이미지 푸시
# -------------------------------------------------------------------
build:
runs-on: ubuntu-latest
steps:
# 1) 저장소 코드 가져오기
- name: Checkout code
uses: actions/checkout@v3

# 2) Java 21 설치
- name: Set up Java 21
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'

# 3) Gradle 관련 파일들 캐싱
- name: Cache Gradle packages
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
gradle-${{ runner.os }}-

# 4) application.yml 생성 (secrets에서 불러오기)
- name: Generate Configuration Files
run: |
# application.yml 생성
mkdir -p src/main/resources
cat > src/main/resources/application.yml << 'EOF'
${{ secrets.APP_YML }}
EOF

# 5) Gradle로 빌드
- name: Run Gradle Build
run: ./gradlew clean build -x test --no-daemon --build-cache

# 6) Docker Hub 로그인
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

# 7) Docker 이미지 빌드 및 푸시
- name: Build and push Docker image
id: build_push
run: |
IMAGE_TAG=${{ secrets.DOCKER_USERNAME }}/tokkit-springboot:latest
docker build \
--file Dockerfile.cicd \
--tag $IMAGE_TAG \
.
docker push $IMAGE_TAG
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_OUTPUT

# -------------------------------------------------------------------
# 2) CD: EC2 서버에 배포
# -------------------------------------------------------------------
deploy:
needs: build
runs-on: ubuntu-latest

steps:
# 1) SSH 키 설정
- name: Setup SSH key
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.EC2_SSH_KEY }}

# 2) EC2에 접속해 코드 및 설정 파일 반영, Docker Compose로 배포
- name: Deploy to EC2
run: |
# 명확한 SSH 연결 정보 설정
SSH_HOST="${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }}"

# 원격 서버에 명령 실행
ssh -o StrictHostKeyChecking=no $SSH_HOST <<'EOT'
set -e
cd ${{ secrets.APP_DIR }}

echo "📥 Git pull"
git fetch origin
git checkout -B ${{ github.ref_name }} origin/${{ github.ref_name }}
git pull origin ${{ github.ref_name }}

echo "📝 .env 생성"
cat > ./.env << 'EOF'
${{ secrets.ENV_FILE }}
EOF

echo "📝 application.yml 생성"
mkdir -p src/main/resources
cat > ./src/main/resources/application.yml << 'EOF'
${{ secrets.APP_YML }}
EOF

echo "🔒 Docker Hub 로그인"
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login \
--username "${{ secrets.DOCKER_USERNAME }}" \
--password-stdin

# Docker Hub 사용자명 환경 변수 설정
export DOCKER_USERNAME="${{ secrets.DOCKER_USERNAME }}"

echo "🔄 springboot 최신 이미지 가져오기"
docker pull ${DOCKER_USERNAME}/tokkit-springboot:latest

echo "🚀 docker-compose로 서비스 실행"
docker-compose -f docker-compose.dev.yml -f docker-compose.prod.yml up -d
EOT

# 3) 컨테이너 헬스체크 및 배포 상태 확인
- name: Check deployment status
run: |
# SSH 연결 정보 설정
SSH_HOST="${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }}"

# 헬스체크 루프
# 헬스 체크 최대 10회(30초 간격)
MAX_RETRIES=10
RETRY_INTERVAL=30

# depend_on에 의해 springboot가 가장 마지막 실행
for i in $(seq 1 $MAX_RETRIES); do
HEALTH_STATUS=$(ssh -o StrictHostKeyChecking=no $SSH_HOST "docker inspect --format='{{.State.Health.Status}}' tokkit-springboot 2>/dev/null || echo 'none'")

if [ "$HEALTH_STATUS" = "healthy" ]; then
echo "✅ tokkit-springboot 컨테이너 헬스체크 성공"
break
elif [ "$HEALTH_STATUS" = "none" ]; then
echo "⏳ 헬스체크 대기 중... 시도 $i/$MAX_RETRIES (현재 상태: 컨테이너가 없거나 상태 확인 불가)"

# 컨테이너 로그 확인 (컨테이너가 존재하는 경우)
CONTAINER_EXISTS=$(ssh -o StrictHostKeyChecking=no $SSH_HOST "docker ps -a --format '{{.Names}}' | grep tokkit-springboot || echo ''")
if [ ! -z "$CONTAINER_EXISTS" ]; then
echo "📋 tokkit-springboot 컨테이너 로그 (최근 30줄):"
ssh -o StrictHostKeyChecking=no $SSH_HOST "docker logs tokkit-springboot --tail 30"
fi
else
echo "⏳ 헬스체크 대기 중... 시도 $i/$MAX_RETRIES (현재 상태: $HEALTH_STATUS)"
fi

sleep $RETRY_INTERVAL

if [ "$i" -eq "$MAX_RETRIES" ]; then
echo "❌ tokkit-springboot 컨테이너가 헬스체크를 통과하지 못했습니다."

# 배포 상태 확인
echo "📊 docker-compose 상태:"
ssh -o StrictHostKeyChecking=no $SSH_HOST "cd ${{ secrets.APP_DIR }} && docker-compose -f docker-compose.dev.yml -f docker-compose.prod.yml ps"

# 스프링부트 컨테이너 로그 확인
echo "📋 tokkit-springboot 컨테이너 로그 (최근 100줄):"
ssh -o StrictHostKeyChecking=no $SSH_HOST "docker logs tokkit-springboot --tail 100 || echo '로그를 가져올 수 없습니다.'"

# 환경 변수 확인
echo "🔍 환경 변수 확인:"
ssh -o StrictHostKeyChecking=no $SSH_HOST "cd ${{ secrets.APP_DIR }} && grep DOCKER_USERNAME .env || echo 'DOCKER_USERNAME not found in .env'"

exit 1
fi
done

# ✅ 모든 컨테이너 실행 상태 점검
COMPOSE_FILES="-f docker-compose.dev.yml -f docker-compose.prod.yml"
CONTAINERS=$(ssh -o StrictHostKeyChecking=no $SSH_HOST "cd ${{ secrets.APP_DIR }} && docker-compose $COMPOSE_FILES ps -q")
UNHEALTHY_COUNT=0

echo "📊 컨테이너 상태 확인 중..."
for CONTAINER_ID in $CONTAINERS; do
# 각 컨테이너의 상태 확인
CONTAINER_STATUS=$(ssh -o StrictHostKeyChecking=no $SSH_HOST "docker inspect --format='{{.State.Status}}' $CONTAINER_ID")
CONTAINER_NAME=$(ssh -o StrictHostKeyChecking=no $SSH_HOST "docker inspect --format='{{.Name}}' $CONTAINER_ID" | sed 's/^\///')

if [ "$CONTAINER_STATUS" != "running" ]; then
echo "⚠️ 컨테이너 $CONTAINER_NAME 상태: $CONTAINER_STATUS (실행 중 아님)"
UNHEALTHY_COUNT=$((UNHEALTHY_COUNT + 1))
else
echo "✅ 컨테이너 $CONTAINER_NAME 상태: $CONTAINER_STATUS (정상)"
fi
done

# 비정상 컨테이너 처리
if [ "$UNHEALTHY_COUNT" -gt 0 ]; then
echo "❌ 배포 실패: $UNHEALTHY_COUNT 개 컨테이너 비정상"
echo "=== 비정상 컨테이너 상세 상태 ==="
for CID in $CONTAINERS; do
STATUS=$(ssh -o StrictHostKeyChecking=no $SSH_HOST "docker inspect --format='{{.State.Status}}' $CID")
if [ "$STATUS" != "running" ]; then
NAME=$(ssh -o StrictHostKeyChecking=no $SSH_HOST "docker inspect --format='{{.Name}}' $CID" | sed 's/^\///')
echo "🪫 $NAME 상태: $STATUS"
echo "--- $NAME 로그 (tail 50) ---"
ssh -o StrictHostKeyChecking=no $SSH_HOST "docker logs $NAME --tail=50"
fi
done

# 상태 테이블 출력
ssh -o StrictHostKeyChecking=no $SSH_HOST "cd ${{ secrets.APP_DIR }} && docker-compose $COMPOSE_FILES ps"
exit 1

# 성공 처리
else
TOTAL_COUNT=$(echo "$CONTAINERS" | wc -w)
echo "✅ 배포 성공: 모든 컨테이너($TOTAL_COUNT 개) 정상 실행"
ssh -o StrictHostKeyChecking=no $SSH_HOST "cd ${{ secrets.APP_DIR }} && docker-compose $COMPOSE_FILES ps"
fi

# 4) SSH 키 제거 (보안 정리)
- name: Cleanup SSH
if: always()
run: rm -rf ~/.ssh
shell: bash
Loading