diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..80f78c6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI (develop) + +on: + pull_request: + branches: [ "develop" ] + +permissions: + contents: read + +jobs: + build-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4.2.2 + + - name: Set up JDK 17 + uses: actions/setup-java@v4.7.1 + with: + distribution: temurin + java-version: "17" + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build & Test (Gradle) + run: ./gradlew clean build diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml new file mode 100644 index 0000000..8a3de40 --- /dev/null +++ b/.github/workflows/cicd.yml @@ -0,0 +1,101 @@ +name: CI/CD (main) + +on: + push: + branches: [ "main" ] + +permissions: + contents: read + +concurrency: + group: valuedi-prod-deploy + cancel-in-progress: true + +jobs: + build-and-push: + runs-on: ubuntu-latest + + outputs: + image_repo: ${{ steps.meta.outputs.image_repo }} + image_tag: ${{ steps.meta.outputs.image_tag }} + + steps: + - name: Checkout + uses: actions/checkout@v4.2.2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.10.0 + + - name: Log in to Docker Hub + uses: docker/login-action@v3.4.0 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Set image meta + id: meta + run: | + echo "image_repo=${{ secrets.DOCKER_USERNAME }}/valuedi-backend" >> $GITHUB_OUTPUT + echo "image_tag=${{ github.sha }}" >> $GITHUB_OUTPUT + + - name: Build & Push (latest + sha) + uses: docker/build-push-action@v6.16.0 + with: + context: . + file: ./Dockerfile + push: true + platforms: linux/amd64 + tags: | + ${{ steps.meta.outputs.image_repo }}:latest + ${{ steps.meta.outputs.image_repo }}:${{ steps.meta.outputs.image_tag }} + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + runs-on: ubuntu-latest + needs: build-and-push + + steps: + - name: Deploy via SSH (Blue/Green) + uses: appleboy/ssh-action@v1.2.2 + with: + host: ${{ secrets.PROD_SERVER_HOST }} + port: ${{ secrets.PROD_SERVER_PORT }} + username: ${{ secrets.PROD_SERVER_USERNAME }} + key: ${{ secrets.PROD_SERVER_KEY }} + script_stop: true + script: | + set -euo pipefail + + APP_DIR="/home/ubuntu/valuedi/app" + cd "$APP_DIR" + + # 1) PROD_ENV를 .env로 반영 (멀티라인 안전) + cat > .env << 'EOF' + ${{ secrets.PROD_ENV }} + EOF + + # 2) IMAGE_REPO / IMAGE_TAG 강제 주입 (PROD_ENV에 없거나 달라도 덮어씀) + IMAGE_REPO="${{ needs.build-and-push.outputs.image_repo }}" + IMAGE_TAG="${{ needs.build-and-push.outputs.image_tag }}" + + # IMAGE_REPO 업데이트/추가 + if grep -q '^IMAGE_REPO=' .env; then + sed -i "s|^IMAGE_REPO=.*|IMAGE_REPO=${IMAGE_REPO}|" .env + else + echo "IMAGE_REPO=${IMAGE_REPO}" >> .env + fi + + # IMAGE_TAG 업데이트/추가 + if grep -q '^IMAGE_TAG=' .env; then + sed -i "s|^IMAGE_TAG=.*|IMAGE_TAG=${IMAGE_TAG}|" .env + else + echo "IMAGE_TAG=${IMAGE_TAG}" >> .env + fi + + # 3) 최신 이미지 pull 확인 + 배포 + chmod +x deploy.sh scripts/healthcheck.sh scripts/switch_upstream.sh + ./deploy.sh + + # 4) 사용하지 않는 이미지 정리(선택) + docker image prune -f diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b52699c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +# syntax=docker/dockerfile:1.7 + +# ---------- Build Stage ---------- +FROM eclipse-temurin:17-jdk-jammy AS build +WORKDIR /workspace + +# Gradle 캐시 최적화: 설정/래퍼를 먼저 복사 +COPY gradlew . +COPY gradle gradle +COPY settings.gradle* build.gradle* gradle.properties* ./ +# 멀티모듈/버전카탈로그 사용 시 +COPY gradle/libs.versions.toml gradle/libs.versions.toml + +# 의존성 캐시 (소스 없이도 가능한 단계) +RUN chmod +x gradlew \ + && ./gradlew --no-daemon dependencies > /dev/null 2>&1 || true + +# 소스 복사 +COPY src src + +# 빌드 (테스트는 CI에서 수행하므로 컨테이너 빌드에서는 제외 권장) +RUN ./gradlew --no-daemon clean bootJar -x test + +# ---------- Run Stage ---------- +FROM eclipse-temurin:17-jre-jammy AS runtime +WORKDIR /app + +# 보안/권장: non-root 유저로 실행 +RUN useradd -ms /bin/bash appuser +USER appuser + +# bootJar 복사 +COPY --from=build /workspace/build/libs/*.jar app.jar + +EXPOSE 8080 + +# 기본 JVM 옵션(필요 시 docker-compose/.env에서 JAVA_TOOL_OPTIONS로 덮어쓰기 가능) +ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul", "-jar", "app.jar"] diff --git a/build.gradle b/build.gradle index 0862edf..3c41435 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,7 @@ dependencies { // Spring Boot Core implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-actuator' // Lombok compileOnly 'org.projectlombok:lombok' @@ -43,6 +44,7 @@ dependencies { // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testRuntimeOnly 'com.h2database:h2' // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13' diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b91d59e..790477d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,19 +4,27 @@ spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: ${DB_URL} - username: ${DB_USERNAME} - password: ${DB_PASSWORD} + url: ${DB_URL:jdbc:mysql://localhost:3306/valuedi?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8} + username: ${DB_USERNAME:test} + password: ${DB_PASSWORD:test} data: redis: - host: ${REDIS_HOST} - port: ${REDIS_PORT} + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} jpa: database: mysql database-platform: org.hibernate.dialect.MySQLDialect - show-sql: ${SHOW_SQL} + show-sql: ${SHOW_SQL:false} hibernate: - ddl-auto: ${DDL_AUTO} + ddl-auto: ${DDL_AUTO:none} properties: hibernate: - format_sql: true \ No newline at end of file + format_sql: true +management: + endpoints: + web: + exposure: + include: health + endpoint: + health: + show-details: never diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 0000000..77f1642 --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,30 @@ +spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:valuedi_test;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + username: sa + password: + + jpa: + database: h2 + database-platform: org.hibernate.dialect.H2Dialect + show-sql: false + hibernate: + ddl-auto: create-drop + properties: + hibernate: + format_sql: true + + data: + redis: + host: localhost + port: 6379 + +management: + endpoints: + web: + exposure: + include: health + endpoint: + health: + show-details: never