Files
dap-was-dapms/deploy
koseokmin 1e6fa8f22f Portal을 endpoint 원천으로 확정하고 route 단위 실패 격리를 적용한다
내부망 운영은 route↔Tool Service 매핑을 Portal이 소유하고, MCP 배포 하나가 N개
route를 서비스하며, route 하나에 N개 Tool Service가 붙을 수 있다. 이 판단을
ADR-0010으로 남기고 ADR-0007 전체와 ADR-0009 결정 4를 대체한다.

계약
- Portal-MCP registry 조회 계약 v0.1과 예제 JSON을 docs/contracts/portal-mcp/에
  신설한다. 지금까지 이 경로에는 정본이 없었다.
- 예제를 PortalToolRegistryClient의 실제 파싱 경로에 태우는 계약 테스트를 추가해
  문서와 구현이 따로 표류하지 않게 한다.

실패 격리
- fetchAllTools()의 실패 전파를 route 단위로 격리한다. 계약 v0.2의 "aggregate는
  전부 아니면 전무"는 카탈로그 하나를 전제한 규칙인데, route가 N개가 되면서 전
  route로 확대돼 있었다. Tool Service 하나의 장애가 cold start에서 Pod 전체를
  내리고 steady state에서 모든 route의 갱신을 멈추던 동작을 없앤다.
- 제거 판단의 원천을 ToolRegistryClient.knownRoutes()로 분리한다. 조회 결과를
  기준으로 지우면 이번 주기에 실패한 route의 정상 snapshot까지 사라져 "어떤
  실패도 목록을 비우지 않는다" 불변식이 깨진다.
- readiness는 최소 1개 route로 UP을 유지한다. 모든 route를 요구하면 정상 route까지
  트래픽에서 빠져 위 격리를 되돌리기 때문이다. 대신 routesWithoutSnapshot을
  health detail로 노출해 관제가 부분 상태를 감지하게 한다.

설정과 기동
- warm start가 route별 Redis key를 읽도록 확장하고, 읽을 key를 알기 위해 기동
  preload 순서를 registry 조회 → warm start → manifest 조회로 바꾼다.
- 어떤 코드도 읽지 않던 mcp.portal.route-key를 제거한다. route key는 요청 URI에서만
  결정되며, 설정으로 보정하면 잘못된 단일 진입점 호출이 조용히 성공한다.

정리
- ToolRegistryService.java의 이중 인코딩으로 깨져 있던 한글 Javadoc 33줄을 코드
  동작에 맞춰 다시 쓰고, replaceSnapshot 위에 겹쳐 있던 고아 Javadoc 블록을 지운다.
- Helm chart는 mcp.bundles 구성에서 계속 유효하므로 삭제하지 않고, 내부망 운영
  대상이 아니라는 사실을 deploy/README.md와 values.yaml에 명시한다.

검증: 이 환경은 loopback이 막혀 gradlew check를 실행하지 못했다. CodeStyleContract가
보는 항목(줄바꿈, 탭, 행말 공백, 파일 끝 개행, 미사용 import, import 순서)은 변경된
Java 13개 파일에 대해 따로 재현해 확인했다. 컴파일과 테스트 실행은 미확인이다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 18:10:34 +09:00
..

배포 정의

이 디렉터리는 배포될 대상을 정의한다. 빌드·이미지·배포 실행 방식은 사내 표준 CI/CD가 담당하며 이 저장소가 정하지 않는다.

내부망 운영은 이 Chart를 사용하지 않는다(ADR-0010 결정 7). 여기 정의된 토폴로지는 배포 하나가 Tool Service 하나를 보는 mcp.bundles 구성(ADR-0007/0009)을 전제한다. 내부망 운영은 endpoint 목록과 route 매핑의 원천을 Portal로 옮겼고, 배포 하나가 N개 route를 서비스한다.

Chart를 지우지 않는 이유는 mcp.bundles 구성이 코드에서 사라지지 않았고 local 검증과 1:1 배포가 필요한 환경에서 그대로 유효하기 때문이다. 다만 아래 deployments 목록의 업무 이름은 예시이며 실제 배포 대상이 아니다. Portal 구성으로 갈 환경에 이 Chart를 적용하면 route가 하나로 고정된다.

Helm Chart

helm/mcp-server/가 유일한 배포 정의다. values는 두 축으로 나뉜다.

파일 소유하는 것
values.yaml 배포 토폴로지. 어떤 MCP가 어떤 Tool Service를 보는가, 공개 path, 가용성 등급
values-{dev,test,prod}.yaml 환경 차이. namespace, 이미지, 공개 host·허용 CIDR, 등급별 replica·PDB, 리소스

설치할 때 두 번째 축을 -f로, 첫 번째 축에서 고를 배포 하나를 --set deploymentKey=로 지정한다.

helm upgrade --install processing-critical-mcp helm/mcp-server -f helm/mcp-server/values-dev.yaml --set deploymentKey=processing-critical -n <namespace>

deploymentKey에는 기본값이 없다. 지정을 빠뜨리면 렌더링 단계에서 멈춘다. 엉뚱한 배포가 조용히 설치되는 것보다 낫다.

공유 host와 배포별 path

ADR-0009에 따라 한 환경은 하나의 공개 host를 사용하고, 각 Helm release는 고유 path의 OpenShift Route를 만든다. Route는 Service만 선택하고 공개 path를 그대로 전달하며, 컨테이너가 같은 path를 직접 처리한다.

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

공개 URL은 각각 독립된 MCP다. Agent Builder는 URL별로 등록하고 initialize하며, 한 Route나 MCP Pod의 장애가 다른 path의 Deployment로 전파되지 않는다.

MCP 하나는 Tool Service 하나만 본다

ADR-0007의 결정이다. 대상을 늘리는 방법은 bundle 목록을 늘리는 것이 아니라 배포를 하나 더 만드는 것이다.

deployments:
  processing-critical:
    name: processing-critical-mcp
    service: processing-critical-tools   # ← 이름만. 주소는 template이 만든다
    namePrefix: "processing."            # ← 업무 단위. 등급을 넣지 않는다
    tier: critical
    publicPath: /mcp/processing-critical # ← 환경 host 안에서 유일

배포가 10개든 20개든 파일 수는 늘지 않는다. 전체 매핑을 한 화면에서 검토할 수 있고, --set으로 고르는 값 하나만 배포마다 달라진다.

MCP Server와 Tool Service는 같은 namespace에 배포하므로 values에는 서비스 이름만 적고 주소는 template이 조립한다. 환경마다 URL을 반복하지 않으므로 오타로 다른 대상을 호출할 수 없다.

identity{배포 이름}-{global.env}로 template이 조립한다. 현재 Redis cache 구현이 이 값을 사용하지만, key namespace와 공유 정책은 아직 확정되지 않았다.

가용성 등급

배포를 업무 × 중요도로 나누는 목적은 중요 등급에만 비용을 쓰기 위해서다.

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을 내릴 수 있다. HelmDeploymentContractTest는 values와 template의 정적 규칙을 검사한다. dev는 배포마다 Pod 1개로 운영하므로 이 검사 대상이 아니다.

정적 테스트는 Helm 렌더러를 실행하지 않는다. 실제 배포 파이프라인은 사용하는 환경과 등급별로 helm linthelm template을 실행해 병합된 values와 생성 YAML을 확인해야 한다.

helm lint helm/mcp-server -f helm/mcp-server/values-prod.yaml --set deploymentKey=processing-critical
helm template processing-critical-mcp helm/mcp-server -f helm/mcp-server/values-prod.yaml --set deploymentKey=processing-critical
helm template processing-standard-mcp helm/mcp-server -f helm/mcp-server/values-prod.yaml --set deploymentKey=processing-standard

나누는 것만으로 가용성이 생기지는 않는다. 같은 노드 배치, namespace 쿼터, 공통 Redis·클러스터 장애는 분할로 막히지 않는다. 남은 작업은 extension-points.md의 "운영 적용 전 필수 보완"에서 관리한다.

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

먼저 Tool Service가 떠 있는지 확인한다. readiness가 usable snapshot을 요구하므로, Tool Service가 없으면 MCP Pod은 Ready가 되지 못하고 Service endpoint에서 빠진다. dev는 배포마다 Pod 1개라 그 순간 그 MCP로는 아예 연결되지 않는다. "MCP가 죽었다"가 아니라 "읽을 Tool이 없다"는 뜻이다.

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 주기(기본 30초) 안에 스스로 Ready가 된다. 재기동할 필요가 없다. /actuator/toolBundles는 management 포트라 NetworkPolicy가 관제 namespace로 제한하므로, 개발자는 위처럼 port-forward로 본다.

확정 전 임시값

values.yaml의 Tool Service 이름·이미지 경로와 values-{env}.yaml의 namespace·공개 host·Route 허용 CIDR은 자리표시자다. 각 파일의 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
Redis key·TTL·공유 정책은 확정 전이다 extension-points.md
bundle은 정확히 하나다 ADR-0007

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

미확정 항목

배포 정의를 이 저장소가 어디까지 소유하는지, namespace·registry 명명 규칙은 아직 확정되지 않았다. docs/extension-points.md에서 관리한다.