Files
dap-was-dapms/deploy/README.md
koseokmin d1d93f7dc9 GitOps 이전까지 쓸 배포 파이프라인과 Portal 모드 Chart를 추가한다
GitOps 저장소도 ArgoCD Application도 아직 없어, 그때까지 이 저장소가
push 방식 파이프라인(.gitea/workflows/)을 임시로 소유한다. 무엇을
포기하는지와 넘길 때 할 일은 deploy/README.md에 적었다.

Chart는 portal과 bundles 두 배포 모델을 모두 렌더링한다. ADR-0013이
ADR-0007을 대체했으므로 운영은 portal이 기준이지만, bundles 경로를
언제 삭제할지는 아직 정하지 않았다.

- .gitea/workflows/ci.yaml, deploy-openshift.yaml
- deploy/ci/render-manifests.sh, deploy/examples/
- Chart: mode 분기, selectedDeployment/tier helper, imagePullSecrets,
  toolService.apiKeySecret 참조
- extension-points.md에 미결 항목 9~12 추가

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:02:36 +09:00

11 KiB
Raw Blame History

배포 정의

이 디렉터리는 배포될 대상과, 그것을 클러스터에 올리는 임시 경로를 정의한다. 빌드·배포 실행 방식의 정본은 원래 사내 표준 CI/CD이며 이 저장소가 정하지 않는다. 지금 여기 파이프라인이 있는 이유는 아래에 적었다.

Helm Chart

helm/mcp-server/가 유일한 배포 정의다.

배포 모델이 두 가지다

mode 무엇이 route↔Tool Service 매핑을 소유하는가 근거
portal (기본값) Portal. 배포 하나가 N개 route를 서비스하고 route key는 /mcp/{routeKey} URI에서만 온다 ADR-0013
bundles 배포 정의. 배포 하나가 Tool Service 하나만 보고 매핑을 배포 시점에 못박는다 ADR-0007

현재 애플리케이션이 실제로 도는 경로는 portal이다. bundles는 ADR-0013이 대체했지만 코드 경로가 남아 있어 1:1 검증과 격리 배포에 쓸 수 있다. 어느 쪽을 운영에 쓸지는 아직 확정되지 않았고 extension-points.md에서 관리한다.

mode를 바꾸면 ConfigMap의 Tool 원천이 통째로 바뀐다. 값 하나로 배포 성격이 달라지므로 설치 명령에 항상 명시한다.

values는 두 축으로 나뉜다

파일 소유하는 것
values.yaml 배포 토폴로지. mode, portal 배포 정의, bundles 배포 목록, 등급 기준
values-{dev,test,prod}.yaml 환경 차이. namespace, 공개 host·허용 CIDR, Portal registry 주소, 등급별 replica·PDB, 리소스

환경 파일은 토폴로지를 갖지 않는다. HelmDeploymentContractTest가 그 경계를 고정한다.

# portal 모드. 배포가 하나이므로 deploymentKey가 없다.
helm upgrade --install axhub-mcp helm/mcp-server -f helm/mcp-server/values-dev.yaml -n <namespace>

# bundles 모드. 설치할 배포 하나를 반드시 고른다.
helm upgrade --install processing-critical-mcp helm/mcp-server -f helm/mcp-server/values-dev.yaml \
  --set mode=bundles --set deploymentKey=processing-critical -n <namespace>

deploymentKey에는 기본값이 없다. bundles에서 지정을 빠뜨리면 렌더링 단계에서 멈춘다. 엉뚱한 배포가 조용히 설치되는 것보다 낫다. 반대로 portal에서 deploymentKey를 주면 역시 멈춘다. route를 배포 정의에 적기 시작하면 Portal을 원천으로 둔 이유가 사라지기 때문이다.

공개 host와 path

한 환경은 하나의 공개 host를 사용한다(ADR-0009). Route는 Service만 선택하고 공개 path를 그대로 전달하며, 컨테이너가 같은 path를 직접 처리한다.

portal 모드에서 Route path는 /mcp 하나다. OpenShift Route의 path는 prefix 매칭이므로 /mcp/{routeKey} 전체가 이 Route로 들어오고, route 구분은 컨테이너가 한다.

https://mcp-dev.apps.example.internal/mcp/cus -> axhub-mcp:8080/mcp/cus
https://mcp-dev.apps.example.internal/mcp/sal -> axhub-mcp:8080/mcp/sal

bundles 모드에서는 Route가 배포마다 하나씩 생기고 path가 배포별로 다르다.

https://mcp-dev.apps.example.internal/mcp/processing-critical -> processing-critical-mcp:8080/mcp/processing-critical

두 경우 모두 공개 URL은 각각 독립된 MCP다. Agent Builder는 URL별로 등록하고 initialize한다.

가용성 등급

tiers:
  critical: { replicas: 3, podDisruptionBudget: true, spreadAcrossNodes: true }
  standard: { replicas: 2, podDisruptionBudget: false, spreadAcrossNodes: false }

test와 prod의 criticalreplica 2 이상, PodDisruptionBudget, 노드 분산 설정이 필수다. replica가 1이면 rolling update 중 반드시 공백이 생기고, PDB가 없으면 노드 drain이 마지막 Pod을 내릴 수 있다. dev는 배포마다 Pod 1개로 운영하므로 이 검사 대상이 아니다.

portal 모드에서 등급별 물리 분리는 성립하지 않는다. 배포가 하나이므로 전 route가 같은 프로세스·같은 replica set을 공유한다(ADR-0013 전제 2). tiers는 그 하나의 배포에 어떤 가용성 기준을 적용할지만 정한다.

렌더링 검증

HelmDeploymentContractTest는 values와 template의 정적 규칙만 본다. helper 오류, 조건 분기 실수, 들여쓰기는 실제로 렌더링해야 드러난다. 두 검사는 서로를 대신하지 못한다.

deploy/ci/render-manifests.sh   # 환경 × 모드 전 조합 lint + template

CI가 매 push에서 같은 스크립트를 돌리고 결과를 rendered-manifests 아티팩트로 올린다.

GitOps 저장소가 없는 동안의 우회

Chart를 어디에 둘지, 배포를 무엇이 실행할지는 아직 확정되지 않았다. GitOps 저장소도 ArgoCD Application도 없다. 그동안 파이프라인을 멈춰 두지 않기 위해 아래 형태로 돌린다.

파일 트리거 하는 일
.gitea/workflows/ci.yaml main push, PR gradlew check, Chart lint·template, 이미지 빌드·push
.gitea/workflows/deploy-openshift.yaml 수동 실행 고른 환경·모드로 helm upgrade --install
.gitea/workflows/deploy.yaml main push 기존 VM docker compose 배포

이 방식이 무엇을 포기하는가

숨기지 않고 적는다. GitOps로 넘어가는 판단의 근거가 되기 때문이다.

  • 클러스터 상태가 저장소와 자동으로 맞춰지지 않는다. 누가 oc edit으로 고치면 그대로 남는다.
  • 배포 이력이 Helm release history에만 남는다. git revert로 되돌릴 수 없고 helm rollback을 써야 한다.
  • 파이프라인이 클러스터 자격증명을 들고 있어야 한다. 러너를 신뢰 경계 안에 두어야 한다.
  • 어떤 이미지가 어느 환경에 떠 있는지 저장소만 봐서는 모른다. 수동 실행 이력을 봐야 한다.

이 때문에 OpenShift 배포에는 자동 트리거를 두지 않았다. 사람이 환경·모드·이미지 tag를 확인하고 실행한다.

GitOps 저장소가 생기면

  1. deploy-openshift.yaml을 삭제한다. 클러스터 자격증명 secret도 회수한다.
  2. ArgoCD Application이 이 Chart를 참조하게 하거나, Chart 자체를 배포 저장소로 옮긴다.
  3. CI의 rendered-manifests 아티팩트가 인수인계 형태다. 그 시점에 무엇이 배포되고 있었는지가 거기 그대로 있으므로, 옮긴 뒤 diff로 대조한다.
  4. deploy.yaml의 VM compose 배포를 계속 쓸지 결정한다. 스크립트가 저장소 밖(러너의 /home/ubuntu/apps/prd-dap-gateway/deploy.sh)에 있어 이 저장소가 내용을 모른다.

아직 필요한 secret

확정 전까지 CI는 이미지 빌드까지만 하고 push를 건너뛴다. 없는 secret 때문에 파이프라인 전체가 실패로 보이지 않게 하기 위해서다.

secret 쓰는 곳 없으면
REGISTRY_HOST·REGISTRY_USER·REGISTRY_PASSWORD·IMAGE_REPOSITORY CI 이미지 push push 건너뜀
OCP_SERVER·OCP_TOKEN OpenShift 배포 배포 실패
OCP_CA_CERT API 인증서를 사내 CA가 서명했을 때 러너의 신뢰 저장소를 쓴다. 사내 CA면 TLS 검증 실패
OCP_NAMESPACE_DEV·OCP_NAMESPACE_TEST·OCP_NAMESPACE_PROD OpenShift 배포 배포 실패

dev에서 MCP에 연결되지 않을 때

먼저 Tool Service가 떠 있는지 확인한다. readiness가 usable snapshot을 요구하므로, Tool Service가 없으면 MCP Pod은 Ready가 되지 못하고 Service endpoint에서 빠진다. "MCP가 죽었다"가 아니라 "읽을 Tool이 없다"는 뜻이다.

portal 모드에서는 Portal registry 조회부터 확인한다. registry를 못 읽으면 route 자체가 등록되지 않아 /mcp/{routeKey} 호출이 route key 검증에서 거부된다.

kubectl get pod -l app=<배포 이름>          # 0/1 Ready이면 이 경우다
kubectl describe pod <pod>                  # Readiness probe 실패 사유
kubectl port-forward <pod> 9090:9090        # /actuator/toolBundles로 bundle 상태 확인

Tool Service가 뜨면 다음 refresh 주기 안에 스스로 Ready가 된다. 재기동할 필요가 없다. /actuator/toolBundles는 management 포트라 NetworkPolicy가 관제 namespace로 제한하므로, 개발자는 위처럼 port-forward로 본다.

확정 전 임시값

values.yaml의 이미지 경로와 Tool Service 이름, values-{env}.yaml의 namespace·공개 host·Route 허용 CIDR·Portal registry 주소는 자리표시자다. 각 파일의 TODO 주석을 참고해 확정 시 교체하고, 존재하지 않는 배포는 deployments에서 삭제한다.

배포 시 알아야 할 앱 제약

아래는 이 애플리케이션의 동작에서 나온 사실이다. 각 항목의 정본은 링크한 문서이며 값을 여기 옮겨 적지 않는다.

제약 정본
readiness·liveness는 management 포트에서 제공한다 architecture.md
readiness는 첫 Tool 조회 시도 완료 후 usable snapshot이 있을 때만 UP이다 architecture.md
terminationGracePeriodSeconds는 Spring drain보다 길어야 한다 architecture.md의 요청 시간 예산
NetworkPolicy는 필수다. 비활성화 스위치를 두지 않았다 ADR-0006
공개 path는 Route와 컨테이너 endpoint가 동일하게 사용한다 ADR-0009
route↔Tool Service 매핑의 원천은 Portal이다 ADR-0013
Portal 응답 모양과 실패 처리 Portal-MCP 계약 v0.1
Redis key·TTL·공유 정책은 확정 전이다 extension-points.md

Route IP allowlist와 NetworkPolicy는 특히 중요하다. 이 서버는 인증·인가를 하지 않으므로 /mcp에 도달할 수 있다는 것이 곧 인가다. Route는 Agent Builder 고정 egress CIDR만 받고, NetworkPolicy는 Route backend인 ingress controller와 명시한 Agent Builder namespace만 업무 포트에 허용한다.

portal 모드는 여기에 하나를 더한다. MCP는 Portal registry가 준 주소를 그대로 호출한다. Portal이 신뢰 경계 안에 있다는 전제가 깨지면 MCP의 outbound 대상이 통째로 바뀐다(ADR-0013 전제 4). egress 제한은 아직 없으며 extension-points.md에서 관리한다.