배포 가이드

CONE-Watcher N 백엔드(heka-nhn-backend)와 프론트엔드(heka-nhn-frontend)의 이미지 빌드·배포 절차를 정리합니다. 두 리포는 동일한 GitOps 리포(cone-watcher-gitops) 의 동일한 kustomization 파일 을 공유하며, ArgoCD 가 이를 읽어 단일 cone-watcher 네임스페이스에 함께 배포합니다.

1. 배포 아키텍처 한눈에

┌─ [소스 리포 1] heka-nhn-backend ─────────────────────┐
│  docker/base/Dockerfile      ← Python + uv + wkhtml  │
│  docker/api/Dockerfile       ← FROM python:latest   │
│  docker/worker/Dockerfile    ← FROM python:latest   │
│  docker/scheduler/Dockerfile ← FROM python:latest   │
└──────────────────────────────────┬───────────────────┘
                                   │  GitHub Actions
                                   │   · Build Python   (base 재빌드, 의존성 변경 시)
                                   │   · Build Service  (api/worker/scheduler)

┌─ [소스 리포 2] heka-nhn-frontend ───────────────────┐
│  docker/dashboard/Dockerfile ← turbo prune + nginx  │
│  docker/admin/Dockerfile     ← turbo prune + nginx  │
└──────────────────────────────────┬───────────────────┘
                                   │  GitHub Actions
                                   │   · Build          (dashboard/admin)

      ┌──────────────────────────────────────────────┐
      │  NHN NCR: cone-watcher-ncr/{python,api,       │
      │           worker,scheduler,dashboard,admin}   │
      └────────────────────┬─────────────────────────┘
                           │  deploy job → gitops PR (auto-merge)

      ┌──────────────────────────────────────────────┐
      │  grumatic/cone-watcher-gitops (branch:develop)│
      │    app/overlays/{dev|prd}/kustomization.yaml  │
      │      images:                                  │
      │        - cone/api         ← backend 갱신     │
      │        - cone/worker      ← backend 갱신     │
      │        - cone/scheduler   ← backend 갱신     │
      │        - cone/dashboard   ← frontend 갱신    │
      │        - cone/admin       ← frontend 갱신    │
      └────────────────────┬─────────────────────────┘
                           │  ArgoCD ApplicationSet auto-sync

      ┌──────────────────────────────────────────────┐
      │  Kubernetes (NHN NKS) — namespace cone-watcher│
      │    prd-cone-watcher-{api|worker|scheduler|   │
      │                      dashboard|admin}         │
      │  Ingress: watcher.conecloud.io                │
      │    /        → dashboard                       │
      │    /admin   → admin                           │
      │    /api     → api                             │
      └──────────────────────────────────────────────┘

모든 실제 배포 조작은 GitHub Actions workflow_dispatch 수동 트리거 입니다. 커밋 push 만으로는 아무것도 배포되지 않습니다.


2. 이미지 빌드 구조

2.1 백엔드 — base ↔ service 2단 계층

docker/base/Dockerfile 이 Python 런타임 + 의존성 + OS 도구를 묶어 별도 이미지(python:latest)로 먼저 만들어지고, 각 service(api / worker / scheduler) 이미지가 이를 FROM 으로 가져다 소스 파일만 얹습니다.

base 이미지 (${REGISTRY}/python:latest)docker/base/Dockerfile

  • multi-stage builder: uv sync --frozen --no-devpyproject.toml + uv.lock 에 선언된 모든 런타임 의존성을 /usr/src/.venv 에 설치
  • 최종 이미지에 .venv 복사
  • OS: wget, procps, wkhtmltox 0.12.6.1-3 (청구서 PDF 렌더링용)

service 이미지docker/{api|worker|scheduler}/Dockerfile

ARG BASE_IMAGE_URI=python:3.12-slim-bookworm
FROM $BASE_IMAGE_URI
WORKDIR /usr/src
COPY common common
COPY config config
COPY <service> <service>
COPY tmp tmp
COPY .env .env
# ENTRYPOINT / CMD 는 서비스별로 다름

즉 service 이미지는 base 위에 애플리케이션 소스 파일만 얹는 얇은 레이어입니다. 의존성 설치는 전혀 없습니다.

언제 base 를 다시 빌드해야 하는가

변경 파일base 재빌드service 재빌드
common/, api/, worker/, scheduler/, config/Python 소스 코드❌ 불필요✅ 필요
pyproject.toml / uv.lock (의존성 추가·제거·버전 변경)필수✅ 필요
docker/base/Dockerfile (OS 패키지, wkhtmltopdf 버전 등)필수✅ 필요 (새 base 로 얹어야 함)
docker/{service}/Dockerfile (ENTRYPOINT, COPY 목록 등)❌ 불필요✅ 필요
.env.development.example 등 예시/문서❌ 불필요❌ 불필요
GitOps 리포 (cone-watcher-gitops) 수정❌ 불필요❌ 불필요 (ArgoCD 가 자동 반영)

왜 service 이미지만 재빌드하면 안 되나? Service Dockerfile 의 FROM ${BASE_IMAGE_URI} 는 빌드 시점의 python:latest 를 기준으로 .venv 를 그대로 가져옵니다. 새로 추가한 패키지가 .venv 에 없으므로 컨테이너 기동 시 ModuleNotFoundError 로 실패합니다.

2.2 프론트엔드 — 단일 Dockerfile 자체 완결

heka-nhn-frontendpnpm workspace + turbo 모노레포이며, 각 앱(dashboard, admin)이 docker/{app}/Dockerfile 하나로 의존성 설치·빌드·최종 이미지까지 한 번에 처리합니다. 별도 base 이미지가 없습니다.

4-stage 빌드 구조 (두 Dockerfile 동일 패턴)

[base]    node:22-alpine + corepack + pnpm + turbo (글로벌)


[pruner]  turbo prune --scope=@heka/{app} --docker
          → 해당 앱에 필요한 workspace 만 추려서 out/ 디렉터리에 분리


[deps]    pnpm install --frozen-lockfile  (pnpm store cache mount)
          → 추려진 의존성만 설치


[builder] CI=true pnpm build:{app}
          → Vite 빌드 → apps/{app}/dist


[final]   FROM nginx:alpine
          COPY dist → /usr/share/nginx/html (또는 /usr/share/nginx/html/admin)
          COPY docker/{app}/nginx.conf → /etc/nginx/nginx.conf
          CMD nginx

핵심 포인트:

  • 백엔드와 달리 의존성 설치가 매번 빌드 안에서 일어남pnpm-lock.yaml / package.json 변경 시에도 별도 base 재빌드가 필요 없습니다. 그냥 해당 앱을 재빌드하면 됨.
  • turbo prune 덕분에 한 앱만 빌드할 때 다른 앱(예: dashboard 빌드할 때 admin 소스)은 이미지 레이어에 들어가지 않음.
  • 최종 이미지는 nginx 가 정적 파일을 서빙 하는 형태 (80 포트). docker/{app}/nginx.conf 가 번들됩니다.
  • 두 앱은 완전 독립. dashboard 만 변경됐으면 admin 은 재빌드할 필요 없음.

프론트엔드에서 재빌드가 필요한 경우

변경 파일재빌드 대상
apps/dashboard/src/** 혹은 apps/dashboard/package.jsondashboard
apps/admin/src/** 혹은 apps/admin/package.jsonadmin
packages/theme/** 혹은 packages/eslint/**해당 패키지를 쓰는 모든 앱 (현재는 두 앱 모두)
pnpm-lock.yaml 루트 (공통 의존성 변경)두 앱 모두
docker/{app}/Dockerfile 혹은 nginx.conf해당 앱
turbo.json / pnpm-workspace.yaml두 앱 모두 (영향 범위 확인 필수)

3. GitHub Actions 워크플로

3.1 Build Python — 백엔드 base 이미지 빌드 (heka-nhn-backend/.github/workflows/build-base.yml)

수동 트리거 전용 · 실행 빈도 드뭄.

  • 입력: environment (dev / prd)
  • 빌드 파일: docker/base/Dockerfile
  • 태그: ${REGISTRY}/${PREFIX}/python:latest (커밋 SHA 태그 없음)
  • :latest 가 덮어써지므로, base 재빌드 직후 같은 커밋으로 service 를 빌드해야 의도한 base 를 사용합니다.

3.2 Build Service — 백엔드 서비스 이미지 빌드 + 배포 (heka-nhn-backend/.github/workflows/build-service.yml)

가장 자주 쓰는 백엔드 워크플로.

  • 입력: service (api / worker / scheduler), environment (dev / prd), deploy (기본 true)
  • 3단계 job:
    1. check-image${PREFIX}/${service}:sha-${github.sha} 이 이미 레지스트리에 있으면 빌드 스킵.
    2. buildBASE_IMAGE_URI=${REGISTRY}/${PREFIX}/python:latest 로 service 이미지 빌드. 태그: latest + sha-<full-commit>.
    3. deployinputs.deploy == true 이고 이미지가 준비됐으면 cone-watcher-gitopsapp/overlays/${environment}/kustomization.yaml 에서 cone/${service} 이미지 태그를 sha-${github.sha} 로 갱신하는 PR을 생성·auto-merge. ArgoCD 가 auto-sync 로 파드 롤링 업데이트.

3.3 Build — 프론트엔드 이미지 빌드 + 배포 (heka-nhn-frontend/.github/workflows/build.yml)

프론트엔드는 단일 워크플로 가 전담합니다. 구조는 백엔드 Build Service 와 거의 동일.

  • 입력: service (dashboard / admin), environment (dev / prd), deploy (기본 true)
  • 3단계 job:
    1. check-image — 동일 패턴 (${PREFIX}/${service}:sha-${github.sha}).
    2. builddocker/${service}/Dockerfile 을 빌드. BASE_IMAGE_URI 같은 추가 인자 없음 — Dockerfile 자체가 완결돼 있기 때문.
    3. deploy — 백엔드와 완전히 동일한 gitops 리포·동일한 kustomization 파일 을 수정: cone/${service} 이미지 태그 갱신 → PR auto-merge.

주의 — 병행 배포 시 PR 충돌 가능성: 백엔드와 프론트엔드 양쪽의 deploy job 이 동시에 cone-watcher-gitops 의 같은 kustomization.yaml 을 수정하려고 하면 한쪽 PR 이 머지될 때 다른 쪽이 rebase 가 필요해질 수 있습니다. 일반적으로 update-kustomization 액션이 처리하지만, 동시 실행을 피하고 순차적으로 트리거 하는 것을 권장합니다.


4. 배포 절차 (시나리오별)

공통 전제: 변경 사항이 해당 리포의 develop 브랜치에 merge 되어 있어야 하며, 빌드 대상은 merge 커밋 SHA 입니다.

시나리오 A — 백엔드 코드 변경만 (가장 흔함)

예: 컨트롤러 로직 수정, API 엔드포인트 추가

  1. PR 을 heka-nhn-backend/develop 에 merge.
  2. Actions → Build Service → Run workflow.
  3. 입력: service = api (또는 변경된 서비스), environment = prd (혹은 dev 선행), deploy = true.
  4. 자동 생성된 gitops PR 이 머지되는지 확인 → ArgoCD Synced/Healthy 대기 → 파드 rollout 확인.
  5. 여러 서비스가 바뀌었으면 각각 반복 (api → worker → scheduler).

시나리오 B — 백엔드 Python 의존성 변경 (pyproject.toml / uv.lock)

예: 새 라이브러리 추가, 취약점 패치, dramatiq 등 핵심 패키지 업그레이드

base 재빌드 → service 재빌드 순서 엄수.

  1. 로컬에서 uv add <pkg> / uv lock --upgrade-package <pkg> 등으로 pyproject.toml + uv.lock 갱신. 테스트 후 PR → develop merge.
  2. Actions → Build Python → Run workflow (environment 선택).
  3. python:latest 가 새 .venv 로 갱신 완료까지 대기 (몇 분).
  4. 같은 커밋 기준으로 service 들 순차 빌드·배포:
    Build Service → service=api       , environment=prd, deploy=true
    Build Service → service=worker    , environment=prd, deploy=true
    Build Service → service=scheduler , environment=prd, deploy=true
  5. ArgoCD 동기화 및 파드 상태 확인.

⚠️ 주의 1 — base :latest 경주 조건: 다른 개발자가 동시에 다른 커밋으로 base 를 재빌드하면 :latest 가 덮어써집니다. 의존성 변경 배포 중에는 다른 사람에게 base 빌드 실행을 보류하도록 공유하세요.

⚠️ 주의 2: Build Pythonenvironment 입력은 GitHub Actions environment(시크릿/권한) 를 고르는 용도이지 이미지 태그를 나누지 않습니다. 결과물은 단일 python:latest 이라 dev/prd 가 같은 base 를 공유합니다.

시나리오 C — 백엔드 OS 패키지/도구 변경 (docker/base/Dockerfile)

절차는 시나리오 B 와 동일 — base 재빌드 후 service 전체 재빌드.

시나리오 D — 프론트엔드 코드 변경만 (가장 흔함)

예: 페이지 UI 수정, 컴포넌트 추가, 라우팅 변경

  1. PR 을 heka-nhn-frontend/develop 에 merge.
  2. Actions → Build → Run workflow.
  3. 입력: service = dashboard (또는 admin), environment = prd, deploy = true.
  4. gitops PR 머지 확인 → ArgoCD sync → rollout.
  5. packages/theme 등 공용 패키지를 바꿨다면 dashboardadmin 둘 다 각각 빌드해야 합니다.

시나리오 E — 프론트엔드 의존성 변경 (pnpm-lock.yaml / package.json)

예: @mui/material 버전 업, 새 라이브러리 추가

백엔드와 달리 별도 단계 불필요 — 해당 앱을 재빌드하면 끝.

  1. heka-nhn-frontend/develop 에 lockfile 변경 커밋이 포함된 PR merge.
  2. Actions → Build → Run workflow → service 선택 → --execute.
  3. Dockerfile 의 deps stage 가 매번 pnpm install --frozen-lockfile 로 재설치하므로 자동 반영됩니다.
  4. 공용 패키지(packages/theme 등)에 새 의존성이 추가됐다면 양쪽 앱을 각각 빌드.

시나리오 F — 환경변수·시크릿만 변경 (이미지 빌드 불필요)

예: SMTP 비밀번호 교체, 새 API 도메인, 인그레스 호스트 변경

주의: 스케줄러 cron(config/envs/.env.scheduler)은 백엔드 이미지에 번들되므로 시나리오 A(코드 변경) 로 처리해야 합니다 — ConfigMap 이 아닙니다. 04번 가이드 §1.8 참조.

  1. cone-watcher-gitops 에서 app/overlays/${environment}/kustomization.yamlsecretGenerator 블록, 또는 kustomize/mongodb/base/kustomization.yaml 등 대상 경로 수정.
  2. PR 생성 → 리뷰 → develop 머지.
  3. ArgoCD 가 자동으로 새 Secret 을 렌더링.
  4. MONGODB_* / REDIS_* / SMTP_* 등 파드 환경에 주입되는 값이면 파드 재기동 필요 (04번 가이드 §1.7):
    kubectl -n cone-watcher rollout restart \
      deploy/prd-cone-watcher-api \
      deploy/prd-cone-watcher-worker \
      deploy/prd-cone-watcher-scheduler
    프론트엔드는 빌드 타임에 .env* 를 읽어 dist 에 박아 넣으므로, 프론트엔드 URL/키 변경은 시나리오 D 로 처리 하세요 (런타임 Secret 변경 불가).

5. 배포 확인

# 1. ArgoCD Application 동기화 상태 (CLI 또는 UI)
#    prd-cone-watcher 가 Synced + Healthy 여야 함

# 2. 롤아웃 완료 대기 — 변경된 서비스만 확인하면 됨
kubectl -n cone-watcher rollout status deploy/prd-cone-watcher-api
kubectl -n cone-watcher rollout status deploy/prd-cone-watcher-worker
kubectl -n cone-watcher rollout status deploy/prd-cone-watcher-scheduler
kubectl -n cone-watcher rollout status deploy/prd-cone-watcher-dashboard
kubectl -n cone-watcher rollout status deploy/prd-cone-watcher-admin

# 3. 파드가 새 이미지 태그를 쓰고 있는지 확인
kubectl -n cone-watcher get deploy prd-cone-watcher-api \
  -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
# → 475fee20-kr1-registry.container.nhncloud.com/cone-watcher-ncr/api:sha-<expected>

# 4. 헬스체크
kubectl -n cone-watcher get pods -l app.kubernetes.io/component=api
kubectl -n cone-watcher get pods -l app.kubernetes.io/component=dashboard

# 5. 이상 징후 확인
kubectl -n cone-watcher logs -l app.kubernetes.io/component=api --tail=200 \
  | grep -iE 'error|traceback'

# 6. 프론트엔드는 실제 브라우저 접근 (Basic Auth/세션 로그인 경로 따라)
#    https://watcher.conecloud.io/        → dashboard
#    https://watcher.conecloud.io/admin   → admin

6. 롤백

방법 1 — GitOps PR revert (권장)

직전 배포로 만들어진 "chore: update CONE Watcher prd ... - <shortsha>" PR 을 cone-watcher-gitops 에서 revert. ArgoCD 가 이전 이미지 태그로 자동 재배포.

방법 2 — 이전 이미지 태그로 워크플로 재실행

이전에 성공했던 커밋 SHA 를 develop 에 다시 올리고 해당 리포의 Build Service (또는 프론트엔드 Build) 를 실행. check-image job 이 이미 존재하는 이미지를 감지해 빌드는 스킵하고 deploy job 만 수행하므로 빠릅니다.

방법 3 — 긴급 수동 (마지막 수단)

ArgoCD auto-sync 가 다시 덮어쓰기 전까지의 짧은 응급조치로만:

kubectl -n cone-watcher set image deploy/prd-cone-watcher-api \
  fastapi=<registry>/cone-watcher-ncr/api:sha-<previous>

→ ArgoCD 가 다시 동기화하면 GitOps 기준으로 돌아가므로 반드시 방법 1 또는 2 로 영구화 해야 합니다.


7. 체크리스트

배포 전

  • 변경이 해당 리포의 develop 에 merge 됐는가? 머지 커밋 SHA 를 확인했는가?
  • 백엔드: pyproject.toml / uv.lock / docker/base/Dockerfile 중 하나라도 변경됐다면 Build Python 을 먼저 실행했는가?
  • 프론트엔드: packages/theme 등 공용 패키지를 바꿨다면 영향받는 앱 모두 배포 대상에 포함했는가?
  • 백엔드·프론트엔드 동시 배포 상황인가? 같은 gitops 파일(kustomization.yaml) 을 수정하므로 순차 실행을 권장.
  • dev 에서 한번 돌려본 뒤 prd 로 가는 절차를 따랐는가? (긴급 패치가 아니라면)

배포 후

  • ArgoCD Application Synced/Healthy?
  • 파드 이미지 태그가 sha-<머지 커밋> 과 일치?
  • 모든 rollout 완료 (rollout status success)?
  • error_log 및 워커 로그에 배포 직후 새 에러가 쌓이지 않는가?
  • 청구서 배치 등 배치 작업이 예정된 날이라면 실행 시각 전후로 이상 없는지 추가 확인?
  • 프론트엔드 배포면 실제 브라우저에서 주요 화면 로드·로그인·주요 액션 smoke test 완료?

8. FAQ / 주의점

Q. 백엔드 base 이미지는 왜 매 커밋마다 빌드하지 않나? A. 의존성이 거의 바뀌지 않고, wkhtmltopdf 설치가 느립니다(수 분). 매번 빌드하면 CI 시간·레지스트리 용량 낭비. 의존성이 바뀔 때만 재빌드하는 것이 합리적.

Q. 프론트엔드도 base 이미지를 두면 더 빨라지지 않나? A. turbo prune + pnpm store cache mount 덕분에 변경 없는 의존성은 Docker 레이어/캐시에서 재사용됩니다. 현재 구조로도 빌드 시간이 충분히 짧고, base 이미지를 분리하면 node_modules 의 workspace 해석과 turbo prune 이 복잡해집니다. 지금 방식이 단순성·속도 균형이 좋습니다.

Q. check-image job 이 왜 필요한가? A. 같은 커밋을 dev 에 배포한 뒤 prd 에 배포할 때 빌드를 다시 할 필요가 없습니다. 레지스트리에 이미지가 이미 있으면 바로 deploy job 만 돌리는 최적화.

Q. 백엔드 .env 가 이미지에 copy 되는데 시크릿이 이미지에 박히는 것 아닌가? A. docker/{service}/DockerfileCOPY .env .env 가 있지만, 이 .env로컬 개발용 빈 껍데기 입니다. 운영 환경에서는 Kubernetes Secret prd-cone-watcher-env 의 값이 envFrom 으로 컨테이너 env 에 덮어씌워집니다 (app/base/worker/deployment.yaml 등). 이미지 안의 .env 는 사실상 무시됩니다. (향후 .dockerignore.env 를 제외하는 것을 고려.)

Q. 프론트엔드 환경변수는 어디서 주입되나? A. Vite 는 빌드 타임에 .env* 를 읽어 dist 에 상수로 인라인합니다. 즉 프론트엔드 환경변수 변경은 런타임 Secret 으로 못 바꾸고, 리포의 .env* 수정 → 재빌드 가 정공법입니다. 시나리오 D 로 처리하세요. 현재는 동일한 번들이 dev/prd 에 모두 나가므로, 환경별 분기가 필요하면 VITE_* 환경변수를 Dockerfile build-args 로 주입하는 구조를 별도로 설계해야 합니다.

Q. 스케줄러 cron 변경은 왜 이미지 재빌드가 필요한가? A. config/envs/.env.scheduler 가 소스 트리에 있고 docker/scheduler/DockerfileCOPY config config 로 이미지에 들어가기 때문입니다. ConfigMap 으로 외부화돼 있지 않아 런타임 값 변경 불가능. 04번 가이드 §1.8 참조.

Q. dev 와 prd 의 base 이미지가 같은데 괜찮은가? A. 현재 백엔드 python:latest 는 단일 태그로 공유합니다. 의존성 변경을 dev 에서 먼저 검증한 뒤 prd 에 동일한 base 로 배포하므로 일관성은 유지됩니다. 다만 dev 에 새 base 를 올린 직후 검증 없이 prd service 를 빌드하면 새 base 로 덮어씌워진 상태로 prd 에 나가므로, dev 배포 완료 후 일정 관찰 기간을 두고 prd service 빌드 로 진행하는 것이 안전합니다. 프론트엔드는 base 가 없어 이 이슈가 없습니다.

Q. 백엔드·프론트엔드 동시 배포 중 gitops PR 이 충돌하면? A. grumatic/actions/update-kustomization@v1 은 최신 base 로 rebase 후 재시도하도록 구현돼 있어 대부분 자동 해결됩니다. 그래도 운영상 같은 환경(prd) 에 backend + frontend 를 동시 트리거하지 말고 순차적으로 실행하는 것을 권장. 병행이 불가피하면 각 워크플로 실행 로그에서 PR 머지 완료를 확인하고 다음 트리거로 넘어가세요.