[chore] 배포 환경 구축 및 CD 파이프라인 추가 - #8
Conversation
7a4d5cb to
4a5850a
Compare
멀티스테이지 빌드로 실행 가능한 이미지를 만든다. - build 스테이지: Gradle로 boot jar 빌드(테스트 제외) - runtime 스테이지: JRE 21 경량 이미지, 비루트 유저 실행 - plain jar 비활성화로 boot jar만 패키징 - .dockerignore로 빌드 컨텍스트 최소화
dev·prod 브랜치 push 시 앱 이미지를 빌드해 GitHub Container Registry에 push한다. - ghcr.io/nexters/gamss-server 에 브랜치명·커밋 SHA 태그로 push - GITHUB_TOKEN으로 인증(별도 시크릿 불필요) - buildx gha 캐시로 빌드 가속
가비아 VM 생성 후 서버를 준비하는 초기 세팅 스크립트를 추가한다. - deploy/setup-server.sh: Docker 설치·방화벽·배포 유저·디렉토리 (멱등)
환경별 프로필과 서버용 docker-compose를 추가한다. 각 서버에서 앱과
MySQL을 함께 띄우며, 2GB 서버 기준으로 튜닝했다.
- application-dev.yml, application-prod.yml: datasource를 env로 주입
- deploy/{dev,prod}/docker-compose.yml: app + MySQL(외부 포트 비공개)
- 앱 힙 -Xmx768m, MySQL buffer pool 384M (사양 올리면 이 값만 상향)
배포 검증·모니터링용 /actuator/health를 추가한다. - spring-boot-starter-actuator 의존성 - health 엔드포인트만 노출, show-details=never - /actuator/health는 인증 없이 접근 허용(배포 헬스체크용)
이미지 빌드 후 dev/prod 서버에 SCP·SSH로 배포하는 job을 추가한다. 브랜치명으로 GitHub Environment를 골라 환경별 시크릿을 주입한다. 배포는 단순 재시작 방식이다(무중단 아님). 헬스체크에 실패하면 이전 이미지 태그로 되돌린다. 롤백 기준이 되는 IMAGE_TAG는 서버 .env에 기록해 두고 재배포 시 보존한다.
2GB 전제로 잡혀 있던 값을 실제 서버 사양(2vCore/8GB)에 맞춘다. app 힙 768m→3g, MySQL buffer pool 384M→2G. LLM 호출은 응답까지 스레드를 붙잡고 있어 동시성을 올리려면 스레드를 늘려야 하므로, 스택용으로 약 1.2G를 남겨 둔다.
앱이 루프백에만 바인딩돼 있어 외부에서 접근할 수 없던 것을 nginx로 연다. 80 포트를 받아 gamss 네트워크의 app:8080 으로 프록시한다. 앱 컨테이너는 배포마다 재생성되며 IP가 바뀌는데, proxy_pass 에 이름을 그대로 쓰면 nginx가 기동 시점 IP를 캐시해 재배포 후 502가 난다. Docker 내장 DNS를 resolver로 두고 변수로 넘겨 매번 다시 해석하게 했다. actuator는 외부에 노출하지 않는다. LLM 응답을 고려해 프록시 타임아웃은 120초로 잡았다. 도메인이 정해지면 server_name 과 443 블록을 추가한다.
이미지 태그에 브랜치명을 쓰는데 Docker 태그는 슬래시를 허용하지 않는다. deploy job에만 가드가 있어서, workflow_dispatch로 feat/x 같은 브랜치에서 실행하면 빌드를 전부 끝낸 뒤 push 단계에서 invalid reference format 으로 죽는다. build-and-push 에도 같은 가드를 걸어 아예 시작하지 않게 한다.
ci.yml 과 image.yml 이 각각 push 에 반응해 병렬로 돌아서, 테스트가
깨져도 배포가 그대로 나가고 있었다.
ci.yml 을 workflow_call 로 열고 Build & Deploy 가 호출하게 한다.
push 트리거는 제거한다 — 남겨두면 테스트가 두 번 돌 뿐 아니라
배포를 막지 못하는 문제가 그대로다.
concurrency 그룹은 배포·롤백이 한 환경에서 겹치지 않도록 deploy-{환경}
으로 통일하고, cancel-in-progress 를 끈다. SSH로 원격 상태를 바꾸는
중에 취소당하면 .env 만 갱신되고 컨테이너는 그대로인 상태가 남는다.
배포는 성공했으나 버그가 발견된 경우 이전 이미지 태그로 되돌린다. 헬스체크 실패로 인한 롤백은 배포 중 deploy-app.sh 가 이미 처리하므로, 이 워크플로는 그 이후에 사람이 판단해 되돌리는 경로를 담당한다. 서버에 있는 deploy-app.sh 를 재사용해 헬스체크·자동복구 동작을 배포와 동일하게 가져간다. 태그가 대상 환경 접두사와 다르면 실행 전에 막아 prod 에 dev 이미지를 올리는 사고를 방지한다.
4a5850a to
9a475af
Compare
Test Coverage
|
dev/prod 에서 client-ids 가 비면 부팅을 실패시키던 것을, 경고 로그만 남기고 부팅은 계속하도록 바꾼다. client-ids 발급 전에 배포 파이프라인을 검증할 수 있게 하기 위함이다. client-ids 가 비어도 aud 검증은 여전히 fail-closed 라, 소셜 로그인은 안전하게 막힌 채 서버만 뜬다. 설정 누락은 기동 시점 경고로 드러난다(배포 전 필수 설정).
# Conflicts: # src/main/kotlin/com/nexters/gamss/auth/oauth/OAuthClientIdsValidator.kt # src/test/kotlin/com/nexters/gamss/auth/oauth/OAuthClientIdsValidatorTest.kt
Firebase 전환(#10)으로 앱이 더는 이 값을 읽지 않는다. CD의 .env 생성부와 docker-compose(dev·prod)에서 주입하던 죽은 설정을 정리한다. 앱이 실제로 읽는 env(DB_PASSWORD·JWT_SECRET 등)만 남긴다.
|
@coderabbitai 리뷰해봐 |
|
✅ Action performedReview finished.
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (5)
WalkthroughDocker 멀티 스테이지 이미지와 dev/prod Compose 환경을 추가하고, Actuator 헬스체크를 공개 경로로 구성했습니다. GitHub Actions에서 GHCR 이미지 빌드·푸시, SSH 배포, 헬스체크 기반 롤백 및 수동 롤백을 수행하도록 변경했습니다. Changes배포 플랫폼 및 자동화
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant GitHubActions
participant GHCR
participant SSHServer
participant DeployScript
participant DockerCompose
participant App
participant Nginx
GitHubActions->>GHCR: 이미지 빌드 및 태그 push
GitHubActions->>SSHServer: Compose, nginx 설정, env, 스크립트 전송
SSHServer->>DeployScript: 이미지 태그로 배포 실행
DeployScript->>DockerCompose: app 이미지 pull 및 재기동
DeployScript->>App: /actuator/health 확인
DeployScript->>Nginx: 설정 검사 후 reload
DeployScript->>DockerCompose: 실패 시 이전 이미지 태그로 app 롤백
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/scripts/deploy-app.sh:
- Around line 79-88: Wrap the docker compose up -d commands in the main
deployment flow and rollback() with conditional failure handling compatible with
set -e. Preserve the existing behavior that a main startup failure invokes
rollback(), while rollback() continues to wait_for_health and emit its failure
log instead of terminating the script immediately. Follow the existing guarded
docker compose pull app pattern.
In @.github/workflows/image.yml:
- Around line 18-20: Lower the workflow-level packages permission to read in
.github/workflows/image.yml at lines 18-20, then add an explicit contents: read
and packages: write permission block to the build-and-push job only. Also lower
the workflow-level packages permission to read in .github/workflows/rollback.yml
at lines 27-29; the rollback job needs no job-level override.
- Around line 42-43: Update both Checkout steps in the workflow to disable
credential persistence by explicitly setting persist-credentials to false, and
pin each actions/checkout reference to an immutable commit SHA instead of the
mutable `@v4` tag.
- Around line 54-64: Pin every third-party action to a full commit SHA instead
of a mutable tag: update docker/setup-buildx-action, docker/login-action, and
docker/build-push-action in .github/workflows/image.yml lines 54-64;
appleboy/scp-action at line 98 and appleboy/ssh-action at line 108 in
.github/workflows/image.yml; and appleboy/ssh-action at line 51 in
.github/workflows/rollback.yml. Preserve each action’s existing behavior and
configuration.
- Around line 87-96: Update the “Create .env” step to pass DB_PASSWORD,
DB_ROOT_PASSWORD, and JWT_SECRET through the step’s env configuration, then
reference them as shell variables inside the heredoc instead of interpolating
GitHub secret expressions in run. Preserve the existing .env keys and quoted
heredoc behavior.
In @.github/workflows/rollback.yml:
- Around line 78-88: Rollback Summary에서 자유 형식 입력값이 스크립트에 직접 보간되지 않도록 수정하세요.
`Rollback Summary` 단계에 `env`로 `IMAGE_TAG`, `ENVIRONMENT`, `ACTOR`, `JOB_STATUS`를
전달하고, `run` 블록의 요약 출력에서는 `${{ ... }}` 대신 셸 변수(`"${IMAGE_TAG}"` 등)를 사용하세요.
In `@deploy/dev/docker-compose.yml`:
- Around line 53-57: Update the healthcheck test in
deploy/dev/docker-compose.yml lines 53-57 and deploy/prod/docker-compose.yml
lines 53-57 to use CMD-SHELL and pass the root credentials via
MYSQL_ROOT_PASSWORD, preserving the existing host and healthcheck timing
settings.
In `@deploy/nginx/conf/gamss.conf`:
- Around line 21-24: Update the proxying logic in the root location and the
nearby actuator protection so normalized paths cannot bypass the /actuator
restriction; do not rely on passing $request_uri unchanged. Add an early
rejection for traversal forms including ../ and encoded dot-segments before
proxying, while preserving normal request forwarding and the existing actuator
block behavior.
In `@deploy/setup-server.sh`:
- Line 33: Replace the direct curl-to-shell execution in the server setup flow
with a safer Docker installation method: preferably configure Docker’s official
APT repository and install through the package manager, or download the
installation script to a file, verify its integrity, then execute it. Preserve
the existing Docker installation behavior without piping remote content directly
to sh.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 731a506f-5f47-4a8c-a2f0-f3d649840f59
📒 Files selected for processing (16)
.dockerignore.github/scripts/deploy-app.sh.github/workflows/ci.yml.github/workflows/image.yml.github/workflows/rollback.ymlDockerfilebuild.gradle.ktsdeploy/dev/docker-compose.ymldeploy/nginx/conf/gamss.confdeploy/prod/docker-compose.ymldeploy/setup-server.shsrc/main/kotlin/com/nexters/gamss/global/security/SecurityConfig.ktsrc/main/resources/application-dev.ymlsrc/main/resources/application-prod.ymlsrc/main/resources/application.ymlsrc/test/kotlin/com/nexters/gamss/global/config/ActuatorHealthTest.kt
CodeRabbit 리뷰 반영. - deploy-app.sh: set -e 하에서 `docker compose up -d`가 실패하면 스크립트가 즉시 종료돼 자동 롤백이 안 되던 것을, pull 과 동일하게 `if !`로 감싼다. main은 롤백으로, rollback 내부는 경고 후 반환하도록 처리. - image.yml·rollback.yml: 워크플로 기본 packages 권한을 write→read 로 낮추고, GHCR push 가 필요한 build-and-push job 에만 write 를 부여(최소 권한). - rollback.yml: 자유 형식 입력(image_tag 등)을 Summary 스크립트에 직접 보간하던 것을 env 로 받아 셸 변수로 참조(스크립트 인젝션 방지).
kite707
left a comment
There was a problem hiding this comment.
고생하셨습니다!
배포 파이프라인 전체(Dockerfile, dev/prod docker-compose, nginx 리버스 프록시, CD 워크플로, 자동/수동 롤백)를 한 번에 깔끔하게 구성해주셨네요.
컨테이너 재생성 시 IP 캐싱으로 에러가 나는 문제까지 미리 막아두신 점도 배워갑니다!
고생많으셨습니다! 승인할게요!
build.gradle.kts 충돌 해소: Actuator(배포)와 Gemini SDK(LLM) 의존성 양쪽 모두 유지. application.yml 은 dev 기준 + management(actuator) 블록 병합.
🔗 연관 이슈
📌 개요
가비아 클라우드 서버 2대(dev·prod)에 앱과 MySQL을 띄우고, dev·prod 브랜치 push 시 자동 배포되는 CD 파이프라인을 붙입니다. 무중단 배포는 하지 않고 단순 재시작 방식으로 시작합니다.
🔧 주요 변경사항
배포 산출물
Dockerfile— JDK 빌드 / JRE 런타임 2단계, 비루트 실행, exec로 SIGTERM 정상 처리deploy/{dev,prod}/docker-compose.yml— app + MySQL + nginx (서버당 한 벌)deploy/nginx/conf/gamss.conf— 리버스 프록시 (80 → app:8080)deploy/setup-server.sh— 서버 초기 세팅 (Docker·방화벽·배포 디렉토리), 멱등CD (
.github/workflows/image.yml)ci.yml을workflow_call로 열어 CD가 호출 — 테스트가 깨지면 배포로 넘어가지 않습니다dev-a1b2c3d) — 롤백 기준배포 스크립트 (
.github/scripts/deploy-app.sh)up -d→ 헬스체크 → 실패 시 이전 이미지 태그로 자동 롤백IMAGE_TAG는 서버.env에 기록하고 재배포 시 보존수동 롤백 (
.github/workflows/rollback.yml)deploy-app.sh를 재사용해 헬스체크·자동복구가 배포와 동일하게 동작서버 사양 반영
🌐 API · DB 영향
💬 리뷰 포인트
1. nginx의
resolver지시자 — 앱 컨테이너는 배포마다 재생성되며 IP가 바뀝니다.proxy_pass에 이름을 그대로 쓰면 nginx가 기동 시점 IP를 캐시해 재배포 후 502가 납니다. Docker 내장 DNS를 resolver로 두고 변수로 넘겨 매번 재해석하게 했습니다. 컨테이너 IP를 바꿔가며 검증했습니다.2. 무중단 배포가 아닙니다 — 교체 시 20~40초 다운타임이 있습니다. Blue/Green은 nginx upstream 전환과 app 컨테이너 2개 동시 기동이 필요해 이번 범위에서 제외했습니다.
🧹 정리 완료
GOOGLE_CLIENT_IDS·APPLE_CLIENT_IDS를image.yml·docker-compose(dev·prod)에서 제거했습니다..env에는 앱이 실제로 쓰는 값(DB_PASSWORD·DB_ROOT_PASSWORD·JWT_SECRET·IMAGE_TAG)만 남습니다.CodeRabbit 리뷰 반영
deploy-app.sh:set -e하에서docker compose up -d실패 시 자동 롤백이 건너뛰어지던 것을if !로 감싸 롤백으로 넘김image.yml·rollback.yml: 워크플로 기본packages권한을 write→read 로 낮추고, push가 필요한build-and-pushjob 에만 write 부여(최소 권한)rollback.yml: 자유 형식 입력을 Summary 스크립트에 직접 보간하던 것을env로 받아 참조(스크립트 인젝션 방지)mysqladmin ping은 Access denied 여도 exit 0(healthy)이라 오탐. nginx proxy_pass·액션 SHA 고정 등은 저위험/optional로 판단.🚀 배포 순서 (이 PR 머지 = 첫 배포)
이 PR이 dev에 머지되면 CD가 dev 서버에 첫 배포를 시도합니다(이미지 빌드 → GHCR → SCP·SSH →
up -d→ 헬스체크). 현재 서버엔 아무것도 떠 있지 않은 상태라, 이 머지가 실제 첫 기동입니다.gamss.kr구매 완료. A 레코드 전파 확인됨:dev-api.gamss.kr→139.150.11.145(dev)api.gamss.kr→1.201.120.148(prod)http://dev-api.gamss.kr접근 확인🖥 서버 준비 상태
~/app~/app보안 그룹은 SSH 22 / HTTP 80 / HTTPS 443만 열려 있습니다 (기본값의 RDP 3389는 제거).
Summary by CodeRabbit
Summary by CodeRabbit
새 기능
개선
health만 노출하고 상세 정보는 숨겼습니다.테스트
/actuator/health인증 없이200 UP을 확인하는 테스트를 추가했습니다.