sports-application은 로컬 docker-compose 단일 환경만 있고, 그 compose에는 backend·MySQL·MongoDB·Redis·Kafka가 없다(docker-compose.yml에 minio·mock-pg·mock-servers·mailhog만 존재). 앱·데이터스토어를 compose에 편입(M1)한 뒤 dev/prod 두 스택으로 같은 머신에서 분리하고, dev 브랜치 머지 → dev 자동 배포, main 기준 + private-qa PASS 게이트 → prod 배포를 구성한다. docker-compose.yml은 ⑤(옵저버빌리티 스택)·⑦(backend 다중 인스턴스+LB)과 공동 수정 대상이므로 파일 소유 경계를 먼저 확정한다.
Overview
항목
내용
무엇
backend·데이터스토어 컨테이너화 + dev/prod 스택 물리 분리 + dev 자동배포·prod QA게이트 CI/CD
왜
dev/prod 환경 경계가 ⑤ 계측·⑨ 상시 트래픽 시뮬레이터의 선행 조건. QA 미통과 변경의 prod 유입 차단
어떻게
base compose + dev/prod override + -p 프로젝트명으로 네임스페이스 격리. 이미지 태그 핀 배포. GitHub Actions self-hosted runner가 호스트에서 docker compose up -d
도메인 영향
없음 — 인프라/배포 계층 작업, 애플리케이션 도메인 코드·DB 스키마 변경 0
Terminology
용어
정의
스택(stack)
한 환경(dev 또는 prod)의 전체 컨테이너 집합 (backend + 데이터스토어 + mock)
application.yml — datasource/mongo/redis/kafka 연결값이 이미 전부 환경변수로 외부화됨(${DB_URL:jdbc:mysql://localhost:3306/sports...}, ${MONGODB_URI:...}, ${REDIS_HOST:localhost}, ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}). 컨테이너 편입 시 앱 코드 변경 없이 env 주입만으로 대상 전환 가능.
application.yml:171 — management.metrics.tags.env: ${APP_ENV:local}. APP_ENV는 존재하나 dev/prod 주입 정의 없음 → 기본값 local 잔존 위험.
.github/workflows/load-test.yml — target_url 기본값 https://staging.example.com, secrets.STAGING_MCP_TOKEN 참조하는 수동(workflow_dispatch) 부하 시험. 자동 배포 워크플로는 0건.
infra/nginx/mcp.conf:22 — upstream sports_backend { server 127.0.0.1:8080; } 이미 정의(⑦ LB가 얹힐 자리). base compose가 backend를 sports_backend로 노출해야 이 upstream이 유효.
브랜치 흐름 — dev 활성 통합, chore/dev-main-sync로 main 역동기화 (git 이력 확인).
exporter(mysqld/mongodb/redis/kafka exporter) + 관측 스택(prometheus·grafana·datadog-agent)만. 데이터스토어·backend는 정의하지 않고 base의 서비스를 exporter 대상으로 참조. 추가 -f로 얹음
docker-compose.lb.yml
⑦
nginx LB + backend deploy.replicas 서비스 override. base·override 미수정, 추가 -f로 얹음
데이터 인프라 단일 소유자 (senior-pm 정합 #4 확정): 데이터스토어(mysql·mongodb·redis·kafka)와 backend의 단일 소유자는 ⑧ base compose다. ⑤ observability는 이 서비스들을 재정의하지 않고 exporter 대상으로 참조만 한다. 서비스 명명 기준은 ⑧ base — mongodb(구 ⑤ mongo 아님). ⑤ exporter는 이 서비스명(mongodb)으로 접속하므로 base 명명이 SSOT다. ⑤가 데이터스토어를 재정의하면 -f 병합 시 중복 충돌 → 금지.
병합 규칙: 실행 커맨드가 -f docker-compose.yml -f docker-compose.<env>.yml [-f observability] [-f lb] 순으로 병합. ⑤·⑦는 자기 파일만 추가하므로 base와 dev/prod override는 오직 ⑧만 쓴다.
⑦의 다중 인스턴스: docker-compose.lb.yml에서 backend에 deploy: replicas: N + nginx sports_backend upstream(infra/nginx/mcp.conf 재사용) override. base backend는 host 포트를 override에서 부여하되, ⑦ 적용 시 nginx가 앞단 → prod override의 backend 직접 노출 포트를 lb override가 덮어씀. 경계 유지.
스택 격리 — 포트·네임스페이스·볼륨 (FR-2)
서비스
내부 포트
dev 호스트 포트
prod 호스트 포트
backend
8080
8080
18080
mysql
3306
3306
(미노출)
mongodb
27017
27017
(미노출)
redis
6379
6379
(미노출)
kafka
9092
9092
(미노출)
minio
9000/9001
9000/9001
19000/19001
mock-pg
9090
9090
19090
mock 910x
8080
9101-9103
19101-19103
mailhog
1025/8025
1025/8025
11025/18025
네트워크·볼륨: -p sports-dev/-p sports-prod로 compose가 sports-dev_default·sports-dev_mysql-data 등 자동 접두사 → 완전 격리(설정 불필요).
컨테이너 내부 서비스 디스커버리는 두 스택 동일(backend→mysql:3306) — 내부 포트 고정, 호스트 포트만 override.
prod는 DB 포트 미노출(FR-9: prod 직접 변경 경로 최소화). dev만 디버그용 노출.
env 라벨 주입 (FR-5)
docker-compose.dev.yml의 backend environment에 APP_ENV: dev리터럴 주입, prod override에 APP_ENV: prod. 신규 키 도입 없음 — application.yml:171${APP_ENV:local}을 그대로 소비.
.env.local의 기본값 local은 compose 밖 bare gradlew bootRun 개발자 로컬에만 잔존 → dev/prod 스택엔 override가 명시 주입하므로 local 잔존 0건(NFR).
private-qa 에이전트(범위 밖 구현)는 dev 배포본을 PRD 유저 시나리오 E2E로 검증 후 verdict를 qa/verdict.json에 기록: { "status": "PASS|FAIL", "commit": "<sha>", "reportUrl": "...", "ranAt": "<iso8601>" }.
deploy-prod.ymlqa-gate job이 이 파일을 읽어 3중 검증: ① status == PASS ② commit == 배포 대상 SHA(main HEAD) (stale PASS 차단) ③ 파일 존재·형식 유효. 하나라도 실패 → job fail → deploy job 미실행(NFR 0-bypass).
FR-7: qa-gate가 verdict 원문을 actions/upload-artifact + $GITHUB_STEP_SUMMARY에 PASS/FAIL·reportUrl 기록.
이중 안전: deploy job은 environment: production(필수 승인 보호 규칙) 추가 → 자동+수동 2중 관문.
최소 QA 판정 기준(M4 완료 전제, Open Questions): PRD User Scenarios 1~5 해피패스 E2E 전건 통과 = PASS. 상세 하네스는 에이전트 정의 범위.
실패 경로·동시성·멱등 (의무)
상황
처리
dev 배포 health 실패
deploy.sh 5분 폴링 실패 → exit 1 → 워크플로 실패, 이전 컨테이너는 up -d 재생성 전 이미지가 유지(태그 미변경분). 알림 source=deployment
prod 배포 health 실패
deploy job 실패 → rollback.sh <prev_tag> 후속 job 자동 실행(이전 태그 재배포)
QA verdict 누락/stale
qa-gate 3중 검증 실패 → 즉시 종료, prod 배포 0
동시 배포
워크플로 concurrency: deploy-${env} 그룹 → 같은 스택 중복 배포 직렬화
멱등
같은 SHA 재배포 → 같은 태그 up -d no-op 수렴. QA verdict SHA 일치 검증으로 재실행 안전
sequenceDiagram
participant Dev as 개발자
participant GH as GitHub
participant DevWf as deploy-dev.yml
participant Runner as self-hosted runner
participant QA as private-qa
participant ProdWf as deploy-prod.yml
Dev->>GH: PR 머지 → dev
GH->>DevWf: push 트리거
DevWf->>Runner: build+tag, deploy.sh dev
Runner-->>DevWf: health UP
Dev->>GH: chore/dev-main-sync → main
QA->>QA: dev E2E 검증
QA->>GH: verdict.json (PASS, sha)
Dev->>ProdWf: workflow_dispatch
ProdWf->>ProdWf: qa-gate (PASS+sha 일치 검증)
ProdWf->>Runner: deploy.sh prod <sha>
Runner-->>ProdWf: health UP
ERD
해당 없음 — 본 과제는 DB 스키마·컬렉션 변경이 없다(컨테이너 편입만, Flyway 마이그레이션 신규 0건). 데이터스토어는 기본 이미지로 기동하며 기존 application.yml Flyway(classpath:db/migration)가 앱 부팅 시 스키마 적용.