From f9cc5fe7fd10fd61ac52912efa26beda03ea5e03 Mon Sep 17 00:00:00 2001 From: sevineleven <117634128+sevineleven@users.noreply.github.com> Date: Thu, 16 Jul 2026 02:00:56 +0900 Subject: [PATCH 1/4] =?UTF-8?q?perf:=20deploy=20=EA=B0=80=20CI=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=20JAR=20=EC=9D=84=20=EC=9E=AC=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=B4=20=EC=A4=91=EB=B3=B5=20=EB=B9=8C=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI(assemble)와 deploy(bootJar)가 같은 커밋 JAR 을 두 번 빌드하던 중복을 없앤다. CI build 잡이 JAR 을 아티팩트로 올리고, deploy 는 자기를 트리거한 CI 실행(run-id)의 아티팩트를 받아 재빌드(약 47초)를 건너뛴다. run-id 로 못박아 배포 커밋과 다른 커밋의 JAR 을 집는 레이스를 차단한다. 수동배포(workflow_dispatch)는 트리거 CI 가 없어 기존 빌드로 fallback 한다. - build.gradle.kts: plain jar 비활성 — assemble 이 jar 2개를 만들면 COPY build/libs/*.jar 가 모호해져 깨진다. boot jar 하나로 고정. - ci.yml: push 일 때 build/libs/*.jar 를 app-jar 로 업로드(3일 보존). - deploy.yml: actions:read 추가, 자동배포=아티팩트 다운로드/수동배포=빌드 fallback, 실패 알림·요약표에 다운로드 실패 케이스 반영. --- .github/workflows/ci.yml | 12 ++++++++++++ .github/workflows/deploy.yml | 32 ++++++++++++++++++++++++++++---- build.gradle.kts | 7 +++++++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 918e0e48..4fec1cfc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,18 @@ jobs: - name: Build run: ./gradlew assemble --no-daemon + # 배포(deploy.yml)가 이 JAR 을 그대로 받아쓰도록 업로드해, deploy 단계의 재빌드(중복)를 없앤다. + # deploy 는 push CI 의 workflow_run 으로만 트리거되므로(PR 은 배포 안 됨) push 일 때만 올린다. + # 짧게 보존(3일)해 아티팩트 누적을 막고, JAR 이 없으면 즉시 실패시킨다. + - name: Upload JAR artifact + if: github.event_name == 'push' + uses: actions/upload-artifact@v4 + with: + name: app-jar + path: build/libs/*.jar + retention-days: 3 + if-no-files-found: error + test: runs-on: ubuntu-latest if: github.event_name == 'pull_request' diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a731c86d..9df15221 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -87,6 +87,8 @@ jobs: environment: ${{ needs.resolve.outputs.environment }} permissions: contents: read + # 자동 배포 시 트리거한 CI 실행의 JAR 아티팩트를 크로스런 다운로드하려면 actions:read 가 필요하다. + actions: read # dev/prod 가 서로 배포를 막지 않도록 환경별 동시성 그룹. 같은 환경 내 재배포는 직렬화(취소 안 함). # 그룹은 브랜치명이 아니라 "환경"으로 묶어야 한다 — feature 브랜치 수동배포(dispatch)도 dev EC2 로 # 가므로 dev 자동배포와 같은 그룹이어야 같은 박스에서 동시 blue-green 조작 레이스를 막는다. @@ -141,13 +143,30 @@ jobs: blocks sparse-checkout-cone-mode: true - - name: Set up JDK 25 + # 자동 배포(workflow_run): 이 배포를 트리거한 CI 실행이 이미 빌드·업로드한 JAR 을 그대로 받아 + # 러너에서의 재빌드(약 47초)를 없앤다. run-id 로 그 CI 실행에 못박아, 배포 커밋과 다른 커밋의 JAR 을 + # 집는 레이스를 차단한다(그냥 '최신' 아티팩트를 집으면 배포 도중 다른 push 로 커밋 불일치가 난다). + # 자동 경로는 러너에 JDK·Gradle 이 필요 없다 — 도커 빌드의 layertools extract 는 이미지 안에서 돈다. + - name: Download JAR from triggering CI run + id: fetch_jar + if: github.event_name == 'workflow_run' + uses: actions/download-artifact@v4 + with: + name: app-jar + path: build/libs + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + # 수동 배포(workflow_dispatch): 트리거한 CI 실행이 없어 아티팩트가 없다. 이때만 러너에서 직접 빌드한다. + - name: Set up JDK 25 (manual dispatch fallback) + if: github.event_name == 'workflow_dispatch' uses: actions/setup-java@v4 with: java-version: '25' distribution: 'temurin' - - name: Cache Gradle + - name: Cache Gradle (manual dispatch fallback) + if: github.event_name == 'workflow_dispatch' uses: actions/cache@v4 with: path: | @@ -156,8 +175,9 @@ jobs: key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }} restore-keys: gradle-${{ runner.os }}- - - name: Build JAR + - name: Build JAR (manual dispatch fallback) id: build + if: github.event_name == 'workflow_dispatch' run: ./gradlew bootJar --no-daemon - name: Login to Docker Hub @@ -647,6 +667,8 @@ jobs: STATUS="❌ 실패 (런타임 프로비저닝)" elif [ "${{ steps.build.outcome }}" = "failure" ]; then STATUS="❌ 실패 (Gradle 빌드)" + elif [ "${{ steps.fetch_jar.outcome }}" = "failure" ]; then + STATUS="❌ 실패 (CI 아티팩트 다운로드)" else STATUS="⚠️ 알 수 없음" fi @@ -660,7 +682,7 @@ jobs: echo "| 환경 | ${{ needs.resolve.outputs.environment }} |" echo "| 커밋 | [\`$SHORT_SHA\`]($COMMIT_URL) |" echo "| 현재 활성 슬롯 | $NOW |" - echo "| Gradle 빌드 | ${{ steps.build.outcome }} |" + echo "| JAR 준비 | ${{ github.event_name == 'workflow_run' && steps.fetch_jar.outcome || steps.build.outcome }} |" echo "| Docker 푸시 | ${{ steps.docker_push.outcome }} |" echo "| EC2 배포 | ${{ steps.deploy.outcome }} |" echo "| 헬스체크 & 전환 | ${{ steps.health.outcome }} |" @@ -743,6 +765,8 @@ jobs: CAUSE="${CAUSE:-환경 도메인↔EC2_HOST 불일치 (secret 오설정 — dev/prod 교차배선)}" elif [ "${{ steps.build.outcome }}" = "failure" ]; then CAUSE="Gradle 빌드 실패 (Build JAR)" + elif [ "${{ steps.fetch_jar.outcome }}" = "failure" ]; then + CAUSE="CI 아티팩트(JAR) 다운로드 실패 (트리거 CI 의 app-jar 누락/만료 · actions:read 권한)" elif [ "${{ steps.docker_login.outcome }}" = "failure" ]; then CAUSE="Docker Hub 로그인 실패" elif [ "${{ steps.docker_push.outcome }}" = "failure" ]; then diff --git a/build.gradle.kts b/build.gradle.kts index 074d62e9..9e1df7b1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -137,3 +137,10 @@ tasks.withType { // JDK 21+부터 동적 에이전트 로딩이 제한됨. Mockito(ByteBuddy)가 런타임에 에이전트를 붙이므로 명시적 허용 필요. jvmArgs("-XX:+EnableDynamicAgentLoading") } + +// Spring Boot 플러그인은 실행 가능한 boot jar 와 라이브러리용 plain jar 를 함께 만든다. 이 앱은 라이브러리로 +// 쓰이지 않아 plain jar 가 불필요하고, build/libs 에 jar 가 2개면 배포·CI 의 `COPY build/libs/*.jar` 와 +// 아티팩트 전달이 어느 jar 인지 모호해져 깨진다. plain jar 를 꺼 산출물을 boot jar 하나로 고정한다. +tasks.named("jar") { + enabled = false +} From 5bf6c0abfec224321dff3ca39b3de5750f15b3e3 Mon Sep 17 00:00:00 2001 From: sevineleven <117634128+sevineleven@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:07:52 +0900 Subject: [PATCH 2/4] =?UTF-8?q?docs:=20=EC=95=84=ED=8B=B0=ED=8C=A9?= =?UTF-8?q?=ED=8A=B8=20=EB=B3=B4=EC=A1=B4=203=EC=9D=BC=EC=9D=B4=20?= =?UTF-8?q?=EC=95=88=EC=A0=84=ED=95=9C=20=EA=B7=BC=EA=B1=B0=EB=A5=BC=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=EC=97=90=20=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit nitpick 대응. 롤백·재배포가 옛 아티팩트에 의존하지 않음(자동=새 push, 수동=러너 빌드 fallback, blue-green=컨테이너 유지)을 주석으로 self-documenting. --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4fec1cfc..c3b5065f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,10 @@ jobs: # 배포(deploy.yml)가 이 JAR 을 그대로 받아쓰도록 업로드해, deploy 단계의 재빌드(중복)를 없앤다. # deploy 는 push CI 의 workflow_run 으로만 트리거되므로(PR 은 배포 안 됨) push 일 때만 올린다. - # 짧게 보존(3일)해 아티팩트 누적을 막고, JAR 이 없으면 즉시 실패시킨다. + # 보존 3일이 안전한 이유: 아티팩트는 이 CI 직후(수분 내) 그 workflow_run 배포가 소비하므로 3일은 큰 여유다. + # 롤백·재배포도 옛 아티팩트에 기대지 않는다 - 자동배포는 새 push 라 늘 새 아티팩트, 수동 재배포는 + # 러너 빌드 fallback(deploy.yml), blue-green 롤백은 돌던 컨테이너 유지. 누적 방지 겸 짧게 둔다. + # JAR 이 없으면 즉시 실패시킨다. - name: Upload JAR artifact if: github.event_name == 'push' uses: actions/upload-artifact@v4 From f6bad96f5ac8a78144de07ad51ebf089fea986cf Mon Sep 17 00:00:00 2001 From: sevineleven <117634128+sevineleven@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:41:03 +0900 Subject: [PATCH 3/4] =?UTF-8?q?perf:=20JAR=20=EC=95=84=ED=8B=B0=ED=8C=A9?= =?UTF-8?q?=ED=8A=B8=20store=20=EC=97=85=EB=A1=9C=EB=93=9C(compression-lev?= =?UTF-8?q?el=200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JAR 은 이미 압축된 zip 이라 기본 level 6 재압축은 크기 이득 없이 CPU 만 쓴다. store 로 업로드 CPU 를 아낀다. CI 는 deploy 가 workflow_run 으로 기다리는 크리티컬 패스. --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3b5065f..96e3db25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,9 @@ jobs: path: build/libs/*.jar retention-days: 3 if-no-files-found: error + # JAR 은 이미 압축된 zip 이라 기본 level 6 재압축은 크기 이득 ~0 에 CPU 만 쓴다. + # store(0)로 헛수고를 없애 업로드를 줄인다. CI 는 deploy 가 workflow_run 으로 기다리는 크리티컬 패스다. + compression-level: 0 test: runs-on: ubuntu-latest From 8a66662b59ef802b017464a50f5aacd46b6c1557 Mon Sep 17 00:00:00 2001 From: sevineleven <117634128+sevineleven@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:51:52 +0900 Subject: [PATCH 4/4] =?UTF-8?q?perf:=20Docker=20=EB=B9=8C=EB=93=9C?= =?UTF-8?q?=EC=97=90=EC=84=9C=20buildx=20=EC=A0=9C=EA=B1=B0=20(arm64=20?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=ED=8B=B0=EB=B8=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 러너가 arm64 네이티브(#749)라 크로스빌드가 불필요해, build-push-action(buildx)을 기본 docker 데몬의 plain docker build+push 로 바꿔 setup-buildx 스텝(약 10초)을 없앤다. 결과 이미지는 동일(arm64 네이티브). 로컬 docker build 로 Dockerfile.ci 전체 빌드 실증. --- .github/workflows/deploy.yml | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9df15221..2a86e38a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -187,20 +187,18 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - + # 러너가 arm64 네이티브(EC2 Graviton 과 동일 arch)라 크로스빌드가 불필요하다. buildx 없이 기본 docker + # 데몬으로 arm64 이미지를 네이티브 빌드해 setup-buildx(약 10초)를 없앤다. 태그·image ref 는 셸 보간 + # injection 을 피해 env 로 받아 "$VAR" 로 쓴다(repo 컨벤션). - name: Build and push Docker image id: docker_push - uses: docker/build-push-action@v6 - with: - context: . - file: Dockerfile.ci - push: true - platforms: linux/arm64 - tags: | - ${{ secrets.DOCKERHUB_USERNAME }}/piki-core:latest - ${{ secrets.DOCKERHUB_USERNAME }}/piki-core:${{ needs.resolve.outputs.ref }} + env: + IMAGE_LATEST: ${{ secrets.DOCKERHUB_USERNAME }}/piki-core:latest + IMAGE_SHA: ${{ secrets.DOCKERHUB_USERNAME }}/piki-core:${{ needs.resolve.outputs.ref }} + run: | + docker build -f Dockerfile.ci -t "$IMAGE_LATEST" -t "$IMAGE_SHA" . + docker push "$IMAGE_LATEST" + docker push "$IMAGE_SHA" - name: Upload nginx config id: upload_nginx