diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 0000000..663ba77 --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -0,0 +1,126 @@ +name: Deploy Prod Backend + +concurrency: + group: be-prod + cancel-in-progress: true # Deploy 작업은 한번에 하나만 실행하도록 제한 + +on: + push: + tags: + - 'v*.*.*-be' + +jobs: + build-and-push: + runs-on: ubuntu-latest + + outputs: + version: ${{ steps.get_version.outputs.VERSION }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # 태그 검증을 위해 전체 Git 히스토리 필요 + + - name: tag가 be/prod branch에 있는지 검증 + run: | + git fetch origin be/prod + if git merge-base --is-ancestor ${{ github.sha }} origin/be/prod; then + echo "이 태그는 be/prod branch에 포함되어 있습니다. 배포를 진행합니다." + else + echo "Error: 이 tag는 be/prod branch에 없습니다." + exit 1 + fi + + - name: Git tag에서 버전 정보 추출 + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" + + - name: Docker Hub 로그인 + run: echo "${{ secrets.DOCKER_ACCESS_TOKEN }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin + + - name: QEMU 설정 + uses: docker/setup-qemu-action@v3 + + - name: Docker Buildx 설정 + uses: docker/setup-buildx-action@v3 + + - name: Docker Image Build & Push + uses: docker/build-push-action@v6 + with: + context: ./ + file: ./Dockerfile + push: true + platforms: linux/arm64 + tags: | + ${{ secrets.PROD_IMAGE_NAME }}:${{ steps.get_version.outputs.VERSION }} + ${{ secrets.PROD_IMAGE_NAME }}:latest + + deploy: + runs-on: [ self-hosted, recycle-study-server-prod ] + needs: build-and-push + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: .env file 생성 + run: | + cat < .env + DOCKER_IMAGE=${{ secrets.PROD_IMAGE_NAME }} + DOCKER_TAG=${{ needs.build-and-push.outputs.version }} + ${{ secrets.ENV_PROD }} + EOF + + - name: log directory 준비 + run: | + sudo mkdir -p /app/log + sudo chown -R 1001:1001 /app/log + + - name: 배포 시작 + run: | + docker compose -f docker-compose.prod.yaml pull + docker compose -f docker-compose.prod.yaml up -d + + - name: 신규 deploy에 대한 health check + run: | + echo "==========================================" + echo "Health Check 시작" + echo "==========================================" + + echo "애플리케이션 초기화 대기 중... (10초)" + sleep 10 + + # 최대 12번 시도 (총 60초) + for i in {1..12}; do + STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/actuator/health || echo "000") + + if [ "$STATUS" -eq 200 ]; then + echo "==========================================" + echo "배포 성공!" + echo "==========================================" + docker ps --filter "name=recycle-study-server" + exit 0 + fi + + echo "[$i/12] 헬스체크 재시도... (응답 코드: $STATUS)" + sleep 5 + done + + # Health Check 실패 시 + echo "==========================================" + echo "Health Check 실패" + echo "==========================================" + echo "컨테이너 상태:" + docker ps -a --filter "name=recycle-study-server" + echo "" + echo "컨테이너 로그:" + docker logs recycle-study-server --tail 100 || true + + exit 1 + + - name: Docker resource 정리 + if: always() + run: | + docker image prune -a -f + docker builder prune -a -f diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ea2cc0d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +# Build stage +FROM amazoncorretto:21-alpine3.19-jdk AS builder + +WORKDIR /app + +COPY gradlew . +COPY gradle gradle +COPY build.gradle . +COPY settings.gradle . +COPY src src + +RUN chmod +x ./gradlew +RUN ./gradlew bootJar --no-daemon + +# Runtime stage +FROM amazoncorretto:21-alpine3.19 + +WORKDIR /app + +RUN apk add --no-cache curl tzdata && \ + cp /usr/share/zoneinfo/Asia/Seoul /etc/localtime && \ + echo "Asia/Seoul" > /etc/timezone && \ + apk del tzdata + +RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -D appuser +RUN mkdir -p /app/log && chown -R appuser:appgroup /app + +COPY --from=builder --chown=appuser:appgroup /app/build/libs/*.jar app.jar + +USER appuser + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/build.gradle b/build.gradle index 74bee92..6f8bce6 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,10 @@ dependencies { implementation 'org.flywaydb:flyway-core' implementation 'org.flywaydb:flyway-mysql' + // monitoring + implementation 'org.springframework.boot:spring-boot-starter-actuator' + runtimeOnly 'io.micrometer:micrometer-registry-prometheus' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.rest-assured:rest-assured' testRuntimeOnly 'com.h2database:h2' @@ -112,6 +116,10 @@ tasks.register('copySwaggerDocument', Copy) { into file("src/main/resources/static/docs") } +jar { + enabled = false +} + bootJar { dependsOn copySwaggerDocument } diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml new file mode 100644 index 0000000..4bec73e --- /dev/null +++ b/docker-compose.prod.yaml @@ -0,0 +1,11 @@ +services: + app: + container_name: recycle-study-server + image: ${DOCKER_IMAGE}:${DOCKER_TAG} + restart: unless-stopped + ports: + - "8080:8080" + env_file: + - .env + volumes: + - /app/log:/app/log diff --git a/docker-compose.yaml b/docker-compose.yaml index 1c7ecf2..892f0cd 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,18 @@ services: + app: + container_name: recycle-study-server + build: . + restart: unless-stopped + ports: + - "8080:8080" + env_file: + - .env + depends_on: + mysql: + condition: service_healthy + mysql: + container_name: recycle-study-mysql image: mysql:8.4 volumes: - mysql_volume:/var/lib/mysql @@ -11,6 +24,11 @@ services: - MYSQL_DATABASE=${DB_DATABASE} - MYSQL_USER=${DB_USERNAME} - MYSQL_PASSWORD=${DB_PASSWORD} + healthcheck: + test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] + interval: 10s + timeout: 5s + retries: 5 volumes: mysql_volume: diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 9c29b37..189e82a 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -35,5 +35,11 @@ spring: enable: true auth: true +management: + endpoints: + web: + exposure: + include: prometheus, health + auth: base-url: ${BASE_URL} diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index b3e8a28..dbc16ed 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -2,7 +2,7 @@ - + - - ${LOG_PATH}/app.log - - ${FILE_LOG_PATTERN} - utf8 - - - ${LOG_PATH}/app-%d{yyyy-MM-dd}.%i.log.gz - 10MB - 180 - 3GB - - - + + + ${LOG_PATH}/app.log + + ${FILE_LOG_PATTERN} + utf8 + + + ${LOG_PATH}/app-%d{yyyy-MM-dd}.%i.log.gz + 10MB + 180 + 3GB + +