diff --git a/.github/scripts/mock-deploy.sh b/.github/scripts/mock-deploy.sh new file mode 100755 index 0000000..b0e1c54 --- /dev/null +++ b/.github/scripts/mock-deploy.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Github Actions Runner CD 스크립트 +# 사용 전 필요한 GitHub Secrets +# - DEPLOY_WEBHOOK_URL: 깃허브 배포 알림을 받을 웹훅 URL +# - EC2_HOST: 배포 대상 EC2 호스트 +# - EC2_USER: SSH 접속 유저 +# - EC2_SSH_KEY: SSH 개인키 전체 내용 (-----BEGIN ... 포함) +# - 필요 시 REMOTE_DEPLOY_PATH, CONTAINER_NAME 등 환경변수 오버라이드 가능 + +APP_NAME="${APP_NAME:-doki}" +IMAGE_NAME="${IMAGE_NAME:-doki}" +IMAGE_TAG="${IMAGE_TAG:-latest}" +FULL_IMAGE="${DOCKER_IMAGE:-${IMAGE_NAME}:${IMAGE_TAG}}" +WEBHOOK_URL="${WEBHOOK_URL:-}" +TARGET_HOST="${TARGET_HOST:-}" +TARGET_USER="${TARGET_USER:-}" +EC2_SSH_KEY="${EC2_SSH_KEY:-}" +REMOTE_DEPLOY_PATH="${REMOTE_DEPLOY_PATH:-/opt/${APP_NAME}}" +CONTAINER_NAME="${CONTAINER_NAME:-${APP_NAME}}" + +log() { + echo ">>> [deploy] $*" +} + +log ">>> 앱=${APP_NAME} 이미지=${FULL_IMAGE}" + +if [[ -z "${WEBHOOK_URL}" ]]; then + log ">>> WEBHOOK_URL 비어 있음 → 웹훅 전송 스킵(모의 실행)." +else + log ">>> 웹훅 전송: ${WEBHOOK_URL}" + curl -fsS -X POST \ + -H "Content-Type: application/json" \ + -d "{\"app\":\"${APP_NAME}\",\"image\":\"${FULL_IMAGE}\"}" \ + "${WEBHOOK_URL}" || log ">>> 웹훅 전송 실패" +fi + +if [[ -z "${TARGET_HOST}" || -z "${TARGET_USER}" || -z "${EC2_SSH_KEY}" ]]; then + log ">>> SSH 정보가 없습니다. 원격 도커 배포 스킵합니다." + exit 0 +fi + +log ">>> ${TARGET_USER}@${TARGET_HOST} 접속 후 컨테이너 ${CONTAINER_NAME} 재실행" +KEY_FILE="$(mktemp)" +trap 'rm -f "${KEY_FILE}"' EXIT +chmod 600 "${KEY_FILE}" +printf "%s" "${EC2_SSH_KEY}" > "${KEY_FILE}" + +REMOTE_COMMANDS=$(cat <<'EOF' +set -e +mkdir -p "$REMOTE_DEPLOY_PATH" +cd "$REMOTE_DEPLOY_PATH" +docker pull "$FULL_IMAGE" || true +docker stop "$CONTAINER_NAME" 2>/dev/null || true +docker rm "$CONTAINER_NAME" 2>/dev/null || true +docker run -d --restart=unless-stopped \ + --name "$CONTAINER_NAME" \ + -p 8080:8080 \ + "$FULL_IMAGE" +EOF +) + +ssh -o StrictHostKeyChecking=no -i "${KEY_FILE}" "${TARGET_USER}@${TARGET_HOST}" \ + "FULL_IMAGE='${FULL_IMAGE}' CONTAINER_NAME='${CONTAINER_NAME}' REMOTE_DEPLOY_PATH='${REMOTE_DEPLOY_PATH}' bash -s" <<< "${REMOTE_COMMANDS}" + +log ">>> Github Actions Runner 배포 스크립트 작동 완료" diff --git a/.github/workflows/backend-cd.yml b/.github/workflows/backend-cd.yml new file mode 100644 index 0000000..c324f2a --- /dev/null +++ b/.github/workflows/backend-cd.yml @@ -0,0 +1,87 @@ +name: backend-cd + +# prod 브랜치를 통해 배포 +on: + push: + branches: + - prod + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + IMAGE_TAG: ${{ github.sha }} + APP_NAME: doki + +jobs: + build-and-deploy: + name: build-and-deploy + runs-on: ubuntu-latest + environment: production + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: 'gradle' + + - name: Cache Gradle dependencies + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build jar + run: ./gradlew clean bootJar --no-daemon + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Make deploy script executable + run: chmod +x .github/scripts/mock-deploy.sh + + - name: (Mock) Deploy to EC2 via webhook + SSH + run: .github/scripts/mock-deploy.sh + env: + APP_NAME: ${{ env.APP_NAME }} + IMAGE_NAME: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + IMAGE_TAG: ${{ env.IMAGE_TAG }} + DOCKER_IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + WEBHOOK_URL: ${{ secrets.DEPLOY_WEBHOOK_URL }} + TARGET_HOST: ${{ secrets.EC2_HOST }} + TARGET_USER: ${{ secrets.EC2_USER }} + EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }} + REMOTE_DEPLOY_PATH: /opt/${{ env.APP_NAME }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9fb0f15 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +# 빌드 +FROM eclipse-temurin:17-jdk-jammy AS builder +WORKDIR /app + +COPY gradlew ./ +COPY gradle ./gradle +COPY build.gradle.kts settings.gradle.kts ./ +COPY src ./src + +RUN chmod +x ./gradlew \ + && ./gradlew clean bootJar --no-daemon + +# jre 이미지 생성 +FROM eclipse-temurin:17-jre-jammy +WORKDIR /app + +ENV SPRING_PROFILES_ACTIVE=prod + +# Copy the fat jar built in the previous stage +COPY --from=builder /app/build/libs/*.jar ./app.jar + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "/app/app.jar"]