배포 파이프라인·환경 분리 TDD

Background

근거 PRD: ../배포 파이프라인·환경 분리/PRD.md (검수 PASS).

sports-application은 로컬 docker-compose 단일 환경만 있고, 그 compose에는 backend·MySQL·MongoDB·Redis·Kafka가 없다(docker-compose.ymlminio·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)
base compose서비스 정의만 담은 docker-compose.yml (호스트 포트·환경별 값 없음)
override환경별 값(포트·APP_ENV·볼륨·태그)을 base에 병합하는 docker-compose.{env}.yml
프로젝트명docker compose -p sports-dev / -p sports-prod — 컨테이너·네트워크·볼륨 네임스페이스 접두사 (COMPOSE_PROJECT_NAME)
QA 게이트private-qa 에이전트의 verdict(PASS/FAIL)가 prod 배포 job의 실행 가부를 결정하는 관문
이미지 태그 핀prod가 실행할 backend 이미지를 커밋 SHA 태그로 고정 — 롤백 = 이전 태그 재배포
self-hosted runnerdev/prod 스택이 도는 그 호스트에 등록된 GitHub Actions 러너 (배포 실행 주체)

Define Problem

AS-IS

  • docker-compose.ymlminio·minio-init·mock-pg·mock-kakao-local·mock-data-go-kr·mock-solapi·mailhog 7종만. backend·MySQL·Mongo·Redis·Kafka 없음.
  • backend/에 Dockerfile 없음 (find backend -iname Dockerfile → 0건). 앱은 개발자가 gradlew bootRun으로 로컬 기동. Spring Boot 3.3.5 / Kotlin 1.9.23 / Java 17, bootJar 산출.
  • 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:171management.metrics.tags.env: ${APP_ENV:local}. APP_ENV는 존재하나 dev/prod 주입 정의 없음 → 기본값 local 잔존 위험.
  • .github/workflows/load-test.ymltarget_url 기본값 https://staging.example.com, secrets.STAGING_MCP_TOKEN 참조하는 수동(workflow_dispatch) 부하 시험. 자동 배포 워크플로는 0건.
  • infra/nginx/mcp.conf:22upstream sports_backend { server 127.0.0.1:8080; } 이미 정의(⑦ LB가 얹힐 자리). base compose가 backend를 sports_backend로 노출해야 이 upstream이 유효.
  • 브랜치 흐름 — dev 활성 통합, chore/dev-main-syncmain 역동기화 (git 이력 확인).

TO-BE

  • backend/Dockerfile(멀티스테이지: gradle 빌드 → JRE 런타임)로 backend 이미지화.
  • docker-compose.yml(base)에 backend·MySQL 8.0·MongoDB·Redis·Kafka(KRaft 단일 브로커) 신규 편입 + 기존 mock 유지.
  • docker-compose.dev.yml + .env.dev / docker-compose.prod.yml + .env.prod로 포트·APP_ENV·볼륨·태그 분리. -p sports-dev/-p sports-prod로 네임스페이스 격리.
  • deploy-dev.yml(dev push → 자동 배포) / deploy-prod.yml(main 기준 + QA 게이트 + 롤백) 신규.
  • load-test.yml staging→prod 타깃 통합(FR-6).

Architecture Benchmarking (의무)

제품/사례해결 방식참고할 패턴미참고 사유
Docker Compose 공식 — 다중 파일 병합(-f base -f override) + COMPOSE_PROJECT_NAMEbase + 환경별 override를 -f로 겹치고, -p로 같은 호스트에 여러 스택 인스턴스를 유니크 네임스페이스로 병렬 기동 (docs.docker.com/compose profiles, environment-variables/envvars)채택 — base+override로 dev/prod 공통 정의 1벌 유지, -p로 네트워크·볼륨·컨테이너 완전 격리. ⑤·⑦가 추가 -f로 얹기 좋다Profiles는 한 프로젝트 내 서비스 on/off용 — 두 병렬 풀스택을 별 네트워크·볼륨으로 격리하지 못해 스택 분리엔 부적합(용도 한정 참고만)
Compose 단일 호스트 배포 파이프라인 (RynoM/self-hosted-deployment, Gitea/GH Actions)self-hosted runner가 호스트에서 이미지 태그를 .envDOCKER_TAG로 주입 후 docker compose up -d, 롤백은 이전 태그 재배포 (RynoM/self-hosted-deployment, ecostack.dev)채택 — 커밋 SHA 태그 핀 + 이전 태그 재배포 롤백. 단일 머신·개인 프로젝트 규모에 정확히 맞음Tailscale/SSH 원격 배포·레지스트리 푸시는 같은 머신 local prod 전제(별도 머신 아님)에서 과함 — self-hosted runner 직접 실행으로 단순화
GitHub Actions Environments + Deployment Protection Rules (PRD 벤치)환경별 보호 규칙(필수 승인·상태 체크)으로 배포를 관문 통과 시에만 진행 (github deployment gates)채택environment: production 보호 + qa-gate job으로 QA PASS 강제(FR-4, NFR 0-bypass)외부 승인 서비스 연동은 불필요 — private-qa verdict 파일 + 환경 보호로 충분
GitOps 프로모션 (Argo CD + Kargo) (PRD 벤치)dev→staging→prod 선언적 승격, Git 기반 승격 조건개념(검증 통과 후 다음 환경 승격)만 참고Kubernetes/Argo 전제 — 본 과제는 compose 단일 호스트, GitOps 컨트롤러 도입은 과함

Possible Solutions

방안 비교 — compose 환경 분리 전략

방안설명왜 채택/미채택
A. base + dev/prod override + -p 프로젝트명docker-compose.yml에 서비스 정의(포트 없음), docker-compose.dev.yml/.prod.yml에 호스트 포트·APP_ENV·볼륨·태그. -p sports-dev/sports-prod로 네트워크·볼륨·컨테이너 격리채택 — 공통 정의 1벌(중복 제거), 완전 네임스페이스 격리, ⑤·⑦가 추가 -f로 얹기 최적. Single Writer 경계를 파일 단위로 그을 수 있음
B. profile 단일 파일 (--profile dev/prod)docker-compose.yml에 dev/prod 서비스를 profile로 태깅미채택 — 한 프로젝트=한 네트워크·볼륨 네임스페이스라 두 스택이 볼륨·네트워크를 공유하거나 이름 충돌. 같은 머신 병렬 격리에 부적합. 파일 1개 집중 = ⑤·⑦와 Single Writer 충돌 심화
C. dev/prod 완전 별도 compose 2벌 복제docker-compose.dev.yml·docker-compose.prod.yml을 통째로 각각 작성미채택 — 서비스 정의가 2벌로 중복돼 드리프트 위험(한쪽만 수정). ⑤·⑦가 두 파일을 모두 수정해야 해 Single Writer 붕괴

방안 비교 — prod 배포 게이트

방안설명왜 채택/미채택
D. qa-gate job + environment: production 보호prod 워크플로 1단계 qa-gate job이 private-qa verdict(qa/verdict.json)를 읽어 PASS·SHA 일치 검증, 실패 시 즉시 종료. deploy job은 needs: qa-gate + environment: production(필수 승인)채택 — QA 미통과 prod 배포 경로 0건(FR-4·NFR 강제). verdict 파일 contract만 정의(에이전트 구현은 범위 밖)
E. 브랜치 보호 규칙만main push 시 무조건 prod 배포미채택 — QA 게이트 부재, PRD FR-4 위반
F. Blue-Green / Canary신·구 스택 병존 후 트래픽 전환미채택 — PRD Non-Goal. 개인 프로젝트·단일 호스트에 과함. Open Questions로 이관(⑦ LB 도입 후 재검토)

방안 비교 — 롤백

방안설명왜 채택/미채택
G. 이미지 태그 핀 + 이전 태그 재배포prod는 커밋 SHA 태그 이미지로 배포, .env.prodDOCKER_TAG 갱신. 롤백 = 이전 SHA 태그로 up -d 재실행채택 — 5분 내 복구(NFR), 단순·재현 가능. deploy-prod.ymlworkflow_dispatch 입력 image_tag로 수동 롤백
H. DB 스냅샷 포함 롤백앱+DB 상태를 함께 되돌림미채택 — 본 과제 스키마 변경 0, 데이터 롤백 불필요. 과함

Detail Design

도메인 바운디드 컨텍스트 판단 (의무)

  • 결정: 신규 도메인 분리 없음, 기존 도메인 합류도 없음 — 이 과제는 배포/인프라 계층 작업으로 애플리케이션 도메인 코드(Kotlin)·DB 스키마·Kafka 토픽·Redis 키를 만들거나 바꾸지 않는다.
  • 산출물은 전부 인프라 자산: Dockerfile, compose 파일군, .env.*, GitHub Actions 워크플로, 배포/롤백 스크립트.
  • 미채택(신규 도메인 생성) 사유: 배포 파이프라인은 라이프사이클·데이터 소유가 없는 횡단 인프라라 도메인 경계 대상이 아님. 선행 ① 산출물(도메인 5계층)과 독립.

서버 토폴로지 설계 (의무)

과제 특성은 “환경 격리 + 배포 자동화”로, 요청-응답 처리량 확장이 아니다. 후보와 선택:

서버 형태후보 검토채택 여부
API 서버(backend)환경당 1 인스턴스채택 — 현 규모 단일 인스턴스로 충분. ⑦가 다중 인스턴스+LB를 이 구조 위에 override로 추가(경계 명시)
워커/스케줄러 분리비동기·주기 작업 전용 서버미채택 — 현 앱은 단일 Spring Boot(SSE 포함)에 통합. 분리할 독립 워크로드 없음
소켓 서버 분리실시간 양방향 전용미채택 — SSE는 backend Tomcat 스레드로 처리(application.yml:8 max 400), 별도 서버 불필요
CI 러너self-hosted runner(배포 실행 주체)채택 — dev/prod 스택이 도는 호스트에 등록, docker compose up -d 직접 실행 (별도 머신 아님 전제)

compose 파일 소유 경계 (⑤·⑦ 공동 수정 대비 — 핵심)

Single Writer per File을 과제 간에도 강제한다. 실행 시 여러 -f로 병합.

파일소유 과제책임
docker-compose.yml (base)⑧ (본 과제)backend·mysql·mongodb·redis·kafka·mock 서비스 정의(단일 소유자). 호스트 포트·APP_ENV·환경별 값 없음
docker-compose.dev.yml + .env.devdev 호스트 포트·APP_ENV=dev·볼륨·DB포트 노출(디버그)
docker-compose.prod.yml + .env.prodprod 호스트 포트(backend만 노출)·APP_ENV=prod·이미지 태그 핀·DB포트 미노출(FR-9)
docker-compose.observability.ymlexporter(mysqld/mongodb/redis/kafka exporter) + 관측 스택(prometheus·grafana·datadog-agent)만. 데이터스토어·backend는 정의하지 않고 base의 서비스를 exporter 대상으로 참조. 추가 -f로 얹음
docker-compose.lb.ymlnginx 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에서 backenddeploy: 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 호스트 포트
backend8080808018080
mysql33063306(미노출)
mongodb2701727017(미노출)
redis63796379(미노출)
kafka90929092(미노출)
minio9000/90019000/900119000/19001
mock-pg9090909019090
mock 910x80809101-910319101-19103
mailhog1025/80251025/802511025/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 environmentAPP_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).
  • 검증: docker exec sports-dev-backend-1 env | grep APP_ENVAPP_ENV=dev, prod → APP_ENV=prod.

인터페이스 시그니처 — 배포/롤백 스크립트 계약

구현자 간 해석 차를 없애기 위해 스크립트 인터페이스를 동결한다.

scripts/deploy.sh <env> <image_tag>
  # env: dev|prod, image_tag: 커밋 SHA(또는 dev는 latest)
  # 동작: .env.<env>의 DOCKER_TAG를 image_tag로 치환 →
  #       docker compose -p sports-<env> -f docker-compose.yml -f docker-compose.<env>.yml up -d
  #       → /actuator/health 폴링(최대 5분, 5s 간격), 200+status:UP 이면 exit 0, 아니면 exit 1
  # 멱등: 같은 <env> 재실행 시 concurrency 그룹으로 중복 금지, 같은 tag면 no-op 수렴

scripts/rollback.sh <env> <previous_image_tag>
  # 동작: DOCKER_TAG를 previous_image_tag로 치환 → up -d → health 폴링
  # 목표: 트리거 후 5분 내 복구(NFR)

시스템 역할 경계 (의무)

단위역할소유/책임노출 인터페이스의존
backend/Dockerfile앱 이미지 빌드멀티스테이지 빌드(gradle→JRE17), non-root, /actuator/health HEALTHCHECKsports-backend:<tag> 이미지bootJar
docker-compose.yml (base)서비스 정의8+ 서비스 스펙, 내부 포트, healthcheck, depends_oncompose 병합 대상Dockerfile 이미지
docker-compose.{dev,prod}.yml환경 override호스트 포트·APP_ENV·볼륨·태그-f 병합base
scripts/deploy.sh배포 실행태그 치환 → up -d → health 폴링CLI 계약(위)compose 파일군
scripts/rollback.sh롤백 실행이전 태그 재배포CLI 계약(위)deploy.sh와 동형
deploy-dev.ymldev 자동 배포dev push 트리거 → 빌드·태그·배포GitHub ActionsDockerfile, dev override, deploy.sh
deploy-prod.ymlprod 배포 게이트QA 게이트 → 승인 → 배포/롤백GitHub Actionsprod override, deploy.sh, private-qa verdict
qa/verdict.json (contract)QA 판정 산출물{status, commit, reportUrl, ranAt}파일 계약private-qa(범위 밖 구현)

QA 게이트 연동 지점 (FR-4, FR-7 — private-qa)

  • private-qa 에이전트(범위 밖 구현)는 dev 배포본을 PRD 유저 시나리오 E2E로 검증 후 verdict를 qa/verdict.json에 기록: { "status": "PASS|FAIL", "commit": "<sha>", "reportUrl": "...", "ranAt": "<iso8601>" }.
  • deploy-prod.yml qa-gate job이 이 파일을 읽어 3중 검증: ① status == PASScommit == 배포 대상 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 누락/staleqa-gate 3중 검증 실패 → 즉시 종료, prod 배포 0
동시 배포워크플로 concurrency: deploy-${env} 그룹 → 같은 스택 중복 배포 직렬화
멱등같은 SHA 재배포 → 같은 태그 up -d no-op 수렴. QA verdict SHA 일치 검증으로 재실행 안전
배포 후 이상 감지⑩ 지능형 장애 알림 source=deployment → 개발자가 deploy-prod.yml workflow_dispatchimage_tag=<prev> 롤백

상태 전이 표 — 배포 파이프라인

현재 상태 × 이벤트다음 상태거부/비고
(없음) × dev 브랜치 pushdev 배포중
dev 배포중 × health UPdev 배포완료
dev 배포중 × health 실패(5분)dev 배포실패이전 이미지 유지, 알림
dev 배포완료 × dev-main-syncmain 스냅샷
main 스냅샷 × QA PASS(SHA일치)prod 배포가능
main 스냅샷 × QA FAIL/staleprod 차단알림, deploy job 미실행
prod 배포가능 × 배포 승인·트리거prod 배포중environment: production 승인 필요
prod 배포중 × health UPprod 배포완료
prod 배포중 × health 실패prod 롤백중이전 태그 재배포
prod 배포완료 × 이상감지(source=deployment)prod 롤백중수동 트리거
prod 롤백중 × health UPprod 배포완료(이전버전)5분 내 복구(NFR)

Component Diagram (Mermaid flowchart LR)

flowchart LR
    subgraph CI["GitHub Actions"]
        DevWf["deploy-dev.yml"]
        ProdWf["deploy-prod.yml"]
        QaGate["qa-gate job"]
    end
    subgraph Host["단일 호스트 (self-hosted runner)"]
        Deploy["scripts/deploy.sh"]
        subgraph DevStack["sports-dev (-p)"]
            DevBe["backend :8080"]
            DevDs["MySQL/Mongo/Redis/Kafka"]
        end
        subgraph ProdStack["sports-prod (-p)"]
            ProdBe["backend :18080"]
            ProdDs["MySQL/Mongo/Redis/Kafka"]
        end
    end
    Verdict["qa/verdict.json"]
    DevWf --> Deploy
    Deploy --> DevBe
    DevBe --> DevDs
    QaGate --> Verdict
    ProdWf --> QaGate
    QaGate --> Deploy
    Deploy --> ProdBe
    ProdBe --> ProdDs

Sequence Diagram — dev 자동배포 + prod QA 게이트

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)가 앱 부팅 시 스키마 적용.

Testing Plan

배포/인프라 자산이므로 Kotest 대신 구동 검증이 테스트다. 레이어별:

레벨대상검증 방법
정적compose 파일군docker compose -f ... config 병합 유효성(exit 0), hadolint backend/Dockerfile, actionlint 워크플로
단위(스택)dev 스택 기동deploy.sh dev → 전 컨테이너 healthy, /actuator/health UP, `env
단위(스택)prod 스택 기동deploy.sh prod → backend UP, DB 포트 미노출 확인(docker port 비어있음), APP_ENV=prod
격리dev·prod 동시 기동두 스택 병렬 up, 포트·네트워크·볼륨 충돌 0, docker network lssports-dev_*·sports-prod_* 분리
게이트(실패경로)QA FAIL/staleverdict FAIL·SHA 불일치·파일 부재 각각 → prod deploy job 미실행
게이트(해피)QA PASSverdict PASS+SHA 일치 → deploy job 진행
롤백이전 태그 복구rollback.sh prod <prev> → 5분 내 이전 버전 health UP
멱등같은 SHA 재배포재실행 no-op 수렴, concurrency 직렬화

Release Scenario — 무중단 배포 (의무)

이 과제는 “배포 시스템 자체”를 도입하므로 기존 로컬 개발 흐름을 깨지 않게 additive(expand)로 전개한다.

배포 순서 (코드 먼저 — 인프라 자산 추가는 앱 기동에 영향 없음)

  1. Expand(추가): Dockerfile·base compose·override·스크립트·워크플로를 dev에 머지. 기존 docker-compose.yml의 mock 서비스는 유지(제거 없음) → 기존 개발자 로컬 흐름 무손상.
  2. dev 스택 기동 검증: deploy.sh dev로 앱+데이터스토어 컨테이너 정상 기동 확인. bare gradlew bootRun 개발자는 여전히 ${...:localhost} 기본값으로 동작(하위 호환).
  3. prod 스택 기동 검증: deploy.sh prod로 별도 네임스페이스 기동, dev와 포트·볼륨 충돌 0 확인.
  4. QA 게이트 연결: private-qa verdict contract 확정 후 deploy-prod.yml qa-gate 활성화.
  5. load-test 통합(FR-6): load-test.yml 타깃을 prod로 리네임.

단계별 전환 조건

  • 1→2: compose config 병합 유효 + hadolint/actionlint 통과.
  • 2→3: dev 전 컨테이너 healthy + APP_ENV=dev 확인.
  • 3→4: dev·prod 동시 기동 격리 확인.
  • 4→5: qa-gate 실패경로(FAIL/stale/부재) 3건 모두 배포 차단 확인.

롤백 방법 (단계마다)

단계롤백
compose/워크플로 머지파일 revert(PR 되돌림). 기존 mock 스택은 무영향
dev 스택 배포 실패deploy.sh health 실패 → 이전 이미지 유지. 워크플로 파일 revert로 자동배포 중단
prod 스택 배포 실패rollback.sh prod <prev_tag> 이전 SHA 태그 재배포(5분 내, NFR)
QA 게이트 오작동deploy-prod.yml의 qa-gate를 임시 비활성(피처 플래그 성격 워크플로 입력 skip_gate=false 고정) — 단 승인 보호는 유지
load-test 리네임staging 입력 기본값 복원(단순 문자열 revert)

Observability (조건부 — 신규 CI/외부 연동)

  • 배포 이력: GitHub Actions run 로그(dev push↔배포 트리거 대응, Success Metrics 측정 근거).
  • QA 게이트 이력: qa-gate job artifact + step summary에 PASS/FAIL·reportUrl(FR-7).
  • 배포/게이트 실패 통지: ⑩ 지능형 장애 알림 채널, source=deployment 태깅(런타임 장애와 구분).
  • 상세 계측(대시보드)은 ⑤ 옵저버빌리티 스택 소유 — 본 과제는 배포 이벤트 소스만 제공.

Open Questions

  • private-qa 최소 판정 기준: 본 TDD는 “User Scenarios 1~5 해피패스 E2E 전건 통과 = PASS”로 최소 정의. 회귀 카탈로그 상세는 에이전트 정의(범위 밖).
  • Blue-Green/Canary 도입 시점: ⑦ backend 다중 인스턴스+LB 도입 후 재검토(현 단순 재기동 배포).
  • self-hosted runner 등록 방식(호스트 직접 vs 컨테이너 러너): 운영 시 확정. 본 TDD는 “호스트에서 compose 실행 가능한 러너” 전제.

Document History

날짜변경 내용
2026-07-03최초 작성 — 컨테이너화+dev/prod 분리+CI/CD+QA게이트 설계, compose 소유 경계(⑤⑦), ADR 5건·티켓 8건 분해

Success Metrics 확인 체크리스트 (PRD 판정 기준)

  • dev 머지→반영 자동화 100%: deploy-dev.yml push 트리거 + GitHub Actions run 이력 대응(측정 근거 확보).
  • QA 미통과 prod 배포 차단 100%: qa-gate 3중 검증 + environment: production 보호(NFR 0-bypass).
  • 롤백 5분 내 복구 100%: rollback.sh 이전 태그 재배포.